mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-28 13:43:54 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			103 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			103 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Test for scheduler race condition where removed items still execute."""
 | |
| 
 | |
| import asyncio
 | |
| import re
 | |
| 
 | |
| import pytest
 | |
| 
 | |
| from .types import APIClientConnectedFactory, RunCompiledFunction
 | |
| 
 | |
| 
 | |
| @pytest.mark.asyncio
 | |
| async def test_scheduler_removed_item_race(
 | |
|     yaml_config: str,
 | |
|     run_compiled: RunCompiledFunction,
 | |
|     api_client_connected: APIClientConnectedFactory,
 | |
| ) -> None:
 | |
|     """Test that items marked for removal don't execute.
 | |
| 
 | |
|     This test verifies the fix for a race condition where:
 | |
|     1. cleanup_() only removes items from the front of the heap
 | |
|     2. Items in the middle of the heap marked for removal still execute
 | |
|     3. This causes cancelled timeouts to run when they shouldn't
 | |
|     """
 | |
| 
 | |
|     loop = asyncio.get_running_loop()
 | |
|     test_complete_future: asyncio.Future[bool] = loop.create_future()
 | |
| 
 | |
|     # Track test results
 | |
|     test_passed = False
 | |
|     removed_executed = 0
 | |
|     normal_executed = 0
 | |
| 
 | |
|     # Patterns to match
 | |
|     race_pattern = re.compile(r"RACE: .* executed after being cancelled!")
 | |
|     passed_pattern = re.compile(r"TEST PASSED")
 | |
|     failed_pattern = re.compile(r"TEST FAILED")
 | |
|     complete_pattern = re.compile(r"=== Test Complete ===")
 | |
|     normal_count_pattern = re.compile(r"Normal items executed: (\d+)")
 | |
|     removed_count_pattern = re.compile(r"Removed items executed: (\d+)")
 | |
| 
 | |
|     def check_output(line: str) -> None:
 | |
|         """Check log output for test results."""
 | |
|         nonlocal test_passed, removed_executed, normal_executed
 | |
| 
 | |
|         if race_pattern.search(line):
 | |
|             # Race condition detected - a cancelled item executed
 | |
|             test_passed = False
 | |
| 
 | |
|         if passed_pattern.search(line):
 | |
|             test_passed = True
 | |
|         elif failed_pattern.search(line):
 | |
|             test_passed = False
 | |
| 
 | |
|         normal_match = normal_count_pattern.search(line)
 | |
|         if normal_match:
 | |
|             normal_executed = int(normal_match.group(1))
 | |
| 
 | |
|         removed_match = removed_count_pattern.search(line)
 | |
|         if removed_match:
 | |
|             removed_executed = int(removed_match.group(1))
 | |
| 
 | |
|         if not test_complete_future.done() and complete_pattern.search(line):
 | |
|             test_complete_future.set_result(True)
 | |
| 
 | |
|     async with (
 | |
|         run_compiled(yaml_config, line_callback=check_output),
 | |
|         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-removed-item-race"
 | |
| 
 | |
|         # List services
 | |
|         _, services = await asyncio.wait_for(
 | |
|             client.list_entities_services(), timeout=5.0
 | |
|         )
 | |
| 
 | |
|         # Find run_test service
 | |
|         run_test_service = next((s for s in services if s.name == "run_test"), None)
 | |
|         assert run_test_service is not None, "run_test service not found"
 | |
| 
 | |
|         # Execute the test
 | |
|         client.execute_service(run_test_service, {})
 | |
| 
 | |
|         # Wait for test completion
 | |
|         try:
 | |
|             await asyncio.wait_for(test_complete_future, timeout=5.0)
 | |
|         except TimeoutError:
 | |
|             pytest.fail("Test did not complete within timeout")
 | |
| 
 | |
|         # Verify results
 | |
|         assert test_passed, (
 | |
|             f"Test failed! Removed items executed: {removed_executed}, "
 | |
|             f"Normal items executed: {normal_executed}"
 | |
|         )
 | |
|         assert removed_executed == 0, (
 | |
|             f"Cancelled items should not execute, but {removed_executed} did"
 | |
|         )
 | |
|         assert normal_executed == 4, (
 | |
|             f"Expected 4 normal items to execute, got {normal_executed}"
 | |
|         )
 |