// E5 / E30 §9 — auto S9F3/S9F5 emission for primaries that hit no // registered handler. Uses a back-to-back Connection pair: A runs the // system-under-test with a router that knows only S1F1; B is a raw // peer that sends a few unrecognized primaries and asserts the right // S9F* response comes back on the wire. #include #include #include #include #include #include #include "secsgem/gem/router.hpp" #include "secsgem/hsms/connection.hpp" #include "secsgem/hsms/header.hpp" using namespace secsgem; namespace s2 = secsgem::secs2; namespace { struct Pair { asio::io_context io; asio::ip::tcp::socket a{io}, b{io}; Pair() { asio::ip::tcp::acceptor acc(io, asio::ip::tcp::endpoint( asio::ip::address_v4::loopback(), 0)); const auto port = acc.local_endpoint().port(); bool done_a = false, done_b = false; std::error_code ec_a, ec_b; acc.async_accept(a, [&](std::error_code ec) { ec_a = ec; done_a = true; }); b.async_connect({asio::ip::address_v4::loopback(), port}, [&](std::error_code ec) { ec_b = ec; done_b = true; }); while (!(done_a && done_b)) { if (io.stopped()) io.restart(); if (io.poll() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } REQUIRE_FALSE(ec_a); REQUIRE_FALSE(ec_b); } }; template void pump_until(asio::io_context& io, Pred pred, std::chrono::milliseconds budget = std::chrono::seconds(3)) { const auto deadline = std::chrono::steady_clock::now() + budget; while (!pred()) { if (std::chrono::steady_clock::now() > deadline) FAIL("pump_until budget exceeded"); if (io.stopped()) io.restart(); if (io.poll() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } // namespace TEST_CASE("Router::dispatch_with_s9 emits S9F5 for unhandled function in known stream") { Pair sp; hsms::Timers timers; timers.linktest = std::chrono::milliseconds(0); timers.t3 = std::chrono::milliseconds(500); auto sut = std::make_shared( std::move(sp.a), hsms::Connection::Mode::Passive, 0, timers); auto peer = std::make_shared( std::move(sp.b), hsms::Connection::Mode::Active, 0, timers); gem::Router router; router.on(1, 1, [](const s2::Message&) -> std::optional { return s2::Message(1, 2, false); }); std::weak_ptr wsut = sut; sut->set_message_handler([&router, wsut](const s2::Message& msg) -> std::optional { auto c = wsut.lock(); return router.dispatch_with_s9( [&c](uint8_t f, const std::array& mhead) { if (c) c->emit_s9(f, mhead); }, [&c]() -> std::optional> { if (!c) return std::nullopt; auto* h = c->current_header(); if (!h) return std::nullopt; return h->encode(); }, msg); }); std::vector> seen_at_peer; peer->set_message_handler([&](const s2::Message& m) -> std::optional { seen_at_peer.emplace_back(m.stream, m.function); return std::nullopt; }); bool sut_sel = false, peer_sel = false; sut->set_selected_handler([&] { sut_sel = true; }); peer->set_selected_handler([&] { peer_sel = true; }); sut->start(); peer->start(); pump_until(sp.io, [&] { return sut_sel && peer_sel; }); // Send S1F2 (a "reply" with no transaction — sut should ignore the // body but treat it as an unhandled primary and emit S9F5). // Actually a better stimulus: send S1F5 which is W-bit primary; sut // has no handler for F5 within stream 1. peer->send_request(s2::Message(1, 5, true), [](std::error_code, const s2::Message&) {}); pump_until(sp.io, [&] { for (auto& p : seen_at_peer) if (p.first == 9 && p.second == 5) return true; return false; }); bool saw_s9f5 = false; for (auto& p : seen_at_peer) if (p.first == 9 && p.second == 5) saw_s9f5 = true; CHECK(saw_s9f5); } TEST_CASE("Router::dispatch_with_s9 emits S9F3 for unhandled stream") { Pair sp; hsms::Timers timers; timers.linktest = std::chrono::milliseconds(0); timers.t3 = std::chrono::milliseconds(500); auto sut = std::make_shared( std::move(sp.a), hsms::Connection::Mode::Passive, 0, timers); auto peer = std::make_shared( std::move(sp.b), hsms::Connection::Mode::Active, 0, timers); // Router knows only S1F1 — stream 7 is completely unhandled. gem::Router router; router.on(1, 1, [](const s2::Message&) -> std::optional { return s2::Message(1, 2, false); }); std::weak_ptr wsut = sut; sut->set_message_handler([&router, wsut](const s2::Message& msg) -> std::optional { auto c = wsut.lock(); return router.dispatch_with_s9( [&c](uint8_t f, const std::array& mhead) { if (c) c->emit_s9(f, mhead); }, [&c]() -> std::optional> { if (!c) return std::nullopt; auto* h = c->current_header(); if (!h) return std::nullopt; return h->encode(); }, msg); }); std::vector> seen_at_peer; peer->set_message_handler([&](const s2::Message& m) -> std::optional { seen_at_peer.emplace_back(m.stream, m.function); return std::nullopt; }); bool sut_sel = false, peer_sel = false; sut->set_selected_handler([&] { sut_sel = true; }); peer->set_selected_handler([&] { peer_sel = true; }); sut->start(); peer->start(); pump_until(sp.io, [&] { return sut_sel && peer_sel; }); // S7F19 — stream 7 is unknown to our router. peer->send_request(s2::Message(7, 19, true), [](std::error_code, const s2::Message&) {}); pump_until(sp.io, [&] { for (auto& p : seen_at_peer) if (p.first == 9 && p.second == 3) return true; return false; }); bool saw_s9f3 = false; for (auto& p : seen_at_peer) if (p.first == 9 && p.second == 3) saw_s9f3 = true; CHECK(saw_s9f3); }