diff --git a/CMakeLists.txt b/CMakeLists.txt index 011a8c5..8d316e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,9 @@ target_link_libraries(secs_client PRIVATE secsgem) add_executable(secs_interop_probe apps/secs_interop_probe.cpp) target_link_libraries(secs_interop_probe PRIVATE secsgem) +add_executable(secs_conformance apps/secs_conformance.cpp) +target_link_libraries(secs_conformance PRIVATE secsgem) + # --- Tests ---------------------------------------------------------------- enable_testing() include(FetchContent) diff --git a/README.md b/README.md index 280ace7..a813679 100644 --- a/README.md +++ b/README.md @@ -327,11 +327,19 @@ new dictionary loads. Code changes do require rebuild + restart. - Fork `COMPLIANCE.md` and prune it to *your* tool's claimed coverage; ship that copy with the tool. -- Run an independent validator (GEM RTS or equivalent) against - your specific tool — a passing library is necessary but not - sufficient. -- Capture wire traces from the validator run; archive for - audit. +- Run the in-repo conformance harness against your tool: + ``` + build/secs_conformance --host --port 5000 --device 0 + ``` + Exits 0 with a per-check PASS / FAIL summary covering every E30 + fundamental capability (establish comms, on-line ID, status data, + equipment constants, clock, alarms, PP management, documentation). + Adapt `apps/secs_conformance.cpp` to add your tool's + capability-specific checks. +- Run an independent third-party validator (GEM RTS or equivalent) + against your specific tool — a passing library + in-repo harness + is necessary but not sufficient for certification. +- Capture wire traces from every validator run; archive for audit. ## 9. Testing in production diff --git a/apps/secs_conformance.cpp b/apps/secs_conformance.cpp new file mode 100644 index 0000000..28d1dae --- /dev/null +++ b/apps/secs_conformance.cpp @@ -0,0 +1,205 @@ +// 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; +}