tests: e2e: fix harness on Windows, CI & inside git checkouts

Normalize the `\r\n` that Python on Windows writes to pipes in repo's
output so tests can match multi-line messages.  Pass a str to
`os.readlink`, which only accepts Path objects on Windows since Python
3.8.  Set `GIT_CEILING_DIRECTORIES` for the client so git never finds a
repo above the test dir (e.g. when running with `--basetemp=./tmp` from
a checkout).

Run the real git rather than a `git` wrapper that's first on PATH: the
real git's exec dir (from `git --exec-path`) has a copy of git, so put
it first on PATH.  LUCI's wrapper, for one, retries server errors for
minutes, which hangs tests that simulate them.

Bug: 566270944
Change-Id: Ia0ed5285852638e533632a7272f154239f1e834f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/648543
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Brian Gan <brgan@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
This commit is contained in:
Brian Gan authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-10-26 12:50:26 -07:00
1 parent f4ce9572d1
commit 9aac80dafc
2 files changed
+48 -5

No files matched your search

+8 -2
View File
@@ -113,8 +113,14 @@ def client_env(tmp_path: Path, git_defaults: str) -> Dict[str, str]:
pytest.skip("git is too old to emulate Git 3 defaults")
settings.update(harness.GIT_DEFAULTS[git_defaults])
harness.write_config(home / ".gitconfig", settings)
# Python resolves `~` via USERPROFILE on Windows.
return dict(harness.clean_environ(), HOME=str(home), USERPROFILE=str(home))
# Python resolves `~` via USERPROFILE on Windows. The ceiling keeps git
# from finding a repo above the test dir (e.g. with `--basetemp=./tmp`).
return dict(
harness.clean_environ(),
HOME=str(home),
USERPROFILE=str(home),
GIT_CEILING_DIRECTORIES=str(tmp_path),
)
@pytest.fixture
+40 -3
View File
@@ -161,9 +161,43 @@ def clean_environ() -> Dict[str, str]:
env["GIT_CONFIG_NOSYSTEM"] = "1"
# Tests match on English git & repo output.
env["LC_ALL"] = "C"
exec_dir = _git_exec_dir()
if exec_dir:
env["PATH"] = os.pathsep.join([exec_dir, env.get("PATH", "")])
return env
@functools.lru_cache(maxsize=None)
def _git_exec_dir() -> Optional[str]:
"""Return git's exec dir if it has a `git` binary, to put first on PATH.
`git` on PATH may be a wrapper, e.g. LUCI's, which retries the server
errors tests simulate for minutes. The real git answers `--exec-path`
(wrappers pass it through) & installs a copy of itself there, so putting
that dir first makes the harness, repo & the git that repo runs all use the
real git.
"""
env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
try:
proc = subprocess.run(
["git", "--exec-path"],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
timeout=COMMAND_TIMEOUT,
)
except OSError:
return None
exec_dir = proc.stdout.decode("utf-8").strip()
if proc.returncode or not any(
os.path.isfile(os.path.join(exec_dir, name))
for name in ("git", "git.exe")
):
return None
return exec_dir
def run_git(
*args: str,
cwd: Optional[Union[str, Path]] = None,
@@ -759,7 +793,8 @@ def _snapshot_worktree(root: Path) -> Dict[str, FileContent]:
# Deleted in the worktree but not (yet) in the index.
continue
if stat.S_ISLNK(st_mode):
files[name] = Blob(os.fsencode(os.readlink(path)), "120000")
# Python <3.8 on Windows rejects Path objects here.
files[name] = Blob(os.fsencode(os.readlink(str(path))), "120000")
elif stat.S_ISREG(st_mode):
mode = "100755" if st_mode & 0o111 else "100644"
files[name] = Blob(path.read_bytes(), mode)
@@ -853,11 +888,13 @@ class RepoClient:
timeout=COMMAND_TIMEOUT,
)
elapsed = time.monotonic() - start
# Python on Windows writes `\r\n` to pipes; normalize so tests can match
# multi-line output.
result = RepoResult(
list(args),
proc.returncode,
proc.stdout.decode("utf-8", "replace"),
proc.stderr.decode("utf-8", "replace"),
proc.stdout.decode("utf-8", "replace").replace("\r\n", "\n"),
proc.stderr.decode("utf-8", "replace").replace("\r\n", "\n"),
)
print(f"$ {cmd} # exit {result.returncode} after {elapsed:.1f}s")
if result.output: