Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org>
122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
"""Integration test for script.wait FIFO ordering (issues #12043, #12044).
|
|
|
|
This test verifies that ScriptWaitAction processes queued items in FIFO order.
|
|
|
|
PR #7972 introduced bugs in ScriptWaitAction:
|
|
- Used emplace_front() causing LIFO ordering instead of FIFO
|
|
- Called loop() synchronously causing reentrancy issues
|
|
- Used while loop processing entire queue causing infinite loops
|
|
|
|
These bugs manifested as:
|
|
- Scripts becoming "zombies" (stuck in running state)
|
|
- script.wait hanging forever
|
|
- Incorrect execution order
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_script_delay_with_params(
|
|
yaml_config: str,
|
|
run_compiled: RunCompiledFunction,
|
|
api_client_connected: APIClientConnectedFactory,
|
|
) -> None:
|
|
"""Test that script.wait processes queued items in FIFO order.
|
|
|
|
This reproduces issues #12043 and #12044 where scripts would hang or become
|
|
zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972.
|
|
"""
|
|
test_complete = asyncio.Event()
|
|
|
|
# Patterns to match in logs
|
|
father_calling_pattern = re.compile(r"Father iteration (\d+): calling son")
|
|
son_started_pattern = re.compile(r"Son script started with iteration (\d+)")
|
|
son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)")
|
|
son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)")
|
|
father_wait_returned_pattern = re.compile(
|
|
r"Father iteration (\d+): son finished, wait returned"
|
|
)
|
|
|
|
# Track which iterations completed
|
|
father_calling = set()
|
|
son_started = set()
|
|
son_delaying = set()
|
|
son_finished = set()
|
|
wait_returned = set()
|
|
|
|
def check_output(line: str) -> None:
|
|
"""Check log output for expected messages."""
|
|
if test_complete.is_set():
|
|
return
|
|
|
|
if mo := father_calling_pattern.search(line):
|
|
father_calling.add(int(mo.group(1)))
|
|
elif mo := son_started_pattern.search(line):
|
|
son_started.add(int(mo.group(1)))
|
|
elif mo := son_delaying_pattern.search(line):
|
|
son_delaying.add(int(mo.group(1)))
|
|
elif mo := son_finished_pattern.search(line):
|
|
son_finished.add(int(mo.group(1)))
|
|
elif mo := father_wait_returned_pattern.search(line):
|
|
iteration = int(mo.group(1))
|
|
wait_returned.add(iteration)
|
|
# Test completes when iteration 9 finishes
|
|
if iteration == 9:
|
|
test_complete.set()
|
|
|
|
# Run with log monitoring
|
|
async with (
|
|
run_compiled(yaml_config, line_callback=check_output),
|
|
api_client_connected() as client,
|
|
):
|
|
# Verify device info
|
|
device_info = await client.device_info()
|
|
assert device_info is not None
|
|
assert device_info.name == "test-script-delay-params"
|
|
|
|
# Get services
|
|
_, services = await client.list_entities_services()
|
|
test_service = next(
|
|
(s for s in services if s.name == "test_repeat_with_delay"), None
|
|
)
|
|
assert test_service is not None, "test_repeat_with_delay service not found"
|
|
|
|
# Execute the test
|
|
await client.execute_service(test_service, {})
|
|
|
|
# Wait for test to complete (10 iterations * ~100ms each + margin)
|
|
try:
|
|
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
|
|
except TimeoutError:
|
|
pytest.fail(
|
|
f"Test timed out. Completed iterations: {sorted(wait_returned)}. "
|
|
f"This likely indicates the script became a zombie (issue #12044)."
|
|
)
|
|
|
|
# Verify all 10 iterations completed successfully
|
|
expected_iterations = set(range(10))
|
|
assert father_calling == expected_iterations, "Not all iterations started"
|
|
assert son_started == expected_iterations, (
|
|
"Son script not started for all iterations"
|
|
)
|
|
assert son_finished == expected_iterations, (
|
|
"Son script not finished for all iterations"
|
|
)
|
|
assert wait_returned == expected_iterations, (
|
|
"script.wait did not return for all iterations"
|
|
)
|
|
|
|
# Verify delays were triggered for iterations >= 5
|
|
expected_delays = set(range(5, 10))
|
|
assert son_delaying == expected_delays, (
|
|
"Delays not triggered for iterations >= 5"
|
|
)
|