D1: E87 carrier and load-port state machines
Per-carrier triple FSM: CIDS (id verification), CSMS (slot-map), CAS
(access). Per-port triple FSM: LPTS (transfer), LRS (reservation), LAS
(association). Wire-byte enum values pinned via static_assert to match
E87-0716 §10.3.
CarrierStateMachine combines the three carrier-side FSMs because they
are independent but always observed together; same for LoadPortState-
Machine. Generic CarrierTransitionTable<State, Event> template is
reused across all six tables — same row shape as the PJ/CJ/Exception
tables that already exist.
Default tables cover the spec's documented transitions:
CIDS: NotConfirmed <-> Confirmed/Mismatched/Unknown, Cancel returns
to NotConfirmed from any state, Bind force-confirms.
CSMS: NotRead -> Read -> {Mismatched, Reset}.
CAS: NotAccessed -> InAccess -> Complete (terminal).
LPTS: OutOfService <-> InService <-> Loading/Unloading.
LRS / LAS: simple boolean toggle pairs.
15 test cases assert the happy-path lifecycles, cross-state cancels,
and that change handlers fire only on real transitions (Read in
NotConfirmed is a no-op, not a handler call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "secsgem/gem/carrier_state.hpp"
|
||||
#include "secsgem/gem/load_port_state.hpp"
|
||||
|
||||
using namespace secsgem::gem;
|
||||
|
||||
// ---- CIDS ---------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CIDS: NotConfirmed -> Confirmed on ProceedWithCarrier") {
|
||||
CarrierStateMachine c;
|
||||
CHECK(c.id_status() == CarrierIDStatus::NotConfirmed);
|
||||
CHECK(c.on_id_event(CarrierIDEvent::ProceedWithCarrier));
|
||||
CHECK(c.id_status() == CarrierIDStatus::Confirmed);
|
||||
}
|
||||
|
||||
TEST_CASE("CIDS: NotConfirmed -> Unknown on IDFailed; Bind recovers") {
|
||||
CarrierStateMachine c;
|
||||
CHECK(c.on_id_event(CarrierIDEvent::IDFailed));
|
||||
CHECK(c.id_status() == CarrierIDStatus::Unknown);
|
||||
CHECK(c.on_id_event(CarrierIDEvent::Bind));
|
||||
CHECK(c.id_status() == CarrierIDStatus::Confirmed);
|
||||
}
|
||||
|
||||
TEST_CASE("CIDS: CancelCarrier from any state returns to NotConfirmed") {
|
||||
CarrierStateMachine c;
|
||||
c.on_id_event(CarrierIDEvent::ProceedWithCarrier);
|
||||
REQUIRE(c.id_status() == CarrierIDStatus::Confirmed);
|
||||
CHECK(c.on_id_event(CarrierIDEvent::CancelCarrier));
|
||||
CHECK(c.id_status() == CarrierIDStatus::NotConfirmed);
|
||||
}
|
||||
|
||||
// ---- CSMS ---------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CSMS: NotRead -> Read -> Mismatched -> NotRead via Reset") {
|
||||
CarrierStateMachine c;
|
||||
CHECK(c.slot_map_status() == SlotMapStatus::NotRead);
|
||||
CHECK(c.on_slot_map_event(SlotMapEvent::Read));
|
||||
CHECK(c.slot_map_status() == SlotMapStatus::Read);
|
||||
CHECK(c.on_slot_map_event(SlotMapEvent::Mismatch));
|
||||
CHECK(c.slot_map_status() == SlotMapStatus::Mismatched);
|
||||
CHECK(c.on_slot_map_event(SlotMapEvent::Reset));
|
||||
CHECK(c.slot_map_status() == SlotMapStatus::NotRead);
|
||||
}
|
||||
|
||||
// ---- CAS ----------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CAS: full access lifecycle BeginAccess -> EndAccess -> Complete") {
|
||||
CarrierStateMachine c;
|
||||
CHECK(c.on_access_event(CarrierAccessEvent::BeginAccess));
|
||||
CHECK(c.access_status() == CarrierAccessStatus::InAccess);
|
||||
CHECK(c.on_access_event(CarrierAccessEvent::EndAccess));
|
||||
CHECK(c.access_status() == CarrierAccessStatus::Complete);
|
||||
// Terminal: no further transitions.
|
||||
CHECK_FALSE(c.on_access_event(CarrierAccessEvent::BeginAccess));
|
||||
}
|
||||
|
||||
TEST_CASE("CAS: Cancel from NotAccessed jumps straight to Complete") {
|
||||
CarrierStateMachine c;
|
||||
CHECK(c.on_access_event(CarrierAccessEvent::Cancel));
|
||||
CHECK(c.access_status() == CarrierAccessStatus::Complete);
|
||||
}
|
||||
|
||||
// ---- LoadPort -----------------------------------------------------------
|
||||
|
||||
TEST_CASE("LPTS: InService -> Loading -> InService") {
|
||||
LoadPortStateMachine p;
|
||||
CHECK(p.transfer_state() == LoadPortTransferState::InService);
|
||||
CHECK(p.on_transfer_event(LoadPortTransferEvent::StartLoading));
|
||||
CHECK(p.transfer_state() == LoadPortTransferState::Loading);
|
||||
CHECK(p.on_transfer_event(LoadPortTransferEvent::CompleteLoading));
|
||||
CHECK(p.transfer_state() == LoadPortTransferState::InService);
|
||||
}
|
||||
|
||||
TEST_CASE("LPTS: any -> OutOfService via LeaveService") {
|
||||
LoadPortStateMachine p;
|
||||
p.on_transfer_event(LoadPortTransferEvent::StartLoading);
|
||||
CHECK(p.on_transfer_event(LoadPortTransferEvent::LeaveService));
|
||||
CHECK(p.transfer_state() == LoadPortTransferState::OutOfService);
|
||||
}
|
||||
|
||||
TEST_CASE("LRS: Reserve / Release") {
|
||||
LoadPortStateMachine p;
|
||||
CHECK(p.on_reservation_event(LoadPortReservationEvent::Reserve));
|
||||
CHECK(p.reservation_state() == LoadPortReservationStatus::Reserved);
|
||||
CHECK(p.on_reservation_event(LoadPortReservationEvent::Release));
|
||||
CHECK(p.reservation_state() == LoadPortReservationStatus::NotReserved);
|
||||
}
|
||||
|
||||
TEST_CASE("LAS: Associate / Disassociate") {
|
||||
LoadPortStateMachine p;
|
||||
CHECK(p.on_association_event(LoadPortAssociationEvent::Associate));
|
||||
CHECK(p.association_state() == LoadPortAssociationStatus::Associated);
|
||||
CHECK(p.on_association_event(LoadPortAssociationEvent::Disassociate));
|
||||
CHECK(p.association_state() == LoadPortAssociationStatus::NotAssociated);
|
||||
}
|
||||
|
||||
TEST_CASE("CarrierStateMachine: change handlers fire on real transitions only") {
|
||||
CarrierStateMachine c;
|
||||
int id_calls = 0;
|
||||
c.set_id_handler([&](CarrierIDStatus, CarrierIDStatus, CarrierIDEvent) { ++id_calls; });
|
||||
// Read in NotConfirmed has no `to` change, no handler call.
|
||||
c.on_id_event(CarrierIDEvent::Read);
|
||||
CHECK(id_calls == 0);
|
||||
// ProceedWithCarrier does transition.
|
||||
c.on_id_event(CarrierIDEvent::ProceedWithCarrier);
|
||||
CHECK(id_calls == 1);
|
||||
}
|
||||
Reference in New Issue
Block a user