make test race safe

This commit is contained in:
J. Nick Koston 2025-07-04 08:33:24 -05:00
parent 465019e510
commit ca70f17b3b
No known key found for this signature in database
2 changed files with 132 additions and 109 deletions

View File

@ -1,81 +1,5 @@
esphome: esphome:
name: defer-fifo-simple name: defer-fifo-simple
on_boot:
- lambda: |-
// Test 1: Test set_timeout with 0 delay (direct scheduler call)
static int set_timeout_order = 0;
static bool set_timeout_passed = true;
ESP_LOGD("defer_test", "Test 1: Testing set_timeout(0) for FIFO order...");
for (int i = 0; i < 10; i++) {
int expected = i;
App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
if (set_timeout_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
set_timeout_passed = false;
}
set_timeout_order++;
if (set_timeout_order == 10) {
if (set_timeout_passed) {
ESP_LOGI("defer_test", "✓ Test 1 PASSED - set_timeout(0) maintains FIFO order");
} else {
ESP_LOGE("defer_test", "✗ Test 1 FAILED - set_timeout(0) executed out of order");
}
// Start Test 2 after Test 1 completes
App.scheduler.set_timeout((Component*)nullptr, nullptr, 100, []() {
// Test 2: Test defer() method (component method)
static int defer_order = 0;
static bool defer_passed = true;
ESP_LOGD("defer_test", "Test 2: Testing defer() for FIFO order...");
// Create a test component class that exposes defer()
class TestComponent : public Component {
public:
void test_defer() {
for (int i = 0; i < 10; i++) {
int expected = i;
this->defer([expected]() {
ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
if (defer_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
defer_passed = false;
}
defer_order++;
if (defer_order == 10) {
bool all_passed = set_timeout_passed && defer_passed;
if (defer_passed) {
ESP_LOGI("defer_test", "✓ Test 2 PASSED - defer() maintains FIFO order");
if (all_passed) {
ESP_LOGI("defer_test", "✓ ALL TESTS PASSED - Both set_timeout(0) and defer() maintain FIFO order");
}
} else {
ESP_LOGE("defer_test", "✗ Test 2 FAILED - defer() executed out of order");
}
// Publish test results
id(test_complete)->publish_state(true);
id(test_passed)->publish_state(all_passed);
}
});
}
}
};
TestComponent test_component;
test_component.test_defer();
ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
});
}
});
}
ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
host: host:
@ -83,11 +7,100 @@ logger:
level: DEBUG level: DEBUG
api: api:
services:
- service: run_defer_test
then:
- lambda: |-
// Test 1: Test set_timeout with 0 delay (direct scheduler call)
static int set_timeout_order = 0;
static bool set_timeout_passed = true;
binary_sensor: ESP_LOGD("defer_test", "Test 1: Testing set_timeout(0) for FIFO order...");
for (int i = 0; i < 10; i++) {
int expected = i;
App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
if (set_timeout_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
set_timeout_passed = false;
}
set_timeout_order++;
if (set_timeout_order == 10) {
if (set_timeout_passed) {
ESP_LOGI("defer_test", "✓ Test 1 PASSED - set_timeout(0) maintains FIFO order");
} else {
ESP_LOGE("defer_test", "✗ Test 1 FAILED - set_timeout(0) executed out of order");
}
// Start Test 2 after Test 1 completes
App.scheduler.set_timeout((Component*)nullptr, nullptr, 100, []() {
// Test 2: Test defer() method (component method)
static int defer_order = 0;
static bool defer_passed = true;
ESP_LOGD("defer_test", "Test 2: Testing defer() for FIFO order...");
// Create a test component class that exposes defer()
class TestComponent : public Component {
public:
void test_defer() {
for (int i = 0; i < 10; i++) {
int expected = i;
this->defer([expected]() {
ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
if (defer_order != expected) {
ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
defer_passed = false;
}
defer_order++;
if (defer_order == 10) {
bool all_passed = set_timeout_passed && defer_passed;
if (defer_passed) {
ESP_LOGI("defer_test", "✓ Test 2 PASSED - defer() maintains FIFO order");
if (all_passed) {
ESP_LOGI("defer_test", "✓ ALL TESTS PASSED - Both set_timeout(0) and defer() maintain FIFO order");
}
} else {
ESP_LOGE("defer_test", "✗ Test 2 FAILED - defer() executed out of order");
}
// Fire test result events
if (all_passed) {
id(test_result)->trigger("passed");
} else {
id(test_result)->trigger("failed");
}
id(test_complete)->trigger("test_finished");
}
});
}
}
};
TestComponent test_component;
test_component.test_defer();
ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
});
}
});
}
ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
event:
- platform: template - platform: template
name: "Test Complete" name: "Test Complete"
id: test_complete id: test_complete
device_class: button
event_types:
- "test_finished"
- platform: template - platform: template
name: "Test Passed" name: "Test Result"
id: test_passed id: test_result
device_class: button
event_types:
- "passed"
- "failed"

