I2: Router::dispatch_with_s9 helper + end-to-end S9F3/F5 tests
The S9F3/F5 fallback was previously inlined in apps/secs_server.cpp;
this commit lifts it onto Router as a template helper and adds two
focused tests asserting the wire behaviour against a real back-to-
back HSMS Connection pair.
template <typename EmitFn, typename HeaderProvider>
std::optional<Message> dispatch_with_s9(emit, header, msg);
The helper does the has_handler / has_handler_for_stream check and
calls the supplied emit function with S9F3 (unknown stream) or S9F5
(unknown function in known stream). The header_provider returns the
optional MHEAD bytes — keeping the helper free of any direct
Connection coupling.
Tests:
- SUT registered only for S1F1; peer sends S1F5 -> SUT replies
S9F5 to the peer.
- SUT registered only for S1F1; peer sends S7F19 -> SUT replies
S9F3 to the peer.
Closes Tranche I — SML parser and the auto-S9F* fallback closeout
both verified end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// 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 <doctest/doctest.h>
|
||||
|
||||
#include <asio.hpp>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#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 <typename Pred>
|
||||
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<hsms::Connection>(
|
||||
std::move(sp.a), hsms::Connection::Mode::Passive, 0, timers);
|
||||
auto peer = std::make_shared<hsms::Connection>(
|
||||
std::move(sp.b), hsms::Connection::Mode::Active, 0, timers);
|
||||
|
||||
gem::Router router;
|
||||
router.on(1, 1, [](const s2::Message&) -> std::optional<s2::Message> {
|
||||
return s2::Message(1, 2, false);
|
||||
});
|
||||
|
||||
std::weak_ptr<hsms::Connection> wsut = sut;
|
||||
sut->set_message_handler([&router, wsut](const s2::Message& msg)
|
||||
-> std::optional<s2::Message> {
|
||||
auto c = wsut.lock();
|
||||
return router.dispatch_with_s9(
|
||||
[&c](uint8_t f, const std::array<uint8_t, 10>& mhead) {
|
||||
if (c) c->emit_s9(f, mhead);
|
||||
},
|
||||
[&c]() -> std::optional<std::array<uint8_t, 10>> {
|
||||
if (!c) return std::nullopt;
|
||||
auto* h = c->current_header();
|
||||
if (!h) return std::nullopt;
|
||||
return h->encode();
|
||||
},
|
||||
msg);
|
||||
});
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> seen_at_peer;
|
||||
peer->set_message_handler([&](const s2::Message& m) -> std::optional<s2::Message> {
|
||||
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<hsms::Connection>(
|
||||
std::move(sp.a), hsms::Connection::Mode::Passive, 0, timers);
|
||||
auto peer = std::make_shared<hsms::Connection>(
|
||||
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<s2::Message> {
|
||||
return s2::Message(1, 2, false);
|
||||
});
|
||||
|
||||
std::weak_ptr<hsms::Connection> wsut = sut;
|
||||
sut->set_message_handler([&router, wsut](const s2::Message& msg)
|
||||
-> std::optional<s2::Message> {
|
||||
auto c = wsut.lock();
|
||||
return router.dispatch_with_s9(
|
||||
[&c](uint8_t f, const std::array<uint8_t, 10>& mhead) {
|
||||
if (c) c->emit_s9(f, mhead);
|
||||
},
|
||||
[&c]() -> std::optional<std::array<uint8_t, 10>> {
|
||||
if (!c) return std::nullopt;
|
||||
auto* h = c->current_header();
|
||||
if (!h) return std::nullopt;
|
||||
return h->encode();
|
||||
},
|
||||
msg);
|
||||
});
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> seen_at_peer;
|
||||
peer->set_message_handler([&](const s2::Message& m) -> std::optional<s2::Message> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user