From 5494190d979c3f2fc28f7c5c1ebec39fc467c7bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2026 21:40:52 +0200 Subject: [PATCH] Address review: survive peer reap before lock, alias tolerant marker scan, document shared_yaml, visible edge warnings --- script/helpers.py | 4 ++-- tests/integration/README.md | 7 +++++++ tests/integration/conftest.py | 16 +++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index 2ac59c71b0..6b8267635b 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1104,8 +1104,8 @@ def get_components_per_integration_fixture() -> dict[str, set[str]]: _TEST_FUNC_RE = re.compile(r"async def (test_\w+)") -_SHARED_YAML_RE = re.compile(r"pytest\.mark\.shared_yaml\([\"'](\w+)[\"']\)") -_SHARED_YAML_USE_RE = re.compile(r"pytest\.mark\.shared_yaml\(") +_SHARED_YAML_RE = re.compile(r"mark\.shared_yaml\([\"'](\w+)[\"']\)") +_SHARED_YAML_USE_RE = re.compile(r"\bmark\.shared_yaml\b") @cache diff --git a/tests/integration/README.md b/tests/integration/README.md index 790d9a3a11..bee20409e8 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -21,6 +21,13 @@ The `yaml_config` fixture automatically loads YAML configurations based on the t - The fixture file must exist or the test will fail with a clear error message - The fixture automatically injects a dynamic port number into the API configuration +Tests marked `@pytest.mark.shared_yaml("name")` load `fixtures/name.yaml` instead +of the test-named file and compile it in a shared, hash-keyed build directory, so +the whole group pays one full compile and each test only a relink. The marker +argument must be a single-line string literal (CI test selection maps fixtures to +test files by scanning for it), and marked tests must hand the `yaml_config` +content to `run_compiled` unmodified. + ### Key Fixtures - `run_compiled` - Combines write, compile, and run operations into a single context manager diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index fe50ae391e..8df2222991 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -339,6 +339,7 @@ def _read_stamp(stamp: Path, shared_dir: Path) -> Path | None: warnings.warn(f"Cannot read {stamp}: {err}", stacklevel=2) return None if not text: + warnings.warn(f"Ignoring empty stamp {stamp}", stacklevel=2) return None built = Path(text) # Never trust a stamp pointing outside its own build dir as an unlink target @@ -361,8 +362,9 @@ def _unused_since(stale: Path, cutoff: float) -> bool: except NotADirectoryError: return True # a stray file where a dir should be; reclaimable except OSError as err: + # Treat as reclaimable; the prune attempt will surface the error warnings.warn(f"Cannot age-probe {stale}: {err}", stacklevel=2) - return False + return True newest = mtime if newest is None else max(newest, mtime) return newest is not None and newest < cutoff @@ -387,7 +389,8 @@ def _prune_stale_builds(name: str, keep: Path) -> None: except FileNotFoundError: continue # pruned by another worker meanwhile except NotADirectoryError: - stale.unlink(missing_ok=True) # a stray file, not a build dir + warnings.warn(f"Removing stray file {stale}", stacklevel=2) + stale.unlink(missing_ok=True) continue except OSError as err: warnings.warn(f"Cannot prune {stale}: {err}", stacklevel=2) @@ -514,7 +517,14 @@ async def compile_esphome( # Hand-rolled rather than filelock.FileLock: non-blocking retries keep # the wait cancellable, while a blocking acquire in an executor thread # would survive test cancellation holding the fd - with (shared_dir / ".lock").open("w") as lock_file: + try: + lock_file = (shared_dir / ".lock").open("w") + except FileNotFoundError: + # A peer run pruning divergent hashes reaped the dir between our + # mkdir and this open; recreate it and pay a full rebuild + shared_dir.mkdir(parents=True, exist_ok=True) + lock_file = (shared_dir / ".lock").open("w") + with lock_file: start = time.monotonic() last_report = start while True: