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