Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions scripts/run_tests_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
40 changes: 40 additions & 0 deletions tests/test_run_tests_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down