Files
secs-gem/tests/test_modules.cpp
T
raphael 82fac6fd17 H1: ModuleStateMachine + ModuleStore (E157 §6)
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>
2026-06-08 03:37:20 +02:00

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);
}