mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 16:41:50 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			140 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
			
		
		
	
	
			140 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			YAML
		
	
	
	
	
	
esphome:
 | 
						|
  name: scheduler-removed-item-race
 | 
						|
 | 
						|
host:
 | 
						|
 | 
						|
api:
 | 
						|
  services:
 | 
						|
    - service: run_test
 | 
						|
      then:
 | 
						|
        - script.execute: run_test_script
 | 
						|
 | 
						|
logger:
 | 
						|
  level: DEBUG
 | 
						|
 | 
						|
globals:
 | 
						|
  - id: test_passed
 | 
						|
    type: bool
 | 
						|
    initial_value: 'true'
 | 
						|
  - id: removed_item_executed
 | 
						|
    type: int
 | 
						|
    initial_value: '0'
 | 
						|
  - id: normal_item_executed
 | 
						|
    type: int
 | 
						|
    initial_value: '0'
 | 
						|
 | 
						|
sensor:
 | 
						|
  - platform: template
 | 
						|
    id: test_sensor
 | 
						|
    name: "Test Sensor"
 | 
						|
    update_interval: never
 | 
						|
    lambda: return 0.0;
 | 
						|
 | 
						|
script:
 | 
						|
  - id: run_test_script
 | 
						|
    then:
 | 
						|
      - logger.log: "=== Starting Removed Item Race Test ==="
 | 
						|
 | 
						|
      # This test creates a scenario where:
 | 
						|
      # 1. First item in heap is NOT cancelled (cleanup stops immediately)
 | 
						|
      # 2. Items behind it ARE cancelled (remain in heap after cleanup)
 | 
						|
      # 3. All items execute at the same time, including cancelled ones
 | 
						|
 | 
						|
      - lambda: |-
 | 
						|
          // The key to hitting the race:
 | 
						|
          // 1. Add items in a specific order to control heap structure
 | 
						|
          // 2. Cancel ONLY items that won't be at the front
 | 
						|
          // 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately
 | 
						|
 | 
						|
          // Schedule all items to execute at the SAME time (1ms from now)
 | 
						|
          // Using 1ms instead of 0 to avoid defer queue on multi-core platforms
 | 
						|
          // This ensures they'll all be ready together and go through the heap
 | 
						|
          const uint32_t exec_time = 1;
 | 
						|
 | 
						|
          // CRITICAL: Add a non-cancellable item FIRST
 | 
						|
          // This will be at the front of the heap and block cleanup_()
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() {
 | 
						|
            ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap");
 | 
						|
            id(normal_item_executed)++;
 | 
						|
          });
 | 
						|
 | 
						|
          // Now add items that we WILL cancel
 | 
						|
          // These will be behind the blocker in the heap
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() {
 | 
						|
            ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!");
 | 
						|
            id(removed_item_executed)++;
 | 
						|
            id(test_passed) = false;
 | 
						|
          });
 | 
						|
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() {
 | 
						|
            ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!");
 | 
						|
            id(removed_item_executed)++;
 | 
						|
            id(test_passed) = false;
 | 
						|
          });
 | 
						|
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() {
 | 
						|
            ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!");
 | 
						|
            id(removed_item_executed)++;
 | 
						|
            id(test_passed) = false;
 | 
						|
          });
 | 
						|
 | 
						|
          // Add some more normal items
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() {
 | 
						|
            ESP_LOGD("test", "Normal timeout 1 executed (expected)");
 | 
						|
            id(normal_item_executed)++;
 | 
						|
          });
 | 
						|
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() {
 | 
						|
            ESP_LOGD("test", "Normal timeout 2 executed (expected)");
 | 
						|
            id(normal_item_executed)++;
 | 
						|
          });
 | 
						|
 | 
						|
          App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() {
 | 
						|
            ESP_LOGD("test", "Normal timeout 3 executed (expected)");
 | 
						|
            id(normal_item_executed)++;
 | 
						|
          });
 | 
						|
 | 
						|
          // Force items into the heap before cancelling
 | 
						|
          App.scheduler.process_to_add();
 | 
						|
 | 
						|
          // NOW cancel the items - they're behind "blocker" in the heap
 | 
						|
          // When cleanup_() runs, it will see "blocker" (not removed) at the front
 | 
						|
          // and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap
 | 
						|
          bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1");
 | 
						|
          bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2");
 | 
						|
          bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3");
 | 
						|
 | 
						|
          ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s",
 | 
						|
                   c1 ? "true" : "false",
 | 
						|
                   c2 ? "true" : "false",
 | 
						|
                   c3 ? "true" : "false");
 | 
						|
 | 
						|
          // The heap now has:
 | 
						|
          // - "blocker" at front (not cancelled)
 | 
						|
          // - cancelled items behind it (marked remove=true but still in heap)
 | 
						|
          // - When all execute at once, cleanup_() stops at "blocker"
 | 
						|
          // - The loop then executes ALL ready items including cancelled ones
 | 
						|
 | 
						|
          ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it");
 | 
						|
 | 
						|
      # Wait for all timeouts to execute (or not)
 | 
						|
      - delay: 20ms
 | 
						|
 | 
						|
      # Check results
 | 
						|
      - lambda: |-
 | 
						|
          ESP_LOGI("test", "=== Test Results ===");
 | 
						|
          ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed));
 | 
						|
          ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed));
 | 
						|
 | 
						|
          if (id(removed_item_executed) > 0) {
 | 
						|
            ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed));
 | 
						|
            id(test_passed) = false;
 | 
						|
          } else if (id(normal_item_executed) != 4) {
 | 
						|
            ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed));
 | 
						|
            id(test_passed) = false;
 | 
						|
          } else {
 | 
						|
            ESP_LOGI("test", "TEST PASSED: No cancelled items were executed");
 | 
						|
          }
 | 
						|
 | 
						|
          ESP_LOGI("test", "=== Test Complete ===");
 |