chore(phase0): name validation, golden frames, daemon into library tree, TSan daemon lane
Item 8a — ConfigValidator warns on non-identifier variable/event/alarm/ command names ([A-Za-z_][A-Za-z0-9_]*): language bindings expose names as kwargs/attributes, so 'Chamber Pressure' would be unusable in the planned Python client. Warning not error — the wire doesn't care. Tested (4 warning sites + good-name negative). Item 4 tail — golden frames for S5F1 (Binary ALCD / U4 ALID / ASCII ALTX) and a composed S6F11 (the production-critical report shape), bytes hand- computed from E5 encoding rules: external pins on message composition. Item 7 — equipment_service.hpp moved to include/secsgem/daemon/ (apps/ include-path hack removed) and a TSan daemon lane added locally + in CI. tools/tsan.supp suppresses races whose accesses sit entirely inside the UNinstrumented system libgrpc/libgpr/libabsl (epoll wakeups, absl Mutex GraphCycles bookkeeping); our frames stay fully checked. The lane earned its keep on first run: it caught a REAL threading-contract violation — a daemon test reading model stores from the test thread while the io thread serviced posted writes — fixed to use read_sync, exactly per the documented contract. Now TSan-clean under halt_on_error=1 in the full production threading shape. Suites: core 470/3068, daemon Release+TSan 125/125 each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -202,3 +202,32 @@ TEST_CASE("Validate: ships data/equipment.yaml without errors") {
|
||||
}
|
||||
CHECK(v.error_count() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Validate: non-identifier names warn (bindings expose names as kwargs)") {
|
||||
auto p = scratch_path("idname", "yaml");
|
||||
write(p, R"YAML(
|
||||
device:
|
||||
id: 0
|
||||
model_name: "TEST"
|
||||
software_rev: "1.0"
|
||||
svids:
|
||||
- {id: 1, name: "Chamber Pressure", type: F4, value: 0.0}
|
||||
- {id: 2, name: GoodName, type: U4, value: 0}
|
||||
ceids:
|
||||
- {id: 10, name: "wafer-complete"}
|
||||
alarms:
|
||||
- {id: 1, name: "2bad", text: "T", category: 1}
|
||||
host_commands:
|
||||
- {name: "DO IT", ack: Accept}
|
||||
)YAML");
|
||||
ConfigValidator v;
|
||||
v.validate_equipment(p.string());
|
||||
CHECK(v.error_count() == 0); // identifier shape is a WARNING, not an error
|
||||
CHECK(v.warning_count() == 4); // space, hyphen, leading digit, space
|
||||
CHECK(any_match(v.issues(), "svids[0].name"));
|
||||
CHECK(any_match(v.issues(), "ceids[0].name"));
|
||||
CHECK(any_match(v.issues(), "alarms[0].name"));
|
||||
CHECK(any_match(v.issues(), "host_commands[0].name"));
|
||||
CHECK_FALSE(any_match(v.issues(), "svids[1].name"));
|
||||
fs::remove_all(p.parent_path());
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "equipment_service.hpp"
|
||||
#include "secsgem/daemon/equipment_service.hpp"
|
||||
#include "secsgem/gem/default_handlers.hpp"
|
||||
#include "secsgem/gem/messages.hpp"
|
||||
|
||||
@@ -220,8 +220,14 @@ TEST_CASE("GetVariables round-trip under run_async (production threading mode)")
|
||||
pb::VariableQuery req;
|
||||
pb::VariableSnapshot resp;
|
||||
REQUIRE(stub->GetVariables(&ctx, req, &resp).ok());
|
||||
const auto expected = rt.model().svids.size() + rt.model().dvids.all().size();
|
||||
CHECK(resp.values().size() == expected);
|
||||
// The io thread is live here — model reads must go through read_sync
|
||||
// (the first violation of that contract was caught by the TSan lane in
|
||||
// exactly this line).
|
||||
auto expected = rt.read_sync([&rt] {
|
||||
return rt.model().svids.size() + rt.model().dvids.all().size();
|
||||
});
|
||||
REQUIRE(expected.has_value());
|
||||
CHECK(resp.values().size() == *expected);
|
||||
}
|
||||
|
||||
SUBCASE("unknown name is INVALID_ARGUMENT, naming the offender") {
|
||||
|
||||
@@ -172,8 +172,6 @@ TEST_CASE("conformance sweep: every default handler answers with the paired repl
|
||||
// L[2] -> 0x01 0x02
|
||||
// A "HOST" -> 0x41 0x04 'H' 'O' 'S' 'T'
|
||||
// A "1.0" -> 0x41 0x03 '1' '.' '0'
|
||||
// TODO(tests): extend with golden frames for S5F1 and a composed S6F11 once
|
||||
// their ALID/CEID item formats are pinned down from the catalog.
|
||||
TEST_CASE("golden frame: S1F13 body encodes to the hand-computed E5 bytes") {
|
||||
auto msg = gem::s1f13_establish_comms("HOST", "1.0");
|
||||
REQUIRE(msg.body.has_value());
|
||||
@@ -187,3 +185,40 @@ TEST_CASE("golden frame: S1F13 body encodes to the hand-computed E5 bytes") {
|
||||
CHECK(msg.function == 13);
|
||||
CHECK(msg.reply_expected);
|
||||
}
|
||||
|
||||
// S5F1 = L[3]{ Binary ALCD, U4 ALID, ASCII ALTX }. Format bytes per E5:
|
||||
// Binary(0o10=8)->0x21, U4(0o54=44)->0xB1, ASCII(0o20=16)->0x41 (1 len byte).
|
||||
TEST_CASE("golden frame: S5F1 alarm report encodes to the hand-computed E5 bytes") {
|
||||
auto msg = gem::s5f1_alarm_report(0x84, 1, "Chiller Temp High");
|
||||
REQUIRE(msg.body.has_value());
|
||||
std::vector<uint8_t> expected = {
|
||||
0x01, 0x03, // L/3
|
||||
0x21, 0x01, 0x84, // B ALCD = set|category4
|
||||
0xB1, 0x04, 0x00, 0x00, 0x00, 0x01, // U4 ALID = 1
|
||||
0x41, 0x11, // A/17
|
||||
};
|
||||
for (char c : std::string("Chiller Temp High")) expected.push_back(c);
|
||||
CHECK(s2::encode(*msg.body) == expected);
|
||||
CHECK(msg.stream == 5);
|
||||
CHECK(msg.function == 1);
|
||||
}
|
||||
|
||||
// Composed S6F11 = L[3]{ U4 DATAID, U4 CEID, L reports[ L[2]{U4 RPTID,
|
||||
// L values} ] } — the production-critical event-report shape, pinned for one
|
||||
// report (RPTID 1) carrying one ASCII value.
|
||||
TEST_CASE("golden frame: composed S6F11 encodes to the hand-computed E5 bytes") {
|
||||
std::vector<gem::ReportData> reports{{1, {s2::Item::ascii("x")}}};
|
||||
auto msg = gem::s6f11_event_report(0, 300, reports);
|
||||
REQUIRE(msg.body.has_value());
|
||||
const std::vector<uint8_t> expected = {
|
||||
0x01, 0x03, // L/3
|
||||
0xB1, 0x04, 0x00, 0x00, 0x00, 0x00, // U4 DATAID = 0
|
||||
0xB1, 0x04, 0x00, 0x00, 0x01, 0x2C, // U4 CEID = 300
|
||||
0x01, 0x01, // L/1 reports
|
||||
0x01, 0x02, // L/2 report
|
||||
0xB1, 0x04, 0x00, 0x00, 0x00, 0x01, // U4 RPTID = 1
|
||||
0x01, 0x01, // L/1 values
|
||||
0x41, 0x01, 'x', // A "x"
|
||||
};
|
||||
CHECK(s2::encode(*msg.body) == expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user