// Wire-level test for the E84 asio adapter. Drives the FSM through a // real asio::io_context with wall-clock timers and asserts that // HandoffFault fires on actual elapsed time — the integration the unit // tests in test_e84_timers.cpp can't reach because they call // fsm.on_timeout() synthetically. #include #include #include #include #include "secsgem/gem/e84_asio_timers.hpp" #include "secsgem/gem/e84_state.hpp" using namespace secsgem::gem; using namespace std::chrono_literals; namespace { // Run io until either the predicate is true or `cap` elapses. Stops // the harness as soon as the FSM reaches the expected state so the test // doesn't sit on the deadline. template void run_until(asio::io_context& io, Pred p, std::chrono::milliseconds cap) { asio::steady_timer cap_timer(io); cap_timer.expires_after(cap); cap_timer.async_wait([&io](std::error_code) { io.stop(); }); asio::steady_timer poll(io); std::function tick = [&](std::error_code ec) { if (ec) return; if (p()) { io.stop(); return; } poll.expires_after(5ms); poll.async_wait(tick); }; poll.expires_after(5ms); poll.async_wait(tick); io.run(); } } // namespace TEST_CASE("E84 asio: TA1 expires on real wall clock and faults the FSM") { asio::io_context io; E84StateMachine fsm; fsm.set_timeouts({50ms, 0ms, 0ms}); auto driver = std::make_shared(io.get_executor(), fsm); driver->attach(); fsm.on_signal_change(E84Signal::CS_0, true); fsm.on_signal_change(E84Signal::VALID, true); REQUIRE(fsm.state() == E84State::ValidAsserted); REQUIRE(fsm.timer_armed(E84TimerId::TA1)); run_until(io, [&] { return fsm.state() == E84State::HandoffFault; }, 1s); CHECK(fsm.state() == E84State::HandoffFault); CHECK(fsm.fault() == E84Fault::TA1Expired); } TEST_CASE("E84 asio: signal-driven cancel before TA1 expires") { asio::io_context io; E84StateMachine fsm; fsm.set_timeouts({200ms, 0ms, 0ms}); auto driver = std::make_shared(io.get_executor(), fsm); driver->attach(); fsm.on_signal_change(E84Signal::CS_0, true); fsm.on_signal_change(E84Signal::VALID, true); // Schedule the L_REQ assertion well before TA1 fires. After it lands, // the FSM is in LoadReady (TA2 takes over but timeouts.ta2 is 0, so // nothing is armed) and TA1 must NOT have promoted to HandoffFault. asio::steady_timer ack(io); ack.expires_after(20ms); ack.async_wait([&](std::error_code) { fsm.on_signal_change(E84Signal::L_REQ, true); }); run_until(io, [&] { return fsm.state() == E84State::HandoffFault; }, 400ms); CHECK(fsm.state() == E84State::LoadReady); CHECK(fsm.fault() == E84Fault::None); } TEST_CASE("E84 asio: TA3 fires after Transferring exceeds its budget") { asio::io_context io; E84StateMachine fsm; fsm.set_timeouts({0ms, 0ms, 60ms}); auto driver = std::make_shared(io.get_executor(), fsm); driver->attach(); fsm.on_signal_change(E84Signal::CS_0, true); fsm.on_signal_change(E84Signal::VALID, true); fsm.on_signal_change(E84Signal::L_REQ, true); fsm.on_signal_change(E84Signal::BUSY, true); REQUIRE(fsm.state() == E84State::Transferring); REQUIRE(fsm.timer_armed(E84TimerId::TA3)); run_until(io, [&] { return fsm.state() == E84State::HandoffFault; }, 1s); CHECK(fsm.state() == E84State::HandoffFault); CHECK(fsm.fault() == E84Fault::TA3Expired); } TEST_CASE("E84 asio: detach() halts further timer-driven transitions") { asio::io_context io; E84StateMachine fsm; fsm.set_timeouts({40ms, 0ms, 0ms}); auto driver = std::make_shared(io.get_executor(), fsm); driver->attach(); fsm.on_signal_change(E84Signal::CS_0, true); fsm.on_signal_change(E84Signal::VALID, true); driver->detach(); // detach cancels the timer; pending asio handler sees aborted and // returns without calling on_timeout. Let the io drain for double // the timeout to confirm. asio::steady_timer wait(io); wait.expires_after(120ms); wait.async_wait([&](std::error_code) { io.stop(); }); io.run(); CHECK(fsm.state() == E84State::ValidAsserted); CHECK(fsm.fault() == E84Fault::None); }