#include #include #include "secsgem/gem/default_handlers.hpp" #include "secsgem/gem/messages.hpp" #include "secsgem/gem/runtime.hpp" using namespace secsgem; namespace gem = secsgem::gem; namespace s2 = secsgem::secs2; #ifndef SECSGEM_DATA_DIR #error "SECSGEM_DATA_DIR not defined; see CMakeLists.txt" #endif static gem::EquipmentRuntime::Config test_config() { gem::EquipmentRuntime::Config c; c.equipment_yaml = SECSGEM_DATA_DIR "/equipment.yaml"; c.control_state_yaml = SECSGEM_DATA_DIR "/control_state.yaml"; c.process_job_yaml = SECSGEM_DATA_DIR "/process_job_state.yaml"; c.control_job_yaml = SECSGEM_DATA_DIR "/control_job_state.yaml"; c.port = 0; return c; } TEST_CASE("register_default_handlers populates the runtime's Router") { gem::EquipmentRuntime rt(test_config()); CHECK(rt.router().size() == 0); // nothing registered yet gem::register_default_handlers(rt); CHECK(rt.router().size() > 40); // the full GEM handler set (~56) } TEST_CASE("register_default_handlers: S1F1 dispatches to S1F2 On-Line Data") { gem::EquipmentRuntime rt(test_config()); gem::register_default_handlers(rt); auto reply = rt.router().dispatch(s2::Message(1, 1, true)); // Are You There REQUIRE(reply.has_value()); CHECK(reply->stream == 1); CHECK(reply->function == 2); } TEST_CASE("register_default_handlers: S2F41 reaches the on_command behaviour hook") { gem::EquipmentRuntime rt(test_config()); gem::register_default_handlers(rt); bool ran = false; std::string seen; rt.on_command("START", [&](const std::string& rcmd, const std::vector&) { ran = true; seen = rcmd; return gem::HostCmdAck::Accept; }); auto reply = rt.router().dispatch(gem::s2f41_host_command("START", {})); REQUIRE(reply.has_value()); CHECK(reply->stream == 2); CHECK(reply->function == 42); // S2F42 Host Command Ack CHECK(ran); // the handler ran inside the S2F41 dispatch CHECK(seen == "START"); } TEST_CASE("register_default_handlers: unknown command is rejected, hook not invoked") { gem::EquipmentRuntime rt(test_config()); gem::register_default_handlers(rt); auto reply = rt.router().dispatch(gem::s2f41_host_command("NO_SUCH_CMD", {})); REQUIRE(reply.has_value()); CHECK(reply->stream == 2); CHECK(reply->function == 42); // still an S2F42, carrying an error HCACK } TEST_CASE("capability registration is subsettable (the point of the decomposition)") { gem::EquipmentRuntime rt(test_config()); gem::register_identification(rt); gem::register_terminal_services(rt); // What we registered answers... CHECK(rt.router().has_handler(1, 1)); // S1F1 CHECK(rt.router().has_handler(1, 13)); // S1F13 CHECK(rt.router().has_handler(10, 1)); // S10F1 // ...and what we didn't, doesn't: a sensor-class tool with no remote // commands, jobs, or carriers simply never registers them. CHECK_FALSE(rt.router().has_handler(2, 41)); // remote commands CHECK_FALSE(rt.router().has_handler(16, 11)); // E40 jobs CHECK_FALSE(rt.router().has_handler(3, 17)); // E87 carriers } TEST_CASE("role bindings drive the CJ state-change CEIDs and SVID refresh") { gem::EquipmentRuntime rt(test_config()); gem::register_default_handlers(rt); // The shipped roles block binds control_state_svid: 1 — S1F3 refreshes it. auto reply = rt.router().dispatch(gem::s1f3_selected_status_request({1})); REQUIRE(reply.has_value()); auto sv = rt.model().svids.value(1); REQUIRE(sv.has_value()); CHECK(sv->as_ascii() == std::string("HostOffline")); // initial control state }