From 7b4edf77da9e385f04318bc8ac2222d23933f3d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Jul 2025 16:24:27 -1000 Subject: [PATCH] cover --- esphome/core/base_automation.h | 4 +- .../fixtures/delay_action_cancellation.yaml | 41 +++++++ tests/integration/test_automations.py | 115 ++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/delay_action_cancellation.yaml create mode 100644 tests/integration/test_automations.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 13179b90bb..740e10700b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -158,14 +158,14 @@ template class DelayAction : public Action, public Compon void play_complex(Ts... x) override { auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; - this->set_timeout(this->delay_.value(x...), f); + this->set_timeout("delay", this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout(""); } + void stop() override { this->cancel_timeout("delay"); } }; template class LambdaAction : public Action { diff --git a/tests/integration/fixtures/delay_action_cancellation.yaml b/tests/integration/fixtures/delay_action_cancellation.yaml new file mode 100644 index 0000000000..dfb7ee0929 --- /dev/null +++ b/tests/integration/fixtures/delay_action_cancellation.yaml @@ -0,0 +1,41 @@ +esphome: + name: test-delay-action + +host: +api: + actions: + - action: start_script + then: + - script.execute: test_delay_script + + - action: restart_script + then: + - script.execute: test_delay_script + + - action: check_result + then: + - logger.log: + format: "Test completed with %d delay completions" + args: ['id(delay_completed_count)'] + +logger: + level: DEBUG + +globals: + - id: delay_completed_count + type: int + initial_value: "0" + +script: + - id: test_delay_script + mode: restart + then: + - logger.log: + format: "Script execution started (run number %d)" + args: ['id(delay_completed_count) + 1'] + - delay: 3s + - lambda: |- + id(delay_completed_count)++; + - logger.log: + format: "Delay completed! Total completions: %d" + args: ['id(delay_completed_count)'] diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py new file mode 100644 index 0000000000..f1466a3d40 --- /dev/null +++ b/tests/integration/test_automations.py @@ -0,0 +1,115 @@ +"""Test ESPHome automations functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_delay_action_cancellation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that delay actions can be properly cancelled when script restarts.""" + loop = asyncio.get_running_loop() + + # Track events via futures + script_started_future = loop.create_future() + script_restarted_future = loop.create_future() + delay_completed_future = loop.create_future() + test_complete_future = loop.create_future() + + # Track counts + script_started_count = 0 + delay_completed_count = 0 + + # Patterns to match + script_start_pattern = re.compile(r"Script execution started \(run number (\d+)\)") + script_restart_pattern = re.compile(r"restarting \(mode: restart\)") + delay_complete_pattern = re.compile(r"Delay completed! Total completions: (\d+)") + test_complete_pattern = re.compile(r"Test completed with (\d+) delay completions") + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + nonlocal script_started_count, delay_completed_count + + # Check for script start + start_match = script_start_pattern.search(line) + if start_match: + script_started_count += 1 + if not script_started_future.done(): + script_started_future.set_result(True) + + # Check for script restart + if script_restart_pattern.search(line): + if not script_restarted_future.done(): + script_restarted_future.set_result(True) + + # Check for delay completion + complete_match = delay_complete_pattern.search(line) + if complete_match: + delay_completed_count = int(complete_match.group(1)) + if not delay_completed_future.done(): + delay_completed_future.set_result(delay_completed_count) + + # Check for test completion + test_match = test_complete_pattern.search(line) + if test_match: + final_count = int(test_match.group(1)) + if not test_complete_future.done(): + test_complete_future.set_result(final_count) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + entities, services = await client.list_entities_services() + + # Find our test services + start_service = next((s for s in services if s.name == "start_script"), None) + restart_service = next( + (s for s in services if s.name == "restart_script"), None + ) + check_result_service = next( + (s for s in services if s.name == "check_result"), None + ) + + assert start_service is not None, "start_script service not found" + assert restart_service is not None, "restart_script service not found" + assert check_result_service is not None, "check_result service not found" + + # Start the script + client.execute_service(start_service, {}) + + # Wait for script to start + await asyncio.wait_for(script_started_future, timeout=5.0) + assert script_started_count == 1, "Script should have started once" + + # Wait a bit to ensure the delay is running + await asyncio.sleep(0.5) + + # Restart the script + client.execute_service(restart_service, {}) + + # Wait for restart confirmation + await asyncio.wait_for(script_restarted_future, timeout=5.0) + + # Wait for the restarted script to complete its delay + await asyncio.wait_for(delay_completed_future, timeout=5.0) + + # Check the final result + client.execute_service(check_result_service, {}) + final_count = await asyncio.wait_for(test_complete_future, timeout=5.0) + + # If DelayAction cancellation works correctly, we should only have 1 completion + # (from the restarted script). If it doesn't work, we'd have 2 completions. + assert final_count == 1, ( + f"Expected 1 delay completion after restart, but got {final_count}" + )