a52d44ade5
Adds the substrate-ID verification FSM that E90 §6.4.6 calls for: NotConfirmed initial; equipment hasn't read the ID yet WaitingForHost ID has been read; awaiting host accept/reject Confirmed host confirmed (or force-bound) Mismatched host rejected — recoverable via Bind Events: Read NotConfirmed -> WaitingForHost Confirm WaitingForHost -> Confirmed Mismatch WaitingForHost -> Mismatched Bind any -> Confirmed (force-bind) Reset any -> NotConfirmed Wire-byte values pinned via static_assert. The third axis is now exposed on SubstrateStateMachine alongside location_state() and processing_state(); set_id_handler() observes transitions. Existing two-axis API is unchanged. 4 new test cases cover the happy path, Mismatch+Bind recovery, Reset from any state, and same-state event handler suppression. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
175 lines
6.9 KiB
C++
175 lines
6.9 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/store/substrates.hpp"
|
|
#include "secsgem/gem/substrate_state.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
TEST_CASE("SubstrateStateMachine: initial AtSource / NeedsProcessing") {
|
|
SubstrateStateMachine s;
|
|
CHECK(s.location_state() == SubstrateState::AtSource);
|
|
CHECK(s.processing_state() == SubstrateProcessingState::NeedsProcessing);
|
|
}
|
|
|
|
TEST_CASE("Substrate location: AtSource -> AtWork -> AtDestination") {
|
|
SubstrateStateMachine s;
|
|
CHECK(s.on_location_event(SubstrateEvent::Acquire));
|
|
CHECK(s.location_state() == SubstrateState::AtWork);
|
|
CHECK(s.on_location_event(SubstrateEvent::Release));
|
|
CHECK(s.location_state() == SubstrateState::AtDestination);
|
|
}
|
|
|
|
TEST_CASE("Substrate location: Return from AtWork sends back to AtSource") {
|
|
SubstrateStateMachine s;
|
|
s.on_location_event(SubstrateEvent::Acquire);
|
|
CHECK(s.on_location_event(SubstrateEvent::Return));
|
|
CHECK(s.location_state() == SubstrateState::AtSource);
|
|
}
|
|
|
|
TEST_CASE("Substrate processing: happy path Start -> End -> Processed") {
|
|
SubstrateStateMachine s;
|
|
CHECK(s.on_processing_event(SubstrateProcessingEvent::StartProcessing));
|
|
CHECK(s.processing_state() == SubstrateProcessingState::InProcess);
|
|
CHECK(s.on_processing_event(SubstrateProcessingEvent::EndProcessing));
|
|
CHECK(s.processing_state() == SubstrateProcessingState::Processed);
|
|
}
|
|
|
|
TEST_CASE("Substrate processing: Abort/Stop/Reject/Lost are terminal") {
|
|
for (auto ev : {SubstrateProcessingEvent::Abort,
|
|
SubstrateProcessingEvent::Stop,
|
|
SubstrateProcessingEvent::Reject,
|
|
SubstrateProcessingEvent::ReportLost}) {
|
|
SubstrateStateMachine s;
|
|
s.on_processing_event(SubstrateProcessingEvent::StartProcessing);
|
|
CHECK(s.on_processing_event(ev));
|
|
// Terminal: no further transitions accepted.
|
|
CHECK_FALSE(s.on_processing_event(SubstrateProcessingEvent::EndProcessing));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Substrate processing: Skip from NeedsProcessing") {
|
|
SubstrateStateMachine s;
|
|
CHECK(s.on_processing_event(SubstrateProcessingEvent::Skip));
|
|
CHECK(s.processing_state() == SubstrateProcessingState::Skipped);
|
|
}
|
|
|
|
// ---- SubstrateStore -----------------------------------------------------
|
|
|
|
TEST_CASE("SubstrateStore: create + dedup + location update") {
|
|
SubstrateStore s;
|
|
CHECK(s.create("W-001", "CAR-A", /*slot=*/1) ==
|
|
SubstrateStore::CreateResult::Created);
|
|
CHECK(s.create("W-001") == SubstrateStore::CreateResult::Denied_AlreadyExists);
|
|
REQUIRE(s.has("W-001"));
|
|
CHECK(s.get("W-001")->carrierid == "CAR-A");
|
|
CHECK(s.get("W-001")->slot == 1);
|
|
|
|
CHECK(s.fire_location_event("W-001", SubstrateEvent::Acquire, "MOD-A"));
|
|
CHECK(s.get("W-001")->location == "MOD-A");
|
|
CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork);
|
|
}
|
|
|
|
// ---- Substrate ID Status (E90 §6.4.6) -----------------------------------
|
|
|
|
TEST_CASE("Substrate IDS: NotConfirmed -> WaitingForHost -> Confirmed") {
|
|
SubstrateStateMachine s;
|
|
CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed);
|
|
CHECK(s.on_id_event(SubstrateIDEvent::Read));
|
|
CHECK(s.id_state() == SubstrateIDStatus::WaitingForHost);
|
|
CHECK(s.on_id_event(SubstrateIDEvent::Confirm));
|
|
CHECK(s.id_state() == SubstrateIDStatus::Confirmed);
|
|
}
|
|
|
|
TEST_CASE("Substrate IDS: Mismatch + Bind recovers") {
|
|
SubstrateStateMachine s;
|
|
s.on_id_event(SubstrateIDEvent::Read);
|
|
CHECK(s.on_id_event(SubstrateIDEvent::Mismatch));
|
|
CHECK(s.id_state() == SubstrateIDStatus::Mismatched);
|
|
CHECK(s.on_id_event(SubstrateIDEvent::Bind));
|
|
CHECK(s.id_state() == SubstrateIDStatus::Confirmed);
|
|
}
|
|
|
|
TEST_CASE("Substrate IDS: Reset from any state returns to NotConfirmed") {
|
|
SubstrateStateMachine s;
|
|
s.on_id_event(SubstrateIDEvent::Read);
|
|
s.on_id_event(SubstrateIDEvent::Confirm);
|
|
CHECK(s.on_id_event(SubstrateIDEvent::Reset));
|
|
CHECK(s.id_state() == SubstrateIDStatus::NotConfirmed);
|
|
}
|
|
|
|
TEST_CASE("Substrate IDS: change handler observes transitions only") {
|
|
SubstrateStateMachine s;
|
|
int calls = 0;
|
|
s.set_id_handler([&](SubstrateIDStatus, SubstrateIDStatus, SubstrateIDEvent) {
|
|
++calls;
|
|
});
|
|
s.on_id_event(SubstrateIDEvent::Read);
|
|
s.on_id_event(SubstrateIDEvent::Confirm);
|
|
CHECK(calls == 2);
|
|
// Re-confirming from Confirmed: no row, no call.
|
|
s.on_id_event(SubstrateIDEvent::Confirm);
|
|
CHECK(calls == 2);
|
|
}
|
|
|
|
TEST_CASE("SubstrateStore: history records every transition") {
|
|
SubstrateStore s;
|
|
s.create("W-H", "CAR-X", 3);
|
|
s.fire_location_event("W-H", SubstrateEvent::Acquire, "MOD-A");
|
|
s.fire_processing_event("W-H", SubstrateProcessingEvent::StartProcessing);
|
|
s.fire_processing_event("W-H", SubstrateProcessingEvent::EndProcessing);
|
|
s.fire_location_event("W-H", SubstrateEvent::Release, "DEST");
|
|
const auto* hist = s.history("W-H");
|
|
REQUIRE(hist != nullptr);
|
|
CHECK(hist->size() == 4);
|
|
// First entry is a location transition to AtWork.
|
|
CHECK((*hist)[0].location_to == SubstrateState::AtWork);
|
|
CHECK((*hist)[0].processing_to == SubstrateProcessingState::NoState);
|
|
CHECK((*hist)[0].location_label == "MOD-A");
|
|
// Third entry is a processing transition to Processed.
|
|
CHECK((*hist)[2].processing_to == SubstrateProcessingState::Processed);
|
|
CHECK((*hist)[2].location_to == SubstrateState::NoState);
|
|
}
|
|
|
|
TEST_CASE("SubstrateStore: history_limit caps retention") {
|
|
SubstrateStore s;
|
|
s.set_history_limit(3);
|
|
s.create("W-CAP");
|
|
// Generate more transitions than the cap.
|
|
s.fire_location_event("W-CAP", SubstrateEvent::Acquire);
|
|
s.fire_location_event("W-CAP", SubstrateEvent::Return);
|
|
s.fire_location_event("W-CAP", SubstrateEvent::Acquire);
|
|
s.fire_location_event("W-CAP", SubstrateEvent::Release);
|
|
const auto* hist = s.history("W-CAP");
|
|
REQUIRE(hist != nullptr);
|
|
CHECK(hist->size() == 3);
|
|
// Oldest entry (Acquire to AtWork) should be evicted; first remaining
|
|
// entry is the Return.
|
|
CHECK((*hist)[0].location_to == SubstrateState::AtSource);
|
|
}
|
|
|
|
TEST_CASE("SubstrateStore: change handlers observe both axes independently") {
|
|
SubstrateStore s;
|
|
std::vector<std::pair<std::string, SubstrateState>> loc_log;
|
|
std::vector<std::pair<std::string, SubstrateProcessingState>> proc_log;
|
|
s.set_location_handler([&](const std::string& id, SubstrateState,
|
|
SubstrateState to, SubstrateEvent) {
|
|
loc_log.emplace_back(id, to);
|
|
});
|
|
s.set_processing_handler([&](const std::string& id, SubstrateProcessingState,
|
|
SubstrateProcessingState to,
|
|
SubstrateProcessingEvent) {
|
|
proc_log.emplace_back(id, to);
|
|
});
|
|
s.create("W-X");
|
|
s.fire_location_event("W-X", SubstrateEvent::Acquire);
|
|
s.fire_processing_event("W-X", SubstrateProcessingEvent::StartProcessing);
|
|
s.fire_processing_event("W-X", SubstrateProcessingEvent::EndProcessing);
|
|
s.fire_location_event("W-X", SubstrateEvent::Release);
|
|
CHECK(loc_log.size() == 2);
|
|
CHECK(proc_log.size() == 2);
|
|
CHECK(loc_log.back().second == SubstrateState::AtDestination);
|
|
CHECK(proc_log.back().second == SubstrateProcessingState::Processed);
|
|
}
|