diff --git a/script/helpers.py b/script/helpers.py index f897ab3053..199660b6a8 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1126,7 +1126,7 @@ def get_fixture_to_test_files() -> dict[str, frozenset[str]]: result.setdefault(base_name, set()).add(rel_path) # Shared fixtures are named by marker, not by a test function names = _SHARED_YAML_RE.findall(content) - if len(names) != content.count("shared_yaml("): + if len(names) != content.count("pytest.mark.shared_yaml("): # A wrapped or non-literal marker would silently drop the mapping # and CI would select no tests for that fixture raise ValueError( diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 15fecb9b62..2318dab39f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -362,7 +362,8 @@ def _prune_stale_builds(name: str, keep: Path) -> None: for stale in SHARED_BUILDS_ROOT.iterdir(): if stale == keep: continue - if not stale.name.startswith(prefix) and not _unused_since(stale, cutoff): + same_fixture = stale.name.startswith(prefix) + if not same_fixture and not _unused_since(stale, cutoff): continue try: lock_file = (stale / ".lock").open("w") @@ -376,6 +377,10 @@ def _prune_stale_builds(name: str, keep: Path) -> None: fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: continue # still in use by another run + # Re-probe under the lock: a worker freshens its dir before + # locking, so a just-claimed dir no longer looks unused + if not same_fixture and not _unused_since(stale, cutoff): + continue # rmtree tolerates races; a leftover partial tree only costs a # rebuild, since the ELF is deleted before every compile try: @@ -464,6 +469,9 @@ async def compile_esphome( # pay one full compile and later only a main.cpp (port) rebuild + relink shared_dir = _shared_build_dir(name) shared_dir.mkdir(parents=True, exist_ok=True) + # Freshen the dir before locking so a concurrent age sweep, which + # re-probes under the lock, never reaps a dir a worker just claimed + os.utime(shared_dir) if shared_dir not in _pruned_dirs: _pruned_dirs.add(shared_dir) await loop.run_in_executor(None, _prune_stale_builds, name, shared_dir) diff --git a/tests/integration/host_prefs.py b/tests/integration/host_prefs.py index c7f21d8a01..82569a5d66 100644 --- a/tests/integration/host_prefs.py +++ b/tests/integration/host_prefs.py @@ -11,13 +11,19 @@ boot (e.g. forcing safe mode) or to clear stale state between runs. from __future__ import annotations +import os from pathlib import Path import struct def host_prefs_path(device_name: str) -> Path: - """Return the on-disk prefs file path for a host-platform device.""" - return Path.home() / ".esphome" / "prefs" / f"{device_name}.prefs" + """Return the on-disk prefs file path for a host-platform device. + + Honors ESPHOME_PREFDIR, which the autouse isolated_preferences fixture + sets, so seeds land where the binary will look.""" + prefdir = os.environ.get("ESPHOME_PREFDIR") + base = Path(prefdir) if prefdir else Path.home() / ".esphome" / "prefs" + return base / f"{device_name}.prefs" def clear_host_prefs(device_name: str) -> None: diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 99a795a5c1..8f82a121c6 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -2141,11 +2141,11 @@ def test_no_orphan_integration_fixtures() -> None: helpers.get_fixture_to_test_files.cache_clear() mapping = helpers.get_fixture_to_test_files() fixtures_dir = (Path(__file__).parent.parent / "integration" / "fixtures").resolve() + fixtures = list(fixtures_dir.glob("*.yaml")) + assert fixtures, f"no fixtures found under {fixtures_dir}" # cache_init is covered via INTEGRATION_TESTS_TRIGGER_FILES instead orphans = [ - f.stem - for f in fixtures_dir.glob("*.yaml") - if f.stem != "cache_init" and f.stem not in mapping + f.stem for f in fixtures if f.stem != "cache_init" and f.stem not in mapping ] assert not orphans, f"fixtures invisible to CI test selection: {orphans}"