feat(clients)+test(interop): C++ client + Java validation of the daemon (B7)
tests / build-and-test (push) Successful in 2m54s
tests / thread-sanitizer (push) Successful in 3m48s
tests / tshark-dissector (push) Successful in 2m24s
tests / secs4j-interop (push) Successful in 1m44s
tests / python-interop (push) Successful in 3m10s
tests / libfuzzer (push) Successful in 3m38s
tests / build-and-test (push) Successful in 2m54s
tests / thread-sanitizer (push) Successful in 3m48s
tests / tshark-dissector (push) Successful in 2m24s
tests / secs4j-interop (push) Successful in 1m44s
tests / python-interop (push) Successful in 3m10s
tests / libfuzzer (push) Successful in 3m38s
B7 — the daemon's HSMS face under the Java reference: Dockerfile.server now
bakes secs_gemd alongside secs_server (grpc deps in both stages), and
secs4j_validate.sh gains TARGET=gemd to point the 55-check secs4java8 suite
at the daemon instead. Result: 55/55 green. With secsgem-py already
validating both faces, byte-identical GEM between secs_server and secs_gemd
is now proven by both reference implementations, not inferred from shared
code. CI runs the daemon target as an extra step (image layers shared).
Second client — clients/cpp: a header-only C++ twin of the Python client
over the same proto. eq.set("ChamberPressure", 2.5) with bare literals
(integral/floating dispatch avoids variant ambiguity), get/fire/alarm/
clear, control_state/request_control_state/health, on("START", fn) +
listen()/listen_async()/stop() with auto-CompleteCommand, SecsGemError
carrying the daemon's message. cpp_mini_tool (~30 lines) mirrors the
Python mini_tool. Tested end-to-end over real loopback TCP against the
service inside secs_gemd_tests — now 4 cases / 141 assertions — including
set/get round-trips, error text, alarm-by-name into the model, health,
and the full HCACK-4 command loop with parameters.
(Build note: two grpc-heavy TUs at -O3 OOM even at -j2 on Docker Desktop;
built -j1. Known environment limitation, roadmap-documented.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// 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 <doctest/doctest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#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<grpc::Server> 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<double>(vals.at("ChamberPressure")) == doctest::Approx(2.5));
|
||||
CHECK(std::get<int64_t>(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<bool> ran{false};
|
||||
std::string seen_ppid;
|
||||
eq.on("START", [&](const secsgem_client::Command& cmd) {
|
||||
seen_ppid = std::get<std::string>(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();
|
||||
}
|
||||
Reference in New Issue
Block a user