#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); } // ---- multi-observer (HandlerSlot) ------------------------------------------ TEST_CASE("add_state_change_handler observers fire alongside the primary, in order") { ControlStateMachine sm; std::vector order; Recorder primary, obs1, obs2; sm.set_state_change_handler([&](ControlState f, ControlState t, ControlEvent e) { order.push_back(0); primary.handler()(f, t, e); }); sm.add_state_change_handler([&](ControlState f, ControlState t, ControlEvent e) { order.push_back(1); obs1.handler()(f, t, e); }); sm.add_state_change_handler([&](ControlState f, ControlState t, ControlEvent e) { order.push_back(2); obs2.handler()(f, t, e); }); CHECK(sm.on_host_request_online() == OnlineAck::Accept); // 2 transitions // All three saw both transitions, primary before observers each time. CHECK(primary.changes.size() == 2); CHECK(obs1.changes.size() == 2); CHECK(obs2.changes.size() == 2); REQUIRE(order.size() == 6); CHECK(order == std::vector{0, 1, 2, 0, 1, 2}); } TEST_CASE("set_state_change_handler replaces the primary but observers survive") { ControlStateMachine sm; Recorder old_primary, new_primary, observer; sm.set_state_change_handler(old_primary.handler()); sm.add_state_change_handler(observer.handler()); sm.set_state_change_handler(new_primary.handler()); // replaces old_primary ONLY sm.on_host_request_online(); CHECK(old_primary.changes.empty()); // replaced CHECK(new_primary.changes.size() == 2); // active primary CHECK(observer.changes.size() == 2); // survived the re-set } TEST_CASE("observers work with no primary handler installed") { ControlStateMachine sm; Recorder observer; sm.add_state_change_handler(observer.handler()); sm.on_host_request_online(); CHECK(observer.changes.size() == 2); }