mirror of
https://github.com/esphome/esphome.git
synced 2026-05-25 18:47:56 +08:00
[scheduler] Add self-keyed timer API for callers without a Component (#16127)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-self-keyed-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- logger.log: "Starting scheduler self-keyed tests"
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
globals:
|
||||
- id: tests_done
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
|
||||
script:
|
||||
- id: test_self_keyed
|
||||
then:
|
||||
- logger.log: "Testing self-keyed scheduler API"
|
||||
- lambda: |-
|
||||
// Two distinct keys backed by addresses of static markers — they
|
||||
// must not collide even though both are self-keyed and share no
|
||||
// Component pointer. Static storage gives them stable, unique
|
||||
// addresses for the lifetime of the program.
|
||||
static int key_a_marker = 0;
|
||||
static int key_b_marker = 0;
|
||||
void *key_a = &key_a_marker;
|
||||
void *key_b = &key_b_marker;
|
||||
|
||||
// ---- Test 1: Self-keyed timeout fires ----
|
||||
App.scheduler.set_timeout(key_a, 50, []() {
|
||||
ESP_LOGI("test", "Self timeout A fired");
|
||||
});
|
||||
|
||||
// ---- Test 2: Self-keyed cancel cancels only that key ----
|
||||
App.scheduler.set_timeout(key_b, 100, []() {
|
||||
ESP_LOGE("test", "ERROR: Self timeout B should have been cancelled");
|
||||
});
|
||||
App.scheduler.cancel_timeout(key_b);
|
||||
|
||||
// ---- Test 3: Two independent self keys don't collide ----
|
||||
// Using fresh static markers so neither matches key_a / key_b.
|
||||
static int key_c_marker = 0;
|
||||
static int key_d_marker = 0;
|
||||
void *key_c = &key_c_marker;
|
||||
void *key_d = &key_d_marker;
|
||||
App.scheduler.set_timeout(key_c, 150, []() {
|
||||
ESP_LOGI("test", "Self timeout C fired");
|
||||
});
|
||||
App.scheduler.set_timeout(key_d, 150, []() {
|
||||
ESP_LOGI("test", "Self timeout D fired");
|
||||
});
|
||||
|
||||
// ---- Test 4: Self-keyed and component-keyed don't collide ----
|
||||
// Use a self pointer that happens to look like a Component-attached id.
|
||||
// The scheduler must treat them as separate namespaces.
|
||||
static int shared_marker = 0;
|
||||
void *self_shared = &shared_marker;
|
||||
App.scheduler.set_timeout(self_shared, 200, []() {
|
||||
ESP_LOGI("test", "Self timeout shared fired");
|
||||
});
|
||||
App.scheduler.set_timeout(id(test_sensor), 7777U, 200, []() {
|
||||
ESP_LOGI("test", "Component timeout 7777 fired");
|
||||
});
|
||||
|
||||
// ---- Test 5: Self-keyed interval fires multiple times then cancels ----
|
||||
static int interval_count = 0;
|
||||
static int key_e_marker = 0;
|
||||
void *key_e = &key_e_marker;
|
||||
App.scheduler.set_interval(key_e, 80, [key_e]() {
|
||||
interval_count++;
|
||||
if (interval_count == 2) {
|
||||
ESP_LOGI("test", "Self interval E fired twice");
|
||||
App.scheduler.cancel_interval(key_e);
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Test 6: Re-registering same self-key replaces the timer ----
|
||||
// The old timer must NOT fire; only the new one does.
|
||||
static int key_f_marker = 0;
|
||||
void *key_f = &key_f_marker;
|
||||
App.scheduler.set_timeout(key_f, 250, []() {
|
||||
ESP_LOGE("test", "ERROR: Self timeout F first registration should have been replaced");
|
||||
});
|
||||
App.scheduler.set_timeout(key_f, 300, []() {
|
||||
ESP_LOGI("test", "Self timeout F replacement fired");
|
||||
});
|
||||
|
||||
// Log completion after all timers should have fired
|
||||
App.scheduler.set_timeout(id(test_sensor), 9999U, 1500, []() {
|
||||
ESP_LOGI("test", "All self-keyed tests complete");
|
||||
});
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: Test Sensor
|
||||
id: test_sensor
|
||||
lambda: return 1.0;
|
||||
update_interval: never
|
||||
|
||||
interval:
|
||||
- interval: 0.1s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return id(tests_done) == false;'
|
||||
then:
|
||||
- lambda: 'id(tests_done) = true;'
|
||||
- script.execute: test_self_keyed
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Test the self-keyed scheduler API.
|
||||
|
||||
Verifies that `Scheduler::set_timeout(const void *, ...)` /
|
||||
`set_interval(const void *, ...)` and the matching `cancel_*(const void *)`
|
||||
overloads behave correctly: callbacks fire, distinct keys don't collide,
|
||||
self-keyed and component-keyed namespaces are independent, and re-registering
|
||||
the same key replaces the existing timer.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scheduler_self_keyed(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test self-keyed scheduler API."""
|
||||
self_a_fired = asyncio.Event()
|
||||
self_b_error = asyncio.Event()
|
||||
self_c_fired = asyncio.Event()
|
||||
self_d_fired = asyncio.Event()
|
||||
self_shared_fired = asyncio.Event()
|
||||
component_7777_fired = asyncio.Event()
|
||||
self_interval_done = asyncio.Event()
|
||||
self_f_first_error = asyncio.Event()
|
||||
self_f_replacement_fired = asyncio.Event()
|
||||
all_tests_complete = asyncio.Event()
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
|
||||
if "Self timeout A fired" in clean_line:
|
||||
self_a_fired.set()
|
||||
elif "ERROR: Self timeout B" in clean_line:
|
||||
self_b_error.set()
|
||||
elif "Self timeout C fired" in clean_line:
|
||||
self_c_fired.set()
|
||||
elif "Self timeout D fired" in clean_line:
|
||||
self_d_fired.set()
|
||||
elif "Self timeout shared fired" in clean_line:
|
||||
self_shared_fired.set()
|
||||
elif "Component timeout 7777 fired" in clean_line:
|
||||
component_7777_fired.set()
|
||||
elif "Self interval E fired twice" in clean_line:
|
||||
self_interval_done.set()
|
||||
elif "ERROR: Self timeout F first registration" in clean_line:
|
||||
self_f_first_error.set()
|
||||
elif "Self timeout F replacement fired" in clean_line:
|
||||
self_f_replacement_fired.set()
|
||||
elif "All self-keyed tests complete" in clean_line:
|
||||
all_tests_complete.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "scheduler-self-keyed-test"
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Not all self-keyed tests completed within 5 seconds")
|
||||
|
||||
# Test 1: self-keyed timeout fires
|
||||
assert self_a_fired.is_set(), "Self timeout A should have fired"
|
||||
|
||||
# Test 2: cancel_timeout(self) actually cancels
|
||||
assert not self_b_error.is_set(), "Self timeout B should have been cancelled"
|
||||
|
||||
# Test 3: distinct self keys don't collide
|
||||
assert self_c_fired.is_set(), "Self timeout C should have fired"
|
||||
assert self_d_fired.is_set(), "Self timeout D should have fired"
|
||||
|
||||
# Test 4: self-keyed and component-keyed namespaces are independent
|
||||
assert self_shared_fired.is_set(), "Self timeout shared should have fired"
|
||||
assert component_7777_fired.is_set(), "Component timeout 7777 should have fired"
|
||||
|
||||
# Test 5: self-keyed interval fires repeatedly and cancels cleanly
|
||||
assert self_interval_done.is_set(), "Self interval E should have fired twice"
|
||||
|
||||
# Test 6: re-registering same self-key replaces the previous timer
|
||||
assert not self_f_first_error.is_set(), (
|
||||
"Self timeout F first registration should have been replaced"
|
||||
)
|
||||
assert self_f_replacement_fired.is_set(), (
|
||||
"Self timeout F replacement should have fired"
|
||||
)
|
||||
Reference in New Issue
Block a user