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>
95 lines
3.7 KiB
C++
95 lines
3.7 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include <vector>
|
|
|
|
#include "secsgem/gem/control_job_state.hpp"
|
|
#include "secsgem/gem/store/control_jobs.hpp"
|
|
#include "secsgem/gem/store/process_jobs.hpp"
|
|
|
|
using namespace secsgem::gem;
|
|
|
|
TEST_CASE("CJ default initial state is Queued") {
|
|
ControlJobStateMachine cj;
|
|
CHECK(cj.state() == ControlJobState::Queued);
|
|
}
|
|
|
|
TEST_CASE("CJ full cascade: Queued -> Completed via Start path") {
|
|
ControlJobStateMachine cj;
|
|
CHECK(cj.on_internal(ControlJobEvent::Select));
|
|
CHECK(cj.state() == ControlJobState::Selected);
|
|
CHECK(cj.on_internal(ControlJobEvent::SetupComplete));
|
|
CHECK(cj.state() == ControlJobState::WaitingForStart);
|
|
CHECK(cj.on_host_command(ControlJobEvent::Start) == HostCmdAck::Accept);
|
|
CHECK(cj.state() == ControlJobState::Executing);
|
|
CHECK(cj.on_internal(ControlJobEvent::AllJobsComplete));
|
|
CHECK(cj.state() == ControlJobState::Completed);
|
|
}
|
|
|
|
TEST_CASE("CJ Pause/Resume only from Executing") {
|
|
ControlJobStateMachine cj(ControlJobTransitionTable::default_table(),
|
|
ControlJobState::Executing);
|
|
CHECK(cj.on_host_command(ControlJobEvent::Pause) == HostCmdAck::Accept);
|
|
CHECK(cj.state() == ControlJobState::Paused);
|
|
CHECK(cj.on_host_command(ControlJobEvent::Resume) == HostCmdAck::Accept);
|
|
CHECK(cj.state() == ControlJobState::Executing);
|
|
}
|
|
|
|
TEST_CASE("CJ Stop from Queued goes directly to Completed (empty CJ)") {
|
|
ControlJobStateMachine cj;
|
|
CHECK(cj.on_host_command(ControlJobEvent::Stop) == HostCmdAck::Accept);
|
|
CHECK(cj.state() == ControlJobState::Completed);
|
|
}
|
|
|
|
TEST_CASE("CJ Abort from Executing -> Aborting -> Completed") {
|
|
ControlJobStateMachine cj(ControlJobTransitionTable::default_table(),
|
|
ControlJobState::Executing);
|
|
CHECK(cj.on_host_command(ControlJobEvent::Abort) == HostCmdAck::Accept);
|
|
CHECK(cj.state() == ControlJobState::Aborting);
|
|
CHECK(cj.on_internal(ControlJobEvent::AbortComplete));
|
|
CHECK(cj.state() == ControlJobState::Completed);
|
|
}
|
|
|
|
TEST_CASE("CJ Start from Queued is illegal (must Select first)") {
|
|
ControlJobStateMachine cj;
|
|
CHECK(cj.on_host_command(ControlJobEvent::Start) == HostCmdAck::CannotDoNow);
|
|
CHECK(cj.state() == ControlJobState::Queued);
|
|
}
|
|
|
|
TEST_CASE("CTLJOBCMD wire-name mapping") {
|
|
CHECK(ctl_cmd_to_event("CJSTART").value() == ControlJobEvent::Start);
|
|
CHECK(ctl_cmd_to_event("CJPAUSE").value() == ControlJobEvent::Pause);
|
|
CHECK(ctl_cmd_to_event("STOP").value() == ControlJobEvent::Stop);
|
|
CHECK_FALSE(ctl_cmd_to_event("PJSTART").has_value());
|
|
}
|
|
|
|
TEST_CASE("CJStore: create rejects empty PJ list and unknown PJ") {
|
|
ProcessJobStore pjs;
|
|
pjs.create("PJ-1", "R", {});
|
|
ControlJobStore cjs;
|
|
auto pj_known = [&pjs](const std::string& id) { return pjs.has(id); };
|
|
|
|
CHECK(cjs.create("CJ-1", {}, pj_known) ==
|
|
ControlJobStore::CreateResult::Denied_Empty);
|
|
CHECK(cjs.create("CJ-1", {"GHOST"}, pj_known) ==
|
|
ControlJobStore::CreateResult::Denied_UnknownPRJob);
|
|
CHECK(cjs.create("CJ-1", {"PJ-1"}, pj_known) ==
|
|
ControlJobStore::CreateResult::Created);
|
|
CHECK(cjs.create("CJ-1", {"PJ-1"}, pj_known) ==
|
|
ControlJobStore::CreateResult::Denied_AlreadyExists);
|
|
}
|
|
|
|
TEST_CASE("CJStore: state-change handler observes synthetic Created event") {
|
|
ProcessJobStore pjs;
|
|
pjs.create("PJ-1", "R", {});
|
|
ControlJobStore cjs;
|
|
std::vector<std::tuple<std::string, ControlJobState, ControlJobState>> log;
|
|
cjs.set_state_change_handler(
|
|
[&log](const std::string& id, ControlJobState f, ControlJobState t,
|
|
ControlJobEvent) { log.emplace_back(id, f, t); });
|
|
cjs.create("CJ-1", {"PJ-1"});
|
|
REQUIRE(log.size() == 1);
|
|
CHECK(std::get<0>(log[0]) == "CJ-1");
|
|
CHECK(std::get<1>(log[0]) == ControlJobState::NoState);
|
|
CHECK(std::get<2>(log[0]) == ControlJobState::Queued);
|
|
}
|