From db9d6a79952e9068b8f59074c5e8a533f15a4e80 Mon Sep 17 00:00:00 2001 From: linhongkuan Date: Wed, 24 Jun 2026 21:25:18 +0800 Subject: [PATCH] fix(testing): keep duration cache repo-relative --- scripts/run_tests_parallel.py | 28 +++++++++++++++++++--- tests/test_run_tests_parallel.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/scripts/run_tests_parallel.py b/scripts/run_tests_parallel.py index a0f6ec21de4de..3b7e19271b16f 100755 --- a/scripts/run_tests_parallel.py +++ b/scripts/run_tests_parallel.py @@ -45,7 +45,7 @@ import threading import time from concurrent.futures import ThreadPoolExecutor, Future -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Dict, List, Tuple @@ -388,6 +388,22 @@ def _format_file(file: Path, repo_root: Path) -> str: return str(file) +def _duration_cache_key(file: Path, repo_root: Path) -> str | None: + """Return a portable duration-cache key for repo-local files only.""" + try: + return str(file.resolve().relative_to(repo_root.resolve())) + except ValueError: + return None + + +def _is_portable_duration_key(key: str) -> bool: + """Duration caches are portable only when keys are repo-relative.""" + return not ( + PurePosixPath(key).is_absolute() + or PureWindowsPath(key).is_absolute() + ) + + def _print_progress( tests_done: int, total_tests: int, @@ -519,9 +535,15 @@ def _save_durations( repo-relative paths so the cache is portable across checkouts and CI runners. """ - data: dict[str, float] = _load_durations(repo_root) + data: dict[str, float] = { + key: value + for key, value in _load_durations(repo_root).items() + if _is_portable_duration_key(key) + } for f, t in file_times: - key = _format_file(f, repo_root) + key = _duration_cache_key(f, repo_root) + if key is None: + continue data[key] = round(t, 3) path = repo_root / _DURATIONS_FILE path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") diff --git a/tests/test_run_tests_parallel.py b/tests/test_run_tests_parallel.py index 743ba7921890a..5950261c8e6f3 100644 --- a/tests/test_run_tests_parallel.py +++ b/tests/test_run_tests_parallel.py @@ -63,6 +63,46 @@ def _pid_alive(pid: int) -> bool: return True +def test_duration_cache_skips_paths_outside_repo(tmp_path: Path) -> None: + from scripts.run_tests_parallel import _DURATIONS_FILE, _save_durations + + repo_root = tmp_path / "repo" + repo_file = repo_root / "tests" / "test_repo.py" + outside_file = tmp_path / "outside" / "test_external.py" + repo_file.parent.mkdir(parents=True) + outside_file.parent.mkdir(parents=True) + repo_file.write_text("def test_repo():\n assert True\n", encoding="utf-8") + outside_file.write_text("def test_external():\n assert True\n", encoding="utf-8") + + _save_durations([(repo_file, 1.234), (outside_file, 9.876)], repo_root) + + data = json.loads((repo_root / _DURATIONS_FILE).read_text(encoding="utf-8")) + assert data == {str(Path("tests") / "test_repo.py"): 1.234} + + +def test_duration_cache_drops_stale_absolute_keys(tmp_path: Path) -> None: + from scripts.run_tests_parallel import _DURATIONS_FILE, _save_durations + + repo_root = tmp_path / "repo" + repo_root.mkdir() + cache = repo_root / _DURATIONS_FILE + cache.write_text( + json.dumps( + { + "/tmp/hermes/test_external.py": 1.0, + r"C:\Users\runner\AppData\Local\Temp\test_external.py": 2.0, + "tests/test_existing.py": 3.0, + } + ), + encoding="utf-8", + ) + + _save_durations([], repo_root) + + data = json.loads(cache.read_text(encoding="utf-8")) + assert data == {"tests/test_existing.py": 3.0} + + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only probe") @pytest.mark.live_system_guard_bypass def test_grandchild_leak_is_killed_by_runner(tmp_path: Path) -> None: