[core] Resume if and while from the end of their branch instead of an embedded continuation action (#20283)

This commit is contained in:
J. Nick Koston
2026-10-12 13:35:03 -10:00
committed by GitHub
parent cce435a31e
commit 72ca565a0e
8 changed files with 533 additions and 104 deletions
@@ -72,6 +72,26 @@ script:
format: "parallel-after-if-%d"
args: ['run']
- id: stop_in_later_while_check
then:
- lambda: 'id(counter) = 0;'
- while:
condition:
lambda: |-
if (++id(counter) == 2)
id(stop_in_later_while_check).stop();
return true;
then:
- lambda: 'ESP_LOGI("test", "later-check-body-%d", id(counter));'
- logger.log: "after-later-check-should-not-run"
- id: empty_while
then:
- while:
condition:
lambda: 'return true;'
then: []
- logger.log: "after-empty-while"
- id: nested_if_last_in_while
then:
- lambda: 'id(counter) = 0;'
@@ -206,6 +226,14 @@ api:
ESP_LOGI("test", "stop-running=%d", id(stop_in_branch).is_running() ? 1 : 0);
- delay: 600ms
- logger.log: "stop-check-done"
- action: run_stop_in_later_while_check
then:
- script.execute: stop_in_later_while_check
- delay: 200ms
- logger.log: "later-check-done"
- action: run_empty_while
then:
- script.execute: empty_while
- action: run_nested_if_last_in_while
then:
- script.execute: nested_if_last_in_while
@@ -5,7 +5,7 @@ host:
api:
actions:
# Test 1: IfAction with ContinuationAction (then/else branches)
# Test 1: IfAction with then/else branches
- action: test_if_action
variables:
condition: bool
@@ -27,7 +27,7 @@ api:
args: ['value']
- logger.log: "if completed"
# Test 2: Nested IfAction (multiple ContinuationAction instances)
# Test 2: Nested IfAction
- action: test_nested_if
variables:
outer: bool
@@ -51,7 +51,7 @@ api:
- logger.log: "nested-outer-false"
- logger.log: "nested if completed"
# Test 3: WhileAction with WhileLoopContinuation
# Test 3: WhileAction
- action: test_while_action
variables:
max_count: int
@@ -74,7 +74,7 @@ api:
value: !lambda 'return id(continuation_test_counter) + 1;'
- logger.log: "while completed"
# Test 4: RepeatAction with RepeatLoopContinuation
# Test 4: RepeatAction
- action: test_repeat_action
variables:
count: int
@@ -90,7 +90,7 @@ api:
args: ['iteration']
- logger.log: "repeat completed"
# Test 5: Combined continuations (if + while + repeat)
# Test 5: Combined if + while + repeat
- action: test_combined
variables:
do_loop: bool
@@ -123,7 +123,7 @@ api:
- logger.log: "combined-skipped"
- logger.log: "combined completed"
# Test 6: Rapid triggers to verify memory efficiency
# Test 6: Rapid triggers
- action: test_rapid_if
then:
- logger.log: "=== Rapid if test start ==="
@@ -153,7 +153,7 @@ globals:
restore_value: false
initial_value: '0'
# Sensor to test rapid automation triggers with if/else (ContinuationAction)
# Sensor to test rapid automation triggers with if/else
sensor:
- platform: template
id: rapid_sensor
@@ -73,6 +73,20 @@ async def test_branch_resume_async(
assert count("stopped-branch-should-not-run") == 0
assert count("stopped-after-if-should-not-run") == 0
# A while condition that stops the script on a later pass must not start the body again
done = wait_for("later-check-done")
await client.execute_service(service["run_stop_in_later_while_check"], {})
await asyncio.wait_for(done, timeout=2.0)
assert count("later-check-body-1") == 1
assert count("later-check-body-2") == 0
assert count("after-later-check-should-not-run") == 0
# An empty loop body cannot resume the loop, so the while finishes at once
done = wait_for("after-empty-while")
await client.execute_service(service["run_empty_while"], {})
await asyncio.wait_for(done, timeout=2.0)
assert count("after-empty-while") == 1
done = wait_for("after-nested-while")
await client.execute_service(service["run_nested_if_last_in_while"], {})
await asyncio.wait_for(done, timeout=2.0)
@@ -1,4 +1,4 @@
"""Test continuation actions (ContinuationAction, WhileLoopContinuation, RepeatLoopContinuation)."""
"""Test that if, while and repeat resume the actions after them once their branch finishes."""
from __future__ import annotations
@@ -17,10 +17,7 @@ async def test_continuation_actions(
api_client_connected: APIClientConnectedFactory,
) -> None:
"""
Test that continuation actions work correctly for if/while/repeat.
These continuation classes replace LambdaAction with simple parent pointers,
saving 32-36 bytes per instance and eliminating std::function overhead.
Test that if/while/repeat resume the actions after them once their branch finishes.
"""
loop = asyncio.get_running_loop()
@@ -218,7 +215,7 @@ async def test_continuation_actions(
)
assert test_results["combined_complete"], "Combined did not complete"
# Test 6: Rapid triggers (tests memory efficiency of ContinuationAction)
# Test 6: Rapid triggers through an if/else
test_service = next((s for s in services if s.name == "test_rapid_if"), None)
assert test_service is not None, "test_rapid_if service not found"
await client.execute_service(test_service, {})