mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	
							
								
								
									
										125
									
								
								tests/unit_tests/test_substitutions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								tests/unit_tests/test_substitutions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| import glob | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| from esphome import yaml_util | ||||
| from esphome.components import substitutions | ||||
| from esphome.const import CONF_PACKAGES | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # Set to True for dev mode behavior | ||||
| # This will generate the expected version of the test files. | ||||
|  | ||||
| DEV_MODE = False | ||||
|  | ||||
|  | ||||
| def sort_dicts(obj): | ||||
|     """Recursively sort dictionaries for order-insensitive comparison.""" | ||||
|     if isinstance(obj, dict): | ||||
|         return {k: sort_dicts(obj[k]) for k in sorted(obj)} | ||||
|     elif isinstance(obj, list): | ||||
|         # Lists are not sorted; we preserve order | ||||
|         return [sort_dicts(i) for i in obj] | ||||
|     else: | ||||
|         return obj | ||||
|  | ||||
|  | ||||
| def dict_diff(a, b, path=""): | ||||
|     """Recursively find differences between two dict/list structures.""" | ||||
|     diffs = [] | ||||
|     if isinstance(a, dict) and isinstance(b, dict): | ||||
|         a_keys = set(a) | ||||
|         b_keys = set(b) | ||||
|         for key in a_keys - b_keys: | ||||
|             diffs.append(f"{path}/{key} only in actual") | ||||
|         for key in b_keys - a_keys: | ||||
|             diffs.append(f"{path}/{key} only in expected") | ||||
|         for key in a_keys & b_keys: | ||||
|             diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}")) | ||||
|     elif isinstance(a, list) and isinstance(b, list): | ||||
|         min_len = min(len(a), len(b)) | ||||
|         for i in range(min_len): | ||||
|             diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]")) | ||||
|         if len(a) > len(b): | ||||
|             for i in range(min_len, len(a)): | ||||
|                 diffs.append(f"{path}[{i}] only in actual: {a[i]!r}") | ||||
|         elif len(b) > len(a): | ||||
|             for i in range(min_len, len(b)): | ||||
|                 diffs.append(f"{path}[{i}] only in expected: {b[i]!r}") | ||||
|     else: | ||||
|         if a != b: | ||||
|             diffs.append(f"\t{path}: actual={a!r} expected={b!r}") | ||||
|     return diffs | ||||
|  | ||||
|  | ||||
| def write_yaml(path, data): | ||||
|     with open(path, "w", encoding="utf-8") as f: | ||||
|         f.write(yaml_util.dump(data)) | ||||
|  | ||||
|  | ||||
| def test_substitutions_fixtures(fixture_path): | ||||
|     base_dir = fixture_path / "substitutions" | ||||
|     sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) | ||||
|     assert sources, f"No input YAML files found in {base_dir}" | ||||
|  | ||||
|     failures = [] | ||||
|     for source_path in sources: | ||||
|         try: | ||||
|             expected_path = source_path.replace(".input.yaml", ".approved.yaml") | ||||
|             test_case = os.path.splitext(os.path.basename(source_path))[0].replace( | ||||
|                 ".input", "" | ||||
|             ) | ||||
|  | ||||
|             # Load using ESPHome's YAML loader | ||||
|             config = yaml_util.load_yaml(source_path) | ||||
|  | ||||
|             if CONF_PACKAGES in config: | ||||
|                 from esphome.components.packages import do_packages_pass | ||||
|  | ||||
|                 config = do_packages_pass(config) | ||||
|  | ||||
|             substitutions.do_substitution_pass(config, None) | ||||
|  | ||||
|             # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE | ||||
|             if os.path.isfile(expected_path): | ||||
|                 expected = yaml_util.load_yaml(expected_path) | ||||
|             elif DEV_MODE: | ||||
|                 expected = {} | ||||
|             else: | ||||
|                 assert os.path.isfile(expected_path), ( | ||||
|                     f"Expected file missing: {expected_path}" | ||||
|                 ) | ||||
|  | ||||
|             # Sort dicts only (not lists) for comparison | ||||
|             got_sorted = sort_dicts(config) | ||||
|             expected_sorted = sort_dicts(expected) | ||||
|  | ||||
|             if got_sorted != expected_sorted: | ||||
|                 diff = "\n".join(dict_diff(got_sorted, expected_sorted)) | ||||
|                 msg = ( | ||||
|                     f"Substitution result mismatch for {os.path.basename(source_path)}\n" | ||||
|                     f"Diff:\n{diff}\n\n" | ||||
|                     f"Got:      {got_sorted}\n" | ||||
|                     f"Expected: {expected_sorted}" | ||||
|                 ) | ||||
|                 # Write out the received file when test fails | ||||
|                 if DEV_MODE: | ||||
|                     received_path = os.path.join( | ||||
|                         os.path.dirname(source_path), f"{test_case}.received.yaml" | ||||
|                     ) | ||||
|                     write_yaml(received_path, config) | ||||
|                     print(msg) | ||||
|                     failures.append(msg) | ||||
|                 else: | ||||
|                     raise AssertionError(msg) | ||||
|         except Exception as err: | ||||
|             _LOGGER.error("Error in test file %s", source_path) | ||||
|             raise err | ||||
|  | ||||
|     if DEV_MODE and failures: | ||||
|         print(f"\n{len(failures)} substitution test case(s) failed.") | ||||
|  | ||||
|     if DEV_MODE: | ||||
|         _LOGGER.error("Tests passed, but Dev mode is enabled.") | ||||
|     assert not DEV_MODE  # make sure DEV_MODE is disabled after you are finished. | ||||
		Reference in New Issue
	
	Block a user