From 2c35d91a66dc6546e0d0d099bd5505bde58f82dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Oct 2025 15:45:59 -1000 Subject: [PATCH] fix --- script/merge_component_configs.py | 60 +++++++++++++++++++++++++---- script/test_build_components.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 038a996c85..94c7720079 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -102,15 +102,59 @@ def prefix_substitutions_in_dict( return data +def deduplicate_by_id(data: dict) -> dict: + """Deduplicate list items with the same ID. + + Keeps only the first occurrence of each ID. If items with the same ID + are identical, this silently deduplicates. If they differ, the first + one is kept (ESPHome's validation will catch if this causes issues). + + Args: + data: Parsed config dictionary + + Returns: + Config with deduplicated lists + """ + if not isinstance(data, dict): + return data + + result = {} + for key, value in data.items(): + if isinstance(value, list): + # Check for items with 'id' field + seen_ids = set() + deduped_list = [] + + for item in value: + if isinstance(item, dict) and "id" in item: + item_id = item["id"] + if item_id not in seen_ids: + seen_ids.add(item_id) + deduped_list.append(item) + # else: skip duplicate ID (keep first occurrence) + else: + # No ID, just add it + deduped_list.append(item) + + result[key] = deduped_list + elif isinstance(value, dict): + # Recursively deduplicate nested dicts + result[key] = deduplicate_by_id(value) + else: + result[key] = value + + return result + + def prefix_ids_in_dict(data: Any, prefix: str) -> Any: - """Recursively prefix all 'id' fields in a data structure. + """Recursively prefix all 'id' fields and ID references in a data structure. Args: data: YAML data structure (dict, list, or scalar) prefix: Prefix to add to IDs Returns: - Data structure with prefixed IDs + Data structure with prefixed IDs and ID references """ if isinstance(data, dict): result = {} @@ -118,6 +162,9 @@ def prefix_ids_in_dict(data: Any, prefix: str) -> Any: if key == "id" and isinstance(value, str): # Prefix the ID value result[key] = f"{prefix}_{value}" + elif key.endswith("_id") and isinstance(value, str): + # Prefix ID references (uart_id, spi_id, i2c_id, etc.) + result[key] = f"{prefix}_{value}" else: # Recursively process the value result[key] = prefix_ids_in_dict(value, prefix) @@ -186,12 +233,8 @@ def merge_component_configs( # Prefix substitution references throughout the config comp_data = prefix_substitutions_in_dict(comp_data, comp_name) - # Note: We don't prefix IDs because that requires updating all ID references - # throughout the config, which is complex. Instead, we rely on components - # already having unique IDs (which they should if properly designed). - # ESPHome's merge_config will handle ID conflicts by replacing duplicates. - # Use ESPHome's merge_config to merge this component into the result + # merge_config handles list merging with ID-based deduplication automatically merged_config_data = merge_config(merged_config_data, comp_data) # Add packages back (only once, since they're identical) @@ -205,6 +248,9 @@ def merge_component_configs( if "packages" in first_comp_data: merged_config_data["packages"] = first_comp_data["packages"] + # Deduplicate items with same ID (keeps first occurrence) + merged_config_data = deduplicate_by_id(merged_config_data) + # Write merged config output_file.parent.mkdir(parents=True, exist_ok=True) yaml_content = yaml_util.dump(merged_config_data) diff --git a/script/test_build_components.py b/script/test_build_components.py index 31aa22adec..2ad37ce925 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -355,7 +355,61 @@ def test_components( # Run tests failed_tests = [] passed_tests = [] + tested_components = set() # Track which components were tested in groups + # First, run grouped tests if grouping is enabled + if enable_grouping: + for (platform, signature), components in grouped_components.items(): + # Only group if we have multiple components with same signature + if len(components) <= 1: + continue + + # Filter out components not in our test list + components_to_group = [c for c in components if c in all_tests] + if len(components_to_group) <= 1: + continue + + # Get platform base files + if platform not in platform_bases: + continue + + for base_file in platform_bases[platform]: + platform_with_version = extract_platform_with_version(base_file) + + # Skip if platform filter doesn't match + if platform_filter and platform != platform_filter: + continue + if ( + platform_filter + and platform_with_version != platform_filter + and not platform_with_version.startswith(f"{platform_filter}-") + ): + continue + + # Run grouped test + success = run_grouped_test( + components=components_to_group, + platform=platform, + platform_with_version=platform_with_version, + base_file=base_file, + build_dir=build_dir, + tests_dir=tests_dir, + esphome_command=esphome_command, + continue_on_fail=continue_on_fail, + ) + + # Mark all components as tested + for comp in components_to_group: + tested_components.add((comp, platform_with_version)) + + # Record result for each component + test_id = f"GROUPED[{','.join(components_to_group[:3])}{'...' if len(components_to_group) > 3 else ''}].{platform_with_version}" + if success: + passed_tests.append(test_id) + else: + failed_tests.append(test_id) + + # Then run individual tests for components not in groups for component, test_files in sorted(all_tests.items()): for test_file in test_files: test_name, platform = parse_test_filename(test_file) @@ -369,6 +423,11 @@ def test_components( for base_file in base_files: platform_with_version = extract_platform_with_version(base_file) + + # Skip if already tested in a group + if (component, platform_with_version) in tested_components: + continue + success = run_esphome_test( component=component, test_file=test_file, @@ -404,6 +463,10 @@ def test_components( ): continue + # Skip if already tested in a group + if (component, platform_with_version) in tested_components: + continue + success = run_esphome_test( component=component, test_file=test_file,