Address review: survive peer reap before lock, alias tolerant marker scan, document shared_yaml, visible edge warnings

This commit is contained in:
J. Nick Koston
2026-09-03 21:40:52 +02:00
parent 36468022ea
commit 5494190d97
3 changed files with 22 additions and 5 deletions
+2 -2
View File
@@ -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
+7
View File
@@ -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
+13 -3
View File
@@ -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: