This commit is contained in:
J. Nick Koston 2025-07-04 08:59:15 -05:00
parent 0665fcea9e
commit f7ca26eef8
No known key found for this signature in database
2 changed files with 33 additions and 51 deletions

View File

@ -1,65 +1,36 @@
esphome: esphome:
name: defer-stress-test name: defer-stress-test
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
components: [defer_stress_component]
host: host:
logger: logger:
level: DEBUG level: DEBUG
defer_stress_component:
id: defer_stress
api: api:
services: services:
- service: run_stress_test - service: run_stress_test
then: then:
- lambda: |- - lambda: |-
static int total_defers = 0; id(defer_stress)->run_multi_thread_test();
static int executed_defers = 0; - wait_until:
lambda: |-
ESP_LOGI("stress", "Starting defer stress test - rapid sequential defers"); return id(defer_stress)->is_test_complete();
- lambda: |-
// Reset counters if (id(defer_stress)->is_test_passed()) {
total_defers = 0; id(test_result)->trigger("passed");
executed_defers = 0; } else {
id(test_result)->trigger("failed");
// Create a temporary component to access defer() }
class TestComponent : public Component { id(test_complete)->trigger("test_finished");
public:
void run_test() {
// Rapidly defer many callbacks to stress the defer mechanism
for (int batch = 0; batch < 10; batch++) {
for (int i = 0; i < 100; i++) {
int expected_id = total_defers;
this->defer([expected_id]() {
executed_defers++;
ESP_LOGV("stress", "Defer %d executed", expected_id);
});
total_defers++;
}
// Brief yield to let other work happen
delay(1);
}
}
};
TestComponent test_comp;
test_comp.run_test();
ESP_LOGI("stress", "Scheduled %d defers", total_defers);
// Give the main loop time to process all defers
App.scheduler.set_timeout((Component*)nullptr, nullptr, 500, []() {
ESP_LOGI("stress", "Test complete. Defers scheduled: %d, executed: %d", total_defers, executed_defers);
// We should have executed all defers without crashing
if (executed_defers == total_defers && total_defers == 1000) {
ESP_LOGI("stress", "✓ Stress test PASSED - All %d defers executed", total_defers);
id(test_result)->trigger("passed");
} else {
ESP_LOGE("stress", "✗ Stress test FAILED - Expected 1000 executed, got %d", executed_defers);
id(test_result)->trigger("failed");
}
id(test_complete)->trigger("test_finished");
});
event: event:
- platform: template - platform: template

View File

@ -1,6 +1,7 @@
"""Stress test for defer() thread safety with multiple threads.""" """Stress test for defer() thread safety with multiple threads."""
import asyncio import asyncio
from pathlib import Path
from aioesphomeapi import EntityState, Event, EventInfo, UserService from aioesphomeapi import EntityState, Event, EventInfo, UserService
import pytest import pytest
@ -16,6 +17,16 @@ async def test_defer_stress(
) -> None: ) -> None:
"""Test that defer() doesn't crash when called rapidly from multiple threads.""" """Test that defer() doesn't crash when called rapidly from multiple threads."""
# Get the absolute path to the external components directory
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
# Replace the placeholder in the YAML config with the actual path
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
async with run_compiled(yaml_config), api_client_connected() as client: async with run_compiled(yaml_config), api_client_connected() as client:
# Verify we can connect # Verify we can connect
device_info = await client.device_info() device_info = await client.device_info()
@ -79,10 +90,10 @@ async def test_defer_stress(
# Wait for test completion with a longer timeout (threads run for 100ms + processing time) # Wait for test completion with a longer timeout (threads run for 100ms + processing time)
try: try:
await asyncio.wait_for(test_complete_future, timeout=10.0) await asyncio.wait_for(test_complete_future, timeout=15.0)
test_passed = await asyncio.wait_for(test_result_future, timeout=1.0) test_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pytest.fail("Stress test did not complete within 10 seconds") pytest.fail("Stress test did not complete within 15 seconds")
# Verify the test passed # Verify the test passed
assert test_passed is True, ( assert test_passed is True, (