mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	cover
This commit is contained in:
		
							
								
								
									
										156
									
								
								tests/integration/fixtures/scheduler_string_test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								tests/integration/fixtures/scheduler_string_test.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-string-test | ||||||
|  |   on_boot: | ||||||
|  |     priority: -100 | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Starting scheduler string tests" | ||||||
|  |   platformio_options: | ||||||
|  |     build_flags: | ||||||
|  |       - "-DESPHOME_DEBUG_SCHEDULER"  # Enable scheduler debug logging | ||||||
|  |  | ||||||
|  | host: | ||||||
|  | api: | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | globals: | ||||||
|  |   - id: timeout_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: interval_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: dynamic_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: static_tests_done | ||||||
|  |     type: bool | ||||||
|  |     initial_value: 'false' | ||||||
|  |   - id: dynamic_tests_done | ||||||
|  |     type: bool | ||||||
|  |     initial_value: 'false' | ||||||
|  |   - id: results_reported | ||||||
|  |     type: bool | ||||||
|  |     initial_value: 'false' | ||||||
|  |  | ||||||
|  | script: | ||||||
|  |   - id: test_static_strings | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Testing static string timeouts and intervals" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component1 = id(test_sensor1); | ||||||
|  |           // Test 1: Static string literals with set_timeout | ||||||
|  |           App.scheduler.set_timeout(component1, "static_timeout_1", 100, []() { | ||||||
|  |             ESP_LOGI("test", "Static timeout 1 fired"); | ||||||
|  |             id(timeout_counter) += 1; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Test 2: Static const char* with set_timeout | ||||||
|  |           static const char* TIMEOUT_NAME = "static_timeout_2"; | ||||||
|  |           App.scheduler.set_timeout(component1, TIMEOUT_NAME, 200, []() { | ||||||
|  |             ESP_LOGI("test", "Static timeout 2 fired"); | ||||||
|  |             id(timeout_counter) += 1; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Test 3: Static string literal with set_interval | ||||||
|  |           App.scheduler.set_interval(component1, "static_interval_1", 500, []() { | ||||||
|  |             ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter)); | ||||||
|  |             id(interval_counter) += 1; | ||||||
|  |             if (id(interval_counter) >= 3) { | ||||||
|  |               App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1"); | ||||||
|  |               ESP_LOGI("test", "Cancelled static interval 1"); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Test 4: Empty string (should be handled safely) | ||||||
|  |           App.scheduler.set_timeout(component1, "", 300, []() { | ||||||
|  |             ESP_LOGI("test", "Empty string timeout fired"); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |   - id: test_dynamic_strings | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Testing dynamic string timeouts and intervals" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component2 = id(test_sensor2); | ||||||
|  |  | ||||||
|  |           // Test 5: Dynamic string with set_timeout (std::string) | ||||||
|  |           std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++); | ||||||
|  |           App.scheduler.set_timeout(component2, dynamic_name, 150, []() { | ||||||
|  |             ESP_LOGI("test", "Dynamic timeout fired"); | ||||||
|  |             id(timeout_counter) += 1; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Test 6: Dynamic string with set_interval | ||||||
|  |           std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++); | ||||||
|  |           App.scheduler.set_interval(component2, interval_name, 600, [interval_name]() { | ||||||
|  |             ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str()); | ||||||
|  |             id(interval_counter) += 1; | ||||||
|  |             if (id(interval_counter) >= 6) { | ||||||
|  |               App.scheduler.cancel_interval(id(test_sensor2), interval_name); | ||||||
|  |               ESP_LOGI("test", "Cancelled dynamic interval"); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Test 7: Cancel with different string object but same content | ||||||
|  |           std::string cancel_name = "cancel_test"; | ||||||
|  |           App.scheduler.set_timeout(component2, cancel_name, 5000, []() { | ||||||
|  |             ESP_LOGI("test", "This should be cancelled"); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Cancel using a different string object | ||||||
|  |           std::string cancel_name_2 = "cancel_test"; | ||||||
|  |           App.scheduler.cancel_timeout(component2, cancel_name_2); | ||||||
|  |           ESP_LOGI("test", "Cancelled timeout using different string object"); | ||||||
|  |  | ||||||
|  |   - id: report_results | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", | ||||||
|  |                    id(timeout_counter), id(interval_counter)); | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Sensor 1 | ||||||
|  |     id: test_sensor1 | ||||||
|  |     lambda: return 1.0; | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Sensor 2 | ||||||
|  |     id: test_sensor2 | ||||||
|  |     lambda: return 2.0; | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  | interval: | ||||||
|  |   # Run static string tests after boot - using script to run once | ||||||
|  |   - interval: 0.5s | ||||||
|  |     then: | ||||||
|  |       - if: | ||||||
|  |           condition: | ||||||
|  |             lambda: 'return id(static_tests_done) == false;' | ||||||
|  |           then: | ||||||
|  |             - lambda: 'id(static_tests_done) = true;' | ||||||
|  |             - script.execute: test_static_strings | ||||||
|  |             - logger.log: "Started static string tests" | ||||||
|  |  | ||||||
|  |   # Run dynamic string tests after static tests | ||||||
|  |   - interval: 1s | ||||||
|  |     then: | ||||||
|  |       - if: | ||||||
|  |           condition: | ||||||
|  |             lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);' | ||||||
|  |           then: | ||||||
|  |             - lambda: 'id(dynamic_tests_done) = true;' | ||||||
|  |             - delay: 1s | ||||||
|  |             - script.execute: test_dynamic_strings | ||||||
|  |  | ||||||
|  |   # Report results after all tests | ||||||
|  |   - interval: 1s | ||||||
|  |     then: | ||||||
|  |       - if: | ||||||
|  |           condition: | ||||||
|  |             lambda: 'return id(dynamic_tests_done) && !id(results_reported);' | ||||||
|  |           then: | ||||||
|  |             - lambda: 'id(results_reported) = true;' | ||||||
|  |             - delay: 3s | ||||||
|  |             - script.execute: report_results | ||||||
							
								
								
									
										163
									
								
								tests/integration/test_scheduler_string_test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								tests/integration/test_scheduler_string_test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | |||||||
|  | """Test scheduler string optimization with static and dynamic strings.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_string_test( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduler handles both static and dynamic strings correctly.""" | ||||||
|  |     # Track counts | ||||||
|  |     timeout_count = 0 | ||||||
|  |     interval_count = 0 | ||||||
|  |  | ||||||
|  |     # Events for each test completion | ||||||
|  |     static_timeout_1_fired = asyncio.Event() | ||||||
|  |     static_timeout_2_fired = asyncio.Event() | ||||||
|  |     static_interval_fired = asyncio.Event() | ||||||
|  |     static_interval_cancelled = asyncio.Event() | ||||||
|  |     empty_string_timeout_fired = asyncio.Event() | ||||||
|  |     dynamic_timeout_fired = asyncio.Event() | ||||||
|  |     dynamic_interval_fired = asyncio.Event() | ||||||
|  |     cancel_test_done = asyncio.Event() | ||||||
|  |     final_results_logged = asyncio.Event() | ||||||
|  |  | ||||||
|  |     # Track interval counts | ||||||
|  |     static_interval_count = 0 | ||||||
|  |     dynamic_interval_count = 0 | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         nonlocal \ | ||||||
|  |             timeout_count, \ | ||||||
|  |             interval_count, \ | ||||||
|  |             static_interval_count, \ | ||||||
|  |             dynamic_interval_count | ||||||
|  |  | ||||||
|  |         # Strip ANSI color codes | ||||||
|  |         clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) | ||||||
|  |  | ||||||
|  |         # Check for static timeout completions | ||||||
|  |         if "Static timeout 1 fired" in clean_line: | ||||||
|  |             static_timeout_1_fired.set() | ||||||
|  |             timeout_count += 1 | ||||||
|  |  | ||||||
|  |         elif "Static timeout 2 fired" in clean_line: | ||||||
|  |             static_timeout_2_fired.set() | ||||||
|  |             timeout_count += 1 | ||||||
|  |  | ||||||
|  |         # Check for static interval | ||||||
|  |         elif "Static interval 1 fired" in clean_line: | ||||||
|  |             match = re.search(r"count: (\d+)", clean_line) | ||||||
|  |             if match: | ||||||
|  |                 static_interval_count = int(match.group(1)) | ||||||
|  |                 static_interval_fired.set() | ||||||
|  |  | ||||||
|  |         elif "Cancelled static interval 1" in clean_line: | ||||||
|  |             static_interval_cancelled.set() | ||||||
|  |  | ||||||
|  |         # Check for empty string timeout | ||||||
|  |         elif "Empty string timeout fired" in clean_line: | ||||||
|  |             empty_string_timeout_fired.set() | ||||||
|  |  | ||||||
|  |         # Check for dynamic string tests | ||||||
|  |         elif "Dynamic timeout fired" in clean_line: | ||||||
|  |             dynamic_timeout_fired.set() | ||||||
|  |             timeout_count += 1 | ||||||
|  |  | ||||||
|  |         elif "Dynamic interval fired" in clean_line: | ||||||
|  |             dynamic_interval_count += 1 | ||||||
|  |             dynamic_interval_fired.set() | ||||||
|  |  | ||||||
|  |         # Check for cancel test | ||||||
|  |         elif "Cancelled timeout using different string object" in clean_line: | ||||||
|  |             cancel_test_done.set() | ||||||
|  |  | ||||||
|  |         # Check for final results | ||||||
|  |         elif "Final results" in clean_line: | ||||||
|  |             match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line) | ||||||
|  |             if match: | ||||||
|  |                 timeout_count = int(match.group(1)) | ||||||
|  |                 interval_count = int(match.group(2)) | ||||||
|  |                 final_results_logged.set() | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-string-test" | ||||||
|  |  | ||||||
|  |         # Wait for static string tests | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Static timeout 1 did not fire within 3 seconds") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Static timeout 2 did not fire within 3 seconds") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(static_interval_fired.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Static interval did not fire within 3 seconds") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(static_interval_cancelled.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Static interval was not cancelled within 3 seconds") | ||||||
|  |  | ||||||
|  |         # Verify static interval ran at least 3 times | ||||||
|  |         assert static_interval_count >= 2, ( | ||||||
|  |             f"Expected static interval to run at least 3 times, got {static_interval_count + 1}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for dynamic string tests | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=5.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Dynamic timeout did not fire within 5 seconds") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=5.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Dynamic interval did not fire within 5 seconds") | ||||||
|  |  | ||||||
|  |         # Wait for cancel test | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(cancel_test_done.wait(), timeout=5.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Cancel test did not complete within 5 seconds") | ||||||
|  |  | ||||||
|  |         # Wait for final results | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(final_results_logged.wait(), timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Final results were not logged within 10 seconds") | ||||||
|  |  | ||||||
|  |         # Verify results | ||||||
|  |         assert timeout_count >= 3, f"Expected at least 3 timeouts, got {timeout_count}" | ||||||
|  |         assert interval_count >= 3, ( | ||||||
|  |             f"Expected at least 3 interval fires, got {interval_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Empty string timeout DOES fire (scheduler accepts empty names) | ||||||
|  |         assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire" | ||||||
|  |  | ||||||
|  |         # Log final status | ||||||
|  |         print("\nScheduler string test completed successfully:") | ||||||
|  |         print(f"  Timeouts fired: {timeout_count}") | ||||||
|  |         print(f"  Intervals fired: {interval_count}") | ||||||
|  |         print(f"  Static interval count: {static_interval_count + 1}") | ||||||
|  |         print(f"  Dynamic interval count: {dynamic_interval_count}") | ||||||
		Reference in New Issue
	
	Block a user