mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	cover
This commit is contained in:
		| @@ -0,0 +1,58 @@ | |||||||
|  | esphome: | ||||||
|  |   name: host-empty-string-test | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   batch_delay: 50ms | ||||||
|  |  | ||||||
|  | select: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Select Empty First" | ||||||
|  |     id: select_empty_first | ||||||
|  |     optimistic: true | ||||||
|  |     options: | ||||||
|  |       - ""  # Empty string at the beginning | ||||||
|  |       - "Option A" | ||||||
|  |       - "Option B" | ||||||
|  |       - "Option C" | ||||||
|  |     initial_option: "Option A" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: "Select Empty Middle" | ||||||
|  |     id: select_empty_middle | ||||||
|  |     optimistic: true | ||||||
|  |     options: | ||||||
|  |       - "Option 1" | ||||||
|  |       - "Option 2" | ||||||
|  |       - ""  # Empty string in the middle | ||||||
|  |       - "Option 3" | ||||||
|  |       - "Option 4" | ||||||
|  |     initial_option: "Option 1" | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: "Select Empty Last" | ||||||
|  |     id: select_empty_last | ||||||
|  |     optimistic: true | ||||||
|  |     options: | ||||||
|  |       - "Choice X" | ||||||
|  |       - "Choice Y" | ||||||
|  |       - "Choice Z" | ||||||
|  |       - ""  # Empty string at the end | ||||||
|  |     initial_option: "Choice X" | ||||||
|  |  | ||||||
|  | # Add a sensor to ensure we have other entities in the list | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Sensor" | ||||||
|  |     id: test_sensor | ||||||
|  |     lambda: |- | ||||||
|  |       return 42.0; | ||||||
|  |     update_interval: 60s | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Binary Sensor" | ||||||
|  |     id: test_binary_sensor | ||||||
|  |     lambda: |- | ||||||
|  |       return true; | ||||||
							
								
								
									
										110
									
								
								tests/integration/test_host_mode_empty_string_options.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								tests/integration/test_host_mode_empty_string_options.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | """Integration test for protobuf encoding of empty string options in select entities.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | from aioesphomeapi import EntityState, SelectInfo | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_host_mode_empty_string_options( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that select entities with empty string options are correctly encoded in protobuf messages. | ||||||
|  |  | ||||||
|  |     This tests the fix for the bug where the force parameter was not passed in encode_string, | ||||||
|  |     causing empty strings in repeated fields to be skipped during encoding but included in | ||||||
|  |     size calculation, leading to protobuf decoding errors. | ||||||
|  |     """ | ||||||
|  |     # Write, compile and run the ESPHome device, then connect to API | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     async with run_compiled(yaml_config), api_client_connected() as client: | ||||||
|  |         # Verify we can get device info | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "host-empty-string-test" | ||||||
|  |  | ||||||
|  |         # Get list of entities - this will encode ListEntitiesSelectResponse messages | ||||||
|  |         # with empty string options that would trigger the bug | ||||||
|  |         entity_info, services = await client.list_entities_services() | ||||||
|  |  | ||||||
|  |         # Find our select entities | ||||||
|  |         select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] | ||||||
|  |         assert len(select_entities) == 3, ( | ||||||
|  |             f"Expected 3 select entities, got {len(select_entities)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify each select entity by name and check their options | ||||||
|  |         selects_by_name = {e.name: e for e in select_entities} | ||||||
|  |  | ||||||
|  |         # Check "Select Empty First" - empty string at beginning | ||||||
|  |         assert "Select Empty First" in selects_by_name | ||||||
|  |         empty_first = selects_by_name["Select Empty First"] | ||||||
|  |         assert len(empty_first.options) == 4 | ||||||
|  |         assert empty_first.options[0] == ""  # Empty string at beginning | ||||||
|  |         assert empty_first.options[1] == "Option A" | ||||||
|  |         assert empty_first.options[2] == "Option B" | ||||||
|  |         assert empty_first.options[3] == "Option C" | ||||||
|  |  | ||||||
|  |         # Check "Select Empty Middle" - empty string in middle | ||||||
|  |         assert "Select Empty Middle" in selects_by_name | ||||||
|  |         empty_middle = selects_by_name["Select Empty Middle"] | ||||||
|  |         assert len(empty_middle.options) == 5 | ||||||
|  |         assert empty_middle.options[0] == "Option 1" | ||||||
|  |         assert empty_middle.options[1] == "Option 2" | ||||||
|  |         assert empty_middle.options[2] == ""  # Empty string in middle | ||||||
|  |         assert empty_middle.options[3] == "Option 3" | ||||||
|  |         assert empty_middle.options[4] == "Option 4" | ||||||
|  |  | ||||||
|  |         # Check "Select Empty Last" - empty string at end | ||||||
|  |         assert "Select Empty Last" in selects_by_name | ||||||
|  |         empty_last = selects_by_name["Select Empty Last"] | ||||||
|  |         assert len(empty_last.options) == 4 | ||||||
|  |         assert empty_last.options[0] == "Choice X" | ||||||
|  |         assert empty_last.options[1] == "Choice Y" | ||||||
|  |         assert empty_last.options[2] == "Choice Z" | ||||||
|  |         assert empty_last.options[3] == ""  # Empty string at end | ||||||
|  |  | ||||||
|  |         # If we got here without protobuf decoding errors, the fix is working | ||||||
|  |         # The bug would have caused "Invalid protobuf message" errors with trailing bytes | ||||||
|  |  | ||||||
|  |         # Also verify we can interact with the select entities | ||||||
|  |         # Subscribe to state changes | ||||||
|  |         states: dict[int, EntityState] = {} | ||||||
|  |         state_change_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |         def on_state(state: EntityState) -> None: | ||||||
|  |             """Track state changes.""" | ||||||
|  |             states[state.key] = state | ||||||
|  |             # When we receive the state change for our select, resolve the future | ||||||
|  |             if state.key == empty_first.key and not state_change_future.done(): | ||||||
|  |                 state_change_future.set_result(None) | ||||||
|  |  | ||||||
|  |         client.subscribe_states(on_state) | ||||||
|  |  | ||||||
|  |         # Try setting a select to an empty string option | ||||||
|  |         # This further tests that empty strings are handled correctly | ||||||
|  |         client.select_command(empty_first.key, "") | ||||||
|  |  | ||||||
|  |         # Wait for state update with timeout | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(state_change_future, timeout=5.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 "Did not receive state update after setting select to empty string" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify the state was set to empty string | ||||||
|  |         assert empty_first.key in states | ||||||
|  |         select_state = states[empty_first.key] | ||||||
|  |         assert hasattr(select_state, "state") | ||||||
|  |         assert select_state.state == "" | ||||||
|  |  | ||||||
|  |         # The test passes if no protobuf decoding errors occurred | ||||||
|  |         # With the bug, we would have gotten "Invalid protobuf message" errors | ||||||
		Reference in New Issue
	
	Block a user