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