[core] Remove object_id RAM storage - no longer in hot path after #12627

This commit is contained in:
J. Nick Koston
2025-12-22 20:01:57 -10:00
parent 7944fe6993
commit 452246e1c5
8 changed files with 302 additions and 82 deletions
@@ -0,0 +1,76 @@
esphome:
name: fnv1-hash-object-id-test
platformio_options:
build_flags:
- "-DDEBUG"
on_boot:
- lambda: |-
using esphome::fnv1_hash_object_id;
// Test basic lowercase (hash matches Python fnv1_hash_object_id("foo"))
uint32_t hash_foo = fnv1_hash_object_id("foo", 3);
if (hash_foo == 0x408f5e13) {
ESP_LOGI("FNV1_OID", "foo PASSED");
} else {
ESP_LOGE("FNV1_OID", "foo FAILED: 0x%08x != 0x408f5e13", hash_foo);
}
// Test uppercase conversion (should match lowercase)
uint32_t hash_Foo = fnv1_hash_object_id("Foo", 3);
if (hash_Foo == 0x408f5e13) {
ESP_LOGI("FNV1_OID", "upper PASSED");
} else {
ESP_LOGE("FNV1_OID", "upper FAILED: 0x%08x != 0x408f5e13", hash_Foo);
}
// Test space to underscore conversion ("foo bar" -> "foo_bar")
uint32_t hash_space = fnv1_hash_object_id("foo bar", 7);
if (hash_space == 0x3ae35aa1) {
ESP_LOGI("FNV1_OID", "space PASSED");
} else {
ESP_LOGE("FNV1_OID", "space FAILED: 0x%08x != 0x3ae35aa1", hash_space);
}
// Test underscore preserved ("foo_bar")
uint32_t hash_underscore = fnv1_hash_object_id("foo_bar", 7);
if (hash_underscore == 0x3ae35aa1) {
ESP_LOGI("FNV1_OID", "underscore PASSED");
} else {
ESP_LOGE("FNV1_OID", "underscore FAILED: 0x%08x != 0x3ae35aa1", hash_underscore);
}
// Test hyphen preserved ("foo-bar")
uint32_t hash_hyphen = fnv1_hash_object_id("foo-bar", 7);
if (hash_hyphen == 0x438b12e3) {
ESP_LOGI("FNV1_OID", "hyphen PASSED");
} else {
ESP_LOGE("FNV1_OID", "hyphen FAILED: 0x%08x != 0x438b12e3", hash_hyphen);
}
// Test special chars become underscore ("foo!bar" -> "foo_bar")
uint32_t hash_special = fnv1_hash_object_id("foo!bar", 7);
if (hash_special == 0x3ae35aa1) {
ESP_LOGI("FNV1_OID", "special PASSED");
} else {
ESP_LOGE("FNV1_OID", "special FAILED: 0x%08x != 0x3ae35aa1", hash_special);
}
// Test complex name ("My Sensor Name" -> "my_sensor_name")
uint32_t hash_complex = fnv1_hash_object_id("My Sensor Name", 14);
if (hash_complex == 0x2760962a) {
ESP_LOGI("FNV1_OID", "complex PASSED");
} else {
ESP_LOGE("FNV1_OID", "complex FAILED: 0x%08x != 0x2760962a", hash_complex);
}
// Test empty string returns FNV1_OFFSET_BASIS
uint32_t hash_empty = fnv1_hash_object_id("", 0);
if (hash_empty == 0x811c9dc5) {
ESP_LOGI("FNV1_OID", "empty PASSED");
} else {
ESP_LOGE("FNV1_OID", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty);
}
host:
api:
logger:
@@ -0,0 +1,75 @@
"""Integration test for fnv1_hash_object_id function.
This test verifies that the C++ fnv1_hash_object_id() function in
esphome/core/helpers.h produces the same hash values as the Python
fnv1_hash_object_id() function in esphome/helpers.py.
If this test fails, one of the implementations has diverged and needs
to be updated to match the other.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_fnv1_hash_object_id(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that C++ fnv1_hash_object_id matches Python implementation."""
test_results: dict[str, str] = {}
all_tests_complete = asyncio.Event()
expected_tests = {
"foo",
"upper",
"space",
"underscore",
"hyphen",
"special",
"complex",
"empty",
}
def on_log_line(line: str) -> None:
"""Capture log lines with test results."""
# Strip ANSI escape codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
# Look for our test result messages
# Format: "[timestamp][level][FNV1_OID:line]: test_name PASSED"
match = re.search(r"\[FNV1_OID:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line)
if match:
test_name = match.group(1)
result = match.group(2)
test_results[test_name] = result
if set(test_results.keys()) >= expected_tests:
all_tests_complete.set()
async with (
run_compiled(yaml_config, line_callback=on_log_line),
api_client_connected() as client,
):
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "fnv1-hash-object-id-test"
# Wait for all tests to complete or timeout
try:
await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0)
except TimeoutError:
pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}")
# Verify all tests passed
for test_name in expected_tests:
assert test_name in test_results, f"{test_name} test not found"
assert test_results[test_name] == "PASSED", (
f"{test_name} test failed - C++ and Python hash mismatch"
)
+71
View File
@@ -279,6 +279,77 @@ def test_sanitize(text, expected):
assert actual == expected
@pytest.mark.parametrize(
("name", "expected_hash"),
(
# Basic strings - hash of snake_case(sanitize(name))
("foo", 0x408F5E13),
("Foo", 0x408F5E13), # Same as "foo" (lowercase)
("FOO", 0x408F5E13), # Same as "foo" (lowercase)
# Spaces become underscores
("foo bar", 0x3AE35AA1), # Transforms to "foo_bar"
("Foo Bar", 0x3AE35AA1), # Same (lowercase + underscore)
# Already snake_case
("foo_bar", 0x3AE35AA1),
# Special chars become underscores
("foo!bar", 0x3AE35AA1), # Transforms to "foo_bar"
("foo@bar", 0x3AE35AA1), # Transforms to "foo_bar"
# Hyphens are preserved
("foo-bar", 0x438B12E3),
# Numbers are preserved
("foo123", 0xF3B0067D),
# Empty string
("", 0x811C9DC5), # FNV1_OFFSET_BASIS (no chars processed)
# Single char
("a", 0x050C5D7E),
# Mixed case and spaces
("My Sensor Name", 0x2760962A), # Transforms to "my_sensor_name"
),
)
def test_fnv1_hash_object_id(name, expected_hash):
"""Test fnv1_hash_object_id produces expected hashes.
These expected values were computed to match the C++ implementation
in esphome/core/helpers.h. If this test fails after modifying either
implementation, ensure both Python and C++ versions stay in sync.
"""
actual = helpers.fnv1_hash_object_id(name)
assert actual == expected_hash
def _fnv1_hash_py(s: str) -> int:
"""Python implementation of FNV-1 hash for verification."""
hash_val = 2166136261 # FNV1_OFFSET_BASIS
for c in s:
hash_val = (hash_val * 16777619) & 0xFFFFFFFF # FNV1_PRIME
hash_val ^= ord(c)
return hash_val
@pytest.mark.parametrize(
"name",
(
"Simple",
"With Space",
"MixedCase",
"special!@#chars",
"already_snake_case",
"123numbers",
),
)
def test_fnv1_hash_object_id_matches_manual_calculation(name):
"""Verify fnv1_hash_object_id matches snake_case + sanitize + standard FNV-1."""
# Manual calculation: snake_case -> sanitize -> fnv1_hash
transformed = helpers.sanitize(helpers.snake_case(name))
expected = _fnv1_hash_py(transformed)
# Direct calculation via fnv1_hash_object_id
actual = helpers.fnv1_hash_object_id(name)
assert actual == expected
@pytest.mark.parametrize(
"text, expected",
((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),),