fix: resolve Client.Java + Worker.Tests findings (pending windev verification)
Client.Java-040..048, Worker.Tests-034/035/036. Edits applied on the Mac, which has no JRE and cannot build the x86+MXAccess worker tests; findings are marked In Progress pending gradle + x86 build verification on windev. Do not mark Resolved until verified there.
This commit is contained in:
+87
-31
@@ -39,7 +39,10 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
|
||||||
@@ -105,8 +108,14 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test-friendly entry point that runs the CLI against the supplied
|
* Entry point that runs the CLI against the supplied {@link PrintWriter}
|
||||||
* {@link PrintWriter} pair instead of the system streams.
|
* pair instead of the system streams. This overload wires the production
|
||||||
|
* {@link GrpcMxGatewayCliClientFactory} (a real gRPC channel), so it is
|
||||||
|
* suitable for embedding the CLI but not for unit tests that need to stub
|
||||||
|
* the gateway. Tests should use the package-private
|
||||||
|
* {@link #execute(MxGatewayCliClientFactory, PrintWriter, PrintWriter, String...)}
|
||||||
|
* / {@link #commandLine(MxGatewayCliClientFactory)} overloads, which accept
|
||||||
|
* an injectable client factory.
|
||||||
*
|
*
|
||||||
* @param out writer that receives standard output
|
* @param out writer that receives standard output
|
||||||
* @param err writer that receives standard error
|
* @param err writer that receives standard error
|
||||||
@@ -1536,50 +1545,74 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
StreamAlarmsRequest request = StreamAlarmsRequest.newBuilder()
|
StreamAlarmsRequest request = StreamAlarmsRequest.newBuilder()
|
||||||
.setAlarmFilterPrefix(filterPrefix)
|
.setAlarmFilterPrefix(filterPrefix)
|
||||||
.build();
|
.build();
|
||||||
// Client.Java-033 — fail-fast on overflow. A bare
|
// Client.Java-033/040/042 — fail-fast on overflow and on
|
||||||
// queue.offer(value) silently drops messages past capacity,
|
// transport errors. A bare queue.offer(value) silently drops
|
||||||
// which violates the JavaStyleGuide "do not drop events"
|
// messages past capacity (violating the JavaStyleGuide "do not
|
||||||
// contract and lets the CLI exit 0 on a truncated feed.
|
// drop events" contract and letting the CLI exit 0 on a
|
||||||
// Mirrors MxEventStream's overflow branch: detect a failed
|
// truncated feed), and a bare queue.offer(error) on a full
|
||||||
// offer, cancel the subscription, drain the buffer, then
|
// queue would drop the terminal item and deadlock the drain on
|
||||||
// queue an explicit overflow exception followed by the END
|
// queue.take().
|
||||||
// sentinel so the drain loop surfaces a non-zero exit.
|
//
|
||||||
|
// Terminal transitions (overflow, transport error, clean
|
||||||
|
// completion) are now serialised through a single AtomicBoolean
|
||||||
|
// guard plus a dedicated `terminal` slot rather than
|
||||||
|
// re-clearing the shared queue. The first terminal condition
|
||||||
|
// wins; a concurrent onNext on the gRPC I/O thread can no
|
||||||
|
// longer displace it (Client.Java-040). The drain reads the
|
||||||
|
// terminal slot independently of the bounded queue, so a full
|
||||||
|
// queue can never strand the terminal item (Client.Java-042).
|
||||||
AtomicReference<MxGatewayAlarmFeedSubscription> subscriptionRef = new AtomicReference<>();
|
AtomicReference<MxGatewayAlarmFeedSubscription> subscriptionRef = new AtomicReference<>();
|
||||||
|
AtomicBoolean terminated = new AtomicBoolean();
|
||||||
|
AtomicReference<Object> terminal = new AtomicReference<>();
|
||||||
|
Consumer<Object> terminate = item -> {
|
||||||
|
if (terminated.compareAndSet(false, true)) {
|
||||||
|
terminal.set(item);
|
||||||
|
MxGatewayAlarmFeedSubscription sub = subscriptionRef.get();
|
||||||
|
if (sub != null) {
|
||||||
|
sub.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
MxGatewayAlarmFeedSubscription subscription =
|
MxGatewayAlarmFeedSubscription subscription =
|
||||||
client.streamAlarms(request, new StreamObserver<>() {
|
client.streamAlarms(request, new StreamObserver<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onNext(AlarmFeedMessage value) {
|
public void onNext(AlarmFeedMessage value) {
|
||||||
|
if (terminated.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!queue.offer(value)) {
|
if (!queue.offer(value)) {
|
||||||
MxGatewayAlarmFeedSubscription sub = subscriptionRef.get();
|
terminate.accept(new IllegalStateException(
|
||||||
if (sub != null) {
|
|
||||||
sub.cancel();
|
|
||||||
}
|
|
||||||
queue.clear();
|
|
||||||
queue.offer(new IllegalStateException(
|
|
||||||
"stream-alarms queue overflowed (capacity 1024); consumer too slow"));
|
"stream-alarms queue overflowed (capacity 1024); consumer too slow"));
|
||||||
queue.offer(ALARM_FEED_END);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
public void onError(Throwable error) {
|
||||||
queue.offer(error);
|
terminate.accept(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCompleted() {
|
public void onCompleted() {
|
||||||
queue.offer(ALARM_FEED_END);
|
terminate.accept(ALARM_FEED_END);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
subscriptionRef.set(subscription);
|
subscriptionRef.set(subscription);
|
||||||
try {
|
try {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
Object item = queue.take();
|
// Poll with a short timeout so the dedicated terminal
|
||||||
if (item == ALARM_FEED_END) {
|
// slot is observed even when the bounded queue is full
|
||||||
break;
|
// of normal messages the consumer has not yet drained.
|
||||||
}
|
Object item = queue.poll(50, TimeUnit.MILLISECONDS);
|
||||||
if (item instanceof Throwable error) {
|
if (item == null) {
|
||||||
|
Object end = terminal.get();
|
||||||
|
if (end == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (end == ALARM_FEED_END) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Throwable error = (Throwable) end;
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"gateway stream alarms failed: " + error.getMessage(), error);
|
"gateway stream alarms failed: " + error.getMessage(), error);
|
||||||
}
|
}
|
||||||
@@ -2184,13 +2217,36 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
return jsonString(value.toString());
|
return jsonString(value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String jsonString(String value) {
|
// Package-private for the Client.Java-041 escaping regression test.
|
||||||
return '"'
|
static String jsonString(String value) {
|
||||||
+ value.replace("\\", "\\\\")
|
// RFC 8259 requires the two-character escapes for the named control
|
||||||
.replace("\"", "\\\"")
|
// characters and \u00XX escapes for the remaining U+0000–U+001F (and
|
||||||
.replace("\r", "\\r")
|
// U+007F) range. The old implementation escaped only \\ \" \r \n, so a
|
||||||
.replace("\n", "\\n")
|
// value containing a tab, backspace, form-feed, or any other control
|
||||||
+ '"';
|
// character produced malformed JSON (Client.Java-041).
|
||||||
|
StringBuilder builder = new StringBuilder(value.length() + 2);
|
||||||
|
builder.append('"');
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '\\' -> builder.append("\\\\");
|
||||||
|
case '"' -> builder.append("\\\"");
|
||||||
|
case '\r' -> builder.append("\\r");
|
||||||
|
case '\n' -> builder.append("\\n");
|
||||||
|
case '\t' -> builder.append("\\t");
|
||||||
|
case '\b' -> builder.append("\\b");
|
||||||
|
case '\f' -> builder.append("\\f");
|
||||||
|
default -> {
|
||||||
|
if (c < 0x20 || c == 0x7f) {
|
||||||
|
builder.append(String.format("\\u%04x", (int) c));
|
||||||
|
} else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.append('"');
|
||||||
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RawJson(String value) {
|
private record RawJson(String value) {
|
||||||
|
|||||||
+16
@@ -46,6 +46,22 @@ import mxaccess_gateway.v1.MxaccessGateway.SessionState;
|
|||||||
* instance uses a unique server name so harnesses do not collide. The
|
* instance uses a unique server name so harnesses do not collide. The
|
||||||
* {@code directExecutor()} wiring keeps all dispatch on the calling thread, so
|
* {@code directExecutor()} wiring keeps all dispatch on the calling thread, so
|
||||||
* no background threads are leaked.
|
* no background threads are leaked.
|
||||||
|
*
|
||||||
|
* <p><strong>Implemented RPCs.</strong> The scripted services override only the
|
||||||
|
* RPCs the CLI tests currently exercise:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code MxAccessGateway}: {@code streamEvents}, {@code closeSession}.</li>
|
||||||
|
* <li>{@code GalaxyRepository}: {@code discoverHierarchy},
|
||||||
|
* {@code watchDeployEvents}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* Every other RPC (e.g. {@code openSession}, {@code invoke}, {@code register},
|
||||||
|
* {@code streamAlarms}, {@code queryActiveAlarms}, {@code browseChildren}) is
|
||||||
|
* left at the generated {@code *ImplBase} default and therefore returns gRPC
|
||||||
|
* {@code UNIMPLEMENTED} by design. A future test that needs one of those paths
|
||||||
|
* must add the corresponding scripted override here first — otherwise the call
|
||||||
|
* fails with {@code UNIMPLEMENTED} rather than the behaviour under test.
|
||||||
*/
|
*/
|
||||||
final class InProcessGatewayHarness implements AutoCloseable {
|
final class InProcessGatewayHarness implements AutoCloseable {
|
||||||
private final String serverName;
|
private final String serverName;
|
||||||
|
|||||||
+84
-40
@@ -56,17 +56,37 @@ final class MxGatewayCliTests {
|
|||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertEquals("", run.errors());
|
assertEquals("", run.errors());
|
||||||
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
assertTrue(run.output().contains("mxgateway-java 0.1.1"));
|
||||||
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
|
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
|
||||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jsonStringEscapesControlCharacters() {
|
||||||
|
// Client.Java-041 — the hand-rolled jsonString escaped only \\ \" \r \n,
|
||||||
|
// so a tab/backspace/form-feed or any other control char produced
|
||||||
|
// malformed JSON (RFC 8259). After the fix the named control chars use
|
||||||
|
// their two-character escapes and the rest use \u00XX.
|
||||||
|
assertEquals("\"a\\tb\"", MxGatewayCli.jsonString("a\tb"));
|
||||||
|
assertEquals("\"a\\bb\"", MxGatewayCli.jsonString("a\bb"));
|
||||||
|
assertEquals("\"a\\fb\"", MxGatewayCli.jsonString("a\fb"));
|
||||||
|
assertEquals("\"a\\rb\"", MxGatewayCli.jsonString("a\rb"));
|
||||||
|
assertEquals("\"a\\nb\"", MxGatewayCli.jsonString("a\nb"));
|
||||||
|
// A non-named control character (U+0001) must become .
|
||||||
|
assertEquals("\"a\\u0001b\"", MxGatewayCli.jsonString("ab"));
|
||||||
|
// DEL (U+007F) is also escaped.
|
||||||
|
assertEquals("\"a\\u007fb\"", MxGatewayCli.jsonString("ab"));
|
||||||
|
// Quote and backslash still escape; ordinary printable text is verbatim.
|
||||||
|
assertEquals("\"a\\\"\\\\b\"", MxGatewayCli.jsonString("a\"\\b"));
|
||||||
|
assertEquals("\"plain\"", MxGatewayCli.jsonString("plain"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void versionCommandPrintsJson() {
|
void versionCommandPrintsJson() {
|
||||||
CliRun run = execute(new FakeClientFactory(), "version", "--json");
|
CliRun run = execute(new FakeClientFactory(), "version", "--json");
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
assertEquals(0, run.exitCode());
|
||||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
assertTrue(run.output().contains("\"clientVersion\":\"0.1.1\""));
|
||||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
|
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,27 +261,20 @@ final class MxGatewayCliTests {
|
|||||||
void galaxyBrowseParentZeroEmitsWarningToStderr() {
|
void galaxyBrowseParentZeroEmitsWarningToStderr() {
|
||||||
// --parent 0 is the server sentinel for roots; passing it explicitly is
|
// --parent 0 is the server sentinel for roots; passing it explicitly is
|
||||||
// almost certainly a mistake. The CLI must print a warning to stderr
|
// almost certainly a mistake. The CLI must print a warning to stderr
|
||||||
// (matching Go/Rust client behaviour) but must still attempt the call
|
// (matching Go/Rust client behaviour) but must still attempt the call.
|
||||||
// (exit behaviour depends on gateway reachability, not tested here;
|
|
||||||
// we only assert the warning path is triggered by checking the error
|
|
||||||
// writer before any gRPC connection is attempted).
|
|
||||||
//
|
//
|
||||||
// GalaxyBrowseCommand connects to a real GalaxyRepositoryClient, so the
|
// GalaxyBrowseCommand prints the warning, then calls connect() on the
|
||||||
// call() body will throw after printing the warning when no gateway is
|
// GalaxyClientFactory. We inject a stub factory whose connect() throws,
|
||||||
// reachable. We only assert the warning appears on stderr.
|
// so only the warning path runs — no live Netty channel to localhost is
|
||||||
StringWriter output = new StringWriter();
|
// constructed (Client.Java-043). The warning is emitted before
|
||||||
StringWriter errors = new StringWriter();
|
// connect() is reached, so it appears on stderr regardless.
|
||||||
// Non-zero exit is expected (no live gateway), but the warning must
|
CliRun run = executeGalaxy(
|
||||||
// appear on stderr regardless of what happens next.
|
new ThrowingGalaxyClientFactory(),
|
||||||
MxGatewayCli.execute(
|
|
||||||
new FakeClientFactory(),
|
|
||||||
new PrintWriter(output, true),
|
|
||||||
new PrintWriter(errors, true),
|
|
||||||
"galaxy-browse", "--parent", "0", "--depth", "1");
|
"galaxy-browse", "--parent", "0", "--depth", "1");
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
errors.toString().contains("--parent 0"),
|
run.errors().contains("--parent 0"),
|
||||||
"expected '--parent 0' warning on stderr; got: " + errors);
|
"expected '--parent 0' warning on stderr; got: " + run.errors());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- galaxy command-name aliases (D9-java) ----
|
// ---- galaxy command-name aliases (D9-java) ----
|
||||||
@@ -678,21 +691,28 @@ final class MxGatewayCliTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void streamAlarmsCommandFailsFastOnQueueOverflow() {
|
void streamAlarmsCommandFailsFastOnQueueOverflow() {
|
||||||
// Client.Java-033 regression — the CLI's stream-alarms bounded queue
|
// Client.Java-033/040/046 regression — the CLI's stream-alarms bounded
|
||||||
// used queue.offer(value) which silently dropped messages past
|
// queue used queue.offer(value) which silently dropped messages past
|
||||||
// capacity (1024). After the fix the CLI must surface the overflow
|
// capacity (1024). After the fix the CLI must surface the overflow as a
|
||||||
// as a non-zero exit (mirroring MxEventStream's fail-fast contract).
|
// non-zero exit (mirroring MxEventStream's fail-fast contract).
|
||||||
//
|
//
|
||||||
// The OverflowingFakeClient floods the gRPC observer with 2000
|
// The OverflowingFakeClient floods the gRPC observer on a BACKGROUND
|
||||||
// messages synchronously, which exceeds the bounded 1024-element
|
// thread so the subscription is already published when the overflow
|
||||||
// queue. The fix detects the failed offer, cancels the subscription,
|
// fires — exercising the terminate() cancel path with a non-null
|
||||||
// queues an overflow exception, and the drain loop surfaces it.
|
// subscription (Client.Java-046), not just the synchronous-flood path
|
||||||
|
// where subscriptionRef is still null. The fix records the overflow in
|
||||||
|
// a dedicated terminal slot (no queue.clear, Client.Java-040) and the
|
||||||
|
// drain loop surfaces it with the overflow message text.
|
||||||
OverflowingFakeClientFactory factory = new OverflowingFakeClientFactory();
|
OverflowingFakeClientFactory factory = new OverflowingFakeClientFactory();
|
||||||
CliRun run = execute(factory, "stream-alarms", "--filter-prefix", "Flood");
|
CliRun run = execute(factory, "stream-alarms", "--filter-prefix", "Flood");
|
||||||
|
|
||||||
assertFalse(run.exitCode() == 0,
|
assertFalse(run.exitCode() == 0,
|
||||||
"expected non-zero exit when the alarm queue overflows; got exit=" + run.exitCode()
|
"expected non-zero exit when the alarm queue overflows; got exit=" + run.exitCode()
|
||||||
+ " out=\n" + run.output() + "\nerr=\n" + run.errors());
|
+ " out=\n" + run.output() + "\nerr=\n" + run.errors());
|
||||||
|
assertTrue(
|
||||||
|
run.errors().contains("queue overflowed") || run.output().contains("queue overflowed"),
|
||||||
|
"expected the overflow message text to surface; out=\n" + run.output()
|
||||||
|
+ "\nerr=\n" + run.errors());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1050,6 +1070,18 @@ final class MxGatewayCliTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Galaxy client factory whose {@code connect} throws, so a test can exercise
|
||||||
|
* a command's pre-connect path (e.g. the {@code --parent 0} warning) without
|
||||||
|
* constructing a live Netty channel to localhost (Client.Java-043).
|
||||||
|
*/
|
||||||
|
private static final class ThrowingGalaxyClientFactory implements MxGatewayCli.GalaxyClientFactory {
|
||||||
|
@Override
|
||||||
|
public com.zb.mom.ww.mxgateway.client.GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
|
||||||
|
throw new IllegalStateException("galaxy connect not available in this test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class OverflowingFakeClient implements MxGatewayCli.MxGatewayCliClient {
|
private static final class OverflowingFakeClient implements MxGatewayCli.MxGatewayCliClient {
|
||||||
private final PrintWriter out;
|
private final PrintWriter out;
|
||||||
|
|
||||||
@@ -1089,19 +1121,31 @@ final class MxGatewayCliTests {
|
|||||||
@Override
|
@Override
|
||||||
public MxGatewayAlarmFeedSubscription streamAlarms(
|
public MxGatewayAlarmFeedSubscription streamAlarms(
|
||||||
StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> observer) {
|
StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> observer) {
|
||||||
// Synchronously push 2000 messages to overflow the CLI's bounded
|
// Push messages on a BACKGROUND thread (mirroring real gRPC, which
|
||||||
// 1024-element queue. The CLI must surface the overflow rather
|
// delivers onNext on a netty I/O thread) so the CLI's
|
||||||
// than silently dropping the trailing ~976 messages.
|
// subscriptionRef is already published when the overflow fires —
|
||||||
for (int i = 0; i < 2000; i++) {
|
// this exercises the terminate() cancel path with a non-null
|
||||||
observer.onNext(AlarmFeedMessage.newBuilder()
|
// subscription (Client.Java-046), unlike a synchronous flood that
|
||||||
.setActiveAlarm(ActiveAlarmSnapshot.newBuilder()
|
// overflows before streamAlarms even returns. Keeps pushing until
|
||||||
.setAlarmFullReference("Flood." + i)
|
// it observes the CLI cancelling the subscription on overflow, so
|
||||||
.setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE)
|
// no fixed message count is needed and the thread always exits.
|
||||||
.setSeverity(700))
|
MxGatewayAlarmFeedSubscription subscription = new MxGatewayAlarmFeedSubscription();
|
||||||
.build());
|
Thread flood = new Thread(() -> {
|
||||||
}
|
int i = 0;
|
||||||
observer.onCompleted();
|
while (!Thread.currentThread().isInterrupted() && i < 100_000) {
|
||||||
return new MxGatewayAlarmFeedSubscription();
|
observer.onNext(AlarmFeedMessage.newBuilder()
|
||||||
|
.setActiveAlarm(ActiveAlarmSnapshot.newBuilder()
|
||||||
|
.setAlarmFullReference("Flood." + i)
|
||||||
|
.setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE)
|
||||||
|
.setSeverity(700))
|
||||||
|
.build());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
observer.onCompleted();
|
||||||
|
}, "overflowing-fake-alarm-feed");
|
||||||
|
flood.setDaemon(true);
|
||||||
|
flood.start();
|
||||||
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ package com.zb.mom.ww.mxgateway.client;
|
|||||||
public final class MxGatewayClientVersion {
|
public final class MxGatewayClientVersion {
|
||||||
private static final int GATEWAY_PROTOCOL_VERSION = 3;
|
private static final int GATEWAY_PROTOCOL_VERSION = 3;
|
||||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||||
private static final String CLIENT_VERSION = "0.1.0";
|
private static final String CLIENT_VERSION = "0.1.1";
|
||||||
|
|
||||||
private MxGatewayClientVersion() {
|
private MxGatewayClientVersion() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -752,13 +752,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Medium |
|
| Severity | Medium |
|
||||||
| Category | Correctness & logic bugs |
|
| Category | Correctness & logic bugs |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:1552-1561` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:1552-1561` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** The `stream-alarms` overflow handler does `queue.clear()` then `offer(exception)` + `offer(ALARM_FEED_END)` non-atomically on an `ArrayBlockingQueue` shared with the gRPC delivery thread. In production gRPC (netty I/O thread), a concurrent `onNext` between the clear and the offers can re-enqueue a normal message, displacing the overflow exception so the drain loop hits the normal message and may exit before reaching the exception — exiting 0 on a truncated feed. Same race class as Client.Java-002/033.
|
**Description:** The `stream-alarms` overflow handler does `queue.clear()` then `offer(exception)` + `offer(ALARM_FEED_END)` non-atomically on an `ArrayBlockingQueue` shared with the gRPC delivery thread. In production gRPC (netty I/O thread), a concurrent `onNext` between the clear and the offers can re-enqueue a normal message, displacing the overflow exception so the drain loop hits the normal message and may exit before reaching the exception — exiting 0 on a truncated feed. Same race class as Client.Java-002/033.
|
||||||
|
|
||||||
**Recommendation:** Guard the overflow transition with an `AtomicBoolean` (mirror `MxGatewayStreamSubscription.terminate()`'s terminated-flag + lock) instead of re-clearing the queue.
|
**Recommendation:** Guard the overflow transition with an `AtomicBoolean` (mirror `MxGatewayStreamSubscription.terminate()`'s terminated-flag + lock) instead of re-clearing the queue.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed root cause in `StreamAlarmsCommand.call()`: the overflow branch did `queue.clear()` then `offer(exception)` + `offer(ALARM_FEED_END)`, so a concurrent `onNext` between the clear and the offers could re-enqueue a normal message and displace the overflow signal. (Note: `MxGatewayStreamSubscription` has no `terminate()` method; the terminal-guard model lives in `MxEventStream`, which itself still uses the clear+offer shape — I implemented the atomic guard the finding asks for rather than copying the older pattern.) Replaced the clear+offer with a single `AtomicBoolean terminated` guard (`compareAndSet(false,true)` — first terminal wins) plus a dedicated `AtomicReference<Object> terminal` slot that holds the terminal item (overflow exception / transport error / `ALARM_FEED_END`) independently of the bounded queue. `onNext` no longer re-clears the queue; it just stops enqueueing once terminated. The drain loop now `poll(50ms)`s and, when the queue is empty, reads the terminal slot. No re-clear, and a concurrent `onNext` can no longer displace the terminal. Fix applied 2026-06-16, pending gradle verification on windev. Regression test: `MxGatewayCliTests.streamAlarmsCommandFailsFastOnQueueOverflow` (strengthened under Client.Java-046 to drive async delivery and assert the overflow text).
|
||||||
|
|
||||||
### Client.Java-041
|
### Client.Java-041
|
||||||
|
|
||||||
@@ -767,13 +767,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Correctness & logic bugs |
|
| Category | Correctness & logic bugs |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:2187-2194` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:2187-2194` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `jsonString` escapes only `\`, `"`, `\r`, `\n` — not `\t`, `\b`, `\f`, or U+0000–U+001F/U+007F. A tag address/message/reference containing a tab produces malformed JSON (RFC 8259). Affects the hand-rolled `jsonObject`/`jsonString`/`jsonValue` output paths (the protobuf `JsonFormat` path is spec-correct).
|
**Description:** `jsonString` escapes only `\`, `"`, `\r`, `\n` — not `\t`, `\b`, `\f`, or U+0000–U+001F/U+007F. A tag address/message/reference containing a tab produces malformed JSON (RFC 8259). Affects the hand-rolled `jsonObject`/`jsonString`/`jsonValue` output paths (the protobuf `JsonFormat` path is spec-correct).
|
||||||
|
|
||||||
**Recommendation:** Add `\t`/`\b`/`\f` escapes and `\u00XX` for control chars, or route all JSON through a real JSON library.
|
**Recommendation:** Add `\t`/`\b`/`\f` escapes and `\u00XX` for control chars, or route all JSON through a real JSON library.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: `jsonString` escaped only `\\ \" \r \n`, so a tab/backspace/form-feed or any other U+0000–U+001F (or U+007F) char produced malformed JSON. Rewrote `jsonString` as a per-character builder that emits the two-character escapes for `\t \b \f \r \n \" \\` and `\u00XX` for the remaining `< 0x20` range plus DEL (`0x7f`), keeping ordinary printable characters verbatim. Widened `jsonString` from `private` to package-private (matching the Client.Java-032 `commandLine(...)` precedent) so the escaping can be unit-tested directly. Fix applied 2026-06-16, pending gradle verification on windev. Regression test: `MxGatewayCliTests.jsonStringEscapesControlCharacters`.
|
||||||
|
|
||||||
### Client.Java-042
|
### Client.Java-042
|
||||||
|
|
||||||
@@ -782,13 +782,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Error handling & resilience |
|
| Category | Error handling & resilience |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:1565-1567` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:1565-1567` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `StreamAlarmsCommand.onError` calls `queue.offer(error)` without checking the return value. If the queue is full when a transport error arrives, the error is dropped and the drain loop blocks forever on `queue.take()`. Same class as Client.Java-033 on the error path.
|
**Description:** `StreamAlarmsCommand.onError` calls `queue.offer(error)` without checking the return value. If the queue is full when a transport error arrives, the error is dropped and the drain loop blocks forever on `queue.take()`. Same class as Client.Java-033 on the error path.
|
||||||
|
|
||||||
**Recommendation:** Reserve a sentinel slot or use the `terminate(Throwable)` guard from `MxEventStream`; ensure the drain always sees a terminal item.
|
**Recommendation:** Reserve a sentinel slot or use the `terminate(Throwable)` guard from `MxEventStream`; ensure the drain always sees a terminal item.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: `onError` did a bare `queue.offer(error)` that, on a full queue, dropped the error and stranded the drain on `queue.take()` forever. Fixed together with Client.Java-040: `onError` now routes through the shared `terminate(error)` consumer, which records the throwable in the dedicated `terminal` slot (guarded by the `AtomicBoolean`, never enqueued into the bounded `queue`). The drain loop reads that slot via the `poll(50ms)` + terminal-check path, so a transport error is always observed even when the queue is full, and the `take()`-forever deadlock is gone. Fix applied 2026-06-16, pending gradle verification on windev. Covered by the same `streamAlarmsCommandFailsFastOnQueueOverflow` terminal-slot plumbing; the error path shares the slot with the overflow path.
|
||||||
|
|
||||||
### Client.Java-043
|
### Client.Java-043
|
||||||
|
|
||||||
@@ -797,13 +797,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Code organization & conventions |
|
| Category | Code organization & conventions |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java:241-264` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java:241-264` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `galaxyBrowseParentZeroEmitsWarningToStderr` calls `MxGatewayCli.execute(new FakeClientFactory(), ...)` for a galaxy-browse command, which wires the real `GrpcGalaxyClientFactory` and constructs a live Netty channel to localhost:5000 as a side effect (asserting only the warning). Wasteful and non-deterministic if port 5000 is reachable.
|
**Description:** `galaxyBrowseParentZeroEmitsWarningToStderr` calls `MxGatewayCli.execute(new FakeClientFactory(), ...)` for a galaxy-browse command, which wires the real `GrpcGalaxyClientFactory` and constructs a live Netty channel to localhost:5000 as a side effect (asserting only the warning). Wasteful and non-deterministic if port 5000 is reachable.
|
||||||
|
|
||||||
**Recommendation:** Use `executeGalaxy(...)` with a `GalaxyClientFactory` stub that throws, so only the warning path runs.
|
**Recommendation:** Use `executeGalaxy(...)` with a `GalaxyClientFactory` stub that throws, so only the warning path runs.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: the test called `MxGatewayCli.execute(new FakeClientFactory(), ...)`, which routes galaxy commands through the production `GrpcGalaxyClientFactory`; `GalaxyBrowseCommand.call()` prints the `--parent 0` warning then `connect()`s a live `GalaxyRepositoryClient` (Netty channel to localhost:5000) before failing — wasteful and non-deterministic. Rewrote the test to use the existing `executeGalaxy(...)` seam with a new `ThrowingGalaxyClientFactory` stub whose `connect()` throws; the warning is emitted before `connect()` is reached, so only the warning path runs and no live channel is constructed. Fix applied 2026-06-16, pending gradle verification on windev. Test: `MxGatewayCliTests.galaxyBrowseParentZeroEmitsWarningToStderr` (updated).
|
||||||
|
|
||||||
### Client.Java-044
|
### Client.Java-044
|
||||||
|
|
||||||
@@ -812,13 +812,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Code organization & conventions |
|
| Category | Code organization & conventions |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClientVersion.java:12` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/MxGatewayClientVersion.java:12` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `CLIENT_VERSION = "0.1.0"` is out of sync with Gradle `version = '0.1.1'` (cross-ref `clients/java/build.gradle:6`). The `version` command advertises 0.1.0 while the published artifact is 0.1.1; consumers can't use the version string as a reliable artifact check.
|
**Description:** `CLIENT_VERSION = "0.1.0"` is out of sync with Gradle `version = '0.1.1'` (cross-ref `clients/java/build.gradle:6`). The `version` command advertises 0.1.0 while the published artifact is 0.1.1; consumers can't use the version string as a reliable artifact check.
|
||||||
|
|
||||||
**Recommendation:** Bump `CLIENT_VERSION` to `0.1.1` (and the two test assertions), or source it from a Gradle-generated properties file.
|
**Recommendation:** Bump `CLIENT_VERSION` to `0.1.1` (and the two test assertions), or source it from a Gradle-generated properties file.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: `MxGatewayClientVersion.CLIENT_VERSION = "0.1.0"` while `clients/java/build.gradle:16` sets `version = '0.1.1'` and the README Maven coordinate is `:0.1.1`. Bumped `CLIENT_VERSION` to `"0.1.1"` and updated the two test assertions (`MxGatewayCliTests.versionCommandPrintsProtocolVersions` line asserting `"mxgateway-java 0.1.0"` and `versionCommandPrintsJson` asserting `"clientVersion":"0.1.0"`) to `0.1.1`. Left as a hardcoded constant (sourcing from a Gradle-generated properties file was the optional alternative, not required). Fix applied 2026-06-16, pending gradle verification on windev. Tests: `MxGatewayCliTests.versionCommandPrintsProtocolVersions`, `versionCommandPrintsJson`.
|
||||||
|
|
||||||
### Client.Java-045
|
### Client.Java-045
|
||||||
|
|
||||||
@@ -827,13 +827,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Testing coverage |
|
| Category | Testing coverage |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/InProcessGatewayHarness.java` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/InProcessGatewayHarness.java` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** The harness implements only `streamEvents`/`closeSession` (gateway) and `discoverHierarchy`/`watchDeployEvents` (galaxy); all other RPCs return gRPC UNIMPLEMENTED. This is undocumented, so a future test exercising invoke/register through the harness would silently get UNIMPLEMENTED.
|
**Description:** The harness implements only `streamEvents`/`closeSession` (gateway) and `discoverHierarchy`/`watchDeployEvents` (galaxy); all other RPCs return gRPC UNIMPLEMENTED. This is undocumented, so a future test exercising invoke/register through the harness would silently get UNIMPLEMENTED.
|
||||||
|
|
||||||
**Recommendation:** Add a Javadoc note enumerating implemented RPCs and warning that others return UNIMPLEMENTED by design.
|
**Recommendation:** Add a Javadoc note enumerating implemented RPCs and warning that others return UNIMPLEMENTED by design.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed against source (the file lives under `src/test/...`, not `src/main/...` as the finding location states): the scripted fakes override only `streamEvents`/`closeSession` (gateway) and `discoverHierarchy`/`watchDeployEvents` (galaxy); every other RPC inherits the generated `*ImplBase` default and returns gRPC `UNIMPLEMENTED`. Added a "Implemented RPCs" section to the `InProcessGatewayHarness` class Javadoc enumerating the four overridden RPCs and warning that all others (openSession, invoke, register, streamAlarms, queryActiveAlarms, browseChildren, …) return `UNIMPLEMENTED` by design, so a future test must add a scripted override first. Doc-only change. Fix applied 2026-06-16, pending gradle verification on windev. No test needed.
|
||||||
|
|
||||||
### Client.Java-046
|
### Client.Java-046
|
||||||
|
|
||||||
@@ -842,13 +842,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Testing coverage |
|
| Category | Testing coverage |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java:680-696` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/test/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCliTests.java:680-696` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `streamAlarmsCommandFailsFastOnQueueOverflow` delivers all 2000 onNext synchronously from within `streamAlarms`, so `subscriptionRef` is still null when the overflow fires — the `sub.cancel()` branch is never exercised. The test also doesn't assert the overflow message text. It passes for a reason that doesn't generalize to async gRPC delivery.
|
**Description:** `streamAlarmsCommandFailsFastOnQueueOverflow` delivers all 2000 onNext synchronously from within `streamAlarms`, so `subscriptionRef` is still null when the overflow fires — the `sub.cancel()` branch is never exercised. The test also doesn't assert the overflow message text. It passes for a reason that doesn't generalize to async gRPC delivery.
|
||||||
|
|
||||||
**Recommendation:** Deliver messages asynchronously so the cancel path runs, and assert the overflow error text appears in output.
|
**Recommendation:** Deliver messages asynchronously so the cancel path runs, and assert the overflow error text appears in output.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: `OverflowingFakeClient.streamAlarms` pushed all 2000 `onNext` synchronously and returned the subscription only afterward, so `subscriptionRef` was still null when the overflow fired and the `sub.cancel()` branch never ran; the test also asserted only the exit code, not the overflow text. Reworked `OverflowingFakeClient.streamAlarms` to flood on a background daemon thread (mirroring a real netty I/O thread) and return the subscription first, so the overflow fires with a non-null published subscription and exercises the `terminate()` cancel path. Strengthened `streamAlarmsCommandFailsFastOnQueueOverflow` to additionally assert the overflow message text ("queue overflowed") surfaces in stderr/stdout. Fix applied 2026-06-16, pending gradle verification on windev. Test: `MxGatewayCliTests.streamAlarmsCommandFailsFastOnQueueOverflow` (updated; also validates the Client.Java-040 terminal-slot fix).
|
||||||
|
|
||||||
### Client.Java-047
|
### Client.Java-047
|
||||||
|
|
||||||
@@ -857,13 +857,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Documentation & comments |
|
| Category | Documentation & comments |
|
||||||
| Location | `clients/java/README.md` |
|
| Location | `clients/java/README.md` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** README advertises the `0.1.1` artifact coordinate (Gitea Maven section) while the `version` command reports `0.1.0` — the user-visible symptom of Client.Java-044. Cross-ref `MxGatewayClientVersion.java:12`.
|
**Description:** README advertises the `0.1.1` artifact coordinate (Gitea Maven section) while the `version` command reports `0.1.0` — the user-visible symptom of Client.Java-044. Cross-ref `MxGatewayClientVersion.java:12`.
|
||||||
|
|
||||||
**Recommendation:** Resolved by fixing Client.Java-044 (sync the compiled-in version).
|
**Recommendation:** Resolved by fixing Client.Java-044 (sync the compiled-in version).
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Symptom of Client.Java-044, resolved together. The README's `0.1.1` Maven coordinate (`clients/java/README.md:336`) was already correct; the divergence was the compiled-in `CLIENT_VERSION = "0.1.0"`. Bumping `CLIENT_VERSION` to `0.1.1` (Client.Java-044) makes the `version` command report `0.1.1`, matching the README. No README edit needed. Fix applied 2026-06-16, pending gradle verification on windev.
|
||||||
|
|
||||||
### Client.Java-048
|
### Client.Java-048
|
||||||
|
|
||||||
@@ -872,13 +872,13 @@ BrowseChildrenReply reply = galaxy.browseChildren(
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Documentation & comments |
|
| Category | Documentation & comments |
|
||||||
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:88-105` |
|
| Location | `clients/java/zb-mom-ww-mxgateway-cli/src/main/java/com/zb/mom/ww/mxgateway/cli/MxGatewayCli.java:88-105` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** The public `execute(PrintWriter, PrintWriter, String...)` Javadoc calls it "Test-friendly entry point", but it wires `GrpcMxGatewayCliClientFactory` with no injection — the actual test seam is the package-private `execute(MxGatewayCliClientFactory, ...)` / `commandLine(...)` overload. Misleading.
|
**Description:** The public `execute(PrintWriter, PrintWriter, String...)` Javadoc calls it "Test-friendly entry point", but it wires `GrpcMxGatewayCliClientFactory` with no injection — the actual test seam is the package-private `execute(MxGatewayCliClientFactory, ...)` / `commandLine(...)` overload. Misleading.
|
||||||
|
|
||||||
**Recommendation:** Clarify the Javadoc to direct readers to the injectable overload for testing.
|
**Recommendation:** Clarify the Javadoc to direct readers to the injectable overload for testing.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Confirmed: the public `execute(PrintWriter, PrintWriter, String...)` Javadoc called it the "Test-friendly entry point", but it wires the production `GrpcMxGatewayCliClientFactory` with no injection seam — unit tests actually use the package-private `execute(MxGatewayCliClientFactory, ...)` / `commandLine(...)` overloads. Rewrote the Javadoc to drop "test-friendly", explain it wires a real gRPC channel, and direct test authors to the injectable package-private overloads. Doc-only change. Fix applied 2026-06-16, pending gradle verification on windev. No test needed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -640,13 +640,13 @@ Re-review of the worker-test delta covering the new COM seam (`MxAccessCommandEx
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Code organization & conventions |
|
| Category | Code organization & conventions |
|
||||||
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs:2233`, `src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs:97` |
|
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs:2233`, `src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs:97` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `FakeMxStatus` is defined twice — file-scope in `TestSupport/NoopMxAccessServer.cs:97` and nested in `MxAccessCommandExecutorTests.FakeMxAccessComObject:2233` — both exposing the same four public fields that `MxStatusProxyConverter` reflects over. The two copies must stay structurally identical; a future field change to the real COM struct requires updating two places, and the duplication is invisible to a reader consulting only one file.
|
**Description:** `FakeMxStatus` is defined twice — file-scope in `TestSupport/NoopMxAccessServer.cs:97` and nested in `MxAccessCommandExecutorTests.FakeMxAccessComObject:2233` — both exposing the same four public fields that `MxStatusProxyConverter` reflects over. The two copies must stay structurally identical; a future field change to the real COM struct requires updating two places, and the duplication is invisible to a reader consulting only one file.
|
||||||
|
|
||||||
**Recommendation:** Extract `FakeMxStatus` into its own `TestSupport/FakeMxStatus.cs` (or colocate both doubles) and have `MxAccessCommandExecutorTests` use the shared type instead of its nested copy.
|
**Recommendation:** Extract `FakeMxStatus` into its own `TestSupport/FakeMxStatus.cs` (or colocate both doubles) and have `MxAccessCommandExecutorTests` use the shared type instead of its nested copy.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Removed the nested `FakeMxStatus` class from `MxAccessCommandExecutorTests.FakeMxAccessComObject`; the two `new FakeMxStatus { ... }` usages in `Suspend`/`Activate` now resolve to the shared `TestSupport.FakeMxStatus` via the pre-existing `using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;` import. Updated the XML doc on `TestSupport/NoopMxAccessServer.cs:FakeMxStatus` to note the consolidation. Fix applied 2026-06-16, pending build verification on windev.
|
||||||
|
|
||||||
### Worker.Tests-035
|
### Worker.Tests-035
|
||||||
|
|
||||||
@@ -655,13 +655,13 @@ Re-review of the worker-test delta covering the new COM seam (`MxAccessCommandEx
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Testing coverage |
|
| Category | Testing coverage |
|
||||||
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs`, `src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs:99-136` |
|
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs`, `src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs:99-136` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `MxAccessCommandExecutor.Execute` has a `_` discard arm returning `CreateInvalidRequestReply(... "Unsupported MXAccess command kind ...")` — the safety net for an unknown `MxCommandKind` (e.g. a future gateway enum value before the worker is updated). No test passes an unknown kind and asserts `InvalidRequest`. A regression changing the arm to `throw` would propagate an unhandled exception through `WorkerPipeSession` and no test would catch it.
|
**Description:** `MxAccessCommandExecutor.Execute` has a `_` discard arm returning `CreateInvalidRequestReply(... "Unsupported MXAccess command kind ...")` — the safety net for an unknown `MxCommandKind` (e.g. a future gateway enum value before the worker is updated). No test passes an unknown kind and asserts `InvalidRequest`. A regression changing the arm to `throw` would propagate an unhandled exception through `WorkerPipeSession` and no test would catch it.
|
||||||
|
|
||||||
**Recommendation:** Add a `[Fact]` constructing a `StaCommand` with an undefined `MxCommandKind` value and asserting the reply is `ProtocolStatusCode.InvalidRequest` with "Unsupported" in the diagnostic.
|
**Recommendation:** Add a `[Fact]` constructing a `StaCommand` with an undefined `MxCommandKind` value and asserting the reply is `ProtocolStatusCode.InvalidRequest` with "Unsupported" in the diagnostic.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Added `DispatchAsync_WithUnknownCommandKind_ReturnsInvalidRequestWithUnsupportedDiagnostic` to `MxAccessCommandExecutorTests`. Casts `int.MaxValue` to `MxCommandKind` (an undefined value not present in the proto-generated enum), dispatches it through `MxAccessStaSession.DispatchAsync`, asserts `ProtocolStatusCode.InvalidRequest`, and asserts `reply.DiagnosticMessage` contains "Unsupported" (case-insensitive — matching `CreateInvalidRequestReply`'s `"Unsupported MXAccess command kind ..."` message). Fix applied 2026-06-16, pending build verification on windev.
|
||||||
|
|
||||||
### Worker.Tests-036
|
### Worker.Tests-036
|
||||||
|
|
||||||
@@ -670,10 +670,10 @@ Re-review of the worker-test delta covering the new COM seam (`MxAccessCommandEx
|
|||||||
| Severity | Low |
|
| Severity | Low |
|
||||||
| Category | Concurrency & thread safety |
|
| Category | Concurrency & thread safety |
|
||||||
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs:983-996` |
|
| Location | `src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs:983-996` |
|
||||||
| Status | Open |
|
| Status | In Progress |
|
||||||
|
|
||||||
**Description:** `RunAsync_SendsFirstHeartbeatImmediatelyOnEnteringLoop` carries a redundant wall-clock assertion `Assert.True(elapsed < TimeSpan.FromSeconds(5), ...)`. The existing `heartbeatWait` CTS (cancel-after 5s) already enforces the same bound — the extra wall-clock check can only fire if the heartbeat arrived but took >5s to be received, which the CTS already prevents. It is the same coarse wall-clock pattern prior findings (Worker.Tests-003/004/013/020) corrected.
|
**Description:** `RunAsync_SendsFirstHeartbeatImmediatelyOnEnteringLoop` carries a redundant wall-clock assertion `Assert.True(elapsed < TimeSpan.FromSeconds(5), ...)`. The existing `heartbeatWait` CTS (cancel-after 5s) already enforces the same bound — the extra wall-clock check can only fire if the heartbeat arrived but took >5s to be received, which the CTS already prevents. It is the same coarse wall-clock pattern prior findings (Worker.Tests-003/004/013/020) corrected.
|
||||||
|
|
||||||
**Recommendation:** Remove the `start`/`elapsed`/`Assert.True(elapsed < ...)` check; the CTS timeout already pins the timing contract.
|
**Recommendation:** Remove the `start`/`elapsed`/`Assert.True(elapsed < ...)` check; the CTS timeout already pins the timing contract.
|
||||||
|
|
||||||
**Resolution:** _(empty until closed)_
|
**Resolution:** 2026-06-16 — Removed the `DateTimeOffset start`, `TimeSpan elapsed`, and `Assert.True(elapsed < TimeSpan.FromSeconds(5), ...)` wall-clock assertions from `RunAsync_SendsFirstHeartbeatImmediatelyOnEnteringLoop`. The `heartbeatWait` CTS (cancel-after 5s) already enforces the same timing bound. Added an inline comment explaining why the wall-clock floor is omitted, consistent with the Worker.Tests-003/004/013/020 pattern. Fix applied 2026-06-16, pending build verification on windev.
|
||||||
|
|||||||
@@ -980,7 +980,11 @@ public sealed class WorkerPipeSessionTests
|
|||||||
Task runTask = session.RunAsync(cancellation.Token);
|
Task runTask = session.RunAsync(cancellation.Token);
|
||||||
await CompleteGatewayHandshakeAsync(pipePair, cancellation.Token);
|
await CompleteGatewayHandshakeAsync(pipePair, cancellation.Token);
|
||||||
|
|
||||||
DateTimeOffset start = DateTimeOffset.UtcNow;
|
// The heartbeatWait CTS (5s cancel-after) already enforces the timing bound:
|
||||||
|
// if the first heartbeat is not received within 5s, ReadUntilAsync throws
|
||||||
|
// OperationCanceledException and the test fails. A redundant wall-clock
|
||||||
|
// elapsed < 5s assertion would add the same class of flakiness
|
||||||
|
// Workers.Tests-003/004/013/020 corrected elsewhere, so it is omitted here.
|
||||||
using CancellationTokenSource heartbeatWait = CancellationTokenSource
|
using CancellationTokenSource heartbeatWait = CancellationTokenSource
|
||||||
.CreateLinkedTokenSource(cancellation.Token);
|
.CreateLinkedTokenSource(cancellation.Token);
|
||||||
heartbeatWait.CancelAfter(TimeSpan.FromSeconds(5));
|
heartbeatWait.CancelAfter(TimeSpan.FromSeconds(5));
|
||||||
@@ -988,12 +992,8 @@ public sealed class WorkerPipeSessionTests
|
|||||||
pipePair.GatewayReader,
|
pipePair.GatewayReader,
|
||||||
WorkerEnvelope.BodyOneofCase.WorkerHeartbeat,
|
WorkerEnvelope.BodyOneofCase.WorkerHeartbeat,
|
||||||
heartbeatWait.Token);
|
heartbeatWait.Token);
|
||||||
TimeSpan elapsed = DateTimeOffset.UtcNow - start;
|
|
||||||
|
|
||||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHeartbeat, heartbeat.BodyCase);
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHeartbeat, heartbeat.BodyCase);
|
||||||
Assert.True(
|
|
||||||
elapsed < TimeSpan.FromSeconds(5),
|
|
||||||
$"First heartbeat took {elapsed}, expected well under the 30s interval.");
|
|
||||||
|
|
||||||
await SendShutdownAndWaitAsync(pipePair, runTask, cancellation.Token);
|
await SendShutdownAndWaitAsync(pipePair, runTask, cancellation.Token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1123,6 +1123,36 @@ public sealed class MxAccessCommandExecutorTests
|
|||||||
Assert.Equal(500, fakeComObject.SetBufferedUpdateIntervalValue);
|
Assert.Equal(500, fakeComObject.SetBufferedUpdateIntervalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a command with an unknown <see cref="MxCommandKind"/> value returns an
|
||||||
|
/// <see cref="ProtocolStatusCode.InvalidRequest"/> reply whose diagnostic contains "Unsupported".
|
||||||
|
/// This pins the <c>_ => CreateInvalidRequestReply(...)</c> discard arm in
|
||||||
|
/// <c>MxAccessCommandExecutor.Execute</c>: a regression that changed the arm to
|
||||||
|
/// <c>throw</c> would propagate an unhandled exception through <c>WorkerPipeSession</c>
|
||||||
|
/// and no other test would catch it.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_WithUnknownCommandKind_ReturnsInvalidRequestWithUnsupportedDiagnostic()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 999));
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
// Cast an integer outside the defined MxCommandKind range to an unknown kind value.
|
||||||
|
MxCommandKind unknownKind = (MxCommandKind)int.MaxValue;
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
"unknown-kind-correlation",
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = unknownKind,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Contains("Unsupported", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static StaCommand CreateSuspendCommand(
|
private static StaCommand CreateSuspendCommand(
|
||||||
string correlationId,
|
string correlationId,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
@@ -2229,21 +2259,6 @@ public sealed class MxAccessCommandExecutorTests
|
|||||||
SetBufferedUpdateIntervalValue = updateIntervalMilliseconds;
|
SetBufferedUpdateIntervalValue = updateIntervalMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Status stand-in reflected over by the worker's MxStatusProxy converter.</summary>
|
|
||||||
internal sealed class FakeMxStatus
|
|
||||||
{
|
|
||||||
/// <summary>Success indicator read by the status converter.</summary>
|
|
||||||
public int success;
|
|
||||||
|
|
||||||
/// <summary>Status category read by the status converter.</summary>
|
|
||||||
public int category;
|
|
||||||
|
|
||||||
/// <summary>Status detected-by read by the status converter.</summary>
|
|
||||||
public int detectedBy;
|
|
||||||
|
|
||||||
/// <summary>Status detail read by the status converter.</summary>
|
|
||||||
public int detail;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
|
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
|
|||||||
/// <c>success</c>, <c>category</c>, <c>detectedBy</c>, and <c>detail</c>
|
/// <c>success</c>, <c>category</c>, <c>detectedBy</c>, and <c>detail</c>
|
||||||
/// fields, so this fake exposes the same field shape with all-OK values.
|
/// fields, so this fake exposes the same field shape with all-OK values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Previously duplicated as a nested class in <c>MxAccessCommandExecutorTests.FakeMxAccessComObject</c>.
|
||||||
|
/// Consolidated here per Worker.Tests-034 so a future field change to the real COM struct only
|
||||||
|
/// requires updating one place.
|
||||||
|
/// </remarks>
|
||||||
internal sealed class FakeMxStatus
|
internal sealed class FakeMxStatus
|
||||||
{
|
{
|
||||||
// These public fields exist solely so MxStatusProxyConverter can reflect
|
// These public fields exist solely so MxStatusProxyConverter can reflect
|
||||||
|
|||||||
Reference in New Issue
Block a user