Files
secs-gem/tests/test_daemon_service.cpp
T
raphael 99bfa794fc fix(daemon): honour declared SECS-II formats + make service thread-safe
Audit fixes for two real bugs in the gRPC service:

1. Format compliance: to_item() wrote F8/I8 regardless of the variable's
   declared wire format, so values contradicted the S1F11/S1F21 namelists
   (ChamberPressure is F4, WaferCounter U4; the interop trace showed <F8 2.5>
   on the wire). Conversion now targets the declared format — verified
   end-to-end: secsgem-py now receives <F4 2.5> in S6F11.

2. Thread safety: gRPC handler threads called resolve_variable/resolve_event,
   copying live store entries (including Item values) while the io thread
   mutates them. The service now snapshots the immutable name->id/format maps
   at construction (before run_async, per the documented ordering); all writes
   already post to the io thread. Remaining known narrow race (GetControlState
   enum read) documented in DAEMON_ROADMAP.

Also: drop a stale tools/run_interop.sh reference from docker-compose.yml.
Tests: daemon in-process 16/16 (new F4/U4 format assertions), core 459/459,
secsgem-py interop green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:35:53 +02:00

97 lines
3.2 KiB
C++

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
#include <grpcpp/grpcpp.h>
#include "equipment_service.hpp"
using namespace secsgem;
namespace gem = secsgem::gem;
namespace s2 = secsgem::secs2;
namespace pb = secsgem::v1;
namespace dmn = secsgem::daemon;
#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; // ephemeral; the engine isn't run() here, only poll()ed
return c;
}
// Exercises the real gRPC service over an in-process channel: client stub ->
// service -> EquipmentRuntime, proving the RPCs move data, not just compile.
TEST_CASE("Equipment gRPC service over an in-process channel") {
gem::EquipmentRuntime rt(test_config());
dmn::EquipmentService svc(rt);
grpc::ServerBuilder builder;
builder.RegisterService(&svc);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
REQUIRE(server);
auto stub = pb::Equipment::NewStub(server->InProcessChannel(grpc::ChannelArguments{}));
SUBCASE("GetControlState returns the initial control state") {
grpc::ClientContext ctx;
pb::Empty req;
pb::ControlState resp;
auto st = stub->GetControlState(&ctx, req, &resp);
CHECK(st.ok());
CHECK(resp.state() == pb::ControlState::HOST_OFFLINE);
}
SUBCASE("SetVariables converts to the variable's declared wire format") {
// ChamberPressure is declared F4 and WaferCounter U4 in equipment.yaml;
// the daemon must honour those, not write F8/I8, or the host sees values
// whose format contradicts the S1F11/S1F21 namelists.
grpc::ClientContext ctx;
pb::VariableUpdate req;
pb::Ack resp;
(*req.mutable_values())["ChamberPressure"].set_real(2.5);
(*req.mutable_values())["WaferCounter"].set_integer(7);
auto st = stub->SetVariables(&ctx, req, &resp);
CHECK(st.ok());
CHECK(resp.code() == pb::Ack::ACCEPT);
rt.poll(); // drain the posted set_variable onto this thread
CHECK(rt.model().dvids.value(101) == s2::Item::f4(2.5f));
CHECK(rt.model().dvids.value(100) == s2::Item::u4(uint32_t{7}));
}
SUBCASE("SetVariables rejects an unknown variable name") {
grpc::ClientContext ctx;
pb::VariableUpdate req;
pb::Ack resp;
(*req.mutable_values())["definitely_not_a_var"].set_real(1.0);
auto st = stub->SetVariables(&ctx, req, &resp);
CHECK(st.ok());
CHECK(resp.code() == pb::Ack::PARAMETER_INVALID);
}
SUBCASE("FireEvent accepts a known event and rejects an unknown one") {
{
grpc::ClientContext ctx;
pb::Event req;
pb::Ack resp;
req.set_name("ProcessStarted");
CHECK(stub->FireEvent(&ctx, req, &resp).ok());
CHECK(resp.code() == pb::Ack::ACCEPT);
}
{
grpc::ClientContext ctx;
pb::Event req;
pb::Ack resp;
req.set_name("NoSuchEvent");
CHECK(stub->FireEvent(&ctx, req, &resp).ok());
CHECK(resp.code() == pb::Ack::PARAMETER_INVALID);
}
}
server->Shutdown();
}