1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-12 23:03:46 +01:00
Files
esphome/script/analyze_component_buses.py
J. Nick Koston 2b3c1a88ea fix
2025-10-08 15:23:55 -10:00

269 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Analyze component test files to detect which common bus configs they use.
This script scans component test files and extracts which common bus configurations
(i2c, spi, uart, etc.) are included via the packages mechanism. This information
is used to group components that can be tested together.
Components can only be grouped together if they use the EXACT SAME set of common
bus configurations, ensuring that merged configs are compatible.
Example output:
{
"component1": {
"esp32-ard": ["i2c", "uart_19200"],
"esp32-idf": ["i2c", "uart_19200"]
},
"component2": {
"esp32-ard": ["spi"],
"esp32-idf": ["spi"]
}
}
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import sys
# Path to common bus configs
COMMON_BUS_PATH = Path("tests/test_build_components/common")
# Valid common bus config directories
VALID_BUS_CONFIGS = {
"ble",
"i2c",
"i2c_low_freq",
"qspi",
"spi",
"uart",
"uart_115200",
"uart_1200",
"uart_1200_even",
"uart_19200",
"uart_38400",
"uart_4800",
"uart_4800_even",
"uart_9600_even",
}
def extract_common_buses(yaml_file: Path) -> set[str]:
"""Extract which common bus configs are included in a YAML test file.
Args:
yaml_file: Path to the component test YAML file
Returns:
Set of common bus config names (e.g., {'i2c', 'uart_19200'})
"""
if not yaml_file.exists():
return set()
try:
content = yaml_file.read_text()
except Exception:
return set()
buses = set()
# Pattern to match package includes for common bus configs
# Matches: !include ../../test_build_components/common/{bus}/{platform}.yaml
pattern = r"!include\s+\.\./\.\./test_build_components/common/([^/]+)/"
for match in re.finditer(pattern, content):
bus_name = match.group(1)
if bus_name in VALID_BUS_CONFIGS:
buses.add(bus_name)
return buses
def analyze_component(component_dir: Path) -> dict[str, list[str]]:
"""Analyze a component directory to find which buses each platform uses.
Args:
component_dir: Path to the component's test directory
Returns:
Dictionary mapping platform to list of bus configs
Example: {"esp32-ard": ["i2c", "spi"], "esp32-idf": ["i2c"]}
"""
if not component_dir.is_dir():
return {}
platform_buses = {}
# Find all test.*.yaml files
for test_file in component_dir.glob("test.*.yaml"):
# Extract platform name from filename (e.g., test.esp32-ard.yaml -> esp32-ard)
platform = test_file.stem.replace("test.", "")
buses = extract_common_buses(test_file)
if buses:
# Sort for consistent comparison
platform_buses[platform] = sorted(buses)
return platform_buses
def analyze_all_components(tests_dir: Path = None) -> dict[str, dict[str, list[str]]]:
"""Analyze all component test directories.
Args:
tests_dir: Path to tests/components directory (defaults to auto-detect)
Returns:
Dictionary mapping component name to platform->buses mapping
"""
if tests_dir is None:
tests_dir = Path("tests/components")
if not tests_dir.exists():
print(f"Error: {tests_dir} does not exist", file=sys.stderr)
return {}
components = {}
for component_dir in sorted(tests_dir.iterdir()):
if not component_dir.is_dir():
continue
component_name = component_dir.name
platform_buses = analyze_component(component_dir)
if platform_buses:
components[component_name] = platform_buses
return components
def create_grouping_signature(
platform_buses: dict[str, list[str]], platform: str
) -> str:
"""Create a signature string for grouping components.
Components with the same signature can be grouped together for testing.
Args:
platform_buses: Mapping of platform to list of buses
platform: The specific platform to create signature for
Returns:
Signature string (e.g., "i2c+uart_19200" or "spi")
"""
buses = platform_buses.get(platform, [])
if not buses:
return ""
return "+".join(sorted(buses))
def group_components_by_signature(
components: dict[str, dict[str, list[str]]], platform: str
) -> dict[str, list[str]]:
"""Group components by their bus signature for a specific platform.
Args:
components: Component analysis results from analyze_all_components()
platform: Platform to group for (e.g., "esp32-ard")
Returns:
Dictionary mapping signature to list of component names
Example: {"i2c+uart_19200": ["comp1", "comp2"], "spi": ["comp3"]}
"""
signature_groups: dict[str, list[str]] = {}
for component_name, platform_buses in components.items():
if platform not in platform_buses:
continue
signature = create_grouping_signature(platform_buses, platform)
if not signature:
continue
if signature not in signature_groups:
signature_groups[signature] = []
signature_groups[signature].append(component_name)
return signature_groups
def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Analyze component test files to detect common bus usage"
)
parser.add_argument(
"--components",
"-c",
nargs="+",
help="Specific components to analyze (default: all)",
)
parser.add_argument(
"--platform",
"-p",
help="Show grouping for a specific platform",
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
parser.add_argument(
"--group",
action="store_true",
help="Show component groupings by bus signature",
)
args = parser.parse_args()
# Analyze components
tests_dir = Path("tests/components")
if args.components:
# Analyze only specified components
components = {}
for comp in args.components:
comp_dir = tests_dir / comp
platform_buses = analyze_component(comp_dir)
if platform_buses:
components[comp] = platform_buses
else:
# Analyze all components
components = analyze_all_components(tests_dir)
# Output results
if args.group and args.platform:
# Show groupings for a specific platform
groups = group_components_by_signature(components, args.platform)
if args.json:
print(json.dumps(groups, indent=2))
else:
print(f"Component groupings for {args.platform}:")
print()
for signature, comp_list in sorted(groups.items()):
print(f" {signature}:")
for comp in sorted(comp_list):
print(f" - {comp}")
print()
elif args.json:
# JSON output
print(json.dumps(components, indent=2))
else:
# Human-readable output
for component, platform_buses in sorted(components.items()):
print(f"{component}:")
for platform, buses in sorted(platform_buses.items()):
bus_str = ", ".join(buses)
print(f" {platform}: {bus_str}")
print()
print(f"Total components analyzed: {len(components)}")
if __name__ == "__main__":
main()