[scheduler] Remove deprecated set_retry/cancel_retry (#17585)

This commit is contained in:
J. Nick Koston
2026-07-15 17:58:41 -10:00
committed by GitHub
parent 90e838d327
commit e327a7f52f
9 changed files with 41 additions and 966 deletions
@@ -18,9 +18,6 @@ globals:
- id: interval_counter
type: int
initial_value: '0'
- id: retry_counter
type: int
initial_value: '0'
- id: defer_counter
type: int
initial_value: '0'
@@ -118,29 +115,7 @@ script:
id(timeout_counter) += 1;
});
// Test 10: set_retry with numeric ID
App.scheduler.set_retry(component1, 6001U, 50, 3,
[](uint8_t retry_countdown) {
id(retry_counter)++;
ESP_LOGI("test", "Numeric retry 6001 attempt %d (countdown=%d)",
id(retry_counter), retry_countdown);
if (id(retry_counter) >= 2) {
ESP_LOGI("test", "Numeric retry 6001 done");
return RetryResult::DONE;
}
return RetryResult::RETRY;
});
// Test 11: cancel_retry with numeric ID
App.scheduler.set_retry(component1, 6002U, 100, 5,
[](uint8_t retry_countdown) {
ESP_LOGE("test", "ERROR: Numeric retry 6002 should have been cancelled");
return RetryResult::RETRY;
});
App.scheduler.cancel_retry(component1, 6002U);
ESP_LOGI("test", "Cancelled numeric retry 6002");
// Test 12: defer with numeric ID (Component method)
// Test 10: defer with numeric ID (Component method)
class TestDeferComponent : public Component {
public:
void test_defer_methods() {
@@ -161,7 +136,7 @@ script:
static TestDeferComponent test_defer_component;
test_defer_component.test_defer_methods();
// Test 13: cancel_defer with numeric ID (Component method)
// Test 11: cancel_defer with numeric ID (Component method)
class TestCancelDeferComponent : public Component {
public:
void test_cancel_defer() {
@@ -181,8 +156,8 @@ script:
- id: report_results
then:
- lambda: |-
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d",
id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter));
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Defers: %d",
id(timeout_counter), id(interval_counter), id(defer_counter));
sensor:
- platform: template
@@ -1,287 +0,0 @@
esphome:
debug_scheduler: true # Enable scheduler leak detection
name: scheduler-retry-test
on_boot:
priority: -100
then:
- logger.log: "Starting scheduler retry tests"
# Run all tests sequentially with delays
- script.execute: run_all_tests
host:
api:
logger:
level: VERY_VERBOSE
globals:
- id: simple_retry_counter
type: int
initial_value: '0'
- id: backoff_retry_counter
type: int
initial_value: '0'
- id: backoff_last_attempt_time
type: uint32_t
initial_value: '0'
- id: immediate_done_counter
type: int
initial_value: '0'
- id: cancel_retry_counter
type: int
initial_value: '0'
- id: empty_name_retry_counter
type: int
initial_value: '0'
- id: script_retry_counter
type: int
initial_value: '0'
- id: multiple_same_name_counter
type: int
initial_value: '0'
- id: const_char_retry_counter
type: int
initial_value: '0'
- id: static_char_retry_counter
type: int
initial_value: '0'
# Using different component types for each test to ensure isolation
sensor:
- platform: template
name: Simple Retry Test Sensor
id: simple_retry_sensor
lambda: return 1.0;
update_interval: never
- platform: template
name: Backoff Retry Test Sensor
id: backoff_retry_sensor
lambda: return 2.0;
update_interval: never
- platform: template
name: Immediate Done Test Sensor
id: immediate_done_sensor
lambda: return 3.0;
update_interval: never
binary_sensor:
- platform: template
name: Cancel Retry Test Binary Sensor
id: cancel_retry_binary_sensor
lambda: return false;
- platform: template
name: Empty Name Test Binary Sensor
id: empty_name_binary_sensor
lambda: return true;
switch:
- platform: template
name: Script Retry Test Switch
id: script_retry_switch
optimistic: true
- platform: template
name: Multiple Same Name Test Switch
id: multiple_same_name_switch
optimistic: true
script:
- id: run_all_tests
then:
# Test 1: Simple retry
- logger.log: "=== Test 1: Simple retry ==="
- lambda: |-
auto *component = id(simple_retry_sensor);
App.scheduler.set_retry(component, "simple_retry", 50, 3,
[](uint8_t retry_countdown) {
id(simple_retry_counter)++;
ESP_LOGI("test", "Simple retry attempt %d (countdown=%d)",
id(simple_retry_counter), retry_countdown);
if (id(simple_retry_counter) >= 2) {
ESP_LOGI("test", "Simple retry succeeded on attempt %d", id(simple_retry_counter));
return RetryResult::DONE;
}
return RetryResult::RETRY;
});
# Test 2: Backoff retry
- logger.log: "=== Test 2: Retry with backoff ==="
- lambda: |-
auto *component = id(backoff_retry_sensor);
App.scheduler.set_retry(component, "backoff_retry", 50, 4,
[](uint8_t retry_countdown) {
id(backoff_retry_counter)++;
uint32_t now = millis();
uint32_t interval = 0;
// Only calculate interval after first attempt
if (id(backoff_retry_counter) > 1) {
interval = now - id(backoff_last_attempt_time);
}
id(backoff_last_attempt_time) = now;
ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)",
id(backoff_retry_counter), retry_countdown, interval);
if (id(backoff_retry_counter) == 1) {
ESP_LOGI("test", "First call was immediate");
} else if (id(backoff_retry_counter) == 2) {
ESP_LOGI("test", "Second call interval: %dms (expected ~50ms)", interval);
} else if (id(backoff_retry_counter) == 3) {
ESP_LOGI("test", "Third call interval: %dms (expected ~100ms)", interval);
} else if (id(backoff_retry_counter) == 4) {
ESP_LOGI("test", "Fourth call interval: %dms (expected ~200ms)", interval);
ESP_LOGI("test", "Backoff retry completed");
return RetryResult::DONE;
}
return RetryResult::RETRY;
}, 2.0f);
# Test 3: Immediate done
- logger.log: "=== Test 3: Immediate done ==="
- lambda: |-
auto *component = id(immediate_done_sensor);
App.scheduler.set_retry(component, "immediate_done", 50, 5,
[](uint8_t retry_countdown) {
id(immediate_done_counter)++;
ESP_LOGI("test", "Immediate done retry called (countdown=%d)", retry_countdown);
return RetryResult::DONE;
});
# Test 4: Cancel retry
- logger.log: "=== Test 4: Cancel retry ==="
- lambda: |-
auto *component = id(cancel_retry_binary_sensor);
App.scheduler.set_retry(component, "cancel_test", 30, 10,
[](uint8_t retry_countdown) {
id(cancel_retry_counter)++;
ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter));
return RetryResult::RETRY;
});
// Cancel it after 100ms
App.scheduler.set_timeout(component, "cancel_timer", 100, []() {
bool cancelled = App.scheduler.cancel_retry(id(cancel_retry_binary_sensor), "cancel_test");
ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false");
ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter));
});
# Test 5: Empty name retry
- logger.log: "=== Test 5: Empty name retry ==="
- lambda: |-
auto *component = id(empty_name_binary_sensor);
App.scheduler.set_retry(component, "", 100, 5,
[](uint8_t retry_countdown) {
id(empty_name_retry_counter)++;
ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter));
return RetryResult::RETRY;
});
// Try to cancel after 150ms
App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() {
bool cancelled = App.scheduler.cancel_retry(id(empty_name_binary_sensor), "");
ESP_LOGI("test", "Empty name retry cancel result: %s",
cancelled ? "true" : "false");
ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter));
});
# Test 6: Component method
- logger.log: "=== Test 6: Component::set_retry method ==="
- lambda: |-
class TestRetryComponent : public Component {
public:
void test_retry() {
this->set_retry(50, 3,
[](uint8_t retry_countdown) {
id(script_retry_counter)++;
ESP_LOGI("test", "Component retry attempt %d", id(script_retry_counter));
if (id(script_retry_counter) >= 2) {
return RetryResult::DONE;
}
return RetryResult::RETRY;
}, 1.5f);
}
};
static TestRetryComponent test_component;
test_component.test_retry();
# Test 7: Multiple same name
- logger.log: "=== Test 7: Multiple retries with same name ==="
- lambda: |-
auto *component = id(multiple_same_name_switch);
// Set first retry
App.scheduler.set_retry(component, "duplicate_retry", 100, 5,
[](uint8_t retry_countdown) {
id(multiple_same_name_counter) += 1;
ESP_LOGI("test", "First duplicate retry - should not run");
return RetryResult::RETRY;
});
// Set second retry with same name (should cancel first)
App.scheduler.set_retry(component, "duplicate_retry", 50, 3,
[](uint8_t retry_countdown) {
id(multiple_same_name_counter) += 10;
ESP_LOGI("test", "Second duplicate retry attempt (counter=%d)",
id(multiple_same_name_counter));
if (id(multiple_same_name_counter) >= 20) {
return RetryResult::DONE;
}
return RetryResult::RETRY;
});
# Test 8: Const char* overloads
- logger.log: "=== Test 8: Const char* overloads ==="
- lambda: |-
auto *component = id(simple_retry_sensor);
// Test 8a: Direct string literal
App.scheduler.set_retry(component, "const_char_test", 30, 2,
[](uint8_t retry_countdown) {
id(const_char_retry_counter)++;
ESP_LOGI("test", "Const char retry %d", id(const_char_retry_counter));
return RetryResult::DONE;
});
# Test 9: Static const char* variable
- logger.log: "=== Test 9: Static const char* ==="
- lambda: |-
auto *component = id(backoff_retry_sensor);
static const char* STATIC_NAME = "static_retry_test";
App.scheduler.set_retry(component, STATIC_NAME, 20, 1,
[](uint8_t retry_countdown) {
id(static_char_retry_counter)++;
ESP_LOGI("test", "Static const char retry %d", id(static_char_retry_counter));
return RetryResult::DONE;
});
// Cancel with same static const char*
App.scheduler.set_timeout(component, "static_cancel", 10, []() {
static const char* STATIC_NAME = "static_retry_test";
bool result = App.scheduler.cancel_retry(id(backoff_retry_sensor), STATIC_NAME);
ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false");
});
# Wait for all tests to complete before reporting
- delay: 500ms
# Final report
- logger.log: "=== Retry Test Results ==="
- lambda: |-
ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter));
ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter));
ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter));
ESP_LOGI("test", "Cancel retry counter: %d (expected 2-4)", id(cancel_retry_counter));
ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter));
ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter));
ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter));
ESP_LOGI("test", "Const char retry counter: %d (expected 1)", id(const_char_retry_counter));
ESP_LOGI("test", "Static char retry counter: %d (expected 1)", id(static_char_retry_counter));
ESP_LOGI("test", "All retry tests completed");
@@ -18,7 +18,6 @@ async def test_scheduler_numeric_id_test(
# Track counts
timeout_count = 0
interval_count = 0
retry_count = 0
defer_count = 0
# Events for each test completion
@@ -32,8 +31,6 @@ async def test_scheduler_numeric_id_test(
component_interval_fired = asyncio.Event()
zero_id_timeout_fired = asyncio.Event()
max_id_timeout_fired = asyncio.Event()
numeric_retry_done = asyncio.Event()
numeric_retry_cancelled = asyncio.Event()
numeric_defer_7001_fired = asyncio.Event()
numeric_defer_7002_fired = asyncio.Event()
numeric_defer_cancelled = asyncio.Event()
@@ -41,11 +38,10 @@ async def test_scheduler_numeric_id_test(
# Track interval counts
numeric_interval_count = 0
numeric_retry_count = 0
def on_log_line(line: str) -> None:
nonlocal timeout_count, interval_count, retry_count, defer_count
nonlocal numeric_interval_count, numeric_retry_count
nonlocal timeout_count, interval_count, defer_count
nonlocal numeric_interval_count
# Strip ANSI color codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
@@ -97,18 +93,6 @@ async def test_scheduler_numeric_id_test(
max_id_timeout_fired.set()
timeout_count += 1
# Check for numeric retry tests
elif "Numeric retry 6001 attempt" in clean_line:
match = re.search(r"attempt (\d+)", clean_line)
if match:
numeric_retry_count = int(match.group(1))
elif "Numeric retry 6001 done" in clean_line:
numeric_retry_done.set()
elif "Cancelled numeric retry 6002" in clean_line:
numeric_retry_cancelled.set()
# Check for numeric defer tests
elif "Component numeric defer 7001 fired" in clean_line:
numeric_defer_7001_fired.set()
@@ -122,14 +106,13 @@ async def test_scheduler_numeric_id_test(
# Check for final results
elif "Final results" in clean_line:
match = re.search(
r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)",
r"Timeouts: (\d+), Intervals: (\d+), Defers: (\d+)",
clean_line,
)
if match:
timeout_count = int(match.group(1))
interval_count = int(match.group(2))
retry_count = int(match.group(3))
defer_count = int(match.group(4))
defer_count = int(match.group(3))
final_results_logged.set()
async with (
@@ -200,23 +183,6 @@ async def test_scheduler_numeric_id_test(
except TimeoutError:
pytest.fail("Max ID timeout did not fire within 0.5 seconds")
# Wait for numeric retry tests
try:
await asyncio.wait_for(numeric_retry_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Numeric retry 6001 did not complete. Count: {numeric_retry_count}"
)
assert numeric_retry_count >= 2, (
f"Expected at least 2 numeric retry attempts, got {numeric_retry_count}"
)
# Verify numeric retry was cancelled
assert numeric_retry_cancelled.is_set(), (
"Numeric retry 6002 should have been cancelled"
)
# Wait for numeric defer tests
try:
await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5)
@@ -245,7 +211,4 @@ async def test_scheduler_numeric_id_test(
assert interval_count >= 3, (
f"Expected at least 3 interval fires, got {interval_count}"
)
assert retry_count >= 2, (
f"Expected at least 2 retry attempts, got {retry_count}"
)
assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}"
@@ -1,279 +0,0 @@
"""Test scheduler retry functionality."""
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_scheduler_retry_test(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that scheduler retry functionality works correctly."""
# Track test progress
simple_retry_done = asyncio.Event()
backoff_retry_done = asyncio.Event()
immediate_done_done = asyncio.Event()
cancel_retry_done = asyncio.Event()
empty_name_retry_done = asyncio.Event()
component_retry_done = asyncio.Event()
multiple_name_done = asyncio.Event()
const_char_done = asyncio.Event()
static_char_done = asyncio.Event()
test_complete = asyncio.Event()
# Track retry counts
simple_retry_count = 0
backoff_retry_count = 0
immediate_done_count = 0
cancel_retry_count = 0
empty_name_retry_count = 0
component_retry_count = 0
multiple_name_count = 0
const_char_retry_count = 0
static_char_retry_count = 0
# Track specific test results
cancel_result = None
empty_cancel_result = None
backoff_intervals = []
def on_log_line(line: str) -> None:
nonlocal simple_retry_count, backoff_retry_count, immediate_done_count
nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count
nonlocal multiple_name_count, const_char_retry_count, static_char_retry_count
nonlocal cancel_result, empty_cancel_result
# Strip ANSI color codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
# Simple retry test
if "Simple retry attempt" in clean_line:
if match := re.search(r"Simple retry attempt (\d+)", clean_line):
simple_retry_count = int(match.group(1))
elif "Simple retry succeeded on attempt" in clean_line:
simple_retry_done.set()
# Backoff retry test
elif "Backoff retry attempt" in clean_line:
if match := re.search(
r"Backoff retry attempt (\d+).*interval=(\d+)ms", clean_line
):
backoff_retry_count = int(match.group(1))
interval = int(match.group(2))
if backoff_retry_count > 1: # Skip first (immediate) call
backoff_intervals.append(interval)
elif "Backoff retry completed" in clean_line:
backoff_retry_done.set()
# Immediate done test
elif "Immediate done retry called" in clean_line:
immediate_done_count += 1
immediate_done_done.set()
# Cancel retry test
elif "Cancel test retry attempt" in clean_line:
cancel_retry_count += 1
elif "Retry cancellation result:" in clean_line:
cancel_result = "true" in clean_line
cancel_retry_done.set()
# Empty name retry test
elif "Empty name retry attempt" in clean_line:
if match := re.search(r"Empty name retry attempt (\d+)", clean_line):
empty_name_retry_count = int(match.group(1))
elif "Empty name retry cancel result:" in clean_line:
empty_cancel_result = "true" in clean_line
elif "Empty name retry ran" in clean_line:
empty_name_retry_done.set()
# Component retry test
elif "Component retry attempt" in clean_line:
if match := re.search(r"Component retry attempt (\d+)", clean_line):
component_retry_count = int(match.group(1))
if component_retry_count >= 2:
component_retry_done.set()
# Multiple same name test
elif "Second duplicate retry attempt" in clean_line:
if match := re.search(r"counter=(\d+)", clean_line):
multiple_name_count = int(match.group(1))
if multiple_name_count >= 20:
multiple_name_done.set()
# Const char retry test
elif "Const char retry" in clean_line:
if match := re.search(r"Const char retry (\d+)", clean_line):
const_char_retry_count = int(match.group(1))
const_char_done.set()
# Static const char retry test
elif "Static const char retry" in clean_line:
if match := re.search(r"Static const char retry (\d+)", clean_line):
static_char_retry_count = int(match.group(1))
static_char_done.set()
elif "Static cancel result:" in clean_line:
# This is part of test 9, but we don't track it separately
pass
# Test completion
elif "All retry tests completed" in clean_line:
test_complete.set()
async with (
run_compiled(yaml_config, line_callback=on_log_line),
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-retry-test"
# Wait for simple retry test
try:
await asyncio.wait_for(simple_retry_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Simple retry test did not complete. Count: {simple_retry_count}"
)
assert simple_retry_count == 2, (
f"Expected 2 simple retry attempts, got {simple_retry_count}"
)
# Wait for backoff retry test
try:
await asyncio.wait_for(backoff_retry_done.wait(), timeout=3.0)
except TimeoutError:
pytest.fail(
f"Backoff retry test did not complete. Count: {backoff_retry_count}"
)
assert backoff_retry_count == 4, (
f"Expected 4 backoff retry attempts, got {backoff_retry_count}"
)
# Verify backoff intervals (allowing for timing variations)
assert len(backoff_intervals) >= 2, (
f"Expected at least 2 intervals, got {len(backoff_intervals)}"
)
if len(backoff_intervals) >= 3:
# First interval should be ~50ms (very wide tolerance for heavy system load)
assert 20 <= backoff_intervals[0] <= 150, (
f"First interval {backoff_intervals[0]}ms not ~50ms"
)
# Second interval should be ~100ms (50ms * 2.0)
assert 50 <= backoff_intervals[1] <= 250, (
f"Second interval {backoff_intervals[1]}ms not ~100ms"
)
# Third interval should be ~200ms (100ms * 2.0)
assert 100 <= backoff_intervals[2] <= 500, (
f"Third interval {backoff_intervals[2]}ms not ~200ms"
)
# Wait for immediate done test
try:
await asyncio.wait_for(immediate_done_done.wait(), timeout=3.0)
except TimeoutError:
pytest.fail(
f"Immediate done test did not complete. Count: {immediate_done_count}"
)
assert immediate_done_count == 1, (
f"Expected 1 immediate done call, got {immediate_done_count}"
)
# Wait for cancel retry test
try:
await asyncio.wait_for(cancel_retry_done.wait(), timeout=3.0)
except TimeoutError:
pytest.fail(
f"Cancel retry test did not complete. Count: {cancel_retry_count}"
)
assert cancel_result is True, "Retry cancellation should have succeeded"
assert 2 <= cancel_retry_count <= 5, (
f"Expected 2-5 cancel retry attempts before cancellation, got {cancel_retry_count}"
)
# Wait for empty name retry test
try:
await asyncio.wait_for(empty_name_retry_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Empty name retry test did not complete. Count: {empty_name_retry_count}"
)
# Empty name retry should run at least once before being cancelled
assert 1 <= empty_name_retry_count <= 3, (
f"Expected 1-3 empty name retry attempts, got {empty_name_retry_count}"
)
assert empty_cancel_result is True, (
"Empty name retry cancel should have succeeded"
)
# Wait for component retry test
try:
await asyncio.wait_for(component_retry_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Component retry test did not complete. Count: {component_retry_count}"
)
assert component_retry_count >= 2, (
f"Expected at least 2 component retry attempts, got {component_retry_count}"
)
# Wait for multiple same name test
try:
await asyncio.wait_for(multiple_name_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Multiple same name test did not complete. Count: {multiple_name_count}"
)
# Should be 20+ (only second retry should run)
assert multiple_name_count >= 20, (
f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}"
)
# Wait for const char retry test
try:
await asyncio.wait_for(const_char_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Const char retry test did not complete. Count: {const_char_retry_count}"
)
assert const_char_retry_count == 1, (
f"Expected 1 const char retry call, got {const_char_retry_count}"
)
# Wait for static char retry test
try:
await asyncio.wait_for(static_char_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Static char retry test did not complete. Count: {static_char_retry_count}"
)
assert static_char_retry_count == 1, (
f"Expected 1 static char retry call, got {static_char_retry_count}"
)
# Wait for test completion
try:
await asyncio.wait_for(test_complete.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Test did not complete within timeout")