Files
secs-gem/interop/secs4j/Secs4jHostHarness.java
T
raphael db90a21e1d
tests / build-and-test (push) Successful in 2m2s
tests / thread-sanitizer (push) Successful in 2m28s
tests / tshark-dissector (push) Failing after 2m7s
tests / secs4j-interop (push) Failing after 0s
tests / libfuzzer (push) Successful in 3m9s
verify: expand secs4j harness 20 → 55 checks
Every check the user could ask for now lands.  secs4j's
comm.send(stream, function, w, body) takes arbitrary S/F + arbitrary
Secs2 body, so coverage was never coverage-limited by the Java side
— the original 20 was just the minimum to fill the gaps secsgem-py
couldn't reach.

Adds:

- Status data:    S1F3, S1F11
- EC management:  S2F13, S2F15 (set TimeFormat), S2F29
- Event reports:  S2F33, S2F35, S2F37 (full define-link-enable
                  sequence), S6F15, S6F19, S6F21
- Remote control: S2F41 (modern RCMD=START + observed S6F11),
                  S2F21 (legacy RCMD=STOP),
                  S2F41 RCMD=FAULT + observed S5F1
- Alarms:         S5F3, S5F5, S5F7
- Spool:          S2F43, S6F23
- PP management:  S7F1, S7F3, S7F5, S7F17, S7F19
- Terminal:       S10F3 (single), S10F5 (multi-line)
- E40 PJ:         S16F11 (full E40 body — MF + PRRECIPEMETHOD +
                  RecipeSpec + mtrloutspec + processparams),
                  S16F7 (monitor), S16F13 (dequeue)
- Limits:         S2F45, S2F47
- Trace:          S2F23 (5-field body)
- E39:            S14F1 (GetAttr)

Plus a SecsMessageReceiveListener that captures every equipment-
initiated primary into a ConcurrentLinkedQueue and replies to S5F1
(ACKC5=0), S6F11 (ACKC6=0), S16F9 (W=0 no reply) so the
equipment's T3 doesn't fire on our watch.  Two checks now assert
the unsolicited path:

  - After RCMD=START, an S6F11 with the linked report must arrive
    within 400ms
  - After RCMD=FAULT, an S5F1 with the alarm must arrive within
    400ms

Both observed against the demo equipment.

Result: 55/55 PASS.  Two independent implementations
(secsgem-py + secs4java8) now corroborate the wire surface in
overlapping but distinct slices.  Full E40 body — the one that
defeated secsgem-py's SFDL grammar — round-trips cleanly through
secs4j.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 16:30:49 +02:00

543 lines
25 KiB
Java

