fix(tests): Windows-aware path-list split and UTF-8 progress output in parallel runner
Two Windows bugs in scripts/run_tests_parallel.py:
- --files/--paths/HERMES_TEST_PATHS were split on ':', which shreds
absolute Windows paths at the drive letter ('C:\repo\tests' ->
['C', '\repo\tests']): the drive letter became a phantom discovery
root and the rooted remainder only resolved by WindowsPath
re-anchoring it onto repo_root's drive. New _split_pathspec() keeps
drive-letter colons glued to their path and accepts ';' (os.pathsep)
on Windows, while ':'-joined lists (CI generate job) keep working.
- With piped stdout (CI, subprocess capture) Windows encodes the
runner's output as the ANSI code page, so printing the per-file
progress glyphs raised UnicodeEncodeError inside the executor
done-callback and every progress line was silently lost -- which is
also why test_bare_value_flag_keeps_its_value failed on win32 (no
'1[check]' line, and the summary says '1 tests passed', which does not
contain '1 passed'). The runner now reconfigures its own
stdout/stderr to UTF-8 on Windows, and the tests decode the captured
output as UTF-8.
Adds regression tests: os.pathsep-joined absolute roots (all
platforms) and no-phantom-drive-root (win32).
Fixes #57149
This commit is contained in:
parent
5945929d4b
commit
298ef06458
|
|
@ -32,7 +32,9 @@ Usage:
|
|||
|
||||
Environment:
|
||||
HERMES_TEST_WORKERS Override worker count (default: os.cpu_count())
|
||||
HERMES_TEST_PATHS Override discovery roots (colon-sep, default: 'tests')
|
||||
HERMES_TEST_PATHS Override discovery roots (colon-sep; on Windows
|
||||
';' also works and drive letters are handled;
|
||||
default: 'tests')
|
||||
|
||||
Exit code: 0 if every file's pytest exited 0; 1 otherwise.
|
||||
"""
|
||||
|
|
@ -100,6 +102,41 @@ _DEFAULT_FILE_RETRIES = 1
|
|||
_DURATIONS_FILE = "test_durations.json"
|
||||
|
||||
|
||||
def _split_pathspec(value: str) -> List[str]:
|
||||
"""Split a separator-joined path list (``--paths``/``--files``/
|
||||
``HERMES_TEST_PATHS``) into individual paths.
|
||||
|
||||
POSIX: ``:``-separated, as documented.
|
||||
|
||||
Windows: ``;`` (``os.pathsep``) and ``:`` are both accepted as
|
||||
separators, but a ``:`` that forms a drive letter (``C:\\...`` or
|
||||
``C:/...``) stays glued to its path — a naive ``split(":")`` turns
|
||||
``C:\\repo\\tests`` into ``['C', '\\repo\\tests']``, where the bogus
|
||||
``C`` becomes a phantom discovery root and the rooted remainder only
|
||||
resolves by accident of ``Path.__truediv__`` re-anchoring it onto
|
||||
``repo_root``'s drive.
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return [p for p in value.split(":") if p.strip()]
|
||||
parts: List[str] = []
|
||||
for chunk in value.split(";"):
|
||||
raw = chunk.split(":")
|
||||
i = 0
|
||||
while i < len(raw):
|
||||
part = raw[i]
|
||||
if (
|
||||
len(part) == 1
|
||||
and part.isalpha()
|
||||
and i + 1 < len(raw)
|
||||
and raw[i + 1][:1] in ("\\", "/")
|
||||
):
|
||||
part = f"{part}:{raw[i + 1]}"
|
||||
i += 1
|
||||
parts.append(part)
|
||||
i += 1
|
||||
return [p for p in parts if p.strip()]
|
||||
|
||||
|
||||
def _approximately_count_tests(
|
||||
files: List[Path], repo_root: Path
|
||||
) -> dict[Path, int]:
|
||||
|
|
@ -689,7 +726,11 @@ def main() -> int:
|
|||
parser.add_argument(
|
||||
"--paths",
|
||||
default=os.environ.get("HERMES_TEST_PATHS", ":".join(_DEFAULT_ROOTS)),
|
||||
help="Colon-separated discovery roots (default: 'tests')",
|
||||
help=(
|
||||
"Colon-separated discovery roots (default: 'tests'). On "
|
||||
"Windows, ';' also separates and drive letters (C:\\...) are "
|
||||
"kept intact."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-integration",
|
||||
|
|
@ -748,9 +789,10 @@ def main() -> int:
|
|||
"--files",
|
||||
metavar="LIST",
|
||||
help=(
|
||||
"Explicit colon-separated list of test files to run. Bypasses "
|
||||
"discovery entirely — used by CI matrix jobs that receive their "
|
||||
"file list from the generate job."
|
||||
"Explicit colon-separated list of test files to run (on "
|
||||
"Windows, ';' also separates and drive letters are kept "
|
||||
"intact). Bypasses discovery entirely — used by CI matrix "
|
||||
"jobs that receive their file list from the generate job."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
|
|
@ -885,7 +927,7 @@ def main() -> int:
|
|||
|
||||
# --files: explicit file list from the CI generate job — skip discovery.
|
||||
if args.files:
|
||||
files = [repo_root / f for f in args.files.split(":") if f.strip()]
|
||||
files = [repo_root / f for f in _split_pathspec(args.files)]
|
||||
roots = []
|
||||
else:
|
||||
# Resolve discovery roots: positional path args override --paths if any
|
||||
|
|
@ -893,7 +935,7 @@ def main() -> int:
|
|||
if args.paths_positional:
|
||||
roots = [repo_root / p for p in args.paths_positional]
|
||||
else:
|
||||
roots = [repo_root / p for p in args.paths.split(":") if p]
|
||||
roots = [repo_root / p for p in _split_pathspec(args.paths)]
|
||||
|
||||
if args.include_integration:
|
||||
# Caller takes responsibility — typically used via explicit -k filter.
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ def test_positional_path_not_treated_as_flag(tmp_path: Path) -> None:
|
|||
[sys.executable, str(runner), str(probe_dir), "-j", "1",
|
||||
"--file-timeout", "30", "-q"],
|
||||
cwd=repo_root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, timeout=60,
|
||||
encoding="utf-8", errors="replace", timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
# Discovery found the probe file (2 tests), proving the positional path
|
||||
|
|
@ -378,3 +378,48 @@ def test_explicit_k_wins_over_node_id_inference(tmp_path: Path) -> None:
|
|||
# -k test_beta wins: one test ran, and it wasn't filtered to nothing.
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
assert "1 tests passed" in proc.stdout
|
||||
|
||||
|
||||
def test_multiple_absolute_paths_split_on_pathsep(tmp_path: Path) -> None:
|
||||
"""``--paths`` accepts ``os.pathsep``-joined absolute paths.
|
||||
|
||||
On Windows the absolute paths contain drive-letter colons, so a naive
|
||||
``split(":")`` shreds them into phantom roots and only one (or neither)
|
||||
of the two probe dirs would be discovered.
|
||||
"""
|
||||
dir_a = _make_probe_dir(tmp_path)
|
||||
dir_b = tmp_path / "probe_b"
|
||||
dir_b.mkdir()
|
||||
(dir_b / "test_flagprobe_b.py").write_text(
|
||||
"def test_gamma():\n assert True\n"
|
||||
)
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
runner = repo_root / "scripts" / "run_tests_parallel.py"
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(runner),
|
||||
"--paths", os.pathsep.join([str(dir_a), str(dir_b)]),
|
||||
"-j", "1", "--file-timeout", "30", "-q"],
|
||||
cwd=repo_root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
encoding="utf-8", errors="replace", timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
assert "Discovered 2 test files" in proc.stdout, proc.stdout
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="drive-letter paths")
|
||||
def test_drive_letter_colon_is_not_a_path_separator(tmp_path: Path) -> None:
|
||||
"""An absolute ``--paths`` value stays one root on Windows.
|
||||
|
||||
The naive split used to produce a phantom relative root ``'C'`` (the
|
||||
drive letter) alongside the real path; discovery only worked by the
|
||||
accident of ``repo_root / '\\rooted\\rest'`` re-anchoring onto the
|
||||
repo's drive.
|
||||
"""
|
||||
probe_dir = _make_probe_dir(tmp_path)
|
||||
proc = _run_runner(probe_dir, "-q")
|
||||
assert proc.returncode == 0, proc.stdout
|
||||
drive = str(probe_dir)[0]
|
||||
assert f"['{drive}', " not in proc.stdout, (
|
||||
f"drive letter split off as a phantom root:\n{proc.stdout}"
|
||||
)
|
||||
assert "Discovered 1 test files" in proc.stdout, proc.stdout
|
||||
|
|
|
|||
Loading…
Reference in New Issue