mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00: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