Merge branch 'controller_registry' into integration

This commit is contained in:
J. Nick Koston
2025-11-07 17:17:05 -06:00
38 changed files with 877 additions and 197 deletions
+162
View File
@@ -19,6 +19,8 @@ sys.path.insert(0, script_dir)
# Import helpers module for patching
import helpers # noqa: E402
import script.helpers # noqa: E402
spec = importlib.util.spec_from_file_location(
"determine_jobs", os.path.join(script_dir, "determine-jobs.py")
)
@@ -70,6 +72,13 @@ def mock_changed_files() -> Generator[Mock, None, None]:
yield mock
@pytest.fixture
def mock_target_branch_dev() -> Generator[Mock, None, None]:
"""Mock get_target_branch to return 'dev' for memory impact tests."""
with patch.object(determine_jobs, "get_target_branch", return_value="dev") as mock:
yield mock
@pytest.fixture(autouse=True)
def clear_determine_jobs_caches() -> None:
"""Clear all cached functions before each test."""
@@ -125,6 +134,16 @@ def test_main_all_tests_should_run(
["wifi", "api"] if not deps else ["wifi", "api", "sensor"]
),
),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
patch.object(
determine_jobs,
"create_intelligent_batches",
return_value=([["wifi", "api", "sensor"]], {}),
),
):
determine_jobs.main()
@@ -196,6 +215,16 @@ def test_main_no_tests_should_run(
patch.object(
determine_jobs, "get_components_with_dependencies", return_value=[]
),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
patch.object(
determine_jobs,
"create_intelligent_batches",
return_value=([], {}),
),
):
determine_jobs.main()
@@ -259,6 +288,16 @@ def test_main_with_branch_argument(
patch.object(
determine_jobs, "get_components_with_dependencies", return_value=["mqtt"]
),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
patch.object(
determine_jobs,
"create_intelligent_batches",
return_value=([["mqtt"]], {}),
),
):
determine_jobs.main()
@@ -564,6 +603,11 @@ def test_main_filters_components_without_tests(
),
),
patch.object(determine_jobs, "changed_files", return_value=[]),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
):
# Clear the cache since we're mocking root_path
determine_jobs.main()
@@ -663,6 +707,11 @@ def test_main_detects_components_with_variant_tests(
),
),
patch.object(determine_jobs, "changed_files", return_value=[]),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
):
# Clear the cache since we're mocking root_path
determine_jobs.main()
@@ -688,6 +737,7 @@ def test_main_detects_components_with_variant_tests(
# Tests for detect_memory_impact_config function
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> None:
"""Test memory impact detection when components share a common platform."""
# Create test directory structure
@@ -722,6 +772,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non
assert result["use_merged_config"] == "true"
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None:
"""Test memory impact detection with core C++ changes (no component changes)."""
# Create test directory structure with fallback component
@@ -779,6 +830,7 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) ->
assert result["should_run"] == "false"
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None:
"""Test memory impact detection when components have no common platform."""
# Create test directory structure
@@ -855,6 +907,7 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) ->
assert result["should_run"] == "false"
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_includes_base_bus_components(
tmp_path: Path,
) -> None:
@@ -897,6 +950,7 @@ def test_detect_memory_impact_config_includes_base_bus_components(
assert result["platform"] == "esp32-idf" # Common platform
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
"""Test memory impact detection for components with only variant test files.
@@ -1112,6 +1166,16 @@ def test_main_core_files_changed_still_detects_components(
else ["select", "api", "bluetooth_proxy", "logger"]
),
),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
patch.object(
determine_jobs,
"create_intelligent_batches",
return_value=([["select", "api", "bluetooth_proxy", "logger"]], {}),
),
):
determine_jobs.main()
@@ -1125,6 +1189,7 @@ def test_main_core_files_changed_still_detects_components(
assert len(output["changed_components"]) > 0
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266(
tmp_path: Path,
) -> None:
@@ -1178,6 +1243,7 @@ def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266(
assert result["use_merged_config"] == "true"
@pytest.mark.usefixtures("mock_target_branch_dev")
def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32(
tmp_path: Path,
) -> None:
@@ -1353,3 +1419,99 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
# Memory impact should run at exactly 40 components (at limit but not over)
assert result["should_run"] == "true"
assert len(result["components"]) == 40
def test_component_batching_beta_branch_40_per_batch(
tmp_path: Path,
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_changed_files: Mock,
mock_determine_cpp_unit_tests: Mock,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test that beta/release branches create batches with 40 actual components each.
For beta/release branches, all components should be groupable (not isolated),
and each batch should contain 40 actual components with weight 1 each.
This matches the original behavior before consolidation.
"""
# Create 120 test components with test files
component_names = [f"comp_{i:03d}" for i in range(120)]
tests_dir = tmp_path / "tests" / "components"
for comp in component_names:
comp_dir = tests_dir / comp
comp_dir.mkdir(parents=True)
(comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}")
# Setup mocks
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_determine_cpp_unit_tests.return_value = (False, [])
# Mock changed_files to return all component files
changed_files = [
f"esphome/components/{comp}/{comp}.cpp" for comp in component_names
]
mock_changed_files.return_value = changed_files
# Run main function with beta branch
# Don't mock create_intelligent_batches - that's what we're testing!
with (
patch("sys.argv", ["determine-jobs.py", "--branch", "beta"]),
patch.object(determine_jobs, "root_path", str(tmp_path)),
patch.object(helpers, "root_path", str(tmp_path)),
patch.object(script.helpers, "root_path", str(tmp_path)),
patch.object(determine_jobs, "get_target_branch", return_value="beta"),
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
patch.object(
determine_jobs,
"get_changed_components",
return_value=component_names,
),
patch.object(
determine_jobs,
"filter_component_and_test_files",
side_effect=lambda f: f.startswith("esphome/components/"),
),
patch.object(
determine_jobs,
"get_components_with_dependencies",
side_effect=lambda files, deps: component_names,
),
patch.object(
determine_jobs,
"detect_memory_impact_config",
return_value={"should_run": "false"},
),
):
determine_jobs.main()
# Check output
captured = capsys.readouterr()
output = json.loads(captured.out)
# Verify batches are present and properly sized
assert "component_test_batches" in output
batches = output["component_test_batches"]
# Should have 3 batches (120 components / 40 per batch = 3)
assert len(batches) == 3, f"Expected 3 batches, got {len(batches)}"
# Each batch should have approximately 40 components (all weight=1, groupable)
for i, batch_str in enumerate(batches):
batch_components = batch_str.split()
assert len(batch_components) == 40, (
f"Batch {i} should have 40 components, got {len(batch_components)}"
)
# Verify all 120 components are in batches
all_components = []
for batch_str in batches:
all_components.extend(batch_str.split())
assert len(all_components) == 120
assert set(all_components) == set(component_names)