1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-13 07:13:47 +01:00

[ci] Filter out components without tests from CI test jobs (#11134 followup) (#11178)

This commit is contained in:
J. Nick Koston
2025-10-11 13:27:18 -10:00
committed by GitHub
parent dcf2697a2a
commit 3afa73b449
3 changed files with 89 additions and 5 deletions

View File

@@ -177,6 +177,7 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@@ -204,6 +205,7 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests: integration-tests:
@@ -367,7 +369,7 @@ jobs:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
matrix: matrix:
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }} file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
steps: steps:
- name: Cache apt packages - name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3 uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
@@ -414,7 +416,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
# Use intelligent splitter that groups components with same bus configs # Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components }}' components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
echo "Splitting components intelligently..." echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github) output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)

View File

@@ -237,6 +237,16 @@ def main() -> None:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) result = subprocess.run(cmd, capture_output=True, text=True, check=True)
changed_components = parse_list_components_output(result.stdout) changed_components = parse_list_components_output(result.stdout)
# Filter to only components that have test files
# Components without tests shouldn't generate CI test jobs
tests_dir = Path(root_path) / "tests" / "components"
changed_components_with_tests = [
component
for component in changed_components
if (component_test_dir := tests_dir / component).exists()
and any(component_test_dir.glob("test.*.yaml"))
]
# Build output # Build output
output: dict[str, Any] = { output: dict[str, Any] = {
"integration_tests": run_integration, "integration_tests": run_integration,
@@ -244,7 +254,8 @@ def main() -> None:
"clang_format": run_clang_format, "clang_format": run_clang_format,
"python_linters": run_python_linters, "python_linters": run_python_linters,
"changed_components": changed_components, "changed_components": changed_components,
"component_test_count": len(changed_components), "changed_components_with_tests": changed_components_with_tests,
"component_test_count": len(changed_components_with_tests),
} }
# Output as JSON # Output as JSON

View File

@@ -4,6 +4,7 @@ from collections.abc import Generator
import importlib.util import importlib.util
import json import json
import os import os
from pathlib import Path
import subprocess import subprocess
import sys import sys
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
@@ -90,7 +91,13 @@ def test_main_all_tests_should_run(
assert output["clang_format"] is True assert output["clang_format"] is True
assert output["python_linters"] is True assert output["python_linters"] is True
assert output["changed_components"] == ["wifi", "api", "sensor"] assert output["changed_components"] == ["wifi", "api", "sensor"]
assert output["component_test_count"] == 3 # changed_components_with_tests will only include components that actually have test files
assert "changed_components_with_tests" in output
assert isinstance(output["changed_components_with_tests"], list)
# component_test_count matches number of components with tests
assert output["component_test_count"] == len(
output["changed_components_with_tests"]
)
def test_main_no_tests_should_run( def test_main_no_tests_should_run(
@@ -125,6 +132,7 @@ def test_main_no_tests_should_run(
assert output["clang_format"] is False assert output["clang_format"] is False
assert output["python_linters"] is False assert output["python_linters"] is False
assert output["changed_components"] == [] assert output["changed_components"] == []
assert output["changed_components_with_tests"] == []
assert output["component_test_count"] == 0 assert output["component_test_count"] == 0
@@ -197,7 +205,13 @@ def test_main_with_branch_argument(
assert output["clang_format"] is False assert output["clang_format"] is False
assert output["python_linters"] is True assert output["python_linters"] is True
assert output["changed_components"] == ["mqtt"] assert output["changed_components"] == ["mqtt"]
assert output["component_test_count"] == 1 # changed_components_with_tests will only include components that actually have test files
assert "changed_components_with_tests" in output
assert isinstance(output["changed_components_with_tests"], list)
# component_test_count matches number of components with tests
assert output["component_test_count"] == len(
output["changed_components_with_tests"]
)
def test_should_run_integration_tests( def test_should_run_integration_tests(
@@ -377,3 +391,60 @@ def test_should_run_clang_format_with_branch() -> None:
mock_changed.return_value = [] mock_changed.return_value = []
determine_jobs.should_run_clang_format("release") determine_jobs.should_run_clang_format("release")
mock_changed.assert_called_once_with("release") mock_changed.assert_called_once_with("release")
def test_main_filters_components_without_tests(
mock_should_run_integration_tests: Mock,
mock_should_run_clang_tidy: Mock,
mock_should_run_clang_format: Mock,
mock_should_run_python_linters: Mock,
mock_subprocess_run: Mock,
capsys: pytest.CaptureFixture[str],
tmp_path: Path,
) -> None:
"""Test that components without test files are filtered out."""
mock_should_run_integration_tests.return_value = False
mock_should_run_clang_tidy.return_value = False
mock_should_run_clang_format.return_value = False
mock_should_run_python_linters.return_value = False
# Mock list-components.py output with 3 components
# wifi: has tests, sensor: has tests, airthings_ble: no tests
mock_result = Mock()
mock_result.stdout = "wifi\nsensor\nairthings_ble\n"
mock_subprocess_run.return_value = mock_result
# Create test directory structure
tests_dir = tmp_path / "tests" / "components"
# wifi has tests
wifi_dir = tests_dir / "wifi"
wifi_dir.mkdir(parents=True)
(wifi_dir / "test.esp32.yaml").write_text("test: config")
# sensor has tests
sensor_dir = tests_dir / "sensor"
sensor_dir.mkdir(parents=True)
(sensor_dir / "test.esp8266.yaml").write_text("test: config")
# airthings_ble exists but has no test files
airthings_dir = tests_dir / "airthings_ble"
airthings_dir.mkdir(parents=True)
# Mock root_path to use tmp_path
with (
patch.object(determine_jobs, "root_path", str(tmp_path)),
patch("sys.argv", ["determine-jobs.py"]),
):
determine_jobs.main()
# Check output
captured = capsys.readouterr()
output = json.loads(captured.out)
# changed_components should have all components
assert set(output["changed_components"]) == {"wifi", "sensor", "airthings_ble"}
# changed_components_with_tests should only have components with test files
assert set(output["changed_components_with_tests"]) == {"wifi", "sensor"}
# component_test_count should be based on components with tests
assert output["component_test_count"] == 2