mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +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 collections.abc import Callable | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Any | from typing import Any | ||||||
| from unittest.mock import patch |  | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from esphome import config, config_validation as cv, core, yaml_util | from esphome import config_validation as cv, core | ||||||
| from esphome.config import Config |  | ||||||
| from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES | 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 esphome.core.config import Area, validate_area_config | ||||||
|  |  | ||||||
|  | from .common import load_config_from_fixture | ||||||
|  |  | ||||||
| FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" | 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: | def test_validate_area_config_with_string() -> None: | ||||||
|     """Test that string area config is converted to structured format.""" |     """Test that string area config is converted to structured format.""" | ||||||
|     result = validate_area_config("Living Room") |     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: | def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None: | ||||||
|     """Test that device with valid area_id works correctly.""" |     """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 |     assert result is not None | ||||||
|  |  | ||||||
|     esphome_config = result["esphome"] |     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: | def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None: | ||||||
|     """Test multiple areas and devices configuration.""" |     """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 |     assert result is not None | ||||||
|  |  | ||||||
|     esphome_config = result["esphome"] |     esphome_config = result["esphome"] | ||||||
| @@ -129,7 +106,9 @@ def test_legacy_string_area( | |||||||
|     yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture |     yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test legacy string area configuration with deprecation warning.""" |     """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 |     assert result is not None | ||||||
|  |  | ||||||
|     esphome_config = result["esphome"] |     esphome_config = result["esphome"] | ||||||
| @@ -148,7 +127,7 @@ def test_area_id_collision( | |||||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that duplicate area IDs are detected.""" |     """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 |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the specific error message in stdout |     # 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: | def test_device_without_area(yaml_file: Callable[[str], str]) -> None: | ||||||
|     """Test that devices without area_id work correctly.""" |     """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 |     assert result is not None | ||||||
|  |  | ||||||
|     esphome_config = result["esphome"] |     esphome_config = result["esphome"] | ||||||
| @@ -181,7 +162,9 @@ def test_device_with_invalid_area_id( | |||||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that device with non-existent area_id fails validation.""" |     """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 |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the specific error message in stdout |     # 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] |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that device IDs with hash collisions are detected.""" |     """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 |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the specific error message about hash collision |     # 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] |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that area IDs with hash collisions are detected.""" |     """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 |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the specific error message about hash collision |     # 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] |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
| ) -> None: | ) -> None: | ||||||
|     """Test that duplicate device IDs are detected by IDPassValidationStep.""" |     """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 |     assert result is None | ||||||
|  |  | ||||||
|     # Check for the specific error message from IDPassValidationStep |     # Check for the specific error message from IDPassValidationStep | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| """Test get_base_entity_object_id function matches C++ behavior.""" | """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 | import re | ||||||
| from typing import Any | 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.cpp_generator import MockObj | ||||||
| from esphome.helpers import sanitize, snake_case | from esphome.helpers import sanitize, snake_case | ||||||
|  |  | ||||||
|  | from .common import load_config_from_yaml | ||||||
|  |  | ||||||
| # Pre-compiled regex pattern for extracting object IDs from expressions | # Pre-compiled regex pattern for extracting object IDs from expressions | ||||||
| OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') | OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') | ||||||
|  |  | ||||||
|  | FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture(autouse=True) | @pytest.fixture(autouse=True) | ||||||
| def restore_core_state() -> Generator[None, None, None]: | 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'", |         match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", | ||||||
|     ): |     ): | ||||||
|         validator(config3) |         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