mirror of
https://github.com/esphome/esphome.git
synced 2026-11-09 14:45:57 +00:00
155 lines
6.3 KiB
Python
155 lines
6.3 KiB
Python
"""Test ESPHome queued script functionality."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from .log_utils import LineWaiter
|
|
from .state_utils import require_service
|
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_script_queued(
|
|
yaml_config: str,
|
|
run_compiled: RunCompiledFunction,
|
|
api_client_connected: APIClientConnectedFactory,
|
|
) -> None:
|
|
"""Test comprehensive queued script functionality."""
|
|
waiter = LineWaiter()
|
|
|
|
def captured(pattern: re.Pattern[str]) -> list[str]:
|
|
"""Return the first group of every line matching ``pattern`` so far."""
|
|
return [m.group(1) for line in waiter.lines if (m := pattern.search(line))]
|
|
|
|
def items(pattern: re.Pattern[str]) -> list[int]:
|
|
"""Return the distinct item numbers ``pattern`` captured, in first-seen order."""
|
|
return list(dict.fromkeys(int(item) for item in captured(pattern)))
|
|
|
|
# Patterns for Test 1: Queue depth
|
|
queue_start = re.compile(r"Queue test: START item (\d+)")
|
|
queue_reject = "Script 'queue_depth_script' max instances"
|
|
|
|
# Patterns for Test 2: Ring buffer
|
|
ring_start = re.compile(r"Ring buffer: START '([A-Z])'")
|
|
ring_end = re.compile(r"Ring buffer: END '([A-Z])'")
|
|
|
|
# Patterns for Test 3: Stop
|
|
stop_start = re.compile(r"Stop test: START (\d+)")
|
|
|
|
# Patterns for Test 4: Rejection
|
|
reject_start = re.compile(r"Rejection test: START (\d+)")
|
|
reject_reject = "Script 'rejection_script' max instances"
|
|
|
|
# Patterns for Test 5: No params
|
|
no_params_end = "No params: END"
|
|
|
|
# Patterns for boot script (executed twice from on_boot before setup)
|
|
boot_end = re.compile(r"Boot queued: END (\d+)")
|
|
|
|
# Test completion futures
|
|
test1_complete = waiter.expect(re.compile(r"Queue test: END item 5\b"))
|
|
stop_logged = waiter.expect("STOPPING script now")
|
|
test4_complete = waiter.expect(re.compile(r"Rejection test: END 3\b"))
|
|
# Test 6: Re-execute after stop
|
|
test6_complete = waiter.expect(re.compile(r"Stop test: END 9\b"))
|
|
|
|
async with (
|
|
run_compiled(yaml_config, line_callback=waiter.callback),
|
|
api_client_connected() as client,
|
|
):
|
|
# Get services
|
|
_, services = await client.list_entities_services()
|
|
|
|
# Boot: both executions from on_boot must complete, including the one
|
|
# that was queued before QueueingScript::setup() ran
|
|
await waiter.wait_count(boot_end, 2, 2.0, "Boot: on_boot runs did not end")
|
|
boot_ended = sorted(int(item) for item in captured(boot_end))
|
|
assert boot_ended == [1, 2], (
|
|
f"Boot: Expected both on_boot executions to complete, got {boot_ended}"
|
|
)
|
|
|
|
# Test 1: Queue depth limit
|
|
test_service = require_service(services, "test_queue_depth")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait(test1_complete, 2.0, "Test 1: item 5 did not end")
|
|
await asyncio.sleep(0.1) # Give time for rejections
|
|
|
|
# Verify Test 1
|
|
queue_processed = sorted(items(queue_start))
|
|
assert queue_processed == [1, 2, 3, 4, 5], (
|
|
f"Test 1: Expected to process items 1-5 (max_runs=5 means 5 total), got {queue_processed}"
|
|
)
|
|
assert waiter.count(queue_reject) >= 2, (
|
|
"Test 1: Expected at least 2 rejection warnings (items 6-7 should be rejected)"
|
|
)
|
|
|
|
# Test 2: Ring buffer order
|
|
test_service = require_service(services, "test_ring_buffer")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait_count(ring_end, 3, 2.0, "Test 2: ring buffer did not end")
|
|
|
|
# Verify Test 2
|
|
assert captured(ring_start) == ["A", "B", "C"], (
|
|
f"Test 2: Expected start order [A, B, C], got {captured(ring_start)}"
|
|
)
|
|
assert captured(ring_end) == ["A", "B", "C"], (
|
|
f"Test 2: Expected end order [A, B, C], got {captured(ring_end)}"
|
|
)
|
|
|
|
# Test 3: Stop clears queue
|
|
test_service = require_service(services, "test_stop_clears")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait(stop_logged, 1.7, "Test 3: Stop command was not logged")
|
|
# Give time for any queued items to be cleared
|
|
await asyncio.sleep(0.3)
|
|
|
|
# Verify Test 3
|
|
assert items(stop_start) == [1], (
|
|
f"Test 3: Expected only item 1 to process, got {items(stop_start)}"
|
|
)
|
|
|
|
# Test 4: Rejection enforcement (max_runs=3)
|
|
test_service = require_service(services, "test_rejection")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait(test4_complete, 2.0, "Test 4: item 3 did not end")
|
|
await asyncio.sleep(0.1) # Give time for rejections
|
|
|
|
# Verify Test 4
|
|
reject_processed = sorted(items(reject_start))
|
|
assert reject_processed == [1, 2, 3], (
|
|
f"Test 4: Expected to process items 1-3 (max_runs=3 means 3 total), got {reject_processed}"
|
|
)
|
|
rejections = waiter.count(reject_reject)
|
|
assert rejections == 5, (
|
|
f"Test 4: Expected 5 rejections (items 4-8), got {rejections}"
|
|
)
|
|
|
|
# Test 5: No parameters
|
|
test_service = require_service(services, "test_no_params")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait_count(no_params_end, 3, 2.0, "Test 5: runs did not end")
|
|
|
|
# Verify Test 5
|
|
executions = waiter.count(no_params_end)
|
|
assert executions == 3, f"Test 5: Expected 3 executions, got {executions}"
|
|
|
|
# Test 5 again: after the queue fully drained (loop disabled while
|
|
# idle), executing again must still work
|
|
test_service = require_service(services, "test_no_params")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait_count(no_params_end, 6, 2.0, "Test 5 again: runs did not end")
|
|
executions = waiter.count(no_params_end)
|
|
assert executions == 6, (
|
|
f"Test 5 again: Expected 6 executions total, got {executions}"
|
|
)
|
|
|
|
# Test 6: a stopped script (queue cleared, loop disabled) must run
|
|
# again on the next execute; the future resolves only on "END 9"
|
|
test_service = require_service(services, "test_after_stop")
|
|
await client.execute_service(test_service, {})
|
|
await waiter.wait(test6_complete, 2.0, "Test 6: item 9 did not end")
|