dry up tests

This commit is contained in:
J. Nick Koston
2025-12-23 07:52:33 -10:00
parent 071e42d4e7
commit 8505a4dfaf
5 changed files with 174 additions and 148 deletions
+3
View File
@@ -51,6 +51,9 @@ if platform.system() == "Windows":
import pty # not available on Windows
# Register assert rewrite for entity_utils so assertions have proper error messages
pytest.register_assert_rewrite("tests.integration.entity_utils")
def _get_platformio_env(cache_dir: Path) -> dict[str, str]:
"""Get environment variables for PlatformIO with shared cache."""
+144
View File
@@ -0,0 +1,144 @@
"""Utilities for computing entity object_id in integration tests.
This module contains the algorithm that aioesphomeapi will use to compute
object_id client-side from API data.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
if TYPE_CHECKING:
from aioesphomeapi import DeviceInfo, EntityInfo
def compute_object_id(name: str) -> str:
"""Compute object_id from name using snake_case + sanitize."""
return sanitize(snake_case(name))
def infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool:
"""Infer name_add_mac_suffix from device name ending with MAC suffix."""
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
return device_info.name.endswith(f"-{mac_suffix}")
def compute_entity_object_id(
entity: EntityInfo,
device_info: DeviceInfo,
device_id_to_name: dict[int, str],
) -> str:
"""Compute expected object_id for an entity using the algorithm from PR summary.
This is the algorithm that aioesphomeapi will use to compute object_id
client-side from API data.
Args:
entity: The entity to compute object_id for
device_info: Device info from the API
device_id_to_name: Mapping of device_id to device name for sub-devices
Returns:
The computed object_id string
"""
name_add_mac_suffix = infer_name_add_mac_suffix(device_info)
if entity.name:
# Named entity: use entity name
name_for_id = entity.name
elif entity.device_id != 0:
# Empty name on sub-device: use sub-device name
name_for_id = device_id_to_name[entity.device_id]
elif name_add_mac_suffix:
# Empty name on main device with MAC suffix: use friendly_name directly
# (even if empty - this is bug-for-bug compatibility)
name_for_id = device_info.friendly_name
elif device_info.friendly_name:
# Empty name on main device with friendly_name set: use it
name_for_id = device_info.friendly_name
else:
# Empty name on main device, no friendly_name: use device name
name_for_id = device_info.name
return compute_object_id(name_for_id)
def compute_entity_hash(
entity: EntityInfo,
device_info: DeviceInfo,
device_id_to_name: dict[int, str],
) -> int:
"""Compute expected object_id hash for an entity.
Args:
entity: The entity to compute hash for
device_info: Device info from the API
device_id_to_name: Mapping of device_id to device name for sub-devices
Returns:
The computed FNV-1 hash
"""
name_add_mac_suffix = infer_name_add_mac_suffix(device_info)
if entity.name:
name_for_id = entity.name
elif entity.device_id != 0:
name_for_id = device_id_to_name[entity.device_id]
elif name_add_mac_suffix or device_info.friendly_name:
name_for_id = device_info.friendly_name
else:
name_for_id = device_info.name
return fnv1_hash_object_id(name_for_id)
def verify_entity_object_id(
entity: EntityInfo,
device_info: DeviceInfo,
device_id_to_name: dict[int, str],
) -> None:
"""Verify an entity's object_id and hash match the expected values.
Args:
entity: The entity to verify
device_info: Device info from the API
device_id_to_name: Mapping of device_id to device name for sub-devices
Raises:
AssertionError: If object_id or hash doesn't match expected value
"""
expected_object_id = compute_entity_object_id(
entity, device_info, device_id_to_name
)
assert entity.object_id == expected_object_id, (
f"object_id mismatch for entity '{entity.name}': "
f"expected '{expected_object_id}', got '{entity.object_id}'"
)
expected_hash = compute_entity_hash(entity, device_info, device_id_to_name)
assert entity.key == expected_hash, (
f"hash mismatch for entity '{entity.name}': "
f"expected {expected_hash:#x}, got {entity.key:#x}"
)
def verify_all_entities(
entities: list[EntityInfo],
device_info: DeviceInfo,
) -> None:
"""Verify all entities have correct object_id and hash values.
Args:
entities: List of entities to verify
device_info: Device info from the API
Raises:
AssertionError: If any entity's object_id or hash doesn't match
"""
# Build device_id -> name lookup from sub-devices
device_id_to_name = {d.device_id: d.name for d in device_info.devices}
for entity in entities:
verify_entity_object_id(entity, device_info, device_id_to_name)
@@ -25,8 +25,9 @@ from __future__ import annotations
import pytest
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
from esphome.helpers import fnv1_hash_object_id
from .entity_utils import compute_object_id, verify_all_entities
from .types import APIClientConnectedFactory, RunCompiledFunction
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
@@ -62,11 +63,6 @@ SUB_DEVICE_EMPTY_NAME_ENTITIES = [
]
def compute_expected_object_id(name: str) -> str:
"""Compute expected object_id from name using Python helpers."""
return sanitize(snake_case(name))
@pytest.mark.asyncio
async def test_object_id_api_verification(
yaml_config: str,
@@ -120,7 +116,7 @@ async def test_object_id_api_verification(
)
# Verify Python computation matches
computed = compute_expected_object_id(entity_name)
computed = compute_object_id(entity_name)
assert computed == expected_object_id, (
f"Entity '{entity_name}': Python computation mismatch. "
f"Computed '{computed}', expected '{expected_object_id}'"
@@ -160,7 +156,7 @@ async def test_object_id_api_verification(
)
expected_name = device_id_to_name[entity.device_id]
expected_object_id = compute_expected_object_id(expected_name)
expected_object_id = compute_object_id(expected_name)
assert entity.object_id == expected_object_id, (
f"Empty-name entity (device_id={entity.device_id}): object_id mismatch. "
f"API: '{entity.object_id}', expected: '{expected_object_id}' "
@@ -174,44 +170,7 @@ async def test_object_id_api_verification(
f"API key: {entity.key:#x}, expected: {expected_hash:#x}"
)
# === Test 3: Verify ALL entities can have object_id computed from API data ===
# This uses the algorithm from the PR summary that aioesphomeapi will use.
# Infer name_add_mac_suffix from device name ending with MAC suffix.
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
for entity in entities:
if entity.name:
# Named entity: use entity name
name_for_id = entity.name
elif entity.device_id != 0:
# Empty name on sub-device: use sub-device name
name_for_id = device_id_to_name[entity.device_id]
elif name_add_mac_suffix:
# Empty name on main device with MAC suffix: use friendly_name directly
# (even if empty - this is bug-for-bug compatibility)
name_for_id = device_info.friendly_name
elif device_info.friendly_name:
# Empty name on main device with friendly_name set: use it
name_for_id = device_info.friendly_name
else:
# Empty name on main device, no friendly_name: use device name
name_for_id = device_info.name
# Compute object_id from the appropriate name
computed_object_id = compute_expected_object_id(name_for_id)
# Verify it matches what the API returned
assert entity.object_id == computed_object_id, (
f"Entity (name='{entity.name}', device_id={entity.device_id}): "
f"object_id cannot be computed. "
f"API: '{entity.object_id}', Computed from '{name_for_id}': '{computed_object_id}'"
)
# Verify hash can also be computed
computed_hash = fnv1_hash_object_id(name_for_id)
assert entity.key == computed_hash, (
f"Entity (name='{entity.name}', device_id={entity.device_id}): "
f"hash cannot be computed. "
f"API key: {entity.key:#x}, Computed: {computed_hash:#x}"
)
# === Test 3: Verify ALL entities using the algorithm from entity_utils ===
# This uses the algorithm that aioesphomeapi will use to compute object_id
# client-side from API data.
verify_all_entities(entities, device_info)
@@ -11,16 +11,16 @@ from __future__ import annotations
import pytest
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
from esphome.helpers import fnv1_hash_object_id
from .entity_utils import (
compute_object_id,
infer_name_add_mac_suffix,
verify_all_entities,
)
from .types import APIClientConnectedFactory, RunCompiledFunction
def compute_expected_object_id(name: str) -> str:
"""Compute expected object_id from name using Python helpers."""
return sanitize(snake_case(name))
@pytest.mark.asyncio
async def test_object_id_friendly_name_no_mac_suffix(
yaml_config: str,
@@ -54,7 +54,7 @@ async def test_object_id_friendly_name_no_mac_suffix(
entity = empty_name_entities[0]
# Should use friendly_name for object_id (Branch 4)
expected_object_id = compute_expected_object_id("My Friendly Device")
expected_object_id = compute_object_id("My Friendly Device")
assert expected_object_id == "my_friendly_device" # Verify our expectation
assert entity.object_id == expected_object_id, (
f"Expected object_id '{expected_object_id}' from friendly_name, "
@@ -72,35 +72,10 @@ async def test_object_id_friendly_name_no_mac_suffix(
assert len(named_entities) == 1
assert named_entities[0].object_id == "temperature"
# Verify the full algorithm from PR summary works for ALL entities
# Infer name_add_mac_suffix from device name ending with MAC suffix.
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
# Verify our inference: no MAC suffix in this test
assert not name_add_mac_suffix, "Device name should NOT have MAC suffix"
assert not infer_name_add_mac_suffix(device_info), (
"Device name should NOT have MAC suffix"
)
for entity in entities:
if entity.name:
name_for_id = entity.name
elif name_add_mac_suffix:
# MAC suffix enabled: use friendly_name directly (even if empty)
name_for_id = device_info.friendly_name
elif device_info.friendly_name:
# Branch 4: No MAC suffix, but friendly_name is set
name_for_id = device_info.friendly_name
else:
# No MAC suffix, no friendly_name: use device name
name_for_id = device_info.name
computed_object_id = compute_expected_object_id(name_for_id)
assert entity.object_id == computed_object_id, (
f"Algorithm failed for entity '{entity.name}': "
f"expected '{computed_object_id}', got '{entity.object_id}'"
)
computed_hash = fnv1_hash_object_id(name_for_id)
assert entity.key == computed_hash, (
f"Algorithm hash failed for entity '{entity.name}': "
f"expected {computed_hash:#x}, got {entity.key:#x}"
)
# Verify the full algorithm from entity_utils works for ALL entities
verify_all_entities(entities, device_info)
@@ -17,8 +17,9 @@ from __future__ import annotations
import pytest
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
from esphome.helpers import fnv1_hash_object_id
from .entity_utils import compute_object_id, verify_all_entities
from .types import APIClientConnectedFactory, RunCompiledFunction
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
@@ -28,11 +29,6 @@ MAC_SUFFIX = "abf679"
FNV1_OFFSET_BASIS = 2166136261
def compute_expected_object_id(name: str) -> str:
"""Compute expected object_id from name using Python helpers."""
return sanitize(snake_case(name))
@pytest.mark.asyncio
async def test_object_id_no_friendly_name_with_mac_suffix(
yaml_config: str,
@@ -85,33 +81,8 @@ async def test_object_id_no_friendly_name_with_mac_suffix(
assert len(named_entities) == 1
assert named_entities[0].object_id == "temperature"
# Verify the full algorithm from PR summary works for ALL entities
# Infer name_add_mac_suffix from device name ending with MAC suffix.
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
for entity in entities:
if entity.name:
name_for_id = entity.name
elif name_add_mac_suffix:
# MAC suffix enabled: use friendly_name directly (even if empty)
name_for_id = device_info.friendly_name
elif device_info.friendly_name:
name_for_id = device_info.friendly_name
else:
name_for_id = device_info.name
computed_object_id = compute_expected_object_id(name_for_id)
assert entity.object_id == computed_object_id, (
f"Algorithm failed for entity '{entity.name}': "
f"expected '{computed_object_id}', got '{entity.object_id}'"
)
computed_hash = fnv1_hash_object_id(name_for_id)
assert entity.key == computed_hash, (
f"Algorithm hash failed for entity '{entity.name}': "
f"expected {computed_hash:#x}, got {entity.key:#x}"
)
# Verify the full algorithm from entity_utils works for ALL entities
verify_all_entities(entities, device_info)
@pytest.mark.asyncio
@@ -148,7 +119,7 @@ async def test_object_id_no_friendly_name_no_mac_suffix(
entity = empty_name_entities[0]
# OLD behavior: object_id was computed from device name
expected_object_id = compute_expected_object_id("test-device")
expected_object_id = compute_object_id("test-device")
assert entity.object_id == expected_object_id, (
f"Expected object_id '{expected_object_id}' from device name, "
f"got '{entity.object_id}'"
@@ -165,31 +136,5 @@ async def test_object_id_no_friendly_name_no_mac_suffix(
assert len(named_entities) == 1
assert named_entities[0].object_id == "temperature"
# Verify the full algorithm from PR summary works for ALL entities
# Infer name_add_mac_suffix from device name ending with MAC suffix.
mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower()
name_add_mac_suffix = device_info.name.endswith(f"-{mac_suffix}")
for entity in entities:
if entity.name:
name_for_id = entity.name
elif name_add_mac_suffix:
# MAC suffix enabled: use friendly_name directly (even if empty)
name_for_id = device_info.friendly_name
elif device_info.friendly_name:
name_for_id = device_info.friendly_name
else:
# No MAC suffix, no friendly_name: use device name
name_for_id = device_info.name
computed_object_id = compute_expected_object_id(name_for_id)
assert entity.object_id == computed_object_id, (
f"Algorithm failed for entity '{entity.name}': "
f"expected '{computed_object_id}', got '{entity.object_id}'"
)
computed_hash = fnv1_hash_object_id(name_for_id)
assert entity.key == computed_hash, (
f"Algorithm hash failed for entity '{entity.name}': "
f"expected {computed_hash:#x}, got {entity.key:#x}"
)
# Verify the full algorithm from entity_utils works for ALL entities
verify_all_entities(entities, device_info)