// Cross-validate our C++ secs_server against secs4java8 (Apache 2.0,
// kenta-shimizu). Independent SECS/HSMS implementation by a different
// author + language ecosystem = independent spec interpretation. If
// secs4java8 and our C++ codec both parse each other's bytes cleanly,
// spec-correctness has corroboration that secsgem-py alone can't give.
//
// Targets the wire surface the secsgem-py interop deliberately skipped:
// GEM 300 (S3 E87, S14 E94, S16 E40) with full-body messages, S2F49
// enhanced commands, S5F13 exception recovery, S2F23 trace init.
//
// Build (via Dockerfile in this directory):
// javac -cp /opt/secs4java8/Export.jar Secs4jHostHarness.java
// java -cp /opt/secs4java8/Export.jar:. Secs4jHostHarness <host> <port>
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.shimizukenta.secs.SecsMessage;
import com.shimizukenta.secs.hsms.HsmsConnectionMode;
import com.shimizukenta.secs.hsmsss.HsmsSsCommunicator;
import com.shimizukenta.secs.hsmsss.HsmsSsCommunicatorConfig;
import com.shimizukenta.secs.secs2.Secs2;
public class Secs4jHostHarness {
static final AtomicInteger pass = new AtomicInteger();
static final AtomicInteger fail = new AtomicInteger();
static final List<String> failures = new ArrayList<>();
static void check(String label, boolean ok, String detail) {
String tag = ok ? "[OK ]" : "[FAIL]";
System.out.println(tag + " " + label + (detail.isEmpty() ? "" : " — " + detail));
if (ok) pass.incrementAndGet();
else { fail.incrementAndGet(); failures.add(label + ": " + detail); }
}
// Run a request and check the reply's S/F matches the expected pair.
// We deliberately don't assert specific ACK values — secs4java8 and
// our codec must agree on which S/F pair the equipment replies with,
// and that the body parses; the specific ACK ("Carrier ID Unknown"
// vs "Accept") depends on equipment state, which we don't pre-load.
static void roundtrip(HsmsSsCommunicator comm, String label,
int stream, int function, boolean w, Secs2 body,
int expectedReplyStream, int expectedReplyFunction) {
try {
Optional<SecsMessage> reply = comm.send(stream, function, w, body);
if (!reply.isPresent()) {
check(label, false, "no reply (W=" + w + ")");
return;
}
int rs = reply.get().getStream();
int rf = reply.get().getFunction();
boolean ok = rs == expectedReplyStream && rf == expectedReplyFunction;
check(label, ok,
ok ? ("got S" + rs + "F" + rf)
: ("expected S" + expectedReplyStream + "F" + expectedReplyFunction
+ " got S" + rs + "F" + rf));
} catch (Exception e) {
check(label, false, "threw " + e.getClass().getSimpleName()
+ ": " + e.getMessage());
}
}
public static void main(String[] args) throws Exception {
String host = args.length > 0 ? args[0] : "127.0.0.1";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 5099;
HsmsSsCommunicatorConfig config = new HsmsSsCommunicatorConfig();
config.socketAddress(new InetSocketAddress(host, port));
config.connectionMode(HsmsConnectionMode.ACTIVE);
config.sessionId(0);
config.isEquip(false);
config.notLinktest();
config.timeout().t3(5.0F);
config.timeout().t5(2.0F);
config.timeout().t6(5.0F);
config.timeout().t8(5.0F);
config.gem().mdln("SECS4J");
config.gem().softrev("1.0");
System.out.println("connecting active to " + host + ":" + port);
try (HsmsSsCommunicator comm = HsmsSsCommunicator.newInstance(config)) {
CountDownLatch selected = new CountDownLatch(1);
comm.addSecsCommunicatableStateChangeListener(s -> {
if (s) selected.countDown();
});
// Capture every equipment-initiated primary into a queue
// so we can assert on unsolicited S6F11 / S5F1 / S16F9
// emissions after triggering events.
ConcurrentLinkedQueue<int[]> observed = new ConcurrentLinkedQueue<>();
comm.addSecsMessageReceiveListener(msg -> {
observed.add(new int[]{msg.getStream(), msg.getFunction()});
// Reply to known equipment-initiated primaries so the
// equipment's T3 doesn't fire on our watch.
try {
int s = msg.getStream(), f = msg.getFunction();
if (s == 5 && f == 1) {
comm.send(msg, 5, 2, false, Secs2.binary((byte)0x00));
} else if (s == 6 && f == 11) {
comm.send(msg, 6, 12, false, Secs2.binary((byte)0x00));
} else if (s == 16 && f == 9) {
// W=0 PJ alert — no reply expected
}
} catch (Exception ignore) {}
});
comm.open();
if (!selected.await(15, TimeUnit.SECONDS)) {
System.err.println("FAIL: never reached SELECTED");
System.exit(2);
}
System.out.println("HSMS SELECTED.");
// -------- Basic round-trips (overlap with secsgem-py) --------
// S1F1 → S1F2 (Are You There)
roundtrip(comm, "S1F1 → S1F2 (Are You There)",
1, 1, true, Secs2.empty(), 1, 2);
// S1F13 → S1F14 (Establish Communications)
// body: <L,2 <A MDLN> <A SOFTREV>>
roundtrip(comm, "S1F13 → S1F14 (Establish Comms)",
1, 13, true,
Secs2.list(Secs2.ascii("SECS4J"), Secs2.ascii("1.0")),
1, 14);
// S1F17 → S1F18 (Request Online)
roundtrip(comm, "S1F17 → S1F18 (Online)",
1, 17, true, Secs2.empty(), 1, 18);
// S1F19 → S1F20 (GEM Compliance) — empty body, list reply
roundtrip(comm, "S1F19 → S1F20 (GEM Compliance)",
1, 19, true, Secs2.empty(), 1, 20);
// S1F21 → S1F22 (DVID namelist) — empty list = all DVIDs
roundtrip(comm, "S1F21 → S1F22 (DVID namelist)",
1, 21, true, Secs2.list(), 1, 22);
// S1F23 → S1F24 (CEID namelist) — empty list = all CEIDs
roundtrip(comm, "S1F23 → S1F24 (CEID namelist)",
1, 23, true, Secs2.list(), 1, 24);
// S2F17 → S2F18 (Clock) — empty body
roundtrip(comm, "S2F17 → S2F18 (Clock)",
2, 17, true, Secs2.empty(), 2, 18);
// -------- Status data (E30 §6.13) -----------------------------
// S1F3 → S1F4 (SVID values) — empty list = all SVIDs
roundtrip(comm, "S1F3 → S1F4 (SVID values, all)",
1, 3, true, Secs2.list(), 1, 4);
// S1F11 → S1F12 (SVID namelist) — empty list = all SVIDs
roundtrip(comm, "S1F11 → S1F12 (SVID namelist, all)",
1, 11, true, Secs2.list(), 1, 12);
// -------- Equipment constants (E30 §6.16) ---------------------
// S2F13 → S2F14 (EC values) — empty list = all ECs
roundtrip(comm, "S2F13 → S2F14 (EC values, all)",
2, 13, true, Secs2.list(), 2, 14);
// S2F29 → S2F30 (EC namelist) — empty list = all ECs
roundtrip(comm, "S2F29 → S2F30 (EC namelist, all)",
2, 29, true, Secs2.list(), 2, 30);
// S2F15 → S2F16 (set EC). ECID 10 = TimeFormat (U4), valid
// range 0..1 per equipment.yaml. Set to 1 — round-trip
// proves the (ecid, value) tuple body parses.
// body: <L,n <L,2 <U4 ECID> <ITEM VALUE>>>
roundtrip(comm, "S2F15 → S2F16 (set EC TimeFormat=1)",
2, 15, true,
Secs2.list(
Secs2.list(Secs2.uint4(10L), Secs2.uint4(1L))),
2, 16);
// -------- Dynamic event reports (E30 §6.6) --------------------
// Define RPTID 9001 covering SVIDs {1, 2}, link CEID 300
// (ProcessStarted) → RPTID 9001, enable CEID 300, then
// query the bound report back via S6F15/F19/F21.
// S2F33 → S2F34 (define report)
roundtrip(comm, "S2F33 → S2F34 (define RPTID 9001 → SVID {1,2})",
2, 33, true,
Secs2.list(
Secs2.uint4(9000L),
Secs2.list(
Secs2.list(
Secs2.uint4(9001L),
Secs2.list(Secs2.uint4(1L), Secs2.uint4(2L))))),
2, 34);
// S2F35 → S2F36 (link event report)
roundtrip(comm, "S2F35 → S2F36 (link CEID 300 → RPTID 9001)",
2, 35, true,
Secs2.list(
Secs2.uint4(9000L),
Secs2.list(
Secs2.list(
Secs2.uint4(300L),
Secs2.list(Secs2.uint4(9001L))))),
2, 36);
// S2F37 → S2F38 (enable event)
roundtrip(comm, "S2F37 → S2F38 (enable CEID 300)",
2, 37, true,
Secs2.list(Secs2.bool(true), Secs2.list(Secs2.uint4(300L))),
2, 38);
// S6F15 → S6F16 (event report request)
roundtrip(comm, "S6F15 → S6F16 (event report request CEID=300)",
6, 15, true, Secs2.uint4(300L), 6, 16);
// S6F19 → S6F20 (individual report request)
roundtrip(comm, "S6F19 → S6F20 (individual report RPTID=9001)",
6, 19, true, Secs2.uint4(9001L), 6, 20);
// S6F21 → S6F22 (annotated individual report)
roundtrip(comm, "S6F21 → S6F22 (annotated report RPTID=9001)",
6, 21, true, Secs2.uint4(9001L), 6, 22);
// -------- Remote control (E30 §6.15) --------------------------
// S2F41 → S2F42 (modern host command), DATAID-less form:
// body: <L,2 <A RCMD> <L,n <L,2 <A name> <ITEM value>>>>
// The equipment.yaml binds START → emit_ceid=300, so this
// also triggers an unsolicited S6F11 we observe below.
roundtrip(comm, "S2F41 → S2F42 (RCMD=START)",
2, 41, true,
Secs2.list(Secs2.ascii("START"), Secs2.list()),
2, 42);
// Give the equipment 400ms to emit the linked S6F11 to us.
Thread.sleep(400);
boolean sawS6F11 = observed.stream()
.anyMatch(p -> p[0] == 6 && p[1] == 11);
check("S6F11 received after RCMD=START (unsolicited)",
sawS6F11,
sawS6F11 ? "observed" : "no S6F11 in 400ms");
// S2F21 → S2F22 (legacy remote command, no params)
roundtrip(comm, "S2F21 → S2F22 (legacy RCMD=STOP)",
2, 21, true, Secs2.ascii("STOP"), 2, 22);
// -------- Alarm management (E30 §6.14) ------------------------
// S5F5 → S5F6 (list alarms) — empty list = all
roundtrip(comm, "S5F5 → S5F6 (list alarms, all)",
5, 5, true, Secs2.list(), 5, 6);
// S5F7 → S5F8 (list enabled)
roundtrip(comm, "S5F7 → S5F8 (list enabled alarms)",
5, 7, true, Secs2.empty(), 5, 8);
// S5F3 → S5F4 (enable alarm 1). ALED bit-7 set = enable.
// body: <L,2 <BINARY ALED> <U4 ALID>>
roundtrip(comm, "S5F3 → S5F4 (enable ALID=1)",
5, 3, true,
Secs2.list(Secs2.binary((byte)0x80), Secs2.uint4(1L)),
5, 4);
// Trigger FAULT host command — equipment.yaml binds it to
// alarm 1 → emits S5F1.
roundtrip(comm, "S2F41 → S2F42 (RCMD=FAULT)",
2, 41, true,
Secs2.list(Secs2.ascii("FAULT"), Secs2.list()),
2, 42);
Thread.sleep(400);
boolean sawS5F1 = observed.stream()
.anyMatch(p -> p[0] == 5 && p[1] == 1);
check("S5F1 received after RCMD=FAULT (unsolicited)",
sawS5F1,
sawS5F1 ? "observed" : "no S5F1 in 400ms");
// -------- Spool (E30 §6.22) -----------------------------------
// S2F43 → S2F44 (reset spoolable streams)
// body: <L,n <U1 stream>>
roundtrip(comm, "S2F43 → S2F44 (reset spool to {5, 6})",
2, 43, true,
Secs2.list(Secs2.uint1(5), Secs2.uint1(6)),
2, 44);
// S6F23 → S6F24 (request spool data — Transmit=0)
// body: <BINARY RSDC>
roundtrip(comm, "S6F23 → S6F24 (request spool transmit)",
6, 23, true, Secs2.binary((byte)0x00), 6, 24);
// -------- Process program management (E30 §6.17) --------------
// S7F19 → S7F20 (current PPID list)
roundtrip(comm, "S7F19 → S7F20 (current PPIDs)",
7, 19, true, Secs2.empty(), 7, 20);
// S7F5 → S7F6 (request PP RECIPE-A)
roundtrip(comm, "S7F5 → S7F6 (request RECIPE-A)",
7, 5, true, Secs2.ascii("RECIPE-A"), 7, 6);
// S7F1 → S7F2 (PP load inquire)
// body: <L,2 <A PPID> <U4 LENGTH>>
roundtrip(comm, "S7F1 → S7F2 (PP load inquire RECIPE-NEW)",
7, 1, true,
Secs2.list(Secs2.ascii("RECIPE-NEW"), Secs2.uint4(128L)),
7, 2);
// S7F3 → S7F4 (PP send)
// body: <L,2 <A PPID> <BINARY|ASCII PPBODY>>
roundtrip(comm, "S7F3 → S7F4 (send PP RECIPE-J)",
7, 3, true,
Secs2.list(Secs2.ascii("RECIPE-J"),
Secs2.binary("from-java".getBytes())),
7, 4);
// S7F17 → S7F18 (delete PP)
// body: <L,n <A PPID>>
roundtrip(comm, "S7F17 → S7F18 (delete RECIPE-J)",
7, 17, true, Secs2.list(Secs2.ascii("RECIPE-J")),
7, 18);
// -------- Limits monitoring (E30 §6.21) -----------------------
// S2F47 → S2F48 (variable limit attributes) — empty list = all
roundtrip(comm, "S2F47 → S2F48 (variable limit attributes)",
2, 47, true, Secs2.list(), 2, 48);
// S2F45 → S2F46 (define variable limits). Body:
// <L,n <L,2 <U4 VID> <L,n <L,3 <U4 LIMITID> <ITEM upper> <ITEM lower>>>>>
// Define an empty limits set on VID 1 — proves the body parses.
roundtrip(comm, "S2F45 → S2F46 (define variable limits, empty set)",
2, 45, true,
Secs2.list(
Secs2.list(Secs2.uint4(1L), Secs2.list())),
2, 46);
// -------- Trace (E30 §6.12) -----------------------------------
// body: <L,5 <U4 TRID> <A DSPER> <U4 TOTSMP> <U4 REPGSZ>
// <L,n <U4 vid>>>
roundtrip(comm, "S2F23 → S2F24 (trace init, 5-field body)",
2, 23, true,
Secs2.list(
Secs2.uint4(8002L),
Secs2.ascii("000002"),
Secs2.uint4(0L),
Secs2.uint4(2L),
Secs2.list(Secs2.uint4(1L), Secs2.uint4(2L))),
2, 24);
// -------- Terminal services (E30 §6.19) -----------------------
// S10F3 → S10F4 (terminal display single, host→equipment)
// body: <L,2 <BINARY TID> <A TEXT>>
roundtrip(comm, "S10F3 → S10F4 (terminal display single)",
10, 3, true,
Secs2.list(Secs2.binary((byte)0x00),
Secs2.ascii("hello from secs4j")),
10, 4);
// S10F5 → S10F6 (terminal display multi-line)
// body: <L,2 <BINARY TID> <L,n <A line>>>
roundtrip(comm, "S10F5 → S10F6 (terminal multi-line)",
10, 5, true,
Secs2.list(
Secs2.binary((byte)0x00),
Secs2.list(Secs2.ascii("line A"), Secs2.ascii("line B"))),
10, 6);
// -------- E40 process jobs with full body --------------------
// S16F11 → S16F12 (PRJobCreate, full E40 body). This is
// the body that defeated secsgem-py's SFDL grammar.
// body: <L,5 <A PRJOBID>
// <BINARY MF>
// <BINARY PRRECIPEMETHOD>
// <L,2 <A PPID> <L,n <L,2 <A RCPPARNM> <ITEM RCPPARVAL>>>>
// <L,n <A MID>>
// <L,n <L,2 <A PARAMNAME> <ITEM PARAMVAL>>>>
roundtrip(comm, "S16F11 → S16F12 (E40 PJ create, full body)",
16, 11, true,
Secs2.list(
Secs2.ascii("PJ-SECS4J-2"),
Secs2.binary((byte)0x01), // MF = Substrate
Secs2.binary((byte)0x02), // RM = RecipeOnly
Secs2.list(
Secs2.ascii("RECIPE-A"),
Secs2.list()), // no rcpvars
Secs2.list(Secs2.ascii("WFR-J-1")),
Secs2.list()), // no params
16, 12);
// S16F7 → S16F8 (PRJobMonitor)
// body: <L,n <L,2 <A PRJOBID> <BINARY PRALERT>>>
roundtrip(comm, "S16F7 → S16F8 (PJ monitor)",
16, 7, true,
Secs2.list(
Secs2.list(Secs2.ascii("PJ-SECS4J-2"),
Secs2.binary((byte)0x80))),
16, 8);
// S16F13 → S16F14 (PRJobDequeue)
// body: <A PRJOBID>
roundtrip(comm, "S16F13 → S16F14 (PJ dequeue)",
16, 13, true, Secs2.ascii("PJ-SECS4J-2"), 16, 14);
// -------- E39 generic object service --------------------------
// S14F1 → S14F2 (GetAttr) — Denied_UnknownObject is a
// well-formed F-coded reply for an unknown OBJSPEC.
// body: <L,3 <A OBJSPEC> <A OBJTYPE> <L,n <A ATTRID>>>
roundtrip(comm, "S14F1 → S14F2 (E39 GetAttr unknown object)",
14, 1, true,
Secs2.list(
Secs2.ascii("OBJ-NONEXISTENT"),
Secs2.ascii("EQUIPMENT"),
Secs2.list()),
14, 2);
// -------- GEM 300 streams: the gap secsgem-py couldn't fill ----
// S3F17 → S3F18 (E87 carrier action):
// body: <L,4 <U4 DATAID> <A CARRIERACTION> <A CARRIERID> <L PARAMS>>
roundtrip(comm, "S3F17 → S3F18 (E87 carrier action, full body)",
3, 17, true,
Secs2.list(
Secs2.uint4(1L),
Secs2.ascii("ProceedWithCarrier"),
Secs2.ascii("CAR-SECS4J-1"),
Secs2.list()),
3, 18);
// S3F19 → S3F20 (Slot map verify):
// body: <L,2 <A CARRIERID> <L,n <B slot.state>>>
roundtrip(comm, "S3F19 → S3F20 (slot map verify)",
3, 19, true,
Secs2.list(
Secs2.ascii("CAR-SECS4J-1"),
Secs2.list()),
3, 20);
// S3F25 → S3F26 (Carrier transfer):
// body: <L,3 <A CARRIERID> <U1 source_port> <U1 target_port>>
roundtrip(comm, "S3F25 → S3F26 (carrier transfer)",
3, 25, true,
Secs2.list(
Secs2.ascii("CAR-SECS4J-1"),
Secs2.uint1(1),
Secs2.uint1(2)),
3, 26);
// S3F27 → S3F28 (Cancel carrier): body: <A CARRIERID>
roundtrip(comm, "S3F27 → S3F28 (cancel carrier)",
3, 27, true, Secs2.ascii("CAR-SECS4J-1"), 3, 28);
// S14F9 → S14F10 (E94 ControlJob create):
// body: <L,2 <A CTLJOBID> <L,n <A PRJOBID>>>
roundtrip(comm, "S14F9 → S14F10 (E94 CJ create, full body)",
14, 9, true,
Secs2.list(
Secs2.ascii("CJ-SECS4J-1"),
Secs2.list(Secs2.ascii("PJ-SECS4J-1"))),
14, 10);
// S16F5 → S16F6 (E40 PRJobCommand):
// body: <L,2 <A PRJOBID> <A PRCMD>>
roundtrip(comm, "S16F5 → S16F6 (E40 PJ command)",
16, 5, true,
Secs2.list(
Secs2.ascii("PJ-NONEXISTENT"),
Secs2.ascii("PJABORT")),
16, 6);
// S16F27 → S16F28 (E94 CJobCommand):
// body: <L,2 <A CTLJOBID> <A CTLJOBCMD>>
roundtrip(comm, "S16F27 → S16F28 (E94 CJ command)",
16, 27, true,
Secs2.list(
Secs2.ascii("CJ-SECS4J-1"),
Secs2.ascii("CJSTOP")),
16, 28);
// S14F11 → S14F12 (E94 DeleteControlJob): body: <A CTLJOBID>
roundtrip(comm, "S14F11 → S14F12 (E94 CJ delete)",
14, 11, true, Secs2.ascii("CJ-SECS4J-1"), 14, 12);
// -------- Other streams secsgem-py interop skipped --------
// S2F49 → S2F50 (Enhanced remote command):
// body: <L,4 <U4 DATAID> <A OBJSPEC> <A RCMD> <L PARAMS>>
roundtrip(comm, "S2F49 → S2F50 (enhanced remote command)",
2, 49, true,
Secs2.list(
Secs2.uint4(1L),
Secs2.ascii("EQUIPMENT"),
Secs2.ascii("STOP"),
Secs2.list()),
2, 50);
// S2F23 → S2F24 (Trace init):
// body: <L,5 <U4 TRID> <A DSPER> <U4 TOTSMP> <U4 REPGSZ> <L svids>>
roundtrip(comm, "S2F23 → S2F24 (trace init)",
2, 23, true,
Secs2.list(
Secs2.uint4(8001L),
Secs2.ascii("000001"),
Secs2.uint4(0L),
Secs2.uint4(1L),
Secs2.list(Secs2.uint4(1L))),
2, 24);
// S5F13 → S5F14 (Exception recover):
// body: <L,2 <U4 EXID> <A EXRECVRA>>
roundtrip(comm, "S5F13 → S5F14 (exception recover)",
5, 13, true,
Secs2.list(Secs2.uint4(999L), Secs2.ascii("NOP")),
5, 14);
// S5F17 → S5F18 (Exception recover abort): body: <U4 EXID>
roundtrip(comm, "S5F17 → S5F18 (exception recover abort)",
5, 17, true, Secs2.uint4(999L), 5, 18);
// S1F15 → S1F16 (Request Offline) — cleanup
roundtrip(comm, "S1F15 → S1F16 (offline)",
1, 15, true, Secs2.empty(), 1, 16);
System.out.println();
System.out.println("=== secs4j interop summary ===");
System.out.println(" passed: " + pass.get());
System.out.println(" failed: " + fail.get());
if (!failures.isEmpty()) {
System.out.println();
System.out.println("FAILURES:");
for (String s : failures) System.out.println(" - " + s);
}
}
System.exit(fail.get() == 0 ? 0 : 1);
}
}