mirror of
https://github.com/esphome/esphome.git
synced 2025-10-12 14:53:49 +01:00
fix
This commit is contained in:
170
script/split_components_for_ci.py
Executable file
170
script/split_components_for_ci.py
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split components into batches with intelligent grouping.
|
||||
|
||||
This script analyzes components to identify which ones share common bus configurations
|
||||
and intelligently groups them into batches to maximize the efficiency of the
|
||||
component grouping system in CI.
|
||||
|
||||
Components with the same bus signature are placed in the same batch whenever possible,
|
||||
allowing the test_build_components.py script to merge them into single builds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add esphome to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from script.analyze_component_buses import (
|
||||
analyze_all_components,
|
||||
create_grouping_signature,
|
||||
)
|
||||
|
||||
|
||||
def create_intelligent_batches(
|
||||
components: list[str],
|
||||
tests_dir: Path,
|
||||
batch_size: int = 10,
|
||||
) -> list[list[str]]:
|
||||
"""Create batches optimized for component grouping.
|
||||
|
||||
Args:
|
||||
components: List of component names to batch
|
||||
tests_dir: Path to tests/components directory
|
||||
batch_size: Target size for each batch
|
||||
|
||||
Returns:
|
||||
List of component batches (lists of component names)
|
||||
"""
|
||||
# Analyze all components to get their bus signatures
|
||||
component_buses = analyze_all_components(tests_dir)
|
||||
|
||||
# Group components by their bus signature
|
||||
# Key: (platform, signature), Value: list of components
|
||||
signature_groups: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||
|
||||
for component in components:
|
||||
if component not in component_buses:
|
||||
# Component has no bus configs, put in special group
|
||||
signature_groups[("none", "none")].append(component)
|
||||
continue
|
||||
|
||||
# Group by platform and signature
|
||||
comp_platforms = component_buses[component]
|
||||
for platform, buses in comp_platforms.items():
|
||||
if buses:
|
||||
signature = create_grouping_signature({platform: buses}, platform)
|
||||
signature_groups[(platform, signature)].append(component)
|
||||
break # Only use first platform for grouping
|
||||
else:
|
||||
# No buses found
|
||||
signature_groups[("none", "none")].append(component)
|
||||
|
||||
# Create batches by grouping components with same signature
|
||||
batches = []
|
||||
current_batch = []
|
||||
|
||||
# Sort signature groups by size (largest first) for better packing
|
||||
sorted_groups = sorted(
|
||||
signature_groups.items(),
|
||||
key=lambda x: len(x[1]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for (platform, signature), group_components in sorted_groups:
|
||||
# Add components from this signature group to batches
|
||||
for component in sorted(group_components): # Sort for determinism
|
||||
current_batch.append(component)
|
||||
|
||||
if len(current_batch) >= batch_size:
|
||||
batches.append(current_batch)
|
||||
current_batch = []
|
||||
|
||||
# Add remaining components
|
||||
if current_batch:
|
||||
batches.append(current_batch)
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Split components into intelligent batches for CI testing"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--components",
|
||||
"-c",
|
||||
required=True,
|
||||
help="JSON array of component names",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
"-b",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Target batch size (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tests-dir",
|
||||
type=Path,
|
||||
default=Path("tests/components"),
|
||||
help="Path to tests/components directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
choices=["json", "github"],
|
||||
default="github",
|
||||
help="Output format (json or github for GitHub Actions)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse component list from JSON
|
||||
try:
|
||||
components = json.loads(args.components)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing components JSON: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not isinstance(components, list):
|
||||
print("Components must be a JSON array", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Create intelligent batches
|
||||
batches = create_intelligent_batches(
|
||||
components=components,
|
||||
tests_dir=args.tests_dir,
|
||||
batch_size=args.batch_size,
|
||||
)
|
||||
|
||||
# Convert batches to space-separated strings for CI
|
||||
batch_strings = [" ".join(batch) for batch in batches]
|
||||
|
||||
if args.output == "json":
|
||||
# Output as JSON array
|
||||
print(json.dumps(batch_strings))
|
||||
else:
|
||||
# Output for GitHub Actions (set output)
|
||||
output_json = json.dumps(batch_strings)
|
||||
print(f"components={output_json}")
|
||||
|
||||
# Print summary to stderr so it shows in CI logs
|
||||
print("\n=== Intelligent Batch Summary ===", file=sys.stderr)
|
||||
print(f"Total components: {len(components)}", file=sys.stderr)
|
||||
print(f"Number of batches: {len(batches)}", file=sys.stderr)
|
||||
print(f"Batch size target: {args.batch_size}", file=sys.stderr)
|
||||
print(f"Average batch size: {len(components) / len(batches):.1f}", file=sys.stderr)
|
||||
print(file=sys.stderr)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
@@ -325,13 +325,15 @@ def test_components(
|
||||
print(f"Found {len(all_tests)} components to test")
|
||||
|
||||
# Group components by platform and bus signature
|
||||
grouped_components: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||
|
||||
if enable_grouping:
|
||||
print("Analyzing components for grouping opportunities...")
|
||||
print("\n" + "=" * 80)
|
||||
print("Analyzing components for intelligent grouping...")
|
||||
print("=" * 80)
|
||||
component_buses = analyze_all_components(tests_dir)
|
||||
|
||||
# Group by (platform, bus_signature)
|
||||
grouped_components: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||
|
||||
for component, platforms in component_buses.items():
|
||||
if component not in all_tests:
|
||||
continue
|
||||
@@ -346,11 +348,58 @@ def test_components(
|
||||
signature = create_grouping_signature({platform: buses}, platform)
|
||||
grouped_components[(platform, signature)].append(component)
|
||||
|
||||
# Print grouping analysis
|
||||
total_grouped = sum(
|
||||
len(comps) for comps in grouped_components.values() if len(comps) > 1
|
||||
# Print detailed grouping plan
|
||||
print("\nGrouping Plan:")
|
||||
print("-" * 80)
|
||||
|
||||
groups_to_test = []
|
||||
individual_tests = []
|
||||
|
||||
for (platform, signature), components in sorted(grouped_components.items()):
|
||||
if len(components) > 1:
|
||||
groups_to_test.append((platform, signature, components))
|
||||
elif len(components) == 1:
|
||||
individual_tests.extend(components)
|
||||
|
||||
# Add components without grouping signatures
|
||||
for component in all_tests:
|
||||
if (
|
||||
component not in [c for _, _, comps in groups_to_test for c in comps]
|
||||
and component not in individual_tests
|
||||
):
|
||||
individual_tests.append(component)
|
||||
|
||||
if groups_to_test:
|
||||
print(f"\n✓ {len(groups_to_test)} groups will be tested together:")
|
||||
for platform, signature, components in groups_to_test:
|
||||
component_list = ", ".join(sorted(components))
|
||||
print(f" [{platform}] [{signature}]: {component_list}")
|
||||
print(
|
||||
f" → {len(components)} components in 1 build (saves {len(components) - 1} builds)"
|
||||
)
|
||||
|
||||
if individual_tests:
|
||||
print(
|
||||
f"\n○ {len(individual_tests)} components will be tested individually:"
|
||||
)
|
||||
for comp in sorted(individual_tests[:10]):
|
||||
print(f" - {comp}")
|
||||
if len(individual_tests) > 10:
|
||||
print(f" ... and {len(individual_tests) - 10} more")
|
||||
|
||||
total_grouped = sum(len(comps) for _, _, comps in groups_to_test)
|
||||
total_builds_without_grouping = len(all_tests)
|
||||
total_builds_with_grouping = len(groups_to_test) + len(individual_tests)
|
||||
builds_saved = total_builds_without_grouping - total_builds_with_grouping
|
||||
|
||||
print(f"\n{'=' * 80}")
|
||||
print(f"Summary: {total_builds_with_grouping} builds total")
|
||||
print(f" • {len(groups_to_test)} grouped builds ({total_grouped} components)")
|
||||
print(f" • {len(individual_tests)} individual builds")
|
||||
print(
|
||||
f" • Saves {builds_saved} builds ({builds_saved / total_builds_without_grouping * 100:.1f}% reduction)"
|
||||
)
|
||||
print(f"Grouping analysis: {total_grouped} components can be grouped")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Run tests
|
||||
failed_tests = []
|
||||
|
Reference in New Issue
Block a user