From fc977b7b5e53770bd8244163b3221861228b04d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2026 17:34:32 -0500 Subject: [PATCH] [ci] Cache the integration test PlatformIO dir (#18896) --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ script/determine-jobs.py | 21 +++++++++++++++++++-- tests/integration/conftest.py | 3 ++- tests/script/test_determine_jobs.py | 7 +++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a58d0fe9b..0df4da6386 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,6 +355,15 @@ jobs: fail-fast: false matrix: bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }} + env: + # What the cache steps persist; libdeps is excluded (keyed per xdist + # worker and env, it never crosses runs). + INTEGRATION_PIO_CACHE_PATH: | + ~/.esphome-integration-tests/platformio/platforms + ~/.esphome-integration-tests/platformio/packages + ~/.esphome-integration-tests/platformio/appstate.json + ~/.esphome-integration-tests/platformio/.cache + ~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -373,6 +382,14 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" + - name: Restore integration PlatformIO cache + # Native platform + toolchain installed by shared_platformio_cache in + # tests/integration/conftest.py; a miss self-heals, so no restore-keys. + id: pio-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.INTEGRATION_PIO_CACHE_PATH }} + key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -416,6 +433,13 @@ jobs: # esphome stores the PlatformIO ccache under the machine-global cache # dir (see _ccache_env() in esphome/platformio/toolchain.py). run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s + - name: Save integration PlatformIO cache + # Bucket 0 only; the others would race the same immutable key. + if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.INTEGRATION_PIO_CACHE_PATH }} + key: ${{ steps.pio-cache.outputs.cache-primary-key }} import-time: name: Check import esphome.__main__ time diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 9eead4b38c..add1af5bba 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -101,6 +101,17 @@ COMPONENT_TEST_BATCH_SIZE = 40 INTEGRATION_TESTS_SPLIT_THRESHOLD = 10 INTEGRATION_TESTS_SPLIT_BUCKETS = 3 +# platformio and aioesphomeapi (requirements.txt), the pytest stack +# (requirements_test.txt) and the fixture every session compiles; a change +# to any runs the full matrix +INTEGRATION_TESTS_TRIGGER_FILES = frozenset( + { + "requirements.txt", + "requirements_test.txt", + "tests/integration/fixtures/cache_init.yaml", + } +) + def _split_list(items: list[str], n: int) -> list[list[str]]: """Split a list into n roughly-equal contiguous parts (matches script/clang-tidy).""" @@ -221,12 +232,15 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s 3. Integration test infrastructure files changed - conftest.py, types.py, const.py, entity_utils.py, state_utils.py, etc. + 4. A file in INTEGRATION_TESTS_TRIGGER_FILES changed + - The dependency pins and the session init fixture affect every test + Returns (run_all=False, [test_files...]) when: - 4. Specific integration test files changed + 5. Specific integration test files changed - Only those specific test files are returned - 5. Components used by integration tests (or their dependencies) changed + 6. Components used by integration tests (or their dependencies) changed - Only test files whose fixtures use the changed components are returned Args: @@ -244,6 +258,9 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s # If any core files changed, run all integration tests return (True, []) + if any(f in INTEGRATION_TESTS_TRIGGER_FILES for f in files): + return (True, []) + # If infrastructure Python files changed (conftest, utils, etc.), run all tests # Excludes test files (test_*.py), fixtures, and non-Python files (README.md) if any( diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 483d5392af..12b1407fe1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -78,7 +78,8 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: @pytest.fixture(scope="session") def shared_platformio_cache() -> Generator[Path]: """Initialize a shared PlatformIO cache for all integration tests.""" - # Use a dedicated directory for integration tests to avoid conflicts + # Use a dedicated directory for integration tests to avoid conflicts. + # CI caches parts of this path; keep in sync with ci.yml integration-tests. test_cache_dir = Path.home() / ".esphome-integration-tests" cache_dir = test_cache_dir / "platformio" diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 565f8c563f..7b641e275e 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -552,6 +552,13 @@ def test_determine_integration_tests( assert run_all is True assert test_files == [] + # Dependency pins and the session init fixture trigger run_all + for trigger in sorted(determine_jobs.INTEGRATION_TESTS_TRIGGER_FILES): + with patch.object(determine_jobs, "changed_files", return_value=[trigger]): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] + # Python files directly in esphome/ do NOT trigger tests with patch.object( determine_jobs, "changed_files", return_value=["esphome/config.py"]