90c177b7ce
GEM300 layer: SEMI E40-0705 Process Job and E94-0705 Control Job state machines, plus the E30 §6.1 communication-state machine that sits between HSMS SELECT and full GEM communication. Data-driven via data/process_job_state.yaml and data/control_job_state.yaml, mirroring the existing control_state.yaml pattern. Wire coverage: S14F9/F10 CreateObject (CJ) host -> equipment S14F11/F12 DeleteObject (CJ) host -> equipment S16F5/F6 PRJobCommand host -> equipment S16F9 PRJobAlert equipment -> host S16F11/F12 PRJobCreate (simplified body) host -> equipment S16F13/F14 PRJobDequeue host -> equipment S16F27/F28 CJobCommand host -> equipment Process Job FSM exposes 8 states matching PRJOBSTATE bytes (E40 §10.3.2); HOQ is reorder-aware (move-to-head against an insertion-order vector); Stop/Abort on a Queued PJ routes through ABORTING so the host observes PRJOBSTATE=7 on the wire (§6.3); alert_enabled is settable per-PJ for PRALERT control; FSM dispatches through ProcessJobStore::on_change_ dynamically so a late set_state_change_handler() reaches existing PJs. Hardening: loader rejects NoState (sentinel) as initial/from/to and rejects `on: created` rows; static_asserts pin enum values to wire bytes; ProcessJobStore is non-movable to keep the per-PJ this-capture safe. Server simulator cascades the full CJ -> PJ lifecycle on CJSTART so the wire trace exercises every legal state. CEIDs 400/401 fire on CJ state changes via the existing event-report pipeline. Tests: 60+ new assertions across test_process_jobs, test_control_jobs, test_communication_state, test_hsms_connection, plus loader and messages round-trip coverage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
182 lines
5.5 KiB
C++
182 lines
5.5 KiB
C++
// Unit tests for the E30 §6.5 GEM Communication State Model.
|
|
//
|
|
// The state machine is pure logic — no Asio, no sockets — so we drive
|
|
// it directly: arm_t_cra() and arm_t_delay() are recorded into a
|
|
// `Recorder` struct and we call on_cra_timeout() / on_delay_elapsed()
|
|
// by hand to simulate the timers firing.
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/communication_state.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
namespace {
|
|
|
|
struct Recorder {
|
|
bool sent_s1f13 = false;
|
|
int cra_arms = 0;
|
|
int delay_arms = 0;
|
|
int cancels = 0;
|
|
std::vector<std::pair<CommState, CommState>> transitions;
|
|
};
|
|
|
|
CommEnvironment env_for(Recorder& r) {
|
|
CommEnvironment e;
|
|
e.arm_t_cra = [&r](std::chrono::milliseconds) { ++r.cra_arms; };
|
|
e.arm_t_delay = [&r](std::chrono::milliseconds) { ++r.delay_arms; };
|
|
e.cancel_timers = [&r]() { ++r.cancels; };
|
|
e.send_s1f13 = [&r]() { r.sent_s1f13 = true; };
|
|
return e;
|
|
}
|
|
|
|
CommStateChangeHandler tracker_for(Recorder& r) {
|
|
return [&r](CommState from, CommState to, const std::string&) {
|
|
r.transitions.emplace_back(from, to);
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Initial state is DISABLED") {
|
|
CommunicationStateMachine sm;
|
|
CHECK(sm.state() == CommState::Disabled);
|
|
CHECK_FALSE(sm.communicating());
|
|
}
|
|
|
|
TEST_CASE("Equipment-initiated enable -> WAIT-CRA, fires S1F13, arms T_CRA") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.set_state_change_handler(tracker_for(r));
|
|
|
|
sm.enable();
|
|
|
|
CHECK(sm.state() == CommState::WaitCRA);
|
|
CHECK(r.sent_s1f13);
|
|
CHECK(r.cra_arms == 1);
|
|
CHECK(r.delay_arms == 0);
|
|
CHECK(r.transitions.size() == 1);
|
|
}
|
|
|
|
TEST_CASE("S1F14 with COMMACK=Accept -> COMMUNICATING") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_s1f14_received(0);
|
|
|
|
CHECK(sm.state() == CommState::Communicating);
|
|
CHECK(sm.communicating());
|
|
CHECK(r.cancels >= 1);
|
|
}
|
|
|
|
TEST_CASE("S1F14 with COMMACK=Denied -> WAIT-DELAY and arms T_DELAY") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_s1f14_received(1); // Denied
|
|
|
|
CHECK(sm.state() == CommState::WaitDelay);
|
|
CHECK(r.delay_arms == 1);
|
|
}
|
|
|
|
TEST_CASE("T_CRA timeout -> WAIT-DELAY") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_cra_timeout();
|
|
|
|
CHECK(sm.state() == CommState::WaitDelay);
|
|
CHECK(r.delay_arms == 1);
|
|
}
|
|
|
|
TEST_CASE("T_DELAY elapsed -> retry: back to WAIT-CRA and re-send S1F13") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_cra_timeout(); // -> WaitDelay
|
|
r.sent_s1f13 = false; // arm to detect the re-send
|
|
sm.on_delay_elapsed();
|
|
|
|
CHECK(sm.state() == CommState::WaitCRA);
|
|
CHECK(r.sent_s1f13); // S1F13 re-sent on retry
|
|
CHECK(r.cra_arms == 2); // T_CRA armed again
|
|
}
|
|
|
|
TEST_CASE("Disable from any state returns to DISABLED and cancels timers") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_s1f14_received(0); // COMMUNICATING
|
|
REQUIRE(sm.communicating());
|
|
|
|
sm.disable();
|
|
CHECK(sm.state() == CommState::Disabled);
|
|
CHECK(r.cancels >= 1);
|
|
}
|
|
|
|
TEST_CASE("Host-initiated mode: enable parks in WAIT-CRA without sending S1F13") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Host);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
|
|
CHECK(sm.state() == CommState::WaitCRA);
|
|
CHECK_FALSE(r.sent_s1f13);
|
|
CHECK(r.cra_arms == 0); // no T_CRA from our side
|
|
}
|
|
|
|
TEST_CASE("Host-initiated: inbound S1F13 transitions us to COMMUNICATING") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Host);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_s1f13_received();
|
|
|
|
CHECK(sm.state() == CommState::Communicating);
|
|
}
|
|
|
|
TEST_CASE("Connection lost from COMMUNICATING returns to NOT-COMMUNICATING") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
sm.enable();
|
|
sm.on_s1f14_received(0); // -> COMMUNICATING
|
|
sm.on_connection_lost();
|
|
|
|
// Equipment-initiated falls into WAIT-DELAY (so it retries).
|
|
CHECK(sm.state() == CommState::WaitDelay);
|
|
CHECK(r.delay_arms == 1);
|
|
}
|
|
|
|
TEST_CASE("Spurious S1F14 outside WAIT-CRA is ignored") {
|
|
Recorder r;
|
|
CommunicationStateMachine sm({}, CommunicationStateMachine::Initiator::Equipment);
|
|
sm.set_environment(env_for(r));
|
|
// Disabled: still no transition.
|
|
sm.on_s1f14_received(0);
|
|
CHECK(sm.state() == CommState::Disabled);
|
|
|
|
sm.enable();
|
|
sm.on_s1f14_received(0); // -> COMMUNICATING
|
|
REQUIRE(sm.state() == CommState::Communicating);
|
|
// Already communicating: a second spurious S1F14 doesn't move us back.
|
|
sm.on_s1f14_received(0);
|
|
CHECK(sm.state() == CommState::Communicating);
|
|
}
|
|
|
|
TEST_CASE("State name strings cover all four states") {
|
|
CHECK(std::string(comm_state_name(CommState::Disabled)) == "DISABLED");
|
|
CHECK(std::string(comm_state_name(CommState::WaitCRA)) == "WAIT-CRA");
|
|
CHECK(std::string(comm_state_name(CommState::WaitDelay)) == "WAIT-DELAY");
|
|
CHECK(std::string(comm_state_name(CommState::Communicating)) == "COMMUNICATING");
|
|
}
|