mirror of
https://github.com/esphome/esphome.git
synced 2026-05-29 23:07:16 +08:00
[ci] Run downstream device-builder tests against PR Python code (#16214)
This commit is contained in:
@@ -136,6 +136,53 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
device-builder:
|
||||||
|
name: Test downstream esphome/device-builder
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.device-builder == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Check out esphome (this PR)
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
path: esphome
|
||||||
|
- name: Check out esphome/device-builder
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
repository: esphome/device-builder
|
||||||
|
ref: main
|
||||||
|
path: device-builder
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- name: Set up uv
|
||||||
|
# Mirrors the install shape device-builder's own CI uses
|
||||||
|
# (esphome/device-builder#192): uv replaces pip for the
|
||||||
|
# install step (order-of-magnitude faster on cold boots,
|
||||||
|
# with its own wheel cache). actions/setup-python still
|
||||||
|
# provides the interpreter.
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- name: Install device-builder + esphome from PR
|
||||||
|
# Install device-builder with its esphome + test extras
|
||||||
|
# first so its pinned versions of pytest/etc. land, then
|
||||||
|
# overlay the PR's esphome so the downstream tests run
|
||||||
|
# against this PR's Python code. ``--system`` installs into
|
||||||
|
# the runner's Python instead of a venv.
|
||||||
|
run: |
|
||||||
|
uv pip install --system -e './device-builder[esphome,test]'
|
||||||
|
uv pip install --system -e ./esphome
|
||||||
|
- name: Run device-builder pytest
|
||||||
|
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
||||||
|
# own CI). No ``--cov`` here -- this is purely a downstream
|
||||||
|
# smoke check against this PR's esphome code.
|
||||||
|
working-directory: device-builder
|
||||||
|
run: pytest -q -n auto --maxfail=5 --durations=10 --no-cov --ignore=tests/benchmarks
|
||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
name: Run pytest
|
name: Run pytest
|
||||||
strategy:
|
strategy:
|
||||||
@@ -204,6 +251,7 @@ jobs:
|
|||||||
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 }}
|
||||||
import-time: ${{ steps.determine.outputs.import-time }}
|
import-time: ${{ steps.determine.outputs.import-time }}
|
||||||
|
device-builder: ${{ steps.determine.outputs.device-builder }}
|
||||||
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 }}
|
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||||
@@ -247,6 +295,7 @@ jobs:
|
|||||||
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
|
||||||
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
|
||||||
|
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $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 "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||||
@@ -1063,6 +1112,7 @@ jobs:
|
|||||||
- clang-tidy-nosplit
|
- clang-tidy-nosplit
|
||||||
- clang-tidy-split
|
- clang-tidy-split
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
|
- device-builder
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
- pre-commit-ci-lite
|
- pre-commit-ci-lite
|
||||||
- memory-impact-target-branch
|
- memory-impact-target-branch
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ what files have changed. It outputs JSON with the following structure:
|
|||||||
"clang_tidy": true/false,
|
"clang_tidy": true/false,
|
||||||
"clang_format": true/false,
|
"clang_format": true/false,
|
||||||
"python_linters": true/false,
|
"python_linters": true/false,
|
||||||
|
"device_builder": true/false,
|
||||||
"changed_components": ["component1", "component2", ...],
|
"changed_components": ["component1", "component2", ...],
|
||||||
"component_test_count": 5,
|
"component_test_count": 5,
|
||||||
"memory_impact": {
|
"memory_impact": {
|
||||||
@@ -25,6 +26,7 @@ The CI workflow uses this information to:
|
|||||||
- Skip or run clang-tidy (and whether to do a full scan)
|
- Skip or run clang-tidy (and whether to do a full scan)
|
||||||
- Skip or run clang-format
|
- Skip or run clang-format
|
||||||
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
|
- Skip or run Python linters (ruff, flake8, pylint, pyupgrade)
|
||||||
|
- Skip or run downstream esphome/device-builder tests against the PR's Python code
|
||||||
- Determine which components to test individually
|
- Determine which components to test individually
|
||||||
- Decide how to split component tests (if there are many)
|
- Decide how to split component tests (if there are many)
|
||||||
- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes
|
- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes
|
||||||
@@ -440,6 +442,56 @@ def should_run_import_time(branch: str | None = None) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Files outside esphome/**/*.py whose changes can affect the downstream
|
||||||
|
# device-builder build. requirements.txt / pyproject.toml change the runtime
|
||||||
|
# dependency graph that device-builder picks up when it installs esphome.
|
||||||
|
DEVICE_BUILDER_TRIGGER_FILES = frozenset(
|
||||||
|
{
|
||||||
|
"requirements.txt",
|
||||||
|
"pyproject.toml",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_run_device_builder(branch: str | None = None) -> bool:
|
||||||
|
"""Determine if downstream esphome/device-builder tests should run.
|
||||||
|
|
||||||
|
device-builder imports esphome as a library, so whenever the importable
|
||||||
|
Python surface, the runtime dependencies, or any non-C++ file packaged
|
||||||
|
with esphome (pyproject.toml has ``include-package-data = true``, so
|
||||||
|
things like esphome/idf_component.yml ship and can affect installs)
|
||||||
|
changes we re-run its test suite against the PR's code to catch
|
||||||
|
breakage we'd otherwise only see after a release.
|
||||||
|
|
||||||
|
Skipped on beta/release branches: those branches typically lag behind
|
||||||
|
device-builder@main, so a new device-builder API dependency would
|
||||||
|
falsely fail the run without reflecting any problem in the PR itself.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: Branch to compare against. If None, uses default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the device-builder downstream tests should run, False otherwise.
|
||||||
|
"""
|
||||||
|
target_branch = get_target_branch()
|
||||||
|
if target_branch and (
|
||||||
|
target_branch.startswith("release") or target_branch.startswith("beta")
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for file in changed_files(branch):
|
||||||
|
if file in DEVICE_BUILDER_TRIGGER_FILES:
|
||||||
|
return True
|
||||||
|
# Anything under esphome/ that isn't C++ source can change the
|
||||||
|
# importable / packaged surface device-builder consumes
|
||||||
|
# (Python sources, packaged YAML/JSON like idf_component.yml,
|
||||||
|
# etc.). C++ files only affect compiled firmware, not the
|
||||||
|
# Python install device-builder pulls in.
|
||||||
|
if file.startswith("esphome/") and not file.endswith(CPP_FILE_EXTENSIONS):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def determine_cpp_unit_tests(
|
def determine_cpp_unit_tests(
|
||||||
branch: str | None = None,
|
branch: str | None = None,
|
||||||
) -> tuple[bool, list[str]]:
|
) -> tuple[bool, list[str]]:
|
||||||
@@ -874,6 +926,7 @@ def main() -> None:
|
|||||||
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)
|
||||||
run_import_time = should_run_import_time(args.branch)
|
run_import_time = should_run_import_time(args.branch)
|
||||||
|
run_device_builder = should_run_device_builder(args.branch)
|
||||||
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
||||||
|
|
||||||
# Get changed components
|
# Get changed components
|
||||||
@@ -1007,6 +1060,7 @@ def main() -> None:
|
|||||||
"clang_format": run_clang_format,
|
"clang_format": run_clang_format,
|
||||||
"python_linters": run_python_linters,
|
"python_linters": run_python_linters,
|
||||||
"import_time": run_import_time,
|
"import_time": run_import_time,
|
||||||
|
"device_builder": run_device_builder,
|
||||||
"changed_components": changed_components,
|
"changed_components": changed_components,
|
||||||
"changed_components_with_tests": changed_components_with_tests,
|
"changed_components_with_tests": changed_components_with_tests,
|
||||||
"directly_changed_components_with_tests": list(directly_changed_with_tests),
|
"directly_changed_components_with_tests": list(directly_changed_with_tests),
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ def mock_should_run_import_time() -> Generator[Mock, None, None]:
|
|||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_should_run_device_builder() -> Generator[Mock, None, None]:
|
||||||
|
"""Mock should_run_device_builder from determine_jobs."""
|
||||||
|
with patch.object(determine_jobs, "should_run_device_builder") as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]:
|
def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]:
|
||||||
"""Mock determine_cpp_unit_tests from helpers."""
|
"""Mock determine_cpp_unit_tests from helpers."""
|
||||||
@@ -99,6 +106,7 @@ def test_main_all_tests_should_run(
|
|||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
mock_should_run_import_time: Mock,
|
mock_should_run_import_time: Mock,
|
||||||
|
mock_should_run_device_builder: Mock,
|
||||||
mock_changed_files: Mock,
|
mock_changed_files: Mock,
|
||||||
mock_determine_cpp_unit_tests: Mock,
|
mock_determine_cpp_unit_tests: Mock,
|
||||||
capsys: pytest.CaptureFixture[str],
|
capsys: pytest.CaptureFixture[str],
|
||||||
@@ -113,6 +121,7 @@ def test_main_all_tests_should_run(
|
|||||||
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
|
||||||
mock_should_run_import_time.return_value = True
|
mock_should_run_import_time.return_value = True
|
||||||
|
mock_should_run_device_builder.return_value = True
|
||||||
mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"])
|
mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"])
|
||||||
|
|
||||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||||
@@ -193,6 +202,7 @@ 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["import_time"] is True
|
assert output["import_time"] is True
|
||||||
|
assert output["device_builder"] is True
|
||||||
assert output["changed_components"] == ["wifi", "api", "sensor"]
|
assert output["changed_components"] == ["wifi", "api", "sensor"]
|
||||||
# changed_components_with_tests will only include components that actually have test files
|
# changed_components_with_tests will only include components that actually have test files
|
||||||
assert "changed_components_with_tests" in output
|
assert "changed_components_with_tests" in output
|
||||||
@@ -225,6 +235,7 @@ def test_main_no_tests_should_run(
|
|||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
mock_should_run_import_time: Mock,
|
mock_should_run_import_time: Mock,
|
||||||
|
mock_should_run_device_builder: Mock,
|
||||||
mock_changed_files: Mock,
|
mock_changed_files: Mock,
|
||||||
mock_determine_cpp_unit_tests: Mock,
|
mock_determine_cpp_unit_tests: Mock,
|
||||||
capsys: pytest.CaptureFixture[str],
|
capsys: pytest.CaptureFixture[str],
|
||||||
@@ -239,6 +250,7 @@ def test_main_no_tests_should_run(
|
|||||||
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
|
||||||
mock_should_run_import_time.return_value = False
|
mock_should_run_import_time.return_value = False
|
||||||
|
mock_should_run_device_builder.return_value = False
|
||||||
mock_determine_cpp_unit_tests.return_value = (False, [])
|
mock_determine_cpp_unit_tests.return_value = (False, [])
|
||||||
|
|
||||||
# Mock changed_files to return no component files
|
# Mock changed_files to return no component files
|
||||||
@@ -278,6 +290,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["import_time"] is False
|
assert output["import_time"] is False
|
||||||
|
assert output["device_builder"] is False
|
||||||
assert output["changed_components"] == []
|
assert output["changed_components"] == []
|
||||||
assert output["changed_components_with_tests"] == []
|
assert output["changed_components_with_tests"] == []
|
||||||
assert output["component_test_count"] == 0
|
assert output["component_test_count"] == 0
|
||||||
@@ -299,6 +312,7 @@ def test_main_with_branch_argument(
|
|||||||
mock_should_run_clang_format: Mock,
|
mock_should_run_clang_format: Mock,
|
||||||
mock_should_run_python_linters: Mock,
|
mock_should_run_python_linters: Mock,
|
||||||
mock_should_run_import_time: Mock,
|
mock_should_run_import_time: Mock,
|
||||||
|
mock_should_run_device_builder: Mock,
|
||||||
mock_changed_files: Mock,
|
mock_changed_files: Mock,
|
||||||
mock_determine_cpp_unit_tests: Mock,
|
mock_determine_cpp_unit_tests: Mock,
|
||||||
capsys: pytest.CaptureFixture[str],
|
capsys: pytest.CaptureFixture[str],
|
||||||
@@ -313,6 +327,7 @@ def test_main_with_branch_argument(
|
|||||||
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
|
||||||
mock_should_run_import_time.return_value = True
|
mock_should_run_import_time.return_value = True
|
||||||
|
mock_should_run_device_builder.return_value = True
|
||||||
mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"])
|
mock_determine_cpp_unit_tests.return_value = (False, ["mqtt"])
|
||||||
|
|
||||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||||
@@ -350,6 +365,7 @@ def test_main_with_branch_argument(
|
|||||||
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")
|
||||||
mock_should_run_import_time.assert_called_once_with("main")
|
mock_should_run_import_time.assert_called_once_with("main")
|
||||||
|
mock_should_run_device_builder.assert_called_once_with("main")
|
||||||
|
|
||||||
# Check output
|
# Check output
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
@@ -362,6 +378,7 @@ 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["import_time"] is True
|
assert output["import_time"] is True
|
||||||
|
assert output["device_builder"] is True
|
||||||
assert output["changed_components"] == ["mqtt"]
|
assert output["changed_components"] == ["mqtt"]
|
||||||
# changed_components_with_tests will only include components that actually have test files
|
# changed_components_with_tests will only include components that actually have test files
|
||||||
assert "changed_components_with_tests" in output
|
assert "changed_components_with_tests" in output
|
||||||
@@ -734,6 +751,82 @@ def test_should_run_import_time_with_branch() -> None:
|
|||||||
mock_changed.assert_called_once_with("release")
|
mock_changed.assert_called_once_with("release")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("changed_files", "expected_result"),
|
||||||
|
[
|
||||||
|
# esphome Python files trigger downstream device-builder tests
|
||||||
|
(["esphome/__main__.py"], True),
|
||||||
|
(["esphome/components/wifi/__init__.py"], True),
|
||||||
|
(["esphome/core/config.py"], True),
|
||||||
|
(["esphome/types.pyi"], True),
|
||||||
|
# Runtime dependency changes trigger
|
||||||
|
(["requirements.txt"], True),
|
||||||
|
(["pyproject.toml"], True),
|
||||||
|
# Non-C++ files packaged with esphome trigger -- device-builder
|
||||||
|
# picks them up because esphome's pyproject sets
|
||||||
|
# include-package-data = true.
|
||||||
|
(["esphome/idf_component.yml"], True),
|
||||||
|
(["esphome/dashboard/templates/index.html"], True),
|
||||||
|
(["esphome/components/api/api_pb2_service.json"], True),
|
||||||
|
# Mixed: any triggering file is enough
|
||||||
|
(["docs/README.md", "esphome/config.py"], True),
|
||||||
|
# Dev/test-only dependency changes don't trigger device-builder
|
||||||
|
# (they don't affect the importable surface device-builder uses)
|
||||||
|
(["requirements_dev.txt"], False),
|
||||||
|
(["requirements_test.txt"], False),
|
||||||
|
# Files outside esphome/ don't trigger
|
||||||
|
(["script/some_other_script.py"], False),
|
||||||
|
(["tests/script/test_determine_jobs.py"], False),
|
||||||
|
# C++ files under esphome/ don't trigger -- they only affect
|
||||||
|
# compiled firmware, not the Python install device-builder pulls in.
|
||||||
|
(["esphome/core/component.cpp"], False),
|
||||||
|
(["esphome/core/component.h"], False),
|
||||||
|
(["esphome/components/wifi/wifi_component.cpp"], False),
|
||||||
|
# Files outside esphome/ entirely
|
||||||
|
(["tests/components/wifi/test.esp32-idf.yaml"], False),
|
||||||
|
(["README.md"], False),
|
||||||
|
([], False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_should_run_device_builder(
|
||||||
|
changed_files: list[str], expected_result: bool
|
||||||
|
) -> None:
|
||||||
|
"""Test should_run_device_builder function (non-beta/release target)."""
|
||||||
|
with (
|
||||||
|
patch.object(determine_jobs, "changed_files", return_value=changed_files),
|
||||||
|
# Mock target branch to "dev" so the beta/release skip is bypassed
|
||||||
|
# for these per-file behavior checks.
|
||||||
|
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
|
||||||
|
):
|
||||||
|
result = determine_jobs.should_run_device_builder()
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_run_device_builder_with_branch() -> None:
|
||||||
|
"""Test should_run_device_builder with branch argument."""
|
||||||
|
with (
|
||||||
|
patch.object(determine_jobs, "changed_files") as mock_changed,
|
||||||
|
patch.object(determine_jobs, "get_target_branch", return_value="dev"),
|
||||||
|
):
|
||||||
|
mock_changed.return_value = []
|
||||||
|
determine_jobs.should_run_device_builder("release")
|
||||||
|
mock_changed.assert_called_once_with("release")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target_branch", ["beta", "release", "release-2026.5"])
|
||||||
|
def test_should_run_device_builder_skips_beta_release(target_branch: str) -> None:
|
||||||
|
"""Beta/release target branches skip device-builder (lag behind device-builder@main)."""
|
||||||
|
with (
|
||||||
|
patch.object(determine_jobs, "get_target_branch", return_value=target_branch),
|
||||||
|
patch.object(determine_jobs, "changed_files") as mock_changed,
|
||||||
|
):
|
||||||
|
# Even with a triggering file present, the target-branch guard wins.
|
||||||
|
mock_changed.return_value = ["esphome/__main__.py"]
|
||||||
|
assert determine_jobs.should_run_device_builder() is False
|
||||||
|
# changed_files shouldn't even be consulted -- the guard short-circuits.
|
||||||
|
mock_changed.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("changed_files", "expected_result"),
|
("changed_files", "expected_result"),
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user