mirror of
https://github.com/esphome/esphome.git
synced 2025-09-01 19:02:18 +01:00
[host] Fix memory allocation in preferences load() method
This commit is contained in:
@@ -42,9 +42,10 @@ class HostPreferences : public ESPPreferences {
|
||||
if (len > 255)
|
||||
return false;
|
||||
this->setup_();
|
||||
if (this->data.count(key) == 0)
|
||||
auto it = this->data.find(key);
|
||||
if (it == this->data.end())
|
||||
return false;
|
||||
auto vec = this->data[key];
|
||||
const auto &vec = it->second;
|
||||
if (vec.size() != len)
|
||||
return false;
|
||||
memcpy(data, vec.data(), len);
|
||||
|
104
tests/integration/fixtures/host_preferences_save_load.yaml
Normal file
104
tests/integration/fixtures/host_preferences_save_load.yaml
Normal file
@@ -0,0 +1,104 @@
|
||||
esphome:
|
||||
name: test_device
|
||||
on_boot:
|
||||
- lambda: |-
|
||||
ESP_LOGD("test", "Host preferences test starting");
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
|
||||
preferences:
|
||||
flash_write_interval: 0s # Disable automatic saving for test control
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "Test Switch"
|
||||
id: test_switch
|
||||
optimistic: true
|
||||
restore_mode: DISABLED # Don't auto-restore for test control
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "Test Number"
|
||||
id: test_number
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 0.1
|
||||
optimistic: true
|
||||
restore_value: false # Don't auto-restore for test control
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Save Preferences"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// Save current values to preferences
|
||||
ESPPreferenceObject switch_pref = global_preferences->make_preference<bool>(0x1234);
|
||||
ESPPreferenceObject number_pref = global_preferences->make_preference<float>(0x5678);
|
||||
|
||||
bool switch_value = id(test_switch).state;
|
||||
float number_value = id(test_number).state;
|
||||
|
||||
if (switch_pref.save(&switch_value)) {
|
||||
ESP_LOGI("test", "Preference saved: key=switch, value=%.1f", switch_value ? 1.0 : 0.0);
|
||||
}
|
||||
if (number_pref.save(&number_value)) {
|
||||
ESP_LOGI("test", "Preference saved: key=number, value=%.1f", number_value);
|
||||
}
|
||||
|
||||
// Force sync to disk
|
||||
global_preferences->sync();
|
||||
|
||||
- platform: template
|
||||
name: "Load Preferences"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// Load values from preferences
|
||||
ESPPreferenceObject switch_pref = global_preferences->make_preference<bool>(0x1234);
|
||||
ESPPreferenceObject number_pref = global_preferences->make_preference<float>(0x5678);
|
||||
|
||||
// Also try to load non-existent preferences (tests our fix)
|
||||
ESPPreferenceObject fake_pref1 = global_preferences->make_preference<uint32_t>(0x9999);
|
||||
ESPPreferenceObject fake_pref2 = global_preferences->make_preference<uint32_t>(0xAAAA);
|
||||
|
||||
bool switch_value = false;
|
||||
float number_value = 0.0;
|
||||
uint32_t fake_value = 0;
|
||||
|
||||
// These should not exist and shouldn't create map entries
|
||||
fake_pref1.load(&fake_value);
|
||||
fake_pref2.load(&fake_value);
|
||||
|
||||
if (switch_pref.load(&switch_value)) {
|
||||
id(test_switch).publish_state(switch_value);
|
||||
ESP_LOGI("test", "Preference loaded: key=switch, value=%.1f", switch_value ? 1.0 : 0.0);
|
||||
} else {
|
||||
ESP_LOGW("test", "Failed to load switch preference");
|
||||
}
|
||||
|
||||
if (number_pref.load(&number_value)) {
|
||||
id(test_number).publish_state(number_value);
|
||||
ESP_LOGI("test", "Preference loaded: key=number, value=%.1f", number_value);
|
||||
} else {
|
||||
ESP_LOGW("test", "Failed to load number preference");
|
||||
}
|
||||
|
||||
- platform: template
|
||||
name: "Verify Preferences"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
// Verify current values match what we expect
|
||||
bool switch_value = id(test_switch).state;
|
||||
float number_value = id(test_number).state;
|
||||
|
||||
// After loading, switch should be true (1.0) and number should be 42.5
|
||||
if (switch_value == true && number_value == 42.5) {
|
||||
ESP_LOGI("test", "Preferences verified: values match!");
|
||||
} else {
|
||||
ESP_LOGE("test", "Preferences mismatch: switch=%d, number=%.1f",
|
||||
switch_value, number_value);
|
||||
}
|
153
tests/integration/test_host_preferences.py
Normal file
153
tests/integration/test_host_preferences.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Test host preferences save and load functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityInfo, NumberInfo, SwitchInfo
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
def find_entity_by_name(
|
||||
entities: list[EntityInfo], entity_type: type, name: str
|
||||
) -> Any:
|
||||
"""Helper to find an entity by type and name."""
|
||||
return next(
|
||||
(e for e in entities if isinstance(e, entity_type) and e.name == name), None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_preferences_save_load(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that preferences are correctly saved and loaded after our optimization fix."""
|
||||
loop = asyncio.get_running_loop()
|
||||
log_lines: list[str] = []
|
||||
preferences_saved = loop.create_future()
|
||||
preferences_loaded = loop.create_future()
|
||||
values_match = loop.create_future()
|
||||
|
||||
# Patterns to match preference logs
|
||||
save_pattern = re.compile(r"Preference saved: key=(\w+), value=([0-9.]+)")
|
||||
load_pattern = re.compile(r"Preference loaded: key=(\w+), value=([0-9.]+)")
|
||||
verify_pattern = re.compile(r"Preferences verified: values match!")
|
||||
|
||||
saved_values: dict[str, float] = {}
|
||||
loaded_values: dict[str, float] = {}
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for preference operations."""
|
||||
log_lines.append(line)
|
||||
|
||||
# Look for save operations
|
||||
match = save_pattern.search(line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
value = float(match.group(2))
|
||||
saved_values[key] = value
|
||||
if len(saved_values) >= 2 and not preferences_saved.done():
|
||||
preferences_saved.set_result(True)
|
||||
|
||||
# Look for load operations
|
||||
match = load_pattern.search(line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
value = float(match.group(2))
|
||||
loaded_values[key] = value
|
||||
if len(loaded_values) >= 2 and not preferences_loaded.done():
|
||||
preferences_loaded.set_result(True)
|
||||
|
||||
# Look for verification
|
||||
if verify_pattern.search(line) and not values_match.done():
|
||||
values_match.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Get entity list
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find our test entities using helper
|
||||
test_switch = find_entity_by_name(entities, SwitchInfo, "Test Switch")
|
||||
test_number = find_entity_by_name(entities, NumberInfo, "Test Number")
|
||||
save_button = find_entity_by_name(entities, ButtonInfo, "Save Preferences")
|
||||
load_button = find_entity_by_name(entities, ButtonInfo, "Load Preferences")
|
||||
verify_button = find_entity_by_name(entities, ButtonInfo, "Verify Preferences")
|
||||
|
||||
assert test_switch is not None, "Test Switch not found"
|
||||
assert test_number is not None, "Test Number not found"
|
||||
assert save_button is not None, "Save Preferences button not found"
|
||||
assert load_button is not None, "Load Preferences button not found"
|
||||
assert verify_button is not None, "Verify Preferences button not found"
|
||||
|
||||
# Set initial values
|
||||
client.switch_command(test_switch.key, True)
|
||||
client.number_command(test_number.key, 42.5)
|
||||
|
||||
# Save preferences
|
||||
client.button_command(save_button.key)
|
||||
|
||||
# Wait for save to complete
|
||||
try:
|
||||
await asyncio.wait_for(preferences_saved, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Preferences not saved within timeout")
|
||||
|
||||
# Verify we saved the expected values
|
||||
assert "switch" in saved_values, f"Switch preference not saved: {saved_values}"
|
||||
assert "number" in saved_values, f"Number preference not saved: {saved_values}"
|
||||
assert saved_values["switch"] == 1.0, (
|
||||
f"Switch value incorrect: {saved_values['switch']}"
|
||||
)
|
||||
assert saved_values["number"] == 42.5, (
|
||||
f"Number value incorrect: {saved_values['number']}"
|
||||
)
|
||||
|
||||
# Change the values to something else
|
||||
client.switch_command(test_switch.key, False)
|
||||
client.number_command(test_number.key, 13.7)
|
||||
|
||||
# Load preferences (should restore the saved values)
|
||||
client.button_command(load_button.key)
|
||||
|
||||
# Wait for load to complete
|
||||
try:
|
||||
await asyncio.wait_for(preferences_loaded, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Preferences not loaded within timeout")
|
||||
|
||||
# Verify loaded values match saved values
|
||||
assert "switch" in loaded_values, (
|
||||
f"Switch preference not loaded: {loaded_values}"
|
||||
)
|
||||
assert "number" in loaded_values, (
|
||||
f"Number preference not loaded: {loaded_values}"
|
||||
)
|
||||
assert loaded_values["switch"] == saved_values["switch"], (
|
||||
f"Loaded switch value {loaded_values['switch']} doesn't match saved {saved_values['switch']}"
|
||||
)
|
||||
assert loaded_values["number"] == saved_values["number"], (
|
||||
f"Loaded number value {loaded_values['number']} doesn't match saved {saved_values['number']}"
|
||||
)
|
||||
|
||||
# Verify the values were actually restored
|
||||
client.button_command(verify_button.key)
|
||||
|
||||
# Wait for verification
|
||||
try:
|
||||
await asyncio.wait_for(values_match, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Preference verification failed within timeout")
|
||||
|
||||
# Test that non-existent preferences don't crash (tests our fix)
|
||||
# This will trigger load attempts for keys that don't exist
|
||||
# Our fix should prevent map entries from being created
|
||||
client.button_command(load_button.key)
|
Reference in New Issue
Block a user