mirror of
https://github.com/esphome/esphome.git
synced 2025-09-02 11:22:24 +01:00
235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
"""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()
|
|
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
|
|
|
|
# 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, 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()
|
|
|
|
# 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 test completion
|
|
try:
|
|
await asyncio.wait_for(test_complete.wait(), timeout=1.0)
|
|
except TimeoutError:
|
|
pytest.fail("Test did not complete within timeout")
|