1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-12 14:53:49 +01:00
This commit is contained in:
J. Nick Koston
2025-10-08 15:50:49 -10:00
parent 2c35d91a66
commit 495bb4200c
3 changed files with 265 additions and 27 deletions

View File

@@ -381,17 +381,17 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: test_build_components -e config -c ${{ matrix.file }} - name: Validate config for ${{ matrix.file }}
run: | run: |
. venv/bin/activate . venv/bin/activate
./script/test_build_components -e config -c ${{ matrix.file }} python3 script/test_build_components.py -e config -c ${{ matrix.file }}
- name: test_build_components -e compile -c ${{ matrix.file }} - name: Compile config for ${{ matrix.file }}
run: | run: |
. venv/bin/activate . venv/bin/activate
./script/test_build_components -e compile -c ${{ matrix.file }} python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
test-build-components-splitter: test-build-components-splitter:
name: Split components for testing into 10 components per group name: Split components for intelligent grouping (10 per batch)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@@ -402,14 +402,26 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Split components into groups of 10 - name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split id: split
run: | run: |
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(10) | join(" ")]') . venv/bin/activate
echo "components=$components" >> $GITHUB_OUTPUT
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components }}'
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 10 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
name: Test split components name: Test components with intelligent grouping
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
@@ -437,20 +449,27 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config - name: Validate and compile components with intelligent grouping
run: | run: |
. venv/bin/activate . venv/bin/activate
for component in ${{ matrix.components }}; do mkdir -p build_cache
./script/test_build_components -e config -c $component
done
- name: Compile config
run: |
. venv/bin/activate
mkdir build_cache
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
for component in ${{ matrix.components }}; do
./script/test_build_components -e compile -c $component # Convert space-separated components to comma-separated for Python script
done components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
echo "Testing components: $components_csv"
echo ""
# Run config validation with grouping
python3 script/test_build_components.py -e config -c "$components_csv" -f
echo ""
echo "Config validation passed! Starting compilation..."
echo ""
# Run compilation with grouping
python3 script/test_build_components.py -e compile -c "$components_csv" -f
pre-commit-ci-lite: pre-commit-ci-lite:
name: pre-commit.ci lite name: pre-commit.ci lite

170
script/split_components_for_ci.py Executable file
View 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())

View File

@@ -325,13 +325,15 @@ def test_components(
print(f"Found {len(all_tests)} components to test") print(f"Found {len(all_tests)} components to test")
# Group components by platform and bus signature # Group components by platform and bus signature
grouped_components: dict[tuple[str, str], list[str]] = defaultdict(list)
if enable_grouping: 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) component_buses = analyze_all_components(tests_dir)
# Group by (platform, bus_signature) # Group by (platform, bus_signature)
grouped_components: dict[tuple[str, str], list[str]] = defaultdict(list)
for component, platforms in component_buses.items(): for component, platforms in component_buses.items():
if component not in all_tests: if component not in all_tests:
continue continue
@@ -346,11 +348,58 @@ def test_components(
signature = create_grouping_signature({platform: buses}, platform) signature = create_grouping_signature({platform: buses}, platform)
grouped_components[(platform, signature)].append(component) grouped_components[(platform, signature)].append(component)
# Print grouping analysis # Print detailed grouping plan
total_grouped = sum( print("\nGrouping Plan:")
len(comps) for comps in grouped_components.values() if len(comps) > 1 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)"
) )
print(f"Grouping analysis: {total_grouped} components can be grouped")
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("=" * 80 + "\n")
# Run tests # Run tests
failed_tests = [] failed_tests = []