mirror of
https://github.com/esphome/esphome.git
synced 2026-05-27 11:56:11 +08:00
[ci] Only run integration tests for changed components (#14776)
This commit is contained in:
@@ -170,6 +170,8 @@ jobs:
|
|||||||
- common
|
- common
|
||||||
outputs:
|
outputs:
|
||||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||||
|
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||||
|
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
@@ -210,6 +212,8 @@ jobs:
|
|||||||
|
|
||||||
# Extract individual fields
|
# Extract individual fields
|
||||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||||
|
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||||
|
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||||
@@ -261,9 +265,20 @@ jobs:
|
|||||||
- name: Register matcher
|
- name: Register matcher
|
||||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
|
env:
|
||||||
|
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||||
|
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||||
|
echo "Running all integration tests"
|
||||||
|
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||||
|
else
|
||||||
|
# Parse JSON array into bash array to avoid shell expansion issues
|
||||||
|
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||||
|
echo "Running ${#test_files[@]} specific integration tests"
|
||||||
|
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
cpp-unit-tests:
|
cpp-unit-tests:
|
||||||
name: Run C++ unit tests
|
name: Run C++ unit tests
|
||||||
|
|||||||
+67
-37
@@ -6,6 +6,8 @@ what files have changed. It outputs JSON with the following structure:
|
|||||||
|
|
||||||
{
|
{
|
||||||
"integration_tests": true/false,
|
"integration_tests": true/false,
|
||||||
|
"integration_tests_run_all": true/false,
|
||||||
|
"integration_test_files": ["tests/integration/test_foo.py", ...],
|
||||||
"clang_tidy": true/false,
|
"clang_tidy": true/false,
|
||||||
"clang_format": true/false,
|
"clang_format": true/false,
|
||||||
"python_linters": true/false,
|
"python_linters": true/false,
|
||||||
@@ -56,13 +58,13 @@ from helpers import (
|
|||||||
core_changed,
|
core_changed,
|
||||||
filter_component_and_test_cpp_files,
|
filter_component_and_test_cpp_files,
|
||||||
filter_component_and_test_files,
|
filter_component_and_test_files,
|
||||||
get_all_dependencies,
|
|
||||||
get_changed_components,
|
get_changed_components,
|
||||||
get_component_from_path,
|
get_component_from_path,
|
||||||
get_component_test_files,
|
get_component_test_files,
|
||||||
get_components_from_integration_fixtures,
|
|
||||||
get_components_with_dependencies,
|
get_components_with_dependencies,
|
||||||
get_cpp_changed_components,
|
get_cpp_changed_components,
|
||||||
|
get_fixture_to_test_files,
|
||||||
|
get_integration_test_files_for_components,
|
||||||
get_target_branch,
|
get_target_branch,
|
||||||
git_ls_files,
|
git_ls_files,
|
||||||
parse_test_filename,
|
parse_test_filename,
|
||||||
@@ -143,65 +145,88 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def should_run_integration_tests(branch: str | None = None) -> bool:
|
def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[str]]:
|
||||||
"""Determine if integration tests should run based on changed files.
|
"""Determine which integration tests should run based on changed files.
|
||||||
|
|
||||||
This function is used by the CI workflow to intelligently skip integration tests when they're
|
This function is used by the CI workflow to intelligently skip or filter
|
||||||
not needed, saving significant CI time and resources.
|
integration tests, saving significant CI time and resources.
|
||||||
|
|
||||||
Integration tests will run when ANY of the following conditions are met:
|
Returns (run_all=True, []) when ANY of the following conditions are met:
|
||||||
|
|
||||||
1. Core C++ files changed (esphome/core/*)
|
1. Core C++ files changed (esphome/core/*)
|
||||||
- Any .cpp, .h, .tcc files in the core directory
|
- Any .cpp, .h, .tcc files in the core directory
|
||||||
- These files contain fundamental functionality used throughout ESPHome
|
- These files contain fundamental functionality used throughout ESPHome
|
||||||
- Examples: esphome/core/component.cpp, esphome/core/application.h
|
|
||||||
|
|
||||||
2. Core Python files changed (esphome/core/*.py)
|
2. Core Python files changed (esphome/core/*.py)
|
||||||
- Only .py files in the esphome/core/ directory
|
- Only .py files in the esphome/core/ directory
|
||||||
- These are core Python files that affect the entire system
|
- These are core Python files that affect the entire system
|
||||||
- Examples: esphome/core/config.py, esphome/core/__init__.py
|
|
||||||
- NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py
|
|
||||||
|
|
||||||
3. Integration test files changed
|
3. Integration test infrastructure files changed
|
||||||
- Any file in tests/integration/ directory
|
- conftest.py, types.py, const.py, entity_utils.py, state_utils.py, etc.
|
||||||
- This includes test files themselves and fixture YAML files
|
|
||||||
- Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml
|
|
||||||
|
|
||||||
4. Components used by integration tests (or their dependencies) changed
|
Returns (run_all=False, [test_files...]) when:
|
||||||
- The function parses all YAML files in tests/integration/fixtures/
|
|
||||||
- Extracts which components are used in integration tests
|
4. Specific integration test files changed
|
||||||
- Recursively finds all dependencies of those components
|
- Only those specific test files are returned
|
||||||
- If any of these components have changes, tests must run
|
|
||||||
- Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket',
|
5. Components used by integration tests (or their dependencies) changed
|
||||||
then changes to sensor/, api/, or socket/ components trigger tests
|
- Only test files whose fixtures use the changed components are returned
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
branch: Branch to compare against. If None, uses default.
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if integration tests should run, False otherwise.
|
Tuple of (run_all, test_files) where:
|
||||||
|
- run_all: True if all integration tests should run
|
||||||
|
- test_files: List of specific test file paths to run (empty if run_all
|
||||||
|
is True, or if no tests need to run)
|
||||||
"""
|
"""
|
||||||
files = changed_files(branch)
|
files = changed_files(branch)
|
||||||
|
|
||||||
if core_changed(files):
|
if core_changed(files):
|
||||||
# If any core files changed, run integration tests
|
# If any core files changed, run all integration tests
|
||||||
return True
|
return (True, [])
|
||||||
|
|
||||||
# Check if any integration test files changed
|
# If infrastructure Python files changed (conftest, utils, etc.), run all tests
|
||||||
if any("tests/integration" in file for file in files):
|
# Excludes test files (test_*.py), fixtures, and non-Python files (README.md)
|
||||||
return True
|
if any(
|
||||||
|
f.startswith("tests/integration/")
|
||||||
|
and f.endswith(".py")
|
||||||
|
and not f.startswith("tests/integration/test_")
|
||||||
|
and "/fixtures/" not in f
|
||||||
|
for f in files
|
||||||
|
):
|
||||||
|
return (True, [])
|
||||||
|
|
||||||
# Get all components used in integration tests and their dependencies
|
# Collect specific test files that need to run
|
||||||
fixture_components = get_components_from_integration_fixtures()
|
test_files: set[str] = set()
|
||||||
all_required_components = get_all_dependencies(fixture_components)
|
fixture_to_test_files = get_fixture_to_test_files()
|
||||||
|
|
||||||
# Check if any required components changed
|
for f in files:
|
||||||
for file in files:
|
if f.startswith("tests/integration/test_") and f.endswith(".py"):
|
||||||
component = get_component_from_path(file)
|
test_files.add(f)
|
||||||
if component and component in all_required_components:
|
elif f.startswith("tests/integration/fixtures/"):
|
||||||
return True
|
if f.endswith(".yaml"):
|
||||||
|
# Fixture YAML changed - add corresponding test file(s)
|
||||||
|
test_files.update(fixture_to_test_files.get(Path(f).stem, ()))
|
||||||
|
else:
|
||||||
|
# Non-YAML fixture file changed (e.g., external_components/)
|
||||||
|
# Run all tests since we can't determine which tests are affected
|
||||||
|
return (True, [])
|
||||||
|
|
||||||
return False
|
# Find test files whose fixtures use any of the changed components
|
||||||
|
changed_component_set = {
|
||||||
|
component for file in files if (component := get_component_from_path(file))
|
||||||
|
}
|
||||||
|
if changed_component_set:
|
||||||
|
test_files.update(
|
||||||
|
get_integration_test_files_for_components(changed_component_set)
|
||||||
|
)
|
||||||
|
|
||||||
|
if test_files:
|
||||||
|
return (False, sorted(test_files))
|
||||||
|
|
||||||
|
return (False, [])
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
@@ -682,7 +707,10 @@ def main() -> None:
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Determine what should run
|
# Determine what should run
|
||||||
run_integration = should_run_integration_tests(args.branch)
|
integration_run_all, integration_test_files = determine_integration_tests(
|
||||||
|
args.branch
|
||||||
|
)
|
||||||
|
run_integration = integration_run_all or bool(integration_test_files)
|
||||||
run_clang_tidy = should_run_clang_tidy(args.branch)
|
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||||
run_clang_format = should_run_clang_format(args.branch)
|
run_clang_format = should_run_clang_format(args.branch)
|
||||||
run_python_linters = should_run_python_linters(args.branch)
|
run_python_linters = should_run_python_linters(args.branch)
|
||||||
@@ -810,6 +838,8 @@ def main() -> None:
|
|||||||
|
|
||||||
output: dict[str, Any] = {
|
output: dict[str, Any] = {
|
||||||
"integration_tests": run_integration,
|
"integration_tests": run_integration,
|
||||||
|
"integration_tests_run_all": integration_run_all,
|
||||||
|
"integration_test_files": integration_test_files,
|
||||||
"clang_tidy": run_clang_tidy,
|
"clang_tidy": run_clang_tidy,
|
||||||
"clang_tidy_mode": clang_tidy_mode,
|
"clang_tidy_mode": clang_tidy_mode,
|
||||||
"clang_format": run_clang_format,
|
"clang_format": run_clang_format,
|
||||||
|
|||||||
+118
-14
@@ -700,37 +700,141 @@ def get_all_dependencies(
|
|||||||
return all_components
|
return all_components
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_components_from_yaml(config: dict) -> set[str]:
|
||||||
|
"""Extract component names from a parsed YAML config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Parsed YAML configuration dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Set of component names found in the config
|
||||||
|
"""
|
||||||
|
components: set[str] = set()
|
||||||
|
|
||||||
|
# Add all top-level component keys (skip YAML anchor keys starting with '.')
|
||||||
|
components.update(k for k in config if isinstance(k, str) and not k.startswith("."))
|
||||||
|
|
||||||
|
# Add platform values from list entries (e.g., sensor -> platform: template adds "template")
|
||||||
|
for value in config.values():
|
||||||
|
if isinstance(value, list):
|
||||||
|
components.update(
|
||||||
|
item["platform"]
|
||||||
|
for item in value
|
||||||
|
if isinstance(item, dict) and "platform" in item
|
||||||
|
)
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_integration_fixtures() -> set[str]:
|
def get_components_from_integration_fixtures() -> set[str]:
|
||||||
"""Extract all components used in integration test fixtures.
|
"""Extract all components used in integration test fixtures.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Set of component names used in integration test fixtures
|
Set of component names used in integration test fixtures
|
||||||
"""
|
"""
|
||||||
|
return {
|
||||||
|
comp
|
||||||
|
for components in get_components_per_integration_fixture().values()
|
||||||
|
for comp in components
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_components_per_integration_fixture() -> dict[str, set[str]]:
|
||||||
|
"""Extract components used in each integration test fixture.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping fixture name (stem) to set of component names
|
||||||
|
"""
|
||||||
from esphome import yaml_util
|
from esphome import yaml_util
|
||||||
|
|
||||||
components: set[str] = set()
|
result: dict[str, set[str]] = {}
|
||||||
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
|
fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures"
|
||||||
|
|
||||||
for yaml_file in fixtures_dir.glob("*.yaml"):
|
for yaml_file in fixtures_dir.glob("*.yaml"):
|
||||||
config: dict[str, any] | None = yaml_util.load_yaml(yaml_file)
|
config: dict[str, Any] | None = yaml_util.load_yaml(yaml_file)
|
||||||
if not config:
|
if not config:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add all top-level component keys (skip YAML anchor keys starting with '.')
|
result[yaml_file.stem] = _extract_components_from_yaml(config)
|
||||||
components.update(
|
|
||||||
k for k in config if isinstance(k, str) and not k.startswith(".")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add platform components (e.g., output.template)
|
return result
|
||||||
for value in config.values():
|
|
||||||
if not isinstance(value, list):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for item in value:
|
|
||||||
if isinstance(item, dict) and "platform" in item:
|
|
||||||
components.add(item["platform"])
|
|
||||||
|
|
||||||
return components
|
_TEST_FUNC_RE = re.compile(r"async def (test_\w+)")
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get_fixture_to_test_files() -> dict[str, frozenset[str]]:
|
||||||
|
"""Map integration test fixture names to the test files that use them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping fixture name to frozenset of test file paths
|
||||||
|
(relative to repo root)
|
||||||
|
"""
|
||||||
|
integration_dir = Path(__file__).parent.parent / "tests" / "integration"
|
||||||
|
result: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
for test_file in integration_dir.glob("test_*.py"):
|
||||||
|
content = test_file.read_text(encoding="utf-8")
|
||||||
|
rel_path = test_file.relative_to(Path(__file__).parent.parent).as_posix()
|
||||||
|
for func in _TEST_FUNC_RE.findall(content):
|
||||||
|
base_name = func.replace("test_", "").partition("[")[0]
|
||||||
|
result.setdefault(base_name, set()).add(rel_path)
|
||||||
|
|
||||||
|
return {k: frozenset(v) for k, v in result.items()}
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _get_component_to_integration_test_files() -> dict[str, frozenset[str]]:
|
||||||
|
"""Build index mapping each component to the test files that depend on it.
|
||||||
|
|
||||||
|
Resolves full dependency trees once per fixture, then inverts the mapping
|
||||||
|
so lookups are O(1) per component.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping component name to frozenset of test file paths
|
||||||
|
"""
|
||||||
|
fixture_components = get_components_per_integration_fixture()
|
||||||
|
fixture_to_test_files = get_fixture_to_test_files()
|
||||||
|
|
||||||
|
result: dict[str, set[str]] = {}
|
||||||
|
for fixture_name, components in fixture_components.items():
|
||||||
|
test_files = fixture_to_test_files.get(fixture_name)
|
||||||
|
if not test_files:
|
||||||
|
continue
|
||||||
|
# Get full dependency tree for this fixture's components
|
||||||
|
all_deps = get_all_dependencies(components)
|
||||||
|
for dep in all_deps:
|
||||||
|
result.setdefault(dep, set()).update(test_files)
|
||||||
|
|
||||||
|
return {k: frozenset(v) for k, v in result.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def get_integration_test_files_for_components(
|
||||||
|
changed_components: set[str],
|
||||||
|
) -> list[str]:
|
||||||
|
"""Get integration test file paths that use any of the given components.
|
||||||
|
|
||||||
|
Uses a precomputed component → test files index for O(C) lookup
|
||||||
|
where C is the number of changed components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
changed_components: Set of component names that have changed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sorted list of test file paths relative to repo root
|
||||||
|
(e.g., ["tests/integration/test_api.py", ...])
|
||||||
|
"""
|
||||||
|
component_to_tests = _get_component_to_integration_test_files()
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
{
|
||||||
|
test_file
|
||||||
|
for component in changed_components
|
||||||
|
for test_file in component_to_tests.get(component, ())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def filter_component_and_test_files(file_path: str) -> bool:
|
def filter_component_and_test_files(file_path: str) -> bool:
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ spec.loader.exec_module(determine_jobs)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_should_run_integration_tests() -> Generator[Mock, None, None]:
|
def mock_determine_integration_tests() -> Generator[Mock, None, None]:
|
||||||
"""Mock should_run_integration_tests from helpers."""
|
"""Mock determine_integration_tests."""
|
||||||
with patch.object(determine_jobs, "should_run_integration_tests") as mock:
|
with patch.object(determine_jobs, "determine_integration_tests") as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ def clear_determine_jobs_caches() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_main_all_tests_should_run(
|
def test_main_all_tests_should_run(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -100,7 +100,7 @@ def test_main_all_tests_should_run(
|
|||||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = True
|
mock_determine_integration_tests.return_value = (True, [])
|
||||||
mock_should_run_clang_tidy.return_value = True
|
mock_should_run_clang_tidy.return_value = True
|
||||||
mock_should_run_clang_format.return_value = True
|
mock_should_run_clang_format.return_value = True
|
||||||
mock_should_run_python_linters.return_value = True
|
mock_should_run_python_linters.return_value = True
|
||||||
@@ -152,6 +152,8 @@ def test_main_all_tests_should_run(
|
|||||||
output = json.loads(captured.out)
|
output = json.loads(captured.out)
|
||||||
|
|
||||||
assert output["integration_tests"] is True
|
assert output["integration_tests"] is True
|
||||||
|
assert output["integration_tests_run_all"] is True
|
||||||
|
assert output["integration_test_files"] == []
|
||||||
assert output["clang_tidy"] is True
|
assert output["clang_tidy"] is True
|
||||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||||
assert output["clang_format"] is True
|
assert output["clang_format"] is True
|
||||||
@@ -183,7 +185,7 @@ def test_main_all_tests_should_run(
|
|||||||
|
|
||||||
|
|
||||||
def test_main_no_tests_should_run(
|
def test_main_no_tests_should_run(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -196,7 +198,7 @@ def test_main_no_tests_should_run(
|
|||||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = False
|
mock_should_run_clang_tidy.return_value = False
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
@@ -233,6 +235,8 @@ def test_main_no_tests_should_run(
|
|||||||
output = json.loads(captured.out)
|
output = json.loads(captured.out)
|
||||||
|
|
||||||
assert output["integration_tests"] is False
|
assert output["integration_tests"] is False
|
||||||
|
assert output["integration_tests_run_all"] is False
|
||||||
|
assert output["integration_test_files"] == []
|
||||||
assert output["clang_tidy"] is False
|
assert output["clang_tidy"] is False
|
||||||
assert output["clang_tidy_mode"] == "disabled"
|
assert output["clang_tidy_mode"] == "disabled"
|
||||||
assert output["clang_format"] is False
|
assert output["clang_format"] is False
|
||||||
@@ -253,7 +257,7 @@ def test_main_no_tests_should_run(
|
|||||||
|
|
||||||
|
|
||||||
def test_main_with_branch_argument(
|
def test_main_with_branch_argument(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -266,7 +270,7 @@ def test_main_with_branch_argument(
|
|||||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = True
|
mock_should_run_clang_tidy.return_value = True
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = True
|
mock_should_run_python_linters.return_value = True
|
||||||
@@ -302,7 +306,7 @@ def test_main_with_branch_argument(
|
|||||||
determine_jobs.main()
|
determine_jobs.main()
|
||||||
|
|
||||||
# Check that functions were called with branch
|
# Check that functions were called with branch
|
||||||
mock_should_run_integration_tests.assert_called_once_with("main")
|
mock_determine_integration_tests.assert_called_once_with("main")
|
||||||
mock_should_run_clang_tidy.assert_called_once_with("main")
|
mock_should_run_clang_tidy.assert_called_once_with("main")
|
||||||
mock_should_run_clang_format.assert_called_once_with("main")
|
mock_should_run_clang_format.assert_called_once_with("main")
|
||||||
mock_should_run_python_linters.assert_called_once_with("main")
|
mock_should_run_python_linters.assert_called_once_with("main")
|
||||||
@@ -312,6 +316,8 @@ def test_main_with_branch_argument(
|
|||||||
output = json.loads(captured.out)
|
output = json.loads(captured.out)
|
||||||
|
|
||||||
assert output["integration_tests"] is False
|
assert output["integration_tests"] is False
|
||||||
|
assert output["integration_tests_run_all"] is False
|
||||||
|
assert output["integration_test_files"] == []
|
||||||
assert output["clang_tidy"] is True
|
assert output["clang_tidy"] is True
|
||||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||||
assert output["clang_format"] is False
|
assert output["clang_format"] is False
|
||||||
@@ -334,30 +340,33 @@ def test_main_with_branch_argument(
|
|||||||
assert output["cpp_unit_tests_components"] == ["mqtt"]
|
assert output["cpp_unit_tests_components"] == ["mqtt"]
|
||||||
|
|
||||||
|
|
||||||
def test_should_run_integration_tests(
|
def test_determine_integration_tests(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test should_run_integration_tests function."""
|
"""Test determine_integration_tests function."""
|
||||||
# Core C++ files trigger tests
|
# Core C++ files trigger run_all
|
||||||
with patch.object(
|
with patch.object(
|
||||||
determine_jobs, "changed_files", return_value=["esphome/core/component.cpp"]
|
determine_jobs, "changed_files", return_value=["esphome/core/component.cpp"]
|
||||||
):
|
):
|
||||||
result = determine_jobs.should_run_integration_tests()
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
assert result is True
|
assert run_all is True
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
# Core Python files trigger tests
|
# Core Python files trigger run_all
|
||||||
with patch.object(
|
with patch.object(
|
||||||
determine_jobs, "changed_files", return_value=["esphome/core/config.py"]
|
determine_jobs, "changed_files", return_value=["esphome/core/config.py"]
|
||||||
):
|
):
|
||||||
result = determine_jobs.should_run_integration_tests()
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
assert result is True
|
assert run_all is True
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
# Python files directly in esphome/ do NOT trigger tests
|
# Python files directly in esphome/ do NOT trigger tests
|
||||||
with patch.object(
|
with patch.object(
|
||||||
determine_jobs, "changed_files", return_value=["esphome/config.py"]
|
determine_jobs, "changed_files", return_value=["esphome/config.py"]
|
||||||
):
|
):
|
||||||
result = determine_jobs.should_run_integration_tests()
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
assert result is False
|
assert run_all is False
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
# Python files in subdirectories (not core) do NOT trigger tests
|
# Python files in subdirectories (not core) do NOT trigger tests
|
||||||
with patch.object(
|
with patch.object(
|
||||||
@@ -365,35 +374,151 @@ def test_should_run_integration_tests(
|
|||||||
"changed_files",
|
"changed_files",
|
||||||
return_value=["esphome/dashboard/web_server.py"],
|
return_value=["esphome/dashboard/web_server.py"],
|
||||||
):
|
):
|
||||||
result = determine_jobs.should_run_integration_tests()
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
assert result is False
|
assert run_all is False
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
|
|
||||||
def test_should_run_integration_tests_with_branch() -> None:
|
def test_determine_integration_tests_with_branch() -> None:
|
||||||
"""Test should_run_integration_tests with branch argument."""
|
"""Test determine_integration_tests with branch argument."""
|
||||||
with patch.object(determine_jobs, "changed_files") as mock_changed:
|
with patch.object(determine_jobs, "changed_files") as mock_changed:
|
||||||
mock_changed.return_value = []
|
mock_changed.return_value = []
|
||||||
determine_jobs.should_run_integration_tests("release")
|
run_all, test_files = determine_jobs.determine_integration_tests("release")
|
||||||
mock_changed.assert_called_once_with("release")
|
mock_changed.assert_called_once_with("release")
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
|
|
||||||
def test_should_run_integration_tests_component_dependency() -> None:
|
def test_determine_integration_tests_component_dependency() -> None:
|
||||||
"""Test that integration tests run when components used in fixtures change."""
|
"""Test that integration tests return specific test files when components used in fixtures change."""
|
||||||
with (
|
with (
|
||||||
patch.object(
|
patch.object(
|
||||||
determine_jobs,
|
determine_jobs,
|
||||||
"changed_files",
|
"changed_files",
|
||||||
return_value=["esphome/components/api/api.cpp"],
|
return_value=["esphome/components/api/api.cpp"],
|
||||||
),
|
),
|
||||||
|
patch.object(determine_jobs, "get_fixture_to_test_files") as mock_fixture_map,
|
||||||
patch.object(
|
patch.object(
|
||||||
determine_jobs, "get_components_from_integration_fixtures"
|
determine_jobs, "get_integration_test_files_for_components"
|
||||||
) as mock_fixtures,
|
) as mock_test_files,
|
||||||
):
|
):
|
||||||
mock_fixtures.return_value = {"api", "sensor"}
|
mock_fixture_map.return_value = {}
|
||||||
with patch.object(determine_jobs, "get_all_dependencies") as mock_deps:
|
mock_test_files.return_value = [
|
||||||
mock_deps.return_value = {"api", "sensor", "network"}
|
"tests/integration/test_api.py",
|
||||||
result = determine_jobs.should_run_integration_tests()
|
"tests/integration/test_sensor.py",
|
||||||
assert result is True
|
]
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == [
|
||||||
|
"tests/integration/test_api.py",
|
||||||
|
"tests/integration/test_sensor.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_component_only_affected_tests() -> None:
|
||||||
|
"""Test that only tests using the changed component are returned."""
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=["esphome/components/modbus/modbus.cpp"],
|
||||||
|
),
|
||||||
|
patch.object(determine_jobs, "get_fixture_to_test_files", return_value={}),
|
||||||
|
patch.object(
|
||||||
|
determine_jobs, "get_integration_test_files_for_components"
|
||||||
|
) as mock_test_files,
|
||||||
|
):
|
||||||
|
mock_test_files.return_value = [
|
||||||
|
"tests/integration/test_uart_mock_modbus.py",
|
||||||
|
]
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == ["tests/integration/test_uart_mock_modbus.py"]
|
||||||
|
# Verify it was called with the right component
|
||||||
|
mock_test_files.assert_called_once_with({"modbus"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_infra_file_runs_all() -> None:
|
||||||
|
"""Test that changing infrastructure files (conftest.py, etc.) runs all tests."""
|
||||||
|
with patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=["tests/integration/conftest.py"],
|
||||||
|
):
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is True
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_readme_does_not_run_all() -> None:
|
||||||
|
"""Test that changing README.md does not trigger integration tests."""
|
||||||
|
with patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=["tests/integration/README.md"],
|
||||||
|
):
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_changed_test_file() -> None:
|
||||||
|
"""Test that changing a specific test file only runs that test."""
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=["tests/integration/test_syslog.py"],
|
||||||
|
),
|
||||||
|
patch.object(determine_jobs, "get_fixture_to_test_files", return_value={}),
|
||||||
|
patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"get_integration_test_files_for_components",
|
||||||
|
return_value=[],
|
||||||
|
),
|
||||||
|
):
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == ["tests/integration/test_syslog.py"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_changed_fixture_yaml() -> None:
|
||||||
|
"""Test that changing a fixture YAML runs the corresponding test file."""
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=["tests/integration/fixtures/uart_mock_modbus.yaml"],
|
||||||
|
),
|
||||||
|
patch.object(determine_jobs, "get_fixture_to_test_files") as mock_fixture_map,
|
||||||
|
patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"get_integration_test_files_for_components",
|
||||||
|
return_value=[],
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_fixture_map.return_value = {
|
||||||
|
"uart_mock_modbus": frozenset(
|
||||||
|
{"tests/integration/test_uart_mock_modbus.py"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is False
|
||||||
|
assert test_files == ["tests/integration/test_uart_mock_modbus.py"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_integration_tests_non_yaml_fixture_runs_all() -> None:
|
||||||
|
"""Test that non-YAML changes under fixtures/ (e.g., external_components) run all tests."""
|
||||||
|
with patch.object(
|
||||||
|
determine_jobs,
|
||||||
|
"changed_files",
|
||||||
|
return_value=[
|
||||||
|
"tests/integration/fixtures/external_components/test_component/__init__.py"
|
||||||
|
],
|
||||||
|
):
|
||||||
|
run_all, test_files = determine_jobs.determine_integration_tests()
|
||||||
|
assert run_all is True
|
||||||
|
assert test_files == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -538,7 +663,7 @@ def test_count_changed_cpp_files_with_branch() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_main_filters_components_without_tests(
|
def test_main_filters_components_without_tests(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -551,7 +676,7 @@ def test_main_filters_components_without_tests(
|
|||||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = False
|
mock_should_run_clang_tidy.return_value = False
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
@@ -631,7 +756,7 @@ def test_main_filters_components_without_tests(
|
|||||||
|
|
||||||
|
|
||||||
def test_main_detects_components_with_variant_tests(
|
def test_main_detects_components_with_variant_tests(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -649,7 +774,7 @@ def test_main_detects_components_with_variant_tests(
|
|||||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = False
|
mock_should_run_clang_tidy.return_value = False
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
@@ -999,7 +1124,7 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_clang_tidy_mode_full_scan(
|
def test_clang_tidy_mode_full_scan(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -1010,7 +1135,7 @@ def test_clang_tidy_mode_full_scan(
|
|||||||
"""Test that full scan (hash changed) always uses split mode."""
|
"""Test that full scan (hash changed) always uses split mode."""
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = True
|
mock_should_run_clang_tidy.return_value = True
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
@@ -1065,7 +1190,7 @@ def test_clang_tidy_mode_targeted_scan(
|
|||||||
component_count: int,
|
component_count: int,
|
||||||
files_per_component: int,
|
files_per_component: int,
|
||||||
expected_mode: str,
|
expected_mode: str,
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -1076,7 +1201,7 @@ def test_clang_tidy_mode_targeted_scan(
|
|||||||
"""Test clang-tidy mode selection based on files_to_check count."""
|
"""Test clang-tidy mode selection based on files_to_check count."""
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = True
|
mock_should_run_clang_tidy.return_value = True
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
@@ -1123,7 +1248,7 @@ def test_clang_tidy_mode_targeted_scan(
|
|||||||
|
|
||||||
|
|
||||||
def test_main_core_files_changed_still_detects_components(
|
def test_main_core_files_changed_still_detects_components(
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -1135,7 +1260,7 @@ def test_main_core_files_changed_still_detects_components(
|
|||||||
"""Test that component changes are detected even when core files change."""
|
"""Test that component changes are detected even when core files change."""
|
||||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
|
||||||
mock_should_run_integration_tests.return_value = True
|
mock_determine_integration_tests.return_value = (True, [])
|
||||||
mock_should_run_clang_tidy.return_value = True
|
mock_should_run_clang_tidy.return_value = True
|
||||||
mock_should_run_clang_format.return_value = True
|
mock_should_run_clang_format.return_value = True
|
||||||
mock_should_run_python_linters.return_value = True
|
mock_should_run_python_linters.return_value = True
|
||||||
@@ -1604,7 +1729,7 @@ def test_detect_platform_hint_from_filename_case_insensitive(
|
|||||||
|
|
||||||
def test_component_batching_beta_branch_40_per_batch(
|
def test_component_batching_beta_branch_40_per_batch(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mock_should_run_integration_tests: Mock,
|
mock_determine_integration_tests: Mock,
|
||||||
mock_should_run_clang_tidy: Mock,
|
mock_should_run_clang_tidy: Mock,
|
||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
@@ -1628,7 +1753,7 @@ def test_component_batching_beta_branch_40_per_batch(
|
|||||||
(comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}")
|
(comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}")
|
||||||
|
|
||||||
# Setup mocks
|
# Setup mocks
|
||||||
mock_should_run_integration_tests.return_value = False
|
mock_determine_integration_tests.return_value = (False, [])
|
||||||
mock_should_run_clang_tidy.return_value = False
|
mock_should_run_clang_tidy.return_value = False
|
||||||
mock_should_run_clang_format.return_value = False
|
mock_should_run_clang_format.return_value = False
|
||||||
mock_should_run_python_linters.return_value = False
|
mock_should_run_python_linters.return_value = False
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ def clear_helpers_cache() -> None:
|
|||||||
"""Clear cached functions before each test."""
|
"""Clear cached functions before each test."""
|
||||||
helpers._get_github_event_data.cache_clear()
|
helpers._get_github_event_data.cache_clear()
|
||||||
helpers._get_changed_files_github_actions.cache_clear()
|
helpers._get_changed_files_github_actions.cache_clear()
|
||||||
|
helpers.get_components_per_integration_fixture.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -1111,7 +1112,7 @@ def test_get_components_from_integration_fixtures() -> None:
|
|||||||
"gpio",
|
"gpio",
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_yaml_file = Mock()
|
mock_yaml_file = Mock(stem="test_fixture")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("pathlib.Path.glob") as mock_glob,
|
patch("pathlib.Path.glob") as mock_glob,
|
||||||
@@ -1133,7 +1134,7 @@ def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None:
|
|||||||
".binary_filters": {"filters": [{"settle": "50ms"}]},
|
".binary_filters": {"filters": [{"settle": "50ms"}]},
|
||||||
}
|
}
|
||||||
|
|
||||||
mock_yaml_file = Mock()
|
mock_yaml_file = Mock(stem="test_fixture")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("pathlib.Path.glob") as mock_glob,
|
patch("pathlib.Path.glob") as mock_glob,
|
||||||
@@ -1148,6 +1149,31 @@ def test_get_components_from_integration_fixtures_skips_yaml_anchors() -> None:
|
|||||||
assert components == {"sensor", "esphome", "template"}
|
assert components == {"sensor", "esphome", "template"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_integration_test_files_for_components_real_fixtures() -> None:
|
||||||
|
"""Test that component changes map to the correct real integration test files.
|
||||||
|
|
||||||
|
This test uses real fixtures to verify the mapping stays correct
|
||||||
|
as new tests are added.
|
||||||
|
"""
|
||||||
|
# modbus should include at least the modbus test
|
||||||
|
modbus_tests = helpers.get_integration_test_files_for_components({"modbus"})
|
||||||
|
assert "tests/integration/test_uart_mock_modbus.py" in modbus_tests
|
||||||
|
|
||||||
|
# ld2410 should include at least the ld2410 test
|
||||||
|
ld2410_tests = helpers.get_integration_test_files_for_components({"ld2410"})
|
||||||
|
assert "tests/integration/test_uart_mock_ld2410.py" in ld2410_tests
|
||||||
|
|
||||||
|
# syslog should include at least the syslog test
|
||||||
|
syslog_tests = helpers.get_integration_test_files_for_components({"syslog"})
|
||||||
|
assert "tests/integration/test_syslog.py" in syslog_tests
|
||||||
|
|
||||||
|
# A component not used by any fixture should return nothing
|
||||||
|
fake_tests = helpers.get_integration_test_files_for_components(
|
||||||
|
{"nonexistent_component_xyz"}
|
||||||
|
)
|
||||||
|
assert fake_tests == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"output,expected",
|
"output,expected",
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user