1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-31 07:03:55 +00:00
This commit is contained in:
J. Nick Koston
2025-06-24 23:22:18 +02:00
parent 602456db40
commit 192158ef1a
4 changed files with 166 additions and 38 deletions

View File

View 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)

View File

@@ -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

View File

@@ -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