fc3422a4a9
Picks up the file renames that landed alongside the previous commit and fixes everything that pointed at the old root locations: - README.md doc-map updated: every entry now points at docs/X.md, with a new "docs/" lead entry pointing at the guided-tour index. - README inline cross-refs (ARCHITECTURE / INTEGRATION / SECURITY / BENCHMARKS / MES_INTEROP / PROOFS) repointed to docs/. - README "Interop" section rewritten — used to mention only secsgem-py; now covers all four external validators (secsgem-py 31 / secs4java8 55 / tshark 69 frames / libFuzzer 200 k+ runs) with a one-line summary each, plus pointers to interop/README.md and docs/VERIFICATION.md. - README "Deferred follow-ups" cleaned: dropped the explanatory "Listed here so reviewers don't go looking for them in COMPLIANCE.md and find an 'out of scope' entry that sounds defensive" sentence — the section header speaks for itself. - docs/00_index.md "Where the rest of the docs live" table: dropped every `../` prefix since the docs are now siblings. - docs/01_what_is_secs_gem.md PROOFS reference updated to sibling. - docs/02_the_cast.md INTEGRATION + MES_INTEROP refs updated to siblings; dropped the stale "at the repo root" wording. - interop/README.md: VERIFICATION + PROOFS refs updated to ../docs/X.md; stale "~24 + 4 checks" updated to 31 (matches PROOFS.md and README). - examples/pvd_tool/README.md: every doc cross-ref now points at ../../docs/X.md. - Source / data / CI comments mentioning doc names (e.g. "INTEGRATION.md §3", "COMPLIANCE.md gap") rewritten to "docs/INTEGRATION.md §3" etc. — affects 9 files across include/, apps/, tests/, data/, examples/, .gitea/workflows/. Verified: full build under docker passes, 445/445 test cases pass, 2 753/2 753 assertions pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
166 lines
5.4 KiB
C++
166 lines
5.4 KiB
C++
// HSMS-GS integration test: drives a passive equipment with TWO MES
|
|
// sessions (device_id 1 + 2) over a single TCP connection through the
|
|
// Server/Client high-level API customers actually use. Each MES gets
|
|
// its own router + selected handler + send path.
|
|
//
|
|
// This is the "two MES, one tool" pattern that the codebase advertises
|
|
// in docs/COMPLIANCE.md §1 ("HSMS-GS (general-session) ✅") and that
|
|
// docs/INTEGRATION.md §7 mentions. Until now there was no end-to-end test
|
|
// covering the Server/Client integration — only direct Connection
|
|
// wire tests in test_hsms_gs.cpp.
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <asio.hpp>
|
|
#include <chrono>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
#include "secsgem/endpoint.hpp"
|
|
#include "secsgem/gem/messages.hpp"
|
|
#include "secsgem/secs2/message.hpp"
|
|
|
|
using namespace secsgem;
|
|
using namespace std::chrono_literals;
|
|
namespace s2 = secsgem::secs2;
|
|
namespace gem = secsgem::gem;
|
|
|
|
namespace {
|
|
|
|
template <typename Pred>
|
|
void run_until(asio::io_context& io, Pred pred,
|
|
std::chrono::seconds budget = 10s) {
|
|
asio::steady_timer cap(io);
|
|
cap.expires_after(budget);
|
|
cap.async_wait([&io](std::error_code ec) {
|
|
if (!ec) io.stop();
|
|
});
|
|
asio::steady_timer poll(io);
|
|
std::function<void(std::error_code)> tick = [&](std::error_code ec) {
|
|
if (ec) return;
|
|
if (pred()) { io.stop(); return; }
|
|
poll.expires_after(1ms);
|
|
poll.async_wait(tick);
|
|
};
|
|
poll.expires_after(1ms);
|
|
poll.async_wait(tick);
|
|
io.run();
|
|
io.restart();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("HSMS-GS integration: two MES sessions on one Server, distinct dispatch") {
|
|
asio::io_context io;
|
|
|
|
// Pick an OS-allocated port for the test.
|
|
asio::ip::tcp::acceptor probe(
|
|
io, asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), 0));
|
|
const auto port = probe.local_endpoint().port();
|
|
probe.close();
|
|
|
|
// Equipment: passive Server bound to the primary session (device_id=1)
|
|
// by construction, then add_session(2) for the second MES. Each
|
|
// session has its own router (different MDLN/SOFTREV so we can tell
|
|
// them apart on the wire).
|
|
Server::Config sc;
|
|
sc.port = port;
|
|
sc.device_id = 1;
|
|
Server server(io, sc);
|
|
|
|
std::shared_ptr<Connection> equipment_conn;
|
|
int sess1_messages = 0;
|
|
int sess2_messages = 0;
|
|
|
|
server.on_connection([&](std::shared_ptr<Connection> conn) {
|
|
equipment_conn = conn;
|
|
conn->add_session(2);
|
|
|
|
conn->set_session_message_handler(
|
|
1, [&sess1_messages](const s2::Message& m)
|
|
-> std::optional<s2::Message> {
|
|
++sess1_messages;
|
|
if (m.stream == 1 && m.function == 1)
|
|
return gem::s1f2_on_line_data("EQUIP-SESS-1", "1.0");
|
|
return std::nullopt;
|
|
});
|
|
|
|
conn->set_session_message_handler(
|
|
2, [&sess2_messages](const s2::Message& m)
|
|
-> std::optional<s2::Message> {
|
|
++sess2_messages;
|
|
if (m.stream == 1 && m.function == 1)
|
|
return gem::s1f2_on_line_data("EQUIP-SESS-2", "2.0");
|
|
return std::nullopt;
|
|
});
|
|
});
|
|
server.start();
|
|
|
|
// Active host running TWO sessions over a single TCP connection.
|
|
// Mirrors the multi-MES "one tool, two factory schedulers" pattern.
|
|
Client::Config cc;
|
|
cc.host = "127.0.0.1";
|
|
cc.port = port;
|
|
cc.device_id = 1;
|
|
cc.timers.linktest = 0ms;
|
|
Client client(io, cc);
|
|
|
|
std::shared_ptr<Connection> host_conn;
|
|
bool sess1_selected = false;
|
|
bool sess2_selected = false;
|
|
|
|
client.on_connection([&](std::shared_ptr<Connection> conn) {
|
|
host_conn = conn;
|
|
conn->add_session(2);
|
|
conn->set_session_selected_handler(1, [&] { sess1_selected = true; });
|
|
conn->set_session_selected_handler(2, [&] { sess2_selected = true; });
|
|
});
|
|
client.start();
|
|
|
|
// Both sessions go through Select.req individually (Active mode
|
|
// walk-list: session 1 first, then session 2 once 1 is SELECTED).
|
|
run_until(io, [&] { return sess1_selected && sess2_selected; });
|
|
REQUIRE(sess1_selected);
|
|
REQUIRE(sess2_selected);
|
|
REQUIRE(equipment_conn);
|
|
REQUIRE(host_conn);
|
|
|
|
// Host sends S1F1 on session 1 → expects MDLN "EQUIP-SESS-1".
|
|
std::optional<s2::Message> sess1_reply;
|
|
host_conn->send_request(1, s2::Message(1, 1, true),
|
|
[&](std::error_code ec, const s2::Message& m) {
|
|
if (!ec) sess1_reply = m;
|
|
});
|
|
|
|
// Host sends S1F1 on session 2 → expects MDLN "EQUIP-SESS-2".
|
|
std::optional<s2::Message> sess2_reply;
|
|
host_conn->send_request(2, s2::Message(1, 1, true),
|
|
[&](std::error_code ec, const s2::Message& m) {
|
|
if (!ec) sess2_reply = m;
|
|
});
|
|
|
|
run_until(io,
|
|
[&] { return sess1_reply.has_value() && sess2_reply.has_value(); });
|
|
|
|
REQUIRE(sess1_reply.has_value());
|
|
REQUIRE(sess2_reply.has_value());
|
|
CHECK(sess1_reply->stream == 1);
|
|
CHECK(sess1_reply->function == 2);
|
|
CHECK(sess2_reply->stream == 1);
|
|
CHECK(sess2_reply->function == 2);
|
|
|
|
// Both routers fired, each on its own session — no cross-talk.
|
|
CHECK(sess1_messages == 1);
|
|
CHECK(sess2_messages == 1);
|
|
|
|
// The replies have different MDLNs proving the per-session
|
|
// dispatch returned distinct payloads.
|
|
const auto sml1 = sess1_reply->sml();
|
|
const auto sml2 = sess2_reply->sml();
|
|
CHECK(sml1.find("EQUIP-SESS-1") != std::string::npos);
|
|
CHECK(sml2.find("EQUIP-SESS-2") != std::string::npos);
|
|
|
|
host_conn->separate();
|
|
}
|