mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-29 22:24:26 +00:00 
			
		
		
		
	cleanup
This commit is contained in:
		
							
								
								
									
										0
									
								
								tests/unit_tests/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/unit_tests/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								tests/unit_tests/core/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tests/unit_tests/core/common.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| """Common test utilities for core unit tests.""" | ||||
|  | ||||
| from collections.abc import Callable | ||||
| from pathlib import Path | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from esphome import config, yaml_util | ||||
| from esphome.config import Config | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| def load_config_from_yaml( | ||||
|     yaml_file: Callable[[str], str], yaml_content: str | ||||
| ) -> Config | None: | ||||
|     """Load configuration from YAML content.""" | ||||
|     yaml_path = yaml_file(yaml_content) | ||||
|     parsed_yaml = yaml_util.load_yaml(yaml_path) | ||||
|  | ||||
|     # Mock yaml_util.load_yaml to return our parsed content | ||||
|     with ( | ||||
|         patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), | ||||
|         patch.object(CORE, "config_path", yaml_path), | ||||
|     ): | ||||
|         return config.read_config({}) | ||||
|  | ||||
|  | ||||
| def load_config_from_fixture( | ||||
|     yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path | ||||
| ) -> Config | None: | ||||
|     """Load configuration from a fixture file.""" | ||||
|     fixture_path = fixtures_dir / fixture_name | ||||
|     yaml_content = fixture_path.read_text() | ||||
|     return load_config_from_yaml(yaml_file, yaml_content) | ||||
| @@ -3,43 +3,18 @@ | ||||
| from collections.abc import Callable | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| from unittest.mock import patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome import config, config_validation as cv, core, yaml_util | ||||
| from esphome.config import Config | ||||
| from esphome import config_validation as cv, core | ||||
| from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES | ||||
| from esphome.core import CORE | ||||
| from esphome.core.config import Area, validate_area_config | ||||
|  | ||||
| from .common import load_config_from_fixture | ||||
|  | ||||
| FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" | ||||
|  | ||||
|  | ||||
| def load_config_from_yaml( | ||||
|     yaml_file: Callable[[str], str], yaml_content: str | ||||
| ) -> Config | None: | ||||
|     """Load configuration from YAML content.""" | ||||
|     yaml_path = yaml_file(yaml_content) | ||||
|     parsed_yaml = yaml_util.load_yaml(yaml_path) | ||||
|  | ||||
|     # Mock yaml_util.load_yaml to return our parsed content | ||||
|     with ( | ||||
|         patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), | ||||
|         patch.object(CORE, "config_path", yaml_path), | ||||
|     ): | ||||
|         return config.read_config({}) | ||||
|  | ||||
|  | ||||
| def load_config_from_fixture( | ||||
|     yaml_file: Callable[[str], str], fixture_name: str | ||||
| ) -> Config | None: | ||||
|     """Load configuration from a fixture file.""" | ||||
|     fixture_path = FIXTURES_DIR / fixture_name | ||||
|     yaml_content = fixture_path.read_text() | ||||
|     return load_config_from_yaml(yaml_file, yaml_content) | ||||
|  | ||||
|  | ||||
| def test_validate_area_config_with_string() -> None: | ||||
|     """Test that string area config is converted to structured format.""" | ||||
|     result = validate_area_config("Living Room") | ||||
| @@ -70,7 +45,7 @@ def test_validate_area_config_with_dict() -> None: | ||||
|  | ||||
| def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None: | ||||
|     """Test that device with valid area_id works correctly.""" | ||||
|     result = load_config_from_fixture(yaml_file, "valid_area_device.yaml") | ||||
|     result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR) | ||||
|     assert result is not None | ||||
|  | ||||
|     esphome_config = result["esphome"] | ||||
| @@ -93,7 +68,9 @@ def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None: | ||||
|  | ||||
| def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None: | ||||
|     """Test multiple areas and devices configuration.""" | ||||
|     result = load_config_from_fixture(yaml_file, "multiple_areas_devices.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "multiple_areas_devices.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is not None | ||||
|  | ||||
|     esphome_config = result["esphome"] | ||||
| @@ -129,7 +106,9 @@ def test_legacy_string_area( | ||||
|     yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture | ||||
| ) -> None: | ||||
|     """Test legacy string area configuration with deprecation warning.""" | ||||
|     result = load_config_from_fixture(yaml_file, "legacy_string_area.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "legacy_string_area.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is not None | ||||
|  | ||||
|     esphome_config = result["esphome"] | ||||
| @@ -148,7 +127,7 @@ def test_area_id_collision( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that duplicate area IDs are detected.""" | ||||
|     result = load_config_from_fixture(yaml_file, "area_id_collision.yaml") | ||||
|     result = load_config_from_fixture(yaml_file, "area_id_collision.yaml", FIXTURES_DIR) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the specific error message in stdout | ||||
| @@ -159,7 +138,9 @@ def test_area_id_collision( | ||||
|  | ||||
| def test_device_without_area(yaml_file: Callable[[str], str]) -> None: | ||||
|     """Test that devices without area_id work correctly.""" | ||||
|     result = load_config_from_fixture(yaml_file, "device_without_area.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "device_without_area.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is not None | ||||
|  | ||||
|     esphome_config = result["esphome"] | ||||
| @@ -181,7 +162,9 @@ def test_device_with_invalid_area_id( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that device with non-existent area_id fails validation.""" | ||||
|     result = load_config_from_fixture(yaml_file, "device_invalid_area.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "device_invalid_area.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the specific error message in stdout | ||||
| @@ -196,7 +179,9 @@ def test_device_id_hash_collision( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that device IDs with hash collisions are detected.""" | ||||
|     result = load_config_from_fixture(yaml_file, "device_id_collision.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "device_id_collision.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the specific error message about hash collision | ||||
| @@ -212,7 +197,9 @@ def test_area_id_hash_collision( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that area IDs with hash collisions are detected.""" | ||||
|     result = load_config_from_fixture(yaml_file, "area_id_hash_collision.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "area_id_hash_collision.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the specific error message about hash collision | ||||
| @@ -228,7 +215,9 @@ def test_device_duplicate_id( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that duplicate device IDs are detected by IDPassValidationStep.""" | ||||
|     result = load_config_from_fixture(yaml_file, "device_duplicate_id.yaml") | ||||
|     result = load_config_from_fixture( | ||||
|         yaml_file, "device_duplicate_id.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the specific error message from IDPassValidationStep | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| """Test get_base_entity_object_id function matches C++ behavior.""" | ||||
|  | ||||
| from collections.abc import Generator | ||||
| from collections.abc import Callable, Generator | ||||
| from pathlib import Path | ||||
| import re | ||||
| from typing import Any | ||||
|  | ||||
| @@ -13,9 +14,13 @@ from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.helpers import sanitize, snake_case | ||||
|  | ||||
| from .common import load_config_from_yaml | ||||
|  | ||||
| # Pre-compiled regex pattern for extracting object IDs from expressions | ||||
| OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') | ||||
|  | ||||
| FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" | ||||
|  | ||||
|  | ||||
| @pytest.fixture(autouse=True) | ||||
| def restore_core_state() -> Generator[None, None, None]: | ||||
| @@ -548,3 +553,104 @@ def test_entity_duplicate_validator_with_devices() -> None: | ||||
|         match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", | ||||
|     ): | ||||
|         validator(config3) | ||||
|  | ||||
|  | ||||
| def test_duplicate_entity_yaml_validation( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that duplicate entity names are caught during YAML config validation.""" | ||||
|     yaml_content = """ | ||||
| esphome: | ||||
|   name: test-duplicate | ||||
|  | ||||
| esp32: | ||||
|   board: esp32dev | ||||
|  | ||||
| sensor: | ||||
|   - platform: template | ||||
|     name: "Temperature" | ||||
|     lambda: return 21.0; | ||||
|   - platform: template | ||||
|     name: "Temperature"  # Duplicate - should fail | ||||
|     lambda: return 22.0; | ||||
| """ | ||||
|     result = load_config_from_yaml(yaml_file, yaml_content) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the duplicate entity error message | ||||
|     captured = capsys.readouterr() | ||||
|     assert "Duplicate sensor entity with name 'Temperature' found" in captured.out | ||||
|  | ||||
|  | ||||
| def test_duplicate_entity_with_devices_yaml_validation( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test duplicate entity validation with devices.""" | ||||
|     yaml_content = """ | ||||
| esphome: | ||||
|   name: test-duplicate-devices | ||||
|   devices: | ||||
|     - id: device1 | ||||
|       name: "Device 1" | ||||
|     - id: device2 | ||||
|       name: "Device 2" | ||||
|  | ||||
| esp32: | ||||
|   board: esp32dev | ||||
|  | ||||
| sensor: | ||||
|   # Same name on different devices - should pass | ||||
|   - platform: template | ||||
|     device_id: device1 | ||||
|     name: "Temperature" | ||||
|     lambda: return 21.0; | ||||
|   - platform: template | ||||
|     device_id: device2 | ||||
|     name: "Temperature" | ||||
|     lambda: return 22.0; | ||||
|   # Duplicate on same device - should fail | ||||
|   - platform: template | ||||
|     device_id: device1 | ||||
|     name: "Temperature" | ||||
|     lambda: return 23.0; | ||||
| """ | ||||
|     result = load_config_from_yaml(yaml_file, yaml_content) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the duplicate entity error message with device | ||||
|     captured = capsys.readouterr() | ||||
|     assert ( | ||||
|         "Duplicate sensor entity with name 'Temperature' found on device 'device1'" | ||||
|         in captured.out | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_entity_different_platforms_yaml_validation( | ||||
|     yaml_file: Callable[[str], str], | ||||
| ) -> None: | ||||
|     """Test that same entity name on different platforms is allowed.""" | ||||
|     yaml_content = """ | ||||
| esphome: | ||||
|   name: test-different-platforms | ||||
|  | ||||
| esp32: | ||||
|   board: esp32dev | ||||
|  | ||||
| sensor: | ||||
|   - platform: template | ||||
|     name: "Status" | ||||
|     lambda: return 1.0; | ||||
|  | ||||
| binary_sensor: | ||||
|   - platform: template | ||||
|     name: "Status"  # Same name, different platform - should pass | ||||
|     lambda: return true; | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     name: "Status"  # Same name, different platform - should pass | ||||
|     lambda: return {"OK"}; | ||||
| """ | ||||
|     result = load_config_from_yaml(yaml_file, yaml_content) | ||||
|     # This should succeed | ||||
|     assert result is not None | ||||
|   | ||||
		Reference in New Issue
	
	Block a user