mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	some tests
This commit is contained in:
		| @@ -1135,7 +1135,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { | |||||||
|   dump_field(out, "string_", this->string_); |   dump_field(out, "string_", this->string_); | ||||||
|   dump_field(out, "int_", this->int_); |   dump_field(out, "int_", this->int_); | ||||||
|   for (const auto it : this->bool_array) { |   for (const auto it : this->bool_array) { | ||||||
|     dump_field(out, "bool_array", it, 4); |     dump_field(out, "bool_array", static_cast<bool>(it), 4); | ||||||
|   } |   } | ||||||
|   for (const auto &it : this->int_array) { |   for (const auto &it : this->int_array) { | ||||||
|     dump_field(out, "int_array", it, 4); |     dump_field(out, "int_array", it, 4); | ||||||
|   | |||||||
							
								
								
									
										215
									
								
								tests/integration/fixtures/scheduler_pool.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								tests/integration/fixtures/scheduler_pool.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-pool-test | ||||||
|  |   on_boot: | ||||||
|  |     priority: -100 | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Starting scheduler pool tests" | ||||||
|  |   debug_scheduler: true  # Enable scheduler debug logging | ||||||
|  |  | ||||||
|  | host: | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_phase_1 | ||||||
|  |       then: | ||||||
|  |         - script.execute: test_pool_recycling | ||||||
|  |     - service: run_phase_2 | ||||||
|  |       then: | ||||||
|  |         - script.execute: test_sensor_polling | ||||||
|  |     - service: run_phase_3 | ||||||
|  |       then: | ||||||
|  |         - script.execute: test_communication_patterns | ||||||
|  |     - service: run_phase_4 | ||||||
|  |       then: | ||||||
|  |         - script.execute: test_defer_patterns | ||||||
|  |     - service: run_phase_5 | ||||||
|  |       then: | ||||||
|  |         - script.execute: test_pool_reuse_verification | ||||||
|  |     - service: run_complete | ||||||
|  |       then: | ||||||
|  |         - script.execute: complete_test | ||||||
|  | logger: | ||||||
|  |   level: VERY_VERBOSE  # Need VERY_VERBOSE to see pool debug messages | ||||||
|  |  | ||||||
|  | globals: | ||||||
|  |   - id: create_count | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: cancel_count | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: interval_counter | ||||||
|  |     type: int | ||||||
|  |     initial_value: '0' | ||||||
|  |   - id: pool_test_done | ||||||
|  |     type: bool | ||||||
|  |     initial_value: 'false' | ||||||
|  |  | ||||||
|  | script: | ||||||
|  |   - id: test_pool_recycling | ||||||
|  |     then: | ||||||
|  |       - logger.log: "Testing scheduler pool recycling with realistic usage patterns" | ||||||
|  |       - lambda: |- | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |  | ||||||
|  |           // Simulate realistic component behavior with timeouts that complete naturally | ||||||
|  |           ESP_LOGI("test", "Phase 1: Simulating normal component lifecycle"); | ||||||
|  |  | ||||||
|  |           // Sensor update timeouts (common pattern) | ||||||
|  |           App.scheduler.set_timeout(component, "sensor_init", 100, []() { | ||||||
|  |             ESP_LOGD("test", "Sensor initialized"); | ||||||
|  |             id(create_count)++; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Retry timeout (gets cancelled if successful) | ||||||
|  |           App.scheduler.set_timeout(component, "retry_timeout", 500, []() { | ||||||
|  |             ESP_LOGD("test", "Retry timeout executed"); | ||||||
|  |             id(create_count)++; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Simulate successful operation - cancel retry | ||||||
|  |           App.scheduler.set_timeout(component, "success_sim", 200, []() { | ||||||
|  |             ESP_LOGD("test", "Operation succeeded, cancelling retry"); | ||||||
|  |             App.scheduler.cancel_timeout(id(test_sensor), "retry_timeout"); | ||||||
|  |             id(cancel_count)++; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           id(create_count) += 3; | ||||||
|  |           ESP_LOGI("test", "Phase 1 complete"); | ||||||
|  |  | ||||||
|  |   - id: test_sensor_polling | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           // Simulate sensor polling pattern | ||||||
|  |           ESP_LOGI("test", "Phase 2: Simulating sensor polling patterns"); | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |  | ||||||
|  |           // Multiple sensors with different update intervals | ||||||
|  |           App.scheduler.set_interval(component, "temp_sensor", 1000, []() { | ||||||
|  |             ESP_LOGD("test", "Temperature sensor update"); | ||||||
|  |             id(interval_counter)++; | ||||||
|  |             if (id(interval_counter) >= 3) { | ||||||
|  |               App.scheduler.cancel_interval(id(test_sensor), "temp_sensor"); | ||||||
|  |               ESP_LOGD("test", "Temperature sensor stopped"); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           App.scheduler.set_interval(component, "humidity_sensor", 1500, []() { | ||||||
|  |             ESP_LOGD("test", "Humidity sensor update"); | ||||||
|  |             id(interval_counter)++; | ||||||
|  |             if (id(interval_counter) >= 5) { | ||||||
|  |               App.scheduler.cancel_interval(id(test_sensor), "humidity_sensor"); | ||||||
|  |               ESP_LOGD("test", "Humidity sensor stopped"); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           id(create_count) += 2; | ||||||
|  |           ESP_LOGI("test", "Phase 2 complete"); | ||||||
|  |  | ||||||
|  |   - id: test_communication_patterns | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           // Simulate communication patterns (WiFi/API reconnects, etc) | ||||||
|  |           ESP_LOGI("test", "Phase 3: Simulating communication patterns"); | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |  | ||||||
|  |           // Connection timeout pattern | ||||||
|  |           App.scheduler.set_timeout(component, "connect_timeout", 2000, []() { | ||||||
|  |             ESP_LOGD("test", "Connection timeout - would retry"); | ||||||
|  |             id(create_count)++; | ||||||
|  |  | ||||||
|  |             // Schedule retry | ||||||
|  |             App.scheduler.set_timeout(id(test_sensor), "connect_retry", 1000, []() { | ||||||
|  |               ESP_LOGD("test", "Retrying connection"); | ||||||
|  |               id(create_count)++; | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // Heartbeat pattern | ||||||
|  |           App.scheduler.set_interval(component, "heartbeat", 500, []() { | ||||||
|  |             ESP_LOGD("test", "Heartbeat"); | ||||||
|  |             id(interval_counter)++; | ||||||
|  |             if (id(interval_counter) >= 10) { | ||||||
|  |               App.scheduler.cancel_interval(id(test_sensor), "heartbeat"); | ||||||
|  |               ESP_LOGD("test", "Heartbeat stopped"); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           id(create_count) += 2; | ||||||
|  |           ESP_LOGI("test", "Phase 3 complete"); | ||||||
|  |  | ||||||
|  |   - id: test_defer_patterns | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           // Simulate defer patterns (state changes, async operations) | ||||||
|  |           ESP_LOGI("test", "Phase 4: Simulating defer patterns"); | ||||||
|  |  | ||||||
|  |           class TestComponent : public Component { | ||||||
|  |           public: | ||||||
|  |             void simulate_state_changes() { | ||||||
|  |               // Defer state changes (common in switches, lights, etc) | ||||||
|  |               this->defer("state_change_1", []() { | ||||||
|  |                 ESP_LOGD("test", "State change 1 applied"); | ||||||
|  |                 id(create_count)++; | ||||||
|  |               }); | ||||||
|  |  | ||||||
|  |               // Another state change | ||||||
|  |               this->defer("state_change_2", []() { | ||||||
|  |                 ESP_LOGD("test", "State change 2 applied"); | ||||||
|  |                 id(create_count)++; | ||||||
|  |               }); | ||||||
|  |  | ||||||
|  |               // Cleanup operation | ||||||
|  |               this->defer("cleanup", []() { | ||||||
|  |                 ESP_LOGD("test", "Cleanup executed"); | ||||||
|  |                 id(create_count)++; | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           static TestComponent test_comp; | ||||||
|  |           test_comp.simulate_state_changes(); | ||||||
|  |           ESP_LOGI("test", "Phase 4 complete"); | ||||||
|  |  | ||||||
|  |   - id: test_pool_reuse_verification | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGI("test", "Phase 5: Verifying pool reuse after everything settles"); | ||||||
|  |  | ||||||
|  |           // First, ensure any remaining intervals are cancelled to recycle to pool | ||||||
|  |           auto *component = id(test_sensor); | ||||||
|  |           App.scheduler.cancel_interval(component, "temp_sensor"); | ||||||
|  |           App.scheduler.cancel_interval(component, "humidity_sensor"); | ||||||
|  |           App.scheduler.cancel_interval(component, "heartbeat"); | ||||||
|  |  | ||||||
|  |           // Give a moment for items to be recycled | ||||||
|  |           ESP_LOGD("test", "Cancelled any remaining intervals to build up pool"); | ||||||
|  |  | ||||||
|  |           // Now create 6 new timeouts - they should all reuse from pool | ||||||
|  |           int reuse_test_count = 6; | ||||||
|  |           int initial_pool_reused = 0; | ||||||
|  |  | ||||||
|  |           for (int i = 0; i < reuse_test_count; i++) { | ||||||
|  |             std::string name = "reuse_test_" + std::to_string(i); | ||||||
|  |             App.scheduler.set_timeout(component, name, 100 + i * 50, [i]() { | ||||||
|  |               ESP_LOGD("test", "Reuse test %d completed", i); | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           ESP_LOGI("test", "Created %d items for reuse verification", reuse_test_count); | ||||||
|  |           id(create_count) += reuse_test_count; | ||||||
|  |           ESP_LOGI("test", "Phase 5 complete"); | ||||||
|  |  | ||||||
|  |   - id: complete_test | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           ESP_LOGI("test", "Pool recycling test complete - created %d items, cancelled %d, intervals %d", | ||||||
|  |                    id(create_count), id(cancel_count), id(interval_counter)); | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     name: Test Sensor | ||||||
|  |     id: test_sensor | ||||||
|  |     lambda: return 1.0; | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  | # No interval - tests will be triggered from Python via API services | ||||||
							
								
								
									
										194
									
								
								tests/integration/test_scheduler_pool.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								tests/integration/test_scheduler_pool.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | |||||||
|  | """Integration test for scheduler memory pool functionality.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_pool( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that the scheduler memory pool is working correctly with realistic usage. | ||||||
|  |  | ||||||
|  |     This test simulates real-world scheduler usage patterns and verifies that: | ||||||
|  |     1. Items are recycled to the pool when timeouts complete naturally | ||||||
|  |     2. Items are recycled when intervals/timeouts are cancelled | ||||||
|  |     3. Items are reused from the pool for new scheduler operations | ||||||
|  |     4. The pool grows gradually based on actual usage patterns | ||||||
|  |     5. Pool operations are logged correctly with debug scheduler enabled | ||||||
|  |     """ | ||||||
|  |     # Track log messages to verify pool behavior | ||||||
|  |     log_lines: list[str] = [] | ||||||
|  |     pool_reuse_count = 0 | ||||||
|  |     pool_recycle_count = 0 | ||||||
|  |     pool_full_count = 0 | ||||||
|  |     new_alloc_count = 0 | ||||||
|  |  | ||||||
|  |     # Patterns to match pool operations | ||||||
|  |     reuse_pattern = re.compile(r"Reused item from pool \(pool size now: (\d+)\)") | ||||||
|  |     recycle_pattern = re.compile(r"Recycled item to pool \(pool size now: (\d+)\)") | ||||||
|  |     pool_full_pattern = re.compile(r"Pool full \(size: (\d+)\), deleting item") | ||||||
|  |     new_alloc_pattern = re.compile(r"Allocated new item \(pool empty\)") | ||||||
|  |  | ||||||
|  |     # Futures to track when test phases complete | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[bool] = loop.create_future() | ||||||
|  |     phase_futures = { | ||||||
|  |         1: loop.create_future(), | ||||||
|  |         2: loop.create_future(), | ||||||
|  |         3: loop.create_future(), | ||||||
|  |         4: loop.create_future(), | ||||||
|  |         5: loop.create_future(), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def check_output(line: str) -> None: | ||||||
|  |         """Check log output for pool operations and phase completion.""" | ||||||
|  |         nonlocal pool_reuse_count, pool_recycle_count, pool_full_count, new_alloc_count | ||||||
|  |         log_lines.append(line) | ||||||
|  |  | ||||||
|  |         # Track pool operations | ||||||
|  |         if reuse_pattern.search(line): | ||||||
|  |             pool_reuse_count += 1 | ||||||
|  |  | ||||||
|  |         elif recycle_pattern.search(line): | ||||||
|  |             pool_recycle_count += 1 | ||||||
|  |  | ||||||
|  |         elif pool_full_pattern.search(line): | ||||||
|  |             pool_full_count += 1 | ||||||
|  |  | ||||||
|  |         elif new_alloc_pattern.search(line): | ||||||
|  |             new_alloc_count += 1 | ||||||
|  |  | ||||||
|  |         # Track phase completion | ||||||
|  |         for phase_num in range(1, 6): | ||||||
|  |             if ( | ||||||
|  |                 f"Phase {phase_num} complete" in line | ||||||
|  |                 and not phase_futures[phase_num].done() | ||||||
|  |             ): | ||||||
|  |                 phase_futures[phase_num].set_result(True) | ||||||
|  |  | ||||||
|  |         # Check for test completion | ||||||
|  |         if "Pool recycling test complete" in line and not test_complete_future.done(): | ||||||
|  |             test_complete_future.set_result(True) | ||||||
|  |  | ||||||
|  |     # Run the test with log monitoring | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=check_output), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify device is running | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-pool-test" | ||||||
|  |  | ||||||
|  |         # Get list of services | ||||||
|  |         entities, services = await client.list_entities_services() | ||||||
|  |         service_names = {s.name for s in services} | ||||||
|  |  | ||||||
|  |         # Verify all test services are available | ||||||
|  |         expected_services = { | ||||||
|  |             "run_phase_1", | ||||||
|  |             "run_phase_2", | ||||||
|  |             "run_phase_3", | ||||||
|  |             "run_phase_4", | ||||||
|  |             "run_phase_5", | ||||||
|  |             "run_complete", | ||||||
|  |         } | ||||||
|  |         assert expected_services.issubset(service_names), ( | ||||||
|  |             f"Missing services: {expected_services - service_names}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Get service objects | ||||||
|  |         phase_services = { | ||||||
|  |             num: next(s for s in services if s.name == f"run_phase_{num}") | ||||||
|  |             for num in range(1, 6) | ||||||
|  |         } | ||||||
|  |         complete_service = next(s for s in services if s.name == "run_complete") | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             # Phase 1: Component lifecycle | ||||||
|  |             client.execute_service(phase_services[1], {}) | ||||||
|  |             await asyncio.wait_for(phase_futures[1], timeout=3.0) | ||||||
|  |             await asyncio.sleep(0.5)  # Let timeouts complete | ||||||
|  |  | ||||||
|  |             # Phase 2: Sensor polling | ||||||
|  |             client.execute_service(phase_services[2], {}) | ||||||
|  |             await asyncio.wait_for(phase_futures[2], timeout=3.0) | ||||||
|  |             await asyncio.sleep(1.0)  # Let intervals run a bit | ||||||
|  |  | ||||||
|  |             # Phase 3: Communication patterns | ||||||
|  |             client.execute_service(phase_services[3], {}) | ||||||
|  |             await asyncio.wait_for(phase_futures[3], timeout=3.0) | ||||||
|  |             await asyncio.sleep(1.0)  # Let heartbeat run | ||||||
|  |  | ||||||
|  |             # Phase 4: Defer patterns | ||||||
|  |             client.execute_service(phase_services[4], {}) | ||||||
|  |             await asyncio.wait_for(phase_futures[4], timeout=3.0) | ||||||
|  |             await asyncio.sleep(2.0)  # Let everything settle and recycle | ||||||
|  |  | ||||||
|  |             # Phase 5: Pool reuse verification | ||||||
|  |             client.execute_service(phase_services[5], {}) | ||||||
|  |             await asyncio.wait_for(phase_futures[5], timeout=3.0) | ||||||
|  |             await asyncio.sleep(0.5)  # Let reuse tests complete | ||||||
|  |  | ||||||
|  |             # Complete test | ||||||
|  |             client.execute_service(complete_service, {}) | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=2.0) | ||||||
|  |  | ||||||
|  |         except TimeoutError as e: | ||||||
|  |             # Print debug info if test times out | ||||||
|  |             recent_logs = "\n".join(log_lines[-30:]) | ||||||
|  |             phases_completed = [num for num, fut in phase_futures.items() if fut.done()] | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Test timed out waiting for phase/completion. Error: {e}\n" | ||||||
|  |                 f"  Phases completed: {phases_completed}\n" | ||||||
|  |                 f"  Pool stats:\n" | ||||||
|  |                 f"    Reuse count: {pool_reuse_count}\n" | ||||||
|  |                 f"    Recycle count: {pool_recycle_count}\n" | ||||||
|  |                 f"    Pool full count: {pool_full_count}\n" | ||||||
|  |                 f"    New alloc count: {new_alloc_count}\n" | ||||||
|  |                 f"Recent logs:\n{recent_logs}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     # Verify all test phases ran | ||||||
|  |     for phase_num in range(1, 6): | ||||||
|  |         assert phase_futures[phase_num].done(), f"Phase {phase_num} did not complete" | ||||||
|  |  | ||||||
|  |     # Verify pool behavior | ||||||
|  |     assert pool_recycle_count > 0, "Should have recycled items to pool" | ||||||
|  |  | ||||||
|  |     # Check pool metrics | ||||||
|  |     if pool_recycle_count > 0: | ||||||
|  |         max_pool_size = 0 | ||||||
|  |         for line in log_lines: | ||||||
|  |             if match := recycle_pattern.search(line): | ||||||
|  |                 size = int(match.group(1)) | ||||||
|  |                 max_pool_size = max(max_pool_size, size) | ||||||
|  |  | ||||||
|  |         # Pool can grow up to its maximum of 8 | ||||||
|  |         assert max_pool_size <= 8, f"Pool grew beyond maximum ({max_pool_size})" | ||||||
|  |  | ||||||
|  |     # Log summary for debugging | ||||||
|  |     print("\nScheduler Pool Test Summary (Python Orchestrated):") | ||||||
|  |     print(f"  Items recycled to pool: {pool_recycle_count}") | ||||||
|  |     print(f"  Items reused from pool: {pool_reuse_count}") | ||||||
|  |     print(f"  Pool full events: {pool_full_count}") | ||||||
|  |     print(f"  New allocations: {new_alloc_count}") | ||||||
|  |     print("  All phases completed successfully") | ||||||
|  |  | ||||||
|  |     # Verify reuse happened | ||||||
|  |     if pool_reuse_count == 0 and pool_recycle_count > 3: | ||||||
|  |         pytest.fail("Pool had items recycled but none were reused") | ||||||
|  |  | ||||||
|  |     # Success - pool is working | ||||||
|  |     assert pool_recycle_count > 0 or new_alloc_count < 15, ( | ||||||
|  |         "Pool should either recycle items or limit new allocations" | ||||||
|  |     ) | ||||||
		Reference in New Issue
	
	Block a user