mirror of
https://github.com/esphome/esphome.git
synced 2025-10-12 23:03:46 +01:00
325 lines
9.8 KiB
Python
325 lines
9.8 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
|
|
from functools import lru_cache
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
# Path to common bus configs
|
|
COMMON_BUS_PATH = Path("tests/test_build_components/common")
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_common_bus_packages() -> frozenset[str]:
|
|
"""Get the list of common bus package names.
|
|
|
|
Reads from tests/test_build_components/common/ directory
|
|
and caches the result. All bus types support component grouping
|
|
for config validation since --testing-mode bypasses runtime conflicts.
|
|
|
|
Returns:
|
|
Frozenset of common bus package names (i2c, spi, uart, etc.)
|
|
"""
|
|
if not COMMON_BUS_PATH.exists():
|
|
return frozenset()
|
|
|
|
# List all directories in common/ - these are the bus package names
|
|
return frozenset(d.name for d in COMMON_BUS_PATH.iterdir() if d.is_dir())
|
|
|
|
|
|
def uses_local_file_references(component_dir: Path) -> bool:
|
|
"""Check if a component uses local file references via $component_dir.
|
|
|
|
Components that reference local files cannot be grouped because each needs
|
|
a unique component_dir path pointing to their specific directory.
|
|
|
|
Args:
|
|
component_dir: Path to the component's test directory
|
|
|
|
Returns:
|
|
True if the component uses $component_dir for local file references
|
|
"""
|
|
common_yaml = component_dir / "common.yaml"
|
|
if not common_yaml.exists():
|
|
return False
|
|
|
|
try:
|
|
content = common_yaml.read_text()
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
return False
|
|
|
|
# Pattern to match $component_dir or ${component_dir} references
|
|
# These indicate local file usage that prevents grouping
|
|
return bool(re.search(r"\$\{?component_dir\}?", content))
|
|
|
|
|
|
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: # pylint: disable=broad-exception-caught
|
|
return set()
|
|
|
|
buses = set()
|
|
valid_buses = get_common_bus_packages()
|
|
|
|
# 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_buses:
|
|
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,
|
|
) -> tuple[dict[str, dict[str, list[str]]], set[str]]:
|
|
"""Analyze all component test directories.
|
|
|
|
Args:
|
|
tests_dir: Path to tests/components directory (defaults to auto-detect)
|
|
|
|
Returns:
|
|
Tuple of:
|
|
- Dictionary mapping component name to platform->buses mapping
|
|
- Set of component names that use local files (cannot be grouped)
|
|
"""
|
|
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 {}, set()
|
|
|
|
components = {}
|
|
non_groupable = set()
|
|
|
|
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
|
|
|
|
# Check if component uses local file references
|
|
if uses_local_file_references(component_dir):
|
|
non_groupable.add(component_name)
|
|
|
|
return components, non_groupable
|
|
|
|
|
|
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.
|
|
All valid bus types can be grouped since --testing-mode bypasses runtime
|
|
conflicts during config validation.
|
|
|
|
Args:
|
|
platform_buses: Mapping of platform to list of buses
|
|
platform: The specific platform to create signature for
|
|
|
|
Returns:
|
|
Signature string (e.g., "i2c" or "uart") or empty if no valid buses
|
|
"""
|
|
buses = platform_buses.get(platform, [])
|
|
if not buses:
|
|
return ""
|
|
|
|
# Only include valid bus types in signature
|
|
common_buses = get_common_bus_packages()
|
|
valid_buses = [b for b in buses if b in common_buses]
|
|
if not valid_buses:
|
|
return ""
|
|
|
|
return "+".join(sorted(valid_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 = {}
|
|
non_groupable = set()
|
|
for comp in args.components:
|
|
comp_dir = tests_dir / comp
|
|
platform_buses = analyze_component(comp_dir)
|
|
if platform_buses:
|
|
components[comp] = platform_buses
|
|
if uses_local_file_references(comp_dir):
|
|
non_groupable.add(comp)
|
|
else:
|
|
# Analyze all components
|
|
components, non_groupable = 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()):
|
|
non_groupable_marker = (
|
|
" [NON-GROUPABLE]" if component in non_groupable else ""
|
|
)
|
|
print(f"{component}{non_groupable_marker}:")
|
|
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 non_groupable:
|
|
print(f"Non-groupable components (use local files): {len(non_groupable)}")
|
|
for comp in sorted(non_groupable):
|
|
print(f" - {comp}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|