From 2c8a6db2c507adf97402913a945b1a9701bb264f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Oct 2025 15:20:17 -1000 Subject: [PATCH] tester --- script/analyze_component_buses.py | 1 + script/test_component_grouping.py | 174 ++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100755 script/test_component_grouping.py diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 0c503a3633..678dcab9fc 100644 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -62,6 +62,7 @@ BASE_BUS_COMPONENTS = { # These have known build issues that prevent grouping # NOTE: This should be kept in sync with both test_build_components and split_components_for_ci.py ISOLATED_COMPONENTS = { + "animation": "Has display lambda in common.yaml that requires existing display platform - breaks when merged without display", "camera_encoder": "Multiple definition errors: esp32-camera IDF component conflicts with ESPHome camera component", "camera": "Uses relative include paths that break when merged with other components", "esp32_camera": "Leaks config into other components", diff --git a/script/test_component_grouping.py b/script/test_component_grouping.py new file mode 100755 index 0000000000..69d043ad7c --- /dev/null +++ b/script/test_component_grouping.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Test component grouping by finding and testing groups of components. + +This script analyzes components, finds groups that can be tested together, +and runs test builds for those groups. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import subprocess +import sys + +# Add esphome to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from script.analyze_component_buses import ( + analyze_all_components, + group_components_by_signature, +) + + +def test_component_group( + components: list[str], + platform: str, + dry_run: bool = False, +) -> bool: + """Test a group of components together. + + Args: + components: List of component names to test together + platform: Platform to test on (e.g., "esp32-idf") + dry_run: If True, only print the command without running it + + Returns: + True if test passed, False otherwise + """ + components_str = ",".join(components) + cmd = ["./script/test_build_components", "-c", components_str, "-t", platform] + + print(f"\n{'=' * 80}") + print(f"Testing {len(components)} components on {platform}:") + for comp in components: + print(f" - {comp}") + print(f"{'=' * 80}") + print(f"Command: {' '.join(cmd)}\n") + + if dry_run: + print("[DRY RUN] Skipping actual test") + return True + + try: + result = subprocess.run(cmd, check=False) + return result.returncode == 0 + except Exception as e: + print(f"Error running test: {e}") + return False + + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Test component grouping by finding and testing groups" + ) + parser.add_argument( + "--platform", + "-p", + default="esp32-idf", + help="Platform to test (default: esp32-idf)", + ) + parser.add_argument( + "--min-size", + type=int, + default=3, + help="Minimum group size to test (default: 3)", + ) + parser.add_argument( + "--max-size", + type=int, + default=10, + help="Maximum group size to test (default: 10)", + ) + parser.add_argument( + "--max-groups", + type=int, + default=5, + help="Maximum number of groups to test (default: 5)", + ) + parser.add_argument( + "--signature", + "-s", + help="Only test groups with this bus signature (e.g., 'spi', 'i2c', 'uart')", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print commands without running them", + ) + + args = parser.parse_args() + + print("Analyzing all components...") + components, non_groupable, _ = analyze_all_components(Path("tests/components")) + + print(f"Found {len(components)} components, {len(non_groupable)} non-groupable") + + # Group components by signature for the platform + groups = group_components_by_signature(components, args.platform) + + # Filter and sort groups + filtered_groups = [] + for signature, comp_list in groups.items(): + # Filter by signature if specified + if args.signature and signature != args.signature: + continue + + # Remove non-groupable components + comp_list = [c for c in comp_list if c not in non_groupable] + + # Filter by minimum size + if len(comp_list) < args.min_size: + continue + + # If group is larger than max_size, we'll take a subset later + filtered_groups.append((signature, comp_list)) + + # Sort by group size (largest first) + filtered_groups.sort(key=lambda x: len(x[1]), reverse=True) + + # Limit number of groups + filtered_groups = filtered_groups[: args.max_groups] + + if not filtered_groups: + print("\nNo groups found matching criteria:") + print(f" - Platform: {args.platform}") + print(f" - Size: {args.min_size}-{args.max_size}") + if args.signature: + print(f" - Signature: {args.signature}") + return + + print(f"\nFound {len(filtered_groups)} groups to test:") + for signature, comp_list in filtered_groups: + print(f" [{signature}]: {len(comp_list)} components") + + # Test each group + results = [] + for signature, comp_list in filtered_groups: + # Limit to max_size if group is larger + if len(comp_list) > args.max_size: + comp_list = comp_list[: args.max_size] + + success = test_component_group(comp_list, args.platform, args.dry_run) + results.append((signature, comp_list, success)) + + if not args.dry_run and not success: + print(f"\nāŒ FAILED: {signature} group") + break + + # Print summary + print(f"\n{'=' * 80}") + print("TEST SUMMARY") + print(f"{'=' * 80}") + for signature, comp_list, success in results: + status = "āœ… PASS" if success else "āŒ FAIL" + print(f"{status} [{signature}]: {len(comp_list)} components") + + # Exit with error if any tests failed + if not args.dry_run and any(not success for _, _, success in results): + sys.exit(1) + + +if __name__ == "__main__": + main()