Files
esphome/tests/integration/test_script_wait_on_boot.py

131 lines
5.4 KiB
Python

"""Integration test for script.wait during on_boot (issue #12043).
This test verifies that script.wait works correctly when triggered from on_boot.
The issue was that ScriptWaitAction::setup() unconditionally disabled the loop,
even if play_complex() had already been called (from an on_boot trigger at the
same priority level) and enabled it.
The race condition occurs because:
1. on_boot's default priority is 600.0 (setup_priority::DATA)
2. ScriptWaitAction's default setup priority is also DATA (600.0)
3. When they have the same priority, if on_boot runs first and triggers a script,
ScriptWaitAction::play_complex() enables the loop
4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop
5. The wait never completes because the loop is disabled
The fix adds a conditional check (like WaitUntilAction has) to only disable the
loop in setup() if num_running_ is 0.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_script_wait_on_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that script.wait works correctly when triggered from on_boot.
This reproduces issue #12043 where script.wait would hang forever when
triggered from on_boot due to a race condition in ScriptWaitAction::setup().
"""
test_complete = asyncio.Event()
# Track progress through the boot sequence
boot_started = False
first_script_started = False
first_script_completed = False
first_wait_returned = False
second_script_started = False
second_script_completed = False
all_completed = False
# Patterns for boot sequence logs
boot_start_pattern = re.compile(r"on_boot: Starting boot sequence")
show_start_pattern = re.compile(r"show_start_page: Starting")
show_complete_pattern = re.compile(r"show_start_page: Completed")
first_wait_pattern = re.compile(r"on_boot: First script completed")
flip_start_pattern = re.compile(r"flip_thru_pages: Starting")
flip_complete_pattern = re.compile(r"flip_thru_pages: Completed")
all_complete_pattern = re.compile(r"on_boot: All boot scripts completed")
def check_output(line: str) -> None:
"""Check log output for boot sequence progress."""
nonlocal boot_started, first_script_started, first_script_completed
nonlocal first_wait_returned, second_script_started, second_script_completed
nonlocal all_completed
if boot_start_pattern.search(line):
boot_started = True
elif show_start_pattern.search(line):
first_script_started = True
elif show_complete_pattern.search(line):
first_script_completed = True
elif first_wait_pattern.search(line):
first_wait_returned = True
elif flip_start_pattern.search(line):
second_script_started = True
elif flip_complete_pattern.search(line):
second_script_completed = True
elif all_complete_pattern.search(line):
all_completed = True
test_complete.set()
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-wait-on-boot"
# Wait for on_boot sequence to complete
# The boot sequence should complete automatically
# Timeout is generous to allow for delays in the scripts
try:
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
except TimeoutError:
# Build a detailed error message showing where the boot sequence got stuck
progress = []
if boot_started:
progress.append("boot started")
if first_script_started:
progress.append("show_start_page started")
if first_script_completed:
progress.append("show_start_page completed")
if first_wait_returned:
progress.append("first script.wait returned")
if second_script_started:
progress.append("flip_thru_pages started")
if second_script_completed:
progress.append("flip_thru_pages completed")
if not first_wait_returned and first_script_completed:
pytest.fail(
f"Test timed out - script.wait hung after show_start_page completed! "
f"This is the issue #12043 bug. Progress: {', '.join(progress)}"
)
else:
pytest.fail(
f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}"
)
# Verify the complete boot sequence executed in order
assert boot_started, "on_boot did not start"
assert first_script_started, "show_start_page did not start"
assert first_script_completed, "show_start_page did not complete"
assert first_wait_returned, "First script.wait did not return"
assert second_script_started, "flip_thru_pages did not start"
assert second_script_completed, "flip_thru_pages did not complete"
assert all_completed, "Boot sequence did not complete"