#include #include #include "secsgem/gem/control_state.hpp" using namespace secsgem::gem; namespace { struct Recorder { std::vector> changes; ControlStateMachine::StateChangeHandler handler() { return [this](ControlState from, ControlState to, ControlEvent ev) { changes.emplace_back(from, to, ev); }; } }; ControlStateMachine make_sm(ControlState initial) { return ControlStateMachine(ControlTransitionTable::default_table(), initial); } } // namespace TEST_CASE("default initial state is HostOffline") { ControlStateMachine sm; CHECK(sm.state() == ControlState::HostOffline); CHECK_FALSE(sm.online()); } TEST_CASE("custom initial state") { auto sm = make_sm(ControlState::EquipmentOffline); CHECK(sm.state() == ControlState::EquipmentOffline); } TEST_CASE("host request online from HostOffline -> OnlineRemote") { Recorder rec; ControlStateMachine sm; sm.set_state_change_handler(rec.handler()); CHECK(sm.on_host_request_online() == OnlineAck::Accept); CHECK(sm.state() == ControlState::OnlineRemote); CHECK(sm.online()); REQUIRE(rec.changes.size() == 2); CHECK(std::get<1>(rec.changes[0]) == ControlState::AttemptOnline); CHECK(std::get<2>(rec.changes[0]) == ControlEvent::HostRequestOnline); CHECK(std::get<1>(rec.changes[1]) == ControlState::OnlineRemote); CHECK(std::get<2>(rec.changes[1]) == ControlEvent::AttemptComplete); } TEST_CASE("host request online when already online -> AlreadyOnline, no transition") { ControlStateMachine sm; sm.on_host_request_online(); REQUIRE(sm.state() == ControlState::OnlineRemote); Recorder rec; sm.set_state_change_handler(rec.handler()); CHECK(sm.on_host_request_online() == OnlineAck::AlreadyOnline); CHECK(sm.state() == ControlState::OnlineRemote); CHECK(rec.changes.empty()); } TEST_CASE("host request online from EquipmentOffline -> NotAccept") { auto sm = make_sm(ControlState::EquipmentOffline); CHECK(sm.on_host_request_online() == OnlineAck::NotAccept); CHECK(sm.state() == ControlState::EquipmentOffline); } TEST_CASE("host request offline from OnlineRemote -> HostOffline") { ControlStateMachine sm; sm.on_host_request_online(); REQUIRE(sm.state() == ControlState::OnlineRemote); Recorder rec; sm.set_state_change_handler(rec.handler()); CHECK(sm.on_host_request_offline() == OfflineAck::Accept); CHECK(sm.state() == ControlState::HostOffline); REQUIRE(rec.changes.size() == 1); CHECK(std::get<1>(rec.changes[0]) == ControlState::HostOffline); CHECK(std::get<2>(rec.changes[0]) == ControlEvent::HostRequestOffline); } TEST_CASE("host request offline when already offline is idempotent Accept") { ControlStateMachine sm; Recorder rec; sm.set_state_change_handler(rec.handler()); CHECK(sm.on_host_request_offline() == OfflineAck::Accept); CHECK(sm.state() == ControlState::HostOffline); CHECK(rec.changes.empty()); } TEST_CASE("operator online from EquipmentOffline -> OnlineLocal (default table)") { auto sm = make_sm(ControlState::EquipmentOffline); CHECK(sm.operator_online()); CHECK(sm.state() == ControlState::OnlineLocal); } TEST_CASE("operator online when already online is rejected") { auto sm = make_sm(ControlState::OnlineLocal); CHECK_FALSE(sm.operator_online()); CHECK(sm.state() == ControlState::OnlineLocal); } TEST_CASE("operator offline from any online state -> HostOffline") { auto sm = make_sm(ControlState::OnlineRemote); CHECK(sm.operator_offline()); CHECK(sm.state() == ControlState::HostOffline); } TEST_CASE("operator local toggles only from OnlineRemote") { auto sm = make_sm(ControlState::OnlineRemote); CHECK(sm.operator_local()); CHECK(sm.state() == ControlState::OnlineLocal); CHECK_FALSE(sm.operator_local()); } TEST_CASE("operator remote toggles only from OnlineLocal") { auto sm = make_sm(ControlState::OnlineLocal); CHECK(sm.operator_remote()); CHECK(sm.state() == ControlState::OnlineRemote); CHECK_FALSE(sm.operator_remote()); } TEST_CASE("is_online classifier") { CHECK_FALSE(is_online(ControlState::EquipmentOffline)); CHECK_FALSE(is_online(ControlState::AttemptOnline)); CHECK_FALSE(is_online(ControlState::HostOffline)); CHECK(is_online(ControlState::OnlineLocal)); CHECK(is_online(ControlState::OnlineRemote)); } TEST_CASE("default table covers all expected (state, event) pairs") { auto t = ControlTransitionTable::default_table(); // Every state must have an entry for each host event. for (auto s : {ControlState::EquipmentOffline, ControlState::AttemptOnline, ControlState::HostOffline, ControlState::OnlineLocal, ControlState::OnlineRemote}) { CHECK(t.find(s, ControlEvent::HostRequestOnline) != nullptr); CHECK(t.find(s, ControlEvent::HostRequestOffline) != nullptr); } } TEST_CASE("custom table: a row that only sets ack, no transition") { ControlTransitionTable t; t.add({ControlState::HostOffline, ControlEvent::HostRequestOnline, std::nullopt, std::nullopt, static_cast(OnlineAck::NotAccept)}); ControlStateMachine sm(t, ControlState::HostOffline); CHECK(sm.on_host_request_online() == OnlineAck::NotAccept); CHECK(sm.state() == ControlState::HostOffline); }