// The C++ client (clients/cpp) against the real service over loopback TCP — // the same end-to-end shape as the Python client's interop harness, in-tree. #include #include #include #include #include "secsgem/gem/default_handlers.hpp" #include "secsgem/gem/messages.hpp" #include "secsgem/daemon/equipment_service.hpp" #include "secsgem_client/equipment.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 namespace { gem::EquipmentRuntime::Config client_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; } } // namespace TEST_CASE("C++ client end-to-end against the service over loopback TCP") { gem::EquipmentRuntime rt(client_test_config()); gem::register_default_handlers(rt); secsgem::daemon::EquipmentService svc(rt); rt.run_async(); int port = 0; grpc::ServerBuilder builder; builder.AddListeningPort("127.0.0.1:0", grpc::InsecureServerCredentials(), &port); builder.RegisterService(&svc); std::unique_ptr server(builder.BuildAndStart()); REQUIRE(server); REQUIRE(port != 0); secsgem_client::Equipment eq("127.0.0.1:" + std::to_string(port)); // set/get round-trip with C++ literals (int stays integer, double real). eq.set("ChamberPressure", 2.5); eq.set("WaferCounter", 7); auto vals = eq.get({"ChamberPressure", "WaferCounter"}); CHECK(std::get(vals.at("ChamberPressure")) == doctest::Approx(2.5)); CHECK(std::get(vals.at("WaferCounter")) == 7); // Errors carry the daemon's explanation. CHECK_THROWS_WITH_AS(eq.set("NoSuchVariable", 1), doctest::Contains("NoSuchVariable"), secsgem_client::SecsGemError); // Alarms by config name. eq.alarm("chiller_temp_high"); auto active = rt.read_sync([&rt] { return rt.model().alarms.active(1); }); REQUIRE(active.has_value()); CHECK(*active); eq.clear("chiller_temp_high"); // Control state + health. CHECK(eq.control_state() == "HOST_OFFLINE"); auto h = eq.health(); CHECK(h.link == "DISCONNECTED"); CHECK(h.control_state == "HOST_OFFLINE"); // The command loop: handler runs, params arrive, S2F42 says HCACK 4. std::atomic ran{false}; std::string seen_ppid; eq.on("START", [&](const secsgem_client::Command& cmd) { seen_ppid = std::get(cmd.params.at("PPID")); ran = true; }); eq.listen_async(); std::this_thread::sleep_for(std::chrono::milliseconds(200)); // subscribe race auto reply = rt.read_sync([&rt]() { return rt.router().dispatch(gem::s2f41_host_command( "START", {{"PPID", s2::Item::ascii("RECIPE-A")}})); }); REQUIRE(reply.has_value()); REQUIRE(reply->has_value()); auto parsed = gem::parse_s2f42(**reply); REQUIRE(parsed.has_value()); CHECK(parsed->hcack == gem::HostCmdAck::AcceptedWillFinishLater); for (int i = 0; i < 50 && !ran.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); CHECK(ran.load()); CHECK(seen_ppid == "RECIPE-A"); eq.stop(); server->Shutdown(); rt.stop(); }