mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[core] Resume if and while from the end of their branch instead of an embedded continuation action (#20283)
This commit is contained in:
@@ -181,7 +181,7 @@ api:
|
||||
room: !lambda 'return room;'
|
||||
ratio: !lambda 'return level / 100.0f;'
|
||||
- homeassistant.tag_scanned: !lambda 'return room;'
|
||||
# Test ContinuationAction (IfAction with then/else branches)
|
||||
# Test IfAction with then/else branches
|
||||
- action: test_if_action
|
||||
variables:
|
||||
condition: bool
|
||||
@@ -199,7 +199,7 @@ api:
|
||||
format: "Condition false, value: %ld"
|
||||
args: ['(long) value']
|
||||
- logger.log: "After if/else"
|
||||
# Test nested IfAction (multiple ContinuationAction instances)
|
||||
# Test nested IfAction
|
||||
- action: test_nested_if
|
||||
variables:
|
||||
outer: bool
|
||||
@@ -219,7 +219,7 @@ api:
|
||||
else:
|
||||
- logger.log: "Outer false"
|
||||
- logger.log: "After nested if"
|
||||
# Test WhileLoopContinuation (WhileAction)
|
||||
# Test WhileAction
|
||||
- action: test_while_action
|
||||
variables:
|
||||
max_count: int
|
||||
@@ -234,7 +234,7 @@ api:
|
||||
args: ['id(api_continuation_test_counter)']
|
||||
- lambda: 'id(api_continuation_test_counter)++;'
|
||||
- logger.log: "After while loop"
|
||||
# Test RepeatLoopContinuation (RepeatAction)
|
||||
# Test RepeatAction
|
||||
- action: test_repeat_action
|
||||
variables:
|
||||
count: int
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
using Log = std::vector<int>;
|
||||
|
||||
// Records its id when it runs, then continues the chain
|
||||
class RecordAction : public Action<> {
|
||||
public:
|
||||
RecordAction(Log *log, int id) : log_(log), id_(id) {}
|
||||
void play() override { this->log_->push_back(this->id_); }
|
||||
Action<> *next() const { return this->next_; }
|
||||
|
||||
protected:
|
||||
Log *log_;
|
||||
int id_;
|
||||
};
|
||||
|
||||
// Holds the chain until finish() is called, like a delay
|
||||
class DeferredAction : public Action<> {
|
||||
public:
|
||||
explicit DeferredAction(Log *log, int id) : log_(log), id_(id) {}
|
||||
void play_complex() override {
|
||||
this->num_running_++;
|
||||
this->log_->push_back(this->id_);
|
||||
this->pending_ = true;
|
||||
}
|
||||
void play() override {}
|
||||
void stop() override { this->pending_ = false; }
|
||||
void finish() {
|
||||
if (this->pending_) {
|
||||
this->pending_ = false;
|
||||
this->play_next_();
|
||||
}
|
||||
}
|
||||
bool pending() const { return this->pending_; }
|
||||
|
||||
protected:
|
||||
Log *log_;
|
||||
int id_;
|
||||
bool pending_{false};
|
||||
};
|
||||
|
||||
class FlagCondition : public Condition<> {
|
||||
public:
|
||||
bool check() override { return this->value; }
|
||||
bool value{true};
|
||||
};
|
||||
|
||||
// Stops the given list during check(), like script.stop in a lambda condition
|
||||
class StoppingCondition : public Condition<> {
|
||||
public:
|
||||
bool check() override {
|
||||
if (this->list != nullptr)
|
||||
this->list->stop();
|
||||
return true;
|
||||
}
|
||||
ActionList<> *list{nullptr};
|
||||
};
|
||||
|
||||
// Stops the given list on the Nth check and keeps returning true
|
||||
class StopOnCheckCondition : public Condition<> {
|
||||
public:
|
||||
explicit StopOnCheckCondition(int stop_on) : stop_on_(stop_on) {}
|
||||
bool check() override {
|
||||
if (++this->checks_ == this->stop_on_ && this->list != nullptr)
|
||||
this->list->stop();
|
||||
return true;
|
||||
}
|
||||
ActionList<> *list{nullptr};
|
||||
|
||||
protected:
|
||||
int stop_on_;
|
||||
int checks_{0};
|
||||
};
|
||||
|
||||
// Turns its condition off after a number of checks
|
||||
class CountdownCondition : public Condition<> {
|
||||
public:
|
||||
explicit CountdownCondition(int passes) : passes_(passes) {}
|
||||
bool check() override { return this->passes_-- > 0; }
|
||||
|
||||
protected:
|
||||
int passes_;
|
||||
};
|
||||
|
||||
// Reads an action's next_ through Action's own tag helpers
|
||||
class TaggedOwner : public RecordAction {
|
||||
public:
|
||||
using RecordAction::RecordAction;
|
||||
static bool is_tagged(const TaggedOwner *a) { return a->next_is_owner_(); }
|
||||
static Action<> *owner_of(const TaggedOwner *a) { return a->owner_(); }
|
||||
};
|
||||
|
||||
class InspectList : public ActionList<> {
|
||||
public:
|
||||
Action<> *head() const { return this->actions_; }
|
||||
};
|
||||
|
||||
TEST(ActionBranchOwner, SetOwnerTagsOnlyTheLastAction) {
|
||||
Log log;
|
||||
TaggedOwner a(&log, 1), b(&log, 2), c(&log, 3);
|
||||
TaggedOwner owner(&log, 99);
|
||||
InspectList list;
|
||||
list.add_actions({&a, &b});
|
||||
list.add_action(&c); // appending before set_owner keeps order
|
||||
list.set_owner(&owner);
|
||||
EXPECT_EQ(list.head(), &a);
|
||||
EXPECT_EQ(a.next(), &b);
|
||||
EXPECT_EQ(b.next(), &c);
|
||||
EXPECT_FALSE(TaggedOwner::is_tagged(&a));
|
||||
EXPECT_FALSE(TaggedOwner::is_tagged(&b));
|
||||
EXPECT_TRUE(TaggedOwner::is_tagged(&c));
|
||||
EXPECT_EQ(TaggedOwner::owner_of(&c), &owner);
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, EmptyListStaysEmpty) {
|
||||
Log log;
|
||||
TaggedOwner owner(&log, 99);
|
||||
InspectList list;
|
||||
list.set_owner(&owner);
|
||||
EXPECT_EQ(list.head(), nullptr);
|
||||
EXPECT_TRUE(list.empty());
|
||||
}
|
||||
|
||||
// ESPHOME_DEBUG_ASSERT is only live because script/cpp_unit_test.py builds with -DESPHOME_DEBUG
|
||||
TEST(ActionBranchOwnerDeathTest, AppendAfterSetOwnerAsserts) {
|
||||
Log log;
|
||||
RecordAction a(&log, 1), late(&log, 2);
|
||||
TaggedOwner owner(&log, 99);
|
||||
ActionList<> list;
|
||||
list.add_action(&a);
|
||||
list.set_owner(&owner);
|
||||
EXPECT_DEATH(list.add_action(&late), "");
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, IfThenResumesAfterTheIfOnce) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<true> if_action(&cond);
|
||||
RecordAction t1(&log, 1), t2(&log, 2), e1(&log, 3), after(&log, 4);
|
||||
if_action.add_then({&t1, &t2});
|
||||
if_action.add_else({&e1});
|
||||
ActionList<> top;
|
||||
top.add_actions({&if_action, &after});
|
||||
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1, 2, 4}));
|
||||
log.clear();
|
||||
cond.value = false;
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{3, 4}));
|
||||
EXPECT_FALSE(top.is_running());
|
||||
EXPECT_EQ(top.num_running(), 0);
|
||||
}
|
||||
|
||||
// The per-item calls automation.py emits
|
||||
TEST(ActionBranchOwner, PerItemIfAndWhileResumeAfterTheirBranch) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<true> if_action(&cond);
|
||||
RecordAction t1(&log, 1), t2(&log, 2), e1(&log, 3), after(&log, 4);
|
||||
if_action.add_then_action(&t1);
|
||||
if_action.add_then_action(&t2);
|
||||
if_action.finish_then();
|
||||
if_action.add_else_action(&e1);
|
||||
if_action.finish_else();
|
||||
ActionList<> top;
|
||||
top.add_actions({&if_action, &after});
|
||||
top.play();
|
||||
cond.value = false;
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1, 2, 4, 3, 4}));
|
||||
|
||||
log.clear();
|
||||
CountdownCondition loop_cond(2);
|
||||
WhileAction<> loop(&loop_cond);
|
||||
DeferredAction wait(&log, 5);
|
||||
RecordAction after2(&log, 6);
|
||||
loop.add_then_action(&wait);
|
||||
loop.finish_then();
|
||||
ActionList<> top2;
|
||||
top2.add_actions({&loop, &after2});
|
||||
top2.play();
|
||||
wait.finish();
|
||||
wait.finish();
|
||||
EXPECT_EQ(log, (Log{5, 5, 6}));
|
||||
EXPECT_FALSE(top2.is_running());
|
||||
}
|
||||
|
||||
// then: [] generates finish_then() with no actions
|
||||
TEST(ActionBranchOwner, PerItemEmptyBranchesFinishImmediately) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<true> if_action(&cond);
|
||||
if_action.finish_then();
|
||||
if_action.finish_else();
|
||||
WhileAction<> loop(&cond);
|
||||
loop.finish_then();
|
||||
RecordAction after(&log, 1), after2(&log, 2);
|
||||
ActionList<> top, top2;
|
||||
top.add_actions({&if_action, &after});
|
||||
top2.add_actions({&loop, &after2});
|
||||
top.play();
|
||||
cond.value = false;
|
||||
top.play();
|
||||
cond.value = true;
|
||||
top2.play();
|
||||
EXPECT_EQ(log, (Log{1, 1, 2}));
|
||||
EXPECT_FALSE(top2.is_running());
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, ConditionThatStopsTheRunStartsNoBranch) {
|
||||
Log log;
|
||||
StoppingCondition cond;
|
||||
IfAction<true> if_action(&cond);
|
||||
WhileAction<> loop(&cond);
|
||||
RecordAction t1(&log, 1), e1(&log, 2), body(&log, 3), after(&log, 4);
|
||||
if_action.add_then({&t1});
|
||||
if_action.add_else({&e1});
|
||||
loop.add_then({&body});
|
||||
ActionList<> if_list, while_list;
|
||||
if_list.add_actions({&if_action, &after});
|
||||
while_list.add_actions({&loop, &after});
|
||||
|
||||
cond.list = &if_list;
|
||||
if_list.play();
|
||||
cond.list = &while_list;
|
||||
while_list.play();
|
||||
EXPECT_TRUE(log.empty());
|
||||
EXPECT_FALSE(if_list.is_running());
|
||||
EXPECT_FALSE(while_list.is_running());
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, WhileConditionStoppingOnALaterPassEndsTheLoop) {
|
||||
Log log;
|
||||
StopOnCheckCondition cond(2);
|
||||
WhileAction<> loop(&cond);
|
||||
RecordAction body(&log, 1), after(&log, 2);
|
||||
loop.add_then({&body});
|
||||
ActionList<> list;
|
||||
list.add_actions({&loop, &after});
|
||||
cond.list = &list;
|
||||
|
||||
list.play();
|
||||
EXPECT_EQ(log, (Log{1}));
|
||||
EXPECT_FALSE(list.is_running());
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, IfResumesAfterDeferredLastAction) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<false> if_action(&cond);
|
||||
DeferredAction wait(&log, 1);
|
||||
RecordAction after(&log, 2);
|
||||
if_action.add_then({&wait});
|
||||
ActionList<> top;
|
||||
top.add_actions({&if_action, &after});
|
||||
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1}));
|
||||
EXPECT_TRUE(top.is_running());
|
||||
// num_running() counts the top-level chain only: the if, not the action inside its branch
|
||||
EXPECT_EQ(top.num_running(), 1);
|
||||
wait.finish();
|
||||
EXPECT_EQ(log, (Log{1, 2}));
|
||||
EXPECT_FALSE(top.is_running());
|
||||
EXPECT_EQ(top.num_running(), 0);
|
||||
}
|
||||
|
||||
// A deferred action that can hold two runs at once, completed in order
|
||||
class QueueDeferredAction : public Action<> {
|
||||
public:
|
||||
void play_complex() override {
|
||||
this->num_running_++;
|
||||
this->pending_++;
|
||||
}
|
||||
void play() override {}
|
||||
void finish_one() {
|
||||
if (this->pending_ > 0) {
|
||||
this->pending_--;
|
||||
this->play_next_();
|
||||
}
|
||||
}
|
||||
int running() const { return this->num_running_; }
|
||||
|
||||
protected:
|
||||
int pending_{0};
|
||||
};
|
||||
|
||||
TEST(ActionBranchOwner, OverlappingRunsResumeOncePerRun) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<false> if_action(&cond);
|
||||
QueueDeferredAction wait;
|
||||
RecordAction after(&log, 7);
|
||||
if_action.add_then({&wait});
|
||||
ActionList<> top;
|
||||
top.add_actions({&if_action, &after});
|
||||
|
||||
top.play();
|
||||
top.play();
|
||||
EXPECT_EQ(wait.running(), 2); // the tagged last action holds both runs
|
||||
wait.finish_one();
|
||||
EXPECT_EQ(log, (Log{7}));
|
||||
EXPECT_TRUE(top.is_running());
|
||||
wait.finish_one();
|
||||
EXPECT_EQ(log, (Log{7, 7}));
|
||||
EXPECT_FALSE(top.is_running());
|
||||
EXPECT_EQ(wait.running(), 0);
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, EmptyBranchesFinishImmediately) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
IfAction<true> if_action(&cond);
|
||||
RecordAction after(&log, 1);
|
||||
if_action.add_then({});
|
||||
if_action.add_else({});
|
||||
ActionList<> top;
|
||||
top.add_actions({&if_action, &after});
|
||||
top.play();
|
||||
cond.value = false;
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1, 1}));
|
||||
|
||||
FlagCondition always;
|
||||
WhileAction<> loop(&always);
|
||||
loop.add_then({});
|
||||
ActionList<> top2;
|
||||
RecordAction after2(&log, 2);
|
||||
top2.add_actions({&loop, &after2});
|
||||
top2.play();
|
||||
EXPECT_EQ(log, (Log{1, 1, 2}));
|
||||
EXPECT_FALSE(top2.is_running());
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, WhileRechecksEachPassAndExits) {
|
||||
Log log;
|
||||
CountdownCondition cond(3);
|
||||
WhileAction<> loop(&cond);
|
||||
RecordAction body(&log, 1);
|
||||
RecordAction after(&log, 2);
|
||||
loop.add_then({&body});
|
||||
ActionList<> top;
|
||||
top.add_actions({&loop, &after});
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1, 1, 1, 2}));
|
||||
EXPECT_FALSE(top.is_running());
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, StopMidBranchClearsRunning) {
|
||||
Log log;
|
||||
FlagCondition cond;
|
||||
WhileAction<> loop(&cond);
|
||||
DeferredAction wait(&log, 1);
|
||||
RecordAction after(&log, 2);
|
||||
loop.add_then({&wait});
|
||||
ActionList<> top;
|
||||
top.add_actions({&loop, &after});
|
||||
|
||||
top.play();
|
||||
EXPECT_TRUE(top.is_running());
|
||||
top.stop();
|
||||
EXPECT_FALSE(top.is_running());
|
||||
EXPECT_EQ(top.num_running(), 0);
|
||||
wait.finish(); // already stopped: nothing pending, nothing resumes
|
||||
EXPECT_EQ(log, (Log{1}));
|
||||
|
||||
// A fresh run works after the stop
|
||||
cond.value = false;
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1, 2}));
|
||||
}
|
||||
|
||||
TEST(ActionBranchOwner, NestedIfInsideWhileResumesEachOwner) {
|
||||
Log log;
|
||||
CountdownCondition loop_cond(2);
|
||||
FlagCondition if_cond;
|
||||
WhileAction<> loop(&loop_cond);
|
||||
IfAction<false> inner(&if_cond);
|
||||
DeferredAction wait(&log, 1);
|
||||
RecordAction after(&log, 9);
|
||||
inner.add_then({&wait});
|
||||
loop.add_then({&inner}); // the if is the last action of the loop body
|
||||
ActionList<> top;
|
||||
top.add_actions({&loop, &after});
|
||||
|
||||
top.play();
|
||||
EXPECT_EQ(log, (Log{1}));
|
||||
wait.finish(); // resumes the if, then the while, which loops again
|
||||
EXPECT_EQ(log, (Log{1, 1}));
|
||||
wait.finish(); // second pass ends; the while condition is now false
|
||||
EXPECT_EQ(log, (Log{1, 1, 9}));
|
||||
EXPECT_FALSE(top.is_running());
|
||||
EXPECT_EQ(top.num_running(), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
Reference in New Issue
Block a user