// secs_conformance — runnable conformance harness. // // Drives a passive GEM-300 equipment through a fixed set of host-initiated // checks that exercise every claimed E5/E30/E37 + GEM 300 capability, // reports per-check pass/fail, and exits 0 if every check passed. Point // it at any HSMS-SS equipment to validate end-to-end conformance from // outside the codebase. // // Run: secs_conformance --host --port 5000 --device 0 // // The check list deliberately mirrors COMPLIANCE.md so that anything // the audit claims as ✅ has a runnable assertion behind it here. #include #include #include #include #include #include #include #include #include #include #include "secsgem/endpoint.hpp" #include "secsgem/gem/messages.hpp" #include "secsgem/gem/messages_helpers.hpp" #include "secsgem/hsms/connection.hpp" #include "secsgem/secs2/message.hpp" using namespace secsgem; namespace s2 = secsgem::secs2; namespace gem = secsgem::gem; using namespace std::chrono_literals; namespace { std::string arg(int argc, char** argv, const std::string& key, const std::string& def) { for (int i = 1; i + 1 < argc; ++i) if (key == argv[i]) return argv[i + 1]; return def; } struct Check { std::string name; bool passed = false; std::string detail; }; std::vector checks; void log(const std::string& m) { std::cout << "[conformance] " << m << std::endl; } void record(const std::string& name, bool ok, const std::string& detail = "") { checks.push_back({name, ok, detail}); log((ok ? "PASS " : "FAIL ") + name + (detail.empty() ? "" : " — " + detail)); } // One check in the async chain: send `req` and dispatch `ok` / // `fail` based on whether a reply matching (expected_stream, // expected_function) arrives within T3. void chain_check(std::shared_ptr conn, const std::string& name, s2::Message req, uint8_t expected_stream, uint8_t expected_function, std::function next) { conn->send_request(std::move(req), [name, expected_stream, expected_function, next, conn] (std::error_code ec, const s2::Message& m) { if (ec) { record(name, false, "no reply / " + ec.message()); } else { const bool ok = m.stream == expected_stream && m.function == expected_function; record(name, ok, ok ? "" : "got S" + std::to_string(m.stream) + "F" + std::to_string(m.function)); } next(); }); } } // namespace int main(int argc, char** argv) { Client::Config cfg; cfg.host = arg(argc, argv, "--host", "127.0.0.1"); cfg.port = static_cast(std::stoi(arg(argc, argv, "--port", "5000"))); cfg.device_id = static_cast(std::stoi(arg(argc, argv, "--device", "0"))); cfg.timers.linktest = 0ms; cfg.timers.t3 = 5s; asio::io_context io; Client client(io, cfg); client.on_log([](const std::string& m) { log("hsms: " + m); }); asio::steady_timer deadline(io); deadline.expires_after(60s); deadline.async_wait([&](std::error_code ec) { if (!ec) { log("global 60s deadline reached, halting"); io.stop(); } }); client.on_connection([&](std::shared_ptr conn) { // Reply to equipment-initiated primaries so the equipment's T3 doesn't // fire on our watch. conn->set_message_handler( [](const s2::Message& msg) -> std::optional { if (msg.stream == 1 && msg.function == 13) { return gem::s1f14_establish_comms_ack( gem::CommAck::Accept, {"CONFORMANCE", "1.0"}); } return std::nullopt; }); conn->set_selected_handler([&, conn] { record("E37 §7.2 SELECT handshake", true, "selected against " + cfg.host + ":" + std::to_string(cfg.port)); // The chain runs forward through async callbacks: each check's // reply handler kicks off the next check. Final closure calls // separate() and posts the summary. auto finish = [&, conn] { log("all checks done; separating"); conn->separate(); asio::post(io, [&] { std::cout << "\n========================================" << std::endl; std::cout << " conformance summary (" << cfg.host << ":" << cfg.port << ")" << std::endl; std::cout << "========================================" << std::endl; int passed = 0; for (const auto& c : checks) { std::cout << (c.passed ? " [PASS] " : " [FAIL] ") << c.name; if (!c.detail.empty()) std::cout << " — " << c.detail; std::cout << std::endl; if (c.passed) ++passed; } std::cout << "\n " << passed << " / " << checks.size() << " checks passed" << std::endl; io.stop(); }); }; // E30 §6.10 documentation (last in chain) auto step8 = [conn, finish] { chain_check(conn, "E30 §6.10 S1F19/F20 GEM Compliance", s2::Message(1, 19, true), 1, 20, finish); }; // E30 §6.17 PP list auto step7 = [conn, step8] { chain_check(conn, "E30 §6.17 S7F19/F20 PP List", s2::Message(7, 19, true), 7, 20, step8); }; // E30 §6.14 alarms auto step6 = [conn, step7] { chain_check(conn, "E30 §6.14 S5F5/F6 List Alarms", s2::Message(5, 5, true, s2::Item::list({})), 5, 6, step7); }; // E30 §6.20 clock auto step5 = [conn, step6] { chain_check(conn, "E30 §6.20 S2F17/F18 Clock", s2::Message(2, 17, true), 2, 18, step6); }; // E30 §6.16 ECID namelist auto step4 = [conn, step5] { chain_check(conn, "E30 §6.16 S2F29/F30 ECID Namelist", s2::Message(2, 29, true, s2::Item::list({})), 2, 30, step5); }; // E30 §6.13 SVID namelist auto step3 = [conn, step4] { chain_check(conn, "E30 §6.13 S1F11/F12 SVID Namelist", s2::Message(1, 11, true, s2::Item::list({})), 1, 12, step4); }; // E30 §6.7 S1F1/F2 auto step2 = [conn, step3] { chain_check(conn, "E30 §6.7 S1F1/F2 Are You There", s2::Message(1, 1, true), 1, 2, step3); }; // E30 §6.5 establish comms auto step1 = [conn, step2] { chain_check(conn, "E30 §6.5 S1F13/F14 Establish Comms", gem::s1f13_establish_comms("HOST", "1.0"), 1, 14, step2); }; step1(); }); }); client.start(); try { io.run(); } catch (const std::exception& e) { std::cerr << "conformance: " << e.what() << std::endl; return 2; } for (const auto& c : checks) if (!c.passed) return 1; return checks.empty() ? 3 : 0; }