View File

@ -2,7 +2,7 @@
import asyncio import asyncio
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityState from aioesphomeapi import EntityState, Event, EventInfo, UserService
import pytest import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction from .types import APIClientConnectedFactory, RunCompiledFunction
@ -22,57 +22,67 @@ async def test_defer_fifo_simple(
assert device_info is not None assert device_info is not None
assert device_info.name == "defer-fifo-simple" assert device_info.name == "defer-fifo-simple"
# List entities to get the keys # List entities and services
entity_info, _ = await asyncio.wait_for( entity_info, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0 client.list_entities_services(), timeout=5.0
) )
# Find our test entities # Find our test entities
test_complete_entity: BinarySensorInfo | None = None test_complete_entity: EventInfo | None = None
test_passed_entity: BinarySensorInfo | None = None test_result_entity: EventInfo | None = None
for entity in entity_info: for entity in entity_info:
if isinstance(entity, BinarySensorInfo): if isinstance(entity, EventInfo):
if entity.object_id == "test_complete": if entity.object_id == "test_complete":
test_complete_entity = entity test_complete_entity = entity
elif entity.object_id == "test_passed": elif entity.object_id == "test_result":
test_passed_entity = entity test_result_entity = entity
assert test_complete_entity is not None, "test_complete sensor not found" assert test_complete_entity is not None, "test_complete event not found"
assert test_passed_entity is not None, "test_passed sensor not found" assert test_result_entity is not None, "test_result event not found"
# Find our test service
run_defer_test_service: UserService | None = None
for service in services:
if service.name == "run_defer_test":
run_defer_test_service = service
break
assert run_defer_test_service is not None, "run_defer_test service not found"
# Get the event loop # Get the event loop
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
# Subscribe to state changes # Subscribe to states (events are delivered as EventStates through subscribe_states)
states: dict[int, EntityState] = {} test_complete_future: asyncio.Future[bool] = loop.create_future()
test_complete_future: asyncio.Future[BinarySensorState] = loop.create_future() test_result_future: asyncio.Future[bool] = loop.create_future()
test_passed_future: asyncio.Future[BinarySensorState] = loop.create_future()
def on_state(state: EntityState) -> None: def on_state(state: EntityState) -> None:
states[state.key] = state if isinstance(state, Event):
# Check if this is our test_complete binary sensor
if isinstance(state, BinarySensorState):
if state.key == test_complete_entity.key: if state.key == test_complete_entity.key:
if state.state and not test_complete_future.done(): if (
test_complete_future.set_result(state) state.event_type == "test_finished"
elif state.key == test_passed_entity.key: and not test_complete_future.done()
if not test_passed_future.done(): ):
test_passed_future.set_result(state) test_complete_future.set_result(True)
elif state.key == test_result_entity.key:
if not test_result_future.done():
if state.event_type == "passed":
test_result_future.set_result(True)
elif state.event_type == "failed":
test_result_future.set_result(False)
client.subscribe_states(on_state) client.subscribe_states(on_state)
# Call the run_defer_test service to start the test
client.execute_service(run_defer_test_service, {})
# Wait for test completion with timeout # Wait for test completion with timeout
try: try:
await asyncio.wait_for(test_complete_future, timeout=10.0) await asyncio.wait_for(test_complete_future, timeout=10.0)
test_passed_state = await asyncio.wait_for(test_passed_future, timeout=1.0) test_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pytest.fail( pytest.fail("Test did not complete within 10 seconds")
f"Test did not complete within 10 seconds. "
f"Received states: {list(states.values())}"
)
# Verify the test passed # Verify the test passed
assert test_passed_state.state is True, ( assert test_passed is True, "FIFO test failed - items executed out of order"
"FIFO test failed - items executed out of order"
)