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)
|
if (len > 255)
|
||||||
return false;
|
return false;
|
||||||
this->setup_();
|
this->setup_();
|
||||||
if (this->data.count(key) == 0)
|
auto it = this->data.find(key);
|
||||||
|
if (it == this->data.end())
|
||||||
return false;
|
return false;
|
||||||
auto vec = this->data[key];
|
const auto &vec = it->second;
|
||||||
if (vec.size() != len)
|
if (vec.size() != len)
|
||||||
return false;
|
return false;
|
||||||
memcpy(data, vec.data(), len);
|
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