diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 400436f1..ae48861c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,7 @@ endmacro() pip_test(math) pip_test(core) +pip_test(state_machine) pip_test(piobject) if(TARGET pip_client_server) pip_test(client_server pip_client_server) diff --git a/tests/state_machine/testpistatemachine.cpp b/tests/state_machine/testpistatemachine.cpp new file mode 100644 index 00000000..c17cabec --- /dev/null +++ b/tests/state_machine/testpistatemachine.cpp @@ -0,0 +1,691 @@ +#include "pistatemachine.h" +#include "pisystemtime.h" +#include "pithread.h" + +#include "gtest/gtest.h" +#include +#include + +typedef PIVector Log; + + +// gtest printer for PIString (picked up via Koenig lookup): makes Log values +// readable in assertion failure messages +inline void PrintTo(const PIString & s, ::std::ostream * os) { + *os << '"' << s.data() << '"'; +} + + +// Waits until \a cond() returns true or \a timeout_ms expires, polling every 10 ms. +template +static bool waitFor(F cond, int timeout_ms) { + const PISystemTime step = PISystemTime::fromMilliseconds(10); + for (int waited = 0; !cond() && waited < timeout_ms; waited += 10) + step.sleep(); + return cond(); +} + + +// States and transitions are owned by the machine, which is owned by the fixture, +// so everything is cleaned up in TearDown even when a test fails early. +class PIStateMachineTest: public ::testing::Test { +protected: + PIStateMachine * machine = nullptr; + + void TearDown() override { + delete machine; + machine = nullptr; + } +}; + + +TEST_F(PIStateMachineTest, Start_ActivatesInitialState) { + Log log; + auto * idle = new PIStateLambda([&log]() { log << "enter_idle"; }, [&log]() { log << "exit_idle"; }, "idle"); + auto * running = new PIStateLambda([&log]() { log << "enter_running"; }, [&log]() { log << "exit_running"; }, "running"); + machine = new PIStateMachine("Main"); + machine->addState(idle); + machine->addState(running); + machine->setInitialState(idle); + + EXPECT_FALSE(machine->isRunning()); + EXPECT_TRUE(machine->start()); + EXPECT_TRUE(machine->isRunning()); + EXPECT_TRUE(idle->isActive()); + EXPECT_FALSE(running->isActive()); + EXPECT_EQ(machine->activeChild(), idle); + Log expected; + expected << "enter_idle"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, Start_NoInitialState_ReturnsFalse) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, nullptr, "a"); + machine = new PIStateMachine("Main"); + machine->addState(a); + + EXPECT_FALSE(machine->start()); + EXPECT_FALSE(machine->isRunning()); + EXPECT_FALSE(a->isActive()); + EXPECT_EQ(log.size(), 0u); +} + + +TEST_F(PIStateMachineTest, PostEvent_BeforeStart_ReturnsFalse) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, nullptr, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1); + + EXPECT_FALSE(machine->postEvent(1)); + EXPECT_FALSE(machine->isRunning()); + EXPECT_FALSE(a->isActive()); + EXPECT_FALSE(b->isActive()); +} + + +TEST_F(PIStateMachineTest, PostEvent_SimpleTransition) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, [&log]() { log << "exit_b"; }, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_FALSE(a->isActive()); + EXPECT_TRUE(b->isActive()); + EXPECT_EQ(machine->activeChild(), b); + Log expected; + expected << "enter_a" << "exit_a" << "enter_b"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, PostEvent_NoMatchingEvent_ReturnsFalse) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_FALSE(machine->postEvent(42)); + EXPECT_TRUE(a->isActive()); + EXPECT_FALSE(b->isActive()); + Log expected; + expected << "enter_a"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, PostEvent_InactiveSourceTransition_NotTriggered) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, nullptr, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + auto * c = new PIStateLambda([&log]() { log << "enter_c"; }, nullptr, "c"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->addState(c); + machine->setInitialState(a); + b->addTransition(c, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_FALSE(machine->postEvent(1)); + EXPECT_TRUE(a->isActive()); + EXPECT_FALSE(b->isActive()); + EXPECT_FALSE(c->isActive()); +} + + +TEST_F(PIStateMachineTest, PostEvent_AfterFinish_ReturnsFalse) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * f = new PIStateFinal(nullptr, "finish"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(f); + machine->setInitialState(a); + a->addTransition(f, 1); + EXPECT_TRUE(machine->start()); + EXPECT_TRUE(machine->postEvent(1)); + + EXPECT_FALSE(machine->isRunning()); + EXPECT_FALSE(machine->postEvent(1)); + EXPECT_TRUE(f->isActive()); +} + + +TEST_F(PIStateMachineTest, SelfTransition_OnlyActionRuns) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->setInitialState(a); + a->addTransition(a, 1)->addAction([&log]() { log << "action"; }); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(a->isActive()); + Log expected; + expected << "enter_a" << "action"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, Transition_ActionRunsBetweenExitAndEnter) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1)->addAction([&log]() { log << "action"; }); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + Log expected; + expected << "enter_a" << "exit_a" << "action" << "enter_b"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, ChildTransition_WinsOverParent) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * x = new PIStateLambda([&log]() { log << "enter_x"; }, nullptr, "x"); + auto * y = new PIStateLambda([&log]() { log << "enter_y"; }, nullptr, "y"); + machine = new PIStateMachine("Main"); + machine->addState(p); + machine->addState(x); + machine->addState(y); + p->addState(c1); + p->setInitialState(c1); + machine->setInitialState(p); + c1->addTransition(x, 1); + p->addTransition(y, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(x->isActive()); + EXPECT_FALSE(y->isActive()); + EXPECT_FALSE(c1->isActive()); + EXPECT_FALSE(p->isActive()); +} + + +TEST_F(PIStateMachineTest, Guard_False_NoTransition) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1)->addGuard([](int value) -> bool { return value > 10; }); + EXPECT_TRUE(machine->start()); + + EXPECT_FALSE(machine->postEvent(1, 5)); + EXPECT_TRUE(a->isActive()); + EXPECT_FALSE(b->isActive()); + EXPECT_TRUE(machine->postEvent(1, 15)); + EXPECT_FALSE(a->isActive()); + EXPECT_TRUE(b->isActive()); +} + + +TEST_F(PIStateMachineTest, Guard_ReceivesEventArgs) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + // note: guard parameters must be taken by value: postEvent passes args by value, + // a guard with a reference parameter would never match the arguments format + a->addTransition(b, 1)->addGuard([](PIString msg) -> bool { return msg == "allowed"; }); + EXPECT_TRUE(machine->start()); + + EXPECT_FALSE(machine->postEvent(1, PIString("blocked"))); + EXPECT_TRUE(a->isActive()); + EXPECT_FALSE(b->isActive()); + EXPECT_TRUE(machine->postEvent(1, PIString("allowed"))); + EXPECT_FALSE(a->isActive()); + EXPECT_TRUE(b->isActive()); +} + + +TEST_F(PIStateMachineTest, Guard_WrongArgFormat_NoTransition) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda([&log]() { log << "enter_b"; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1)->addGuard([](int) -> bool { return true; }); + EXPECT_TRUE(machine->start()); + + // guard expects int, event carries PIString: transition must not fire + EXPECT_FALSE(machine->postEvent(1, PIString("unexpected"))); + EXPECT_TRUE(a->isActive()); + EXPECT_FALSE(b->isActive()); +} + + +TEST_F(PIStateMachineTest, CompoundState_InitialChildActive) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * c2 = new PIStateLambda([&log]() { log << "enter_c2"; }, [&log]() { log << "exit_c2"; }, "c2"); + machine = new PIStateMachine("Main"); + machine->addState(p); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(p); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(p->isActive()); + EXPECT_TRUE(p->isCompound()); + EXPECT_TRUE(c1->isAtomic()); + EXPECT_TRUE(c1->isActive()); + EXPECT_FALSE(c2->isActive()); + EXPECT_EQ(p->activeChild(), c1); + Log expected; + expected << "enter_p" << "enter_c1"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, CompoundState_InternalTransition) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * c2 = new PIStateLambda([&log]() { log << "enter_c2"; }, [&log]() { log << "exit_c2"; }, "c2"); + machine = new PIStateMachine("Main"); + machine->addState(p); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(p); + c1->addTransition(c2, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(p->isActive()); + EXPECT_FALSE(c1->isActive()); + EXPECT_TRUE(c2->isActive()); + EXPECT_EQ(p->activeChild(), c2); + Log expected; + expected << "enter_p" << "enter_c1" << "exit_c1" << "enter_c2"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, CompoundState_ExitFromNestedChild) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * x = new PIStateLambda([&log]() { log << "enter_x"; }, nullptr, "x"); + machine = new PIStateMachine("Main"); + machine->addState(p); + machine->addState(x); + p->addState(c1); + p->setInitialState(c1); + machine->setInitialState(p); + c1->addTransition(x, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(x->isActive()); + EXPECT_FALSE(c1->isActive()); + EXPECT_FALSE(p->isActive()); + Log expected; + expected << "enter_p" << "enter_c1" << "exit_c1" << "exit_p" << "enter_x"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, CompoundState_EnterFromOutsideToChild) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * c2 = new PIStateLambda([&log]() { log << "enter_c2"; }, [&log]() { log << "exit_c2"; }, "c2"); + auto * e = new PIStateLambda([&log]() { log << "enter_e"; }, [&log]() { log << "exit_e"; }, "e"); + machine = new PIStateMachine("Main"); + machine->addState(p); + machine->addState(e); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(e); + e->addTransition(c2, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(p->isActive()); + EXPECT_TRUE(c2->isActive()); + EXPECT_FALSE(c1->isActive()); + EXPECT_FALSE(e->isActive()); + EXPECT_EQ(p->activeChild(), c2); + Log expected; + expected << "enter_e" << "exit_e" << "enter_p" << "enter_c2"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, CompoundState_TransitionToParentState) { + Log log; + auto * p = new PIStateLambda([&log]() { log << "enter_p"; }, [&log]() { log << "exit_p"; }, "p"); + auto * c1 = new PIStateLambda([&log]() { log << "enter_c1"; }, [&log]() { log << "exit_c1"; }, "c1"); + auto * c2 = new PIStateLambda([&log]() { log << "enter_c2"; }, [&log]() { log << "exit_c2"; }, "c2"); + machine = new PIStateMachine("Main"); + machine->addState(p); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(p); + c1->addTransition(p, 1); + EXPECT_TRUE(machine->start()); + + // transition to the compound parent re-enters its initial child + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(p->isActive()); + EXPECT_TRUE(c1->isActive()); + EXPECT_FALSE(c2->isActive()); + Log expected; + expected << "enter_p" << "enter_c1" << "exit_c1" << "enter_c1"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, ParallelState_ActivatesAllChildren) { + Log log; + auto * par = new PIStateLambda([&log]() { log << "enter_par"; }, [&log]() { log << "exit_par"; }, "par"); + auto * s1 = new PIStateLambda([&log]() { log << "enter_s1"; }, [&log]() { log << "exit_s1"; }, "s1"); + auto * s2 = new PIStateLambda([&log]() { log << "enter_s2"; }, [&log]() { log << "exit_s2"; }, "s2"); + machine = new PIStateMachine("Main"); + machine->addState(par); + par->setParallel(true); + par->addState(s1); + par->addState(s2); + machine->setInitialState(par); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(par->isActive()); + EXPECT_TRUE(par->isParallel()); + EXPECT_TRUE(s1->isActive()); + EXPECT_TRUE(s2->isActive()); + EXPECT_EQ(par->activeChild(), nullptr); + PIVector expected_children; + expected_children << s1 << s2; + EXPECT_EQ(par->activeChildren(), expected_children); + Log expected; + expected << "enter_par" << "enter_s1" << "enter_s2"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, ParallelState_ExitFromChild_ExitsWholeParallel) { + Log log; + auto * par = new PIStateLambda([&log]() { log << "enter_par"; }, [&log]() { log << "exit_par"; }, "par"); + auto * s1 = new PIStateLambda([&log]() { log << "enter_s1"; }, [&log]() { log << "exit_s1"; }, "s1"); + auto * s2 = new PIStateLambda([&log]() { log << "enter_s2"; }, [&log]() { log << "exit_s2"; }, "s2"); + auto * x = new PIStateLambda([&log]() { log << "enter_x"; }, nullptr, "x"); + machine = new PIStateMachine("Main"); + machine->addState(par); + machine->addState(x); + par->setParallel(true); + par->addState(s1); + par->addState(s2); + machine->setInitialState(par); + s1->addTransition(x, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(x->isActive()); + EXPECT_FALSE(par->isActive()); + EXPECT_FALSE(s1->isActive()); + EXPECT_FALSE(s2->isActive()); + Log expected; + expected << "enter_par" << "enter_s1" << "enter_s2" << "exit_s1" << "exit_s2" << "exit_par" << "enter_x"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, ParallelState_EnterFromOutside_ActivatesAllChildren) { + Log log; + auto * par = new PIStateLambda([&log]() { log << "enter_par"; }, [&log]() { log << "exit_par"; }, "par"); + auto * s1 = new PIStateLambda([&log]() { log << "enter_s1"; }, [&log]() { log << "exit_s1"; }, "s1"); + auto * s2 = new PIStateLambda([&log]() { log << "enter_s2"; }, [&log]() { log << "exit_s2"; }, "s2"); + auto * y = new PIStateLambda([&log]() { log << "enter_y"; }, [&log]() { log << "exit_y"; }, "y"); + machine = new PIStateMachine("Main"); + machine->addState(par); + machine->addState(y); + par->setParallel(true); + par->addState(s1); + par->addState(s2); + machine->setInitialState(y); + y->addTransition(s1, 1); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(par->isActive()); + EXPECT_TRUE(s1->isActive()); + EXPECT_TRUE(s2->isActive()); + EXPECT_FALSE(y->isActive()); +} + + +TEST_F(PIStateMachineTest, FinalState_FinishesMachine) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + int final_calls = 0; + int finish_calls = 0; + auto * f = new PIStateFinal([&final_calls]() { final_calls++; }, "finish"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(f); + machine->setInitialState(a); + a->addTransition(f, 1); + machine->setOnFinish([&finish_calls]() { finish_calls++; }); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_FALSE(machine->isRunning()); + EXPECT_TRUE(f->isActive()); + EXPECT_TRUE(f->isFinal()); + EXPECT_FALSE(a->isActive()); + EXPECT_EQ(final_calls, 1); + EXPECT_EQ(finish_calls, 1); + Log expected; + expected << "enter_a" << "exit_a"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, ActiveAtomics_ReturnsActiveLeaves) { + auto * p = new PIStateLambda(nullptr, nullptr, "p"); + auto * c1 = new PIStateLambda(nullptr, nullptr, "c1"); + auto * c2 = new PIStateLambda(nullptr, nullptr, "c2"); + auto * x = new PIStateLambda(nullptr, nullptr, "x"); + machine = new PIStateMachine("Main"); + machine->addState(p); + machine->addState(x); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(p); + EXPECT_TRUE(machine->start()); + + PIVector expected; + expected << c1; + EXPECT_EQ(machine->activeAtomics(), expected); + EXPECT_EQ(p->activeAtomics(), expected); +} + + +TEST_F(PIStateMachineTest, GatherStates_ReturnsAllDescendants) { + auto * p = new PIStateLambda(nullptr, nullptr, "p"); + auto * c1 = new PIStateLambda(nullptr, nullptr, "c1"); + auto * c2 = new PIStateLambda(nullptr, nullptr, "c2"); + auto * x = new PIStateLambda(nullptr, nullptr, "x"); + machine = new PIStateMachine("Main"); + machine->addState(p); + machine->addState(x); + p->addState(c1); + p->addState(c2); + p->setInitialState(c1); + machine->setInitialState(p); + + auto states = machine->gatherStates(); + EXPECT_EQ(states.size(), 5u); + EXPECT_TRUE(states.contains(machine)); + EXPECT_TRUE(states.contains(p)); + EXPECT_TRUE(states.contains(c1)); + EXPECT_TRUE(states.contains(c2)); + EXPECT_TRUE(states.contains(x)); +} + + +TEST_F(PIStateMachineTest, NestedPostEvent_FromOnEnter) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda( + [&log, this]() { + log << "enter_b"; + machine->postEvent(2); + }, + [&log]() { log << "exit_b"; }, + "b"); + auto * c = new PIStateLambda([&log]() { log << "enter_c"; }, nullptr, "c"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->addState(c); + machine->setInitialState(a); + a->addTransition(b, 1); + b->addTransition(c, 2); + EXPECT_TRUE(machine->start()); + + // post from inside onEnter is queued and processed after the outer postEvent returns + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_FALSE(a->isActive()); + EXPECT_FALSE(b->isActive()); + EXPECT_TRUE(c->isActive()); + Log expected; + expected << "enter_a" << "exit_a" << "enter_b" << "exit_b" << "enter_c"; + EXPECT_EQ(log, expected); +} + + +TEST_F(PIStateMachineTest, NestedPostEvent_SequenceProcessedInOrder) { + Log log; + auto * a = new PIStateLambda([&log]() { log << "enter_a"; }, [&log]() { log << "exit_a"; }, "a"); + auto * b = new PIStateLambda( + [&log, this]() { + log << "enter_b"; + machine->postEvent(2); + machine->postEvent(3); + }, + [&log]() { log << "exit_b"; }, + "b"); + auto * c = new PIStateLambda([&log]() { log << "enter_c"; }, [&log]() { log << "exit_c"; }, "c"); + auto * d = new PIStateLambda([&log]() { log << "enter_d"; }, nullptr, "d"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->addState(c); + machine->addState(d); + machine->setInitialState(a); + a->addTransition(b, 1); + b->addTransition(c, 2); + c->addTransition(d, 3); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + EXPECT_TRUE(d->isActive()); + Log expected; + expected << "enter_a" << "exit_a" << "enter_b" << "exit_b" << "enter_c" << "exit_c" << "enter_d"; + EXPECT_EQ(log, expected); +} + + +#ifdef PIP_HAS_THREADS +TEST_F(PIStateMachineTest, TimeoutTransition_Fires) { + std::atomic_bool entered_b{false}; + auto * a = new PIStateLambda([]() {}); + auto * b = new PIStateLambda([&entered_b]() { entered_b = true; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTimeoutTransition(b, PISystemTime::fromMilliseconds(100)); + EXPECT_TRUE(machine->start()); + EXPECT_FALSE(entered_b.load()); + + EXPECT_TRUE(waitFor([&entered_b]() { return entered_b.load(); }, 2000)); + EXPECT_TRUE(b->isActive()); + EXPECT_FALSE(a->isActive()); +} + + +TEST_F(PIStateMachineTest, TimeoutTransition_CancelledWhenSourceExited) { + auto * a = new PIStateLambda([]() {}); + auto * b = new PIStateLambda([]() {}, nullptr, "b"); + auto * c = new PIStateLambda([]() {}, nullptr, "c"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->addState(c); + machine->setInitialState(a); + a->addTransition(b, 1); + a->addTimeoutTransition(c, PISystemTime::fromMilliseconds(100)); + EXPECT_TRUE(machine->start()); + + EXPECT_TRUE(machine->postEvent(1)); + PISystemTime::fromMilliseconds(300).sleep(); + + EXPECT_TRUE(b->isActive()); + EXPECT_FALSE(c->isActive()); +} + + +TEST_F(PIStateMachineTest, PostEvent_FromOtherThread) { + std::atomic_bool entered_b{false}; + auto * a = new PIStateLambda([]() {}); + auto * b = new PIStateLambda([&entered_b]() { entered_b = true; }, nullptr, "b"); + machine = new PIStateMachine("Main"); + machine->addState(a); + machine->addState(b); + machine->setInitialState(a); + a->addTransition(b, 1); + EXPECT_TRUE(machine->start()); + + PIThread worker([this]() { + PISystemTime::fromMilliseconds(50).sleep(); + machine->postEvent(1); + }); + worker.startOnce(); + EXPECT_TRUE(waitFor([&entered_b]() { return entered_b.load(); }, 2000)); + EXPECT_TRUE(b->isActive()); + worker.waitForFinish(PISystemTime::fromSeconds(2)); +} +#endif // PIP_HAS_THREADS