82fac6fd17
Per-module process-tracking state machine. An E157 instance models a
single recipe step at a single module, with the canonical lifecycle:
NotExecuting -> GeneralExecuting (StartGeneral)
-> StepExecuting (StartStep)
-> StepCompleted (CompleteStep)
Plus universal escape hatches: Reset returns any state to
NotExecuting; Abort terminates from any state to StepCompleted.
ModuleStore wraps the FSM with the now-standard pattern:
- non-movable (this-capture lambdas)
- per-module bind() carries current_substid + recipe_step
- fire(module_id, event) delegates to the FSM
- set_state_change_handler observes every transition with module_id
Joins EquipmentDataModel. 5 test cases cover happy path, Reset from
each interior state, Abort, store-level create dedup + bind, and the
multi-module change handler keying.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
74 lines
2.7 KiB
C++
74 lines
2.7 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "secsgem/gem/module_state.hpp"
|
|
#include "secsgem/gem/store/modules.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
// ---- FSM ---------------------------------------------------------------
|
|
|
|
TEST_CASE("ModuleStateMachine: happy path lifecycle") {
|
|
ModuleStateMachine fsm;
|
|
CHECK(fsm.state() == ModuleState::NotExecuting);
|
|
CHECK(fsm.on_event(ModuleEvent::StartGeneral));
|
|
CHECK(fsm.state() == ModuleState::GeneralExecuting);
|
|
CHECK(fsm.on_event(ModuleEvent::StartStep));
|
|
CHECK(fsm.state() == ModuleState::StepExecuting);
|
|
CHECK(fsm.on_event(ModuleEvent::CompleteStep));
|
|
CHECK(fsm.state() == ModuleState::StepCompleted);
|
|
}
|
|
|
|
TEST_CASE("ModuleStateMachine: Reset returns to NotExecuting from any state") {
|
|
for (auto start : {ModuleState::GeneralExecuting, ModuleState::StepExecuting,
|
|
ModuleState::StepCompleted}) {
|
|
ModuleStateMachine fsm;
|
|
// Drive into `start`.
|
|
if (start != ModuleState::NotExecuting) fsm.on_event(ModuleEvent::StartGeneral);
|
|
if (start == ModuleState::StepExecuting || start == ModuleState::StepCompleted)
|
|
fsm.on_event(ModuleEvent::StartStep);
|
|
if (start == ModuleState::StepCompleted) fsm.on_event(ModuleEvent::CompleteStep);
|
|
REQUIRE(fsm.state() == start);
|
|
CHECK(fsm.on_event(ModuleEvent::Reset));
|
|
CHECK(fsm.state() == ModuleState::NotExecuting);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ModuleStateMachine: Abort lands in StepCompleted") {
|
|
ModuleStateMachine fsm;
|
|
fsm.on_event(ModuleEvent::StartGeneral);
|
|
fsm.on_event(ModuleEvent::StartStep);
|
|
CHECK(fsm.on_event(ModuleEvent::Abort));
|
|
CHECK(fsm.state() == ModuleState::StepCompleted);
|
|
}
|
|
|
|
// ---- Store -------------------------------------------------------------
|
|
|
|
TEST_CASE("ModuleStore: create + dedup + bind + fire") {
|
|
ModuleStore s;
|
|
CHECK(s.create("MOD-A"));
|
|
CHECK_FALSE(s.create("MOD-A"));
|
|
CHECK(s.bind("MOD-A", "W-1", "STEP-1"));
|
|
CHECK(s.get("MOD-A")->current_substid == "W-1");
|
|
CHECK(s.get("MOD-A")->recipe_step == "STEP-1");
|
|
CHECK(s.fire("MOD-A", ModuleEvent::StartGeneral));
|
|
CHECK(s.get("MOD-A")->fsm->state() == ModuleState::GeneralExecuting);
|
|
}
|
|
|
|
TEST_CASE("ModuleStore: state-change handler observes per-module") {
|
|
ModuleStore s;
|
|
std::vector<std::pair<std::string, ModuleState>> log;
|
|
s.set_state_change_handler(
|
|
[&](const std::string& id, ModuleState, ModuleState to, ModuleEvent) {
|
|
log.emplace_back(id, to);
|
|
});
|
|
s.create("M1");
|
|
s.create("M2");
|
|
s.fire("M1", ModuleEvent::StartGeneral);
|
|
s.fire("M2", ModuleEvent::StartGeneral);
|
|
s.fire("M1", ModuleEvent::StartStep);
|
|
REQUIRE(log.size() == 3);
|
|
CHECK(log[0].first == "M1");
|
|
CHECK(log[2].first == "M1");
|
|
CHECK(log[2].second == ModuleState::StepExecuting);
|
|
}
|