a0203503a7
Re-reviewed every module/client against the 10-category checklist
(REVIEW-PROCESS.md) at commit 1cd51bb, filed 72 new findings, and
fixed them in three priority waves (3 High, 17 Medium, 52 Low).
Highs
- Server-017: enumerate AcknowledgeAlarm / QueryActiveAlarms in
GatewayGrpcScopeResolver so non-admin keys can use them; document
the mapping in docs/Authorization.md; add interceptor tests.
- Client.Java-013: add the five missing bulk-method stubs to the
CLI FakeSession so the test module compiles on a clean tree.
- Client.Rust-013: fix the clippy::doc_lazy_continuation regression
in generated tonic code by reformatting the ReadBulkCommand proto
comment and scoping a #![allow(...)] to the generated submodules.
Mediums (highlights)
- Server: unify GatewaySession state-lock discipline (-015) and
make DisposeAsync race-safe against in-flight CloseAsync (-016);
add constraint-enforcement test coverage for the bulk-plan path
(-021).
- Worker: introduce StaRuntimeShutdownException so RunAlarmPollLoop
can distinguish graceful shutdown from a real STA-affinity
violation (-016); have the watchdog skip StaHung while
CurrentCommandCorrelationId is non-empty so a legitimate slow
ReadBulk no longer self-faults (-017).
- Tests: add per-method round-trip + cancellation coverage for the
11 GatewaySession bulk methods (-013); replace the real TCP probe
in GalaxyHierarchyCacheTests with an IGalaxyRepository fake
(-016).
- IntegrationTests: drive the StreamEvents writer in the live Write
test and assert OnWriteComplete (-012); add live tests for
Unadvise/RemoveItem/Unregister ordering, WriteSecured, and
abnormal worker exit (-014).
- Worker.Tests: replace MxAccessSession reflection with an internal
CreateForTesting factory (-016); cover WorkerCancel and
unexpected-body envelope branches (-017).
- Client.Java: cancel MxEventStream when close() races
beforeStart() (-014); return a CancellingCompletableFuture that
actually forwards cancellation through .thenApply chains (-015).
- Client.Python: drop the silent localhost-plaintext downgrade in
the CLI; require explicit --plaintext (-013).
- Client.Rust: stop bench-read-bulk from polluting success-latency
histograms with failed-call durations (-015); add coverage for
the five MalformedReply paths, the bulk-write helpers, the
Error::Unavailable mapping, and the unary-fault path (-016).
- Contracts: extend docs/Contracts.md with the bulk read/write
command family (-009).
Lows (highlights)
- Server: cap GalaxyGlobMatcher.RegexCache; align
WorkerAlarmRpcDispatcher missing-session handling; drop the
duplicate dashboard @page routes; refresh IAlarmRpcDispatcher
XML doc.
- Worker: surface SetXmlAlarmQuery COM failures; remove dead
subscriptionExpression / ExecutingCommand arms; preserve
factory-supplied runtime sessions; split MxAlarmSnapshot.cs into
three files.
- Tests: dispose the WebApplication in seven test classes; rebuild
FakeWorkerProcess.WaitForExitAsync against a real TaskCompletion
source; switch the heartbeat-expires test to ManualTimeProvider;
add InvariantCulture to the remaining DateTimeOffset.Parse sites;
document GalaxyFilterInputSafetyTests in GatewayTesting.md.
- IntegrationTests: comment fixes, RecordingServerStreamWriter
IDisposable, class-level [Trait], single-source ZB default
connection string.
- Worker.Tests: replace silent-return gating with LiveMxAccessFact
so absent env vars SKIP not pass; PascalCase rename of probe
[Fact]s; deterministic deadline test; new frame-protocol error
tests; ComputeTransitions diff-coverage; relocate dev-rig probes
to Probes/.
- Contracts: add round-trip coverage and per-field redaction /
Galaxy-identifier comments to the protos.
- Client.Dotnet: introduce clients/dotnet/Directory.Build.props so
TreatWarningsAsErrors / analysers apply; document
DiscoverHierarchyOptions and IMxGatewayCliClient; require typed
bulk-read handles in CLI; surface AcknowledgeAlarm transport
faults through Translate().
- Client.Go: kill dead code in alarms_test / fakeGalaxyServer /
runWriteBulkVariant; document the six new subcommands in
writeUsage; drain galaxy-watch events on limit; switch io.EOF
comparisons to errors.Is.
- Client.Java: shared shutdown helpers + new shutdownTimeout
option; regex-based credential redaction; Long.toUnsignedString
for uint64 sequence; doc fixes.
- Client.Python: combine duplicate imports; add coverage for
_percentile / bench-read-bulk / MAX_AGGREGATE_EVENTS /
_api_key_from_env; populate pyproject metadata and ship py.typed.
- Client.Rust: expose next_correlation_id() so CLI ping/close
stop hard-coding correlation IDs; resync RustClientDesign.md
with the current Session / Error surface and CLI subcommand set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
276 lines
9.7 KiB
C#
276 lines
9.7 KiB
C#
using Grpc.Core;
|
|
using MxGateway.Contracts.Proto;
|
|
|
|
namespace MxGateway.Client.Tests;
|
|
|
|
/// <summary>
|
|
/// Fake implementation of IMxGatewayClientTransport for testing.
|
|
/// </summary>
|
|
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
|
{
|
|
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
|
private readonly List<MxEvent> _events = [];
|
|
|
|
/// <summary>
|
|
/// Gets the gateway client options.
|
|
/// </summary>
|
|
public MxGatewayClientOptions Options { get; } = options;
|
|
|
|
/// <summary>
|
|
/// Gets null, since this is a test fake without a real gRPC client.
|
|
/// </summary>
|
|
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured OpenSessionAsync calls.
|
|
/// </summary>
|
|
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured CloseSessionAsync calls.
|
|
/// </summary>
|
|
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured InvokeAsync calls.
|
|
/// </summary>
|
|
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured StreamEventsAsync calls.
|
|
/// </summary>
|
|
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured AcknowledgeAlarmAsync calls.
|
|
/// </summary>
|
|
public List<(AcknowledgeAlarmRequest Request, CallOptions CallOptions)> AcknowledgeAlarmCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the list of captured QueryActiveAlarmsAsync calls.
|
|
/// </summary>
|
|
public List<(QueryActiveAlarmsRequest Request, CallOptions CallOptions)> QueryActiveAlarmsCalls { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets the queue of exceptions to throw from AcknowledgeAlarmAsync.
|
|
/// </summary>
|
|
public Queue<Exception> AcknowledgeAlarmExceptions { get; } = new();
|
|
|
|
private readonly Queue<AcknowledgeAlarmReply> _acknowledgeReplies = new();
|
|
private readonly List<ActiveAlarmSnapshot> _activeAlarmSnapshots = [];
|
|
|
|
/// <summary>
|
|
/// Gets or sets the reply to return from OpenSessionAsync.
|
|
/// </summary>
|
|
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
|
{
|
|
SessionId = "session-fixture",
|
|
BackendName = "mxaccess-worker",
|
|
GatewayProtocolVersion = 1,
|
|
WorkerProtocolVersion = 1,
|
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
};
|
|
|
|
/// <summary>
|
|
/// Gets or sets the reply to return from CloseSessionAsync.
|
|
/// </summary>
|
|
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
|
{
|
|
SessionId = "session-fixture",
|
|
FinalState = SessionState.Closed,
|
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
};
|
|
|
|
/// <summary>
|
|
/// Gets the queue of exceptions to throw from OpenSessionAsync.
|
|
/// </summary>
|
|
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Gets the queue of exceptions to throw from CloseSessionAsync.
|
|
/// </summary>
|
|
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether thrown <see cref="RpcException"/>s are mapped
|
|
/// to <see cref="MxGatewayException"/> the way the production gRPC transport does. Lets
|
|
/// retry tests exercise the wrapped-exception predicate branch that runs in production.
|
|
/// </summary>
|
|
public bool MapTransportExceptions { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets an optional hook awaited inside CloseSessionAsync after the call is
|
|
/// recorded; lets tests pause a close mid-flight to observe concurrent dispose.
|
|
/// </summary>
|
|
public Func<Task>? CloseSessionHook { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the queue of exceptions to throw from InvokeAsync.
|
|
/// </summary>
|
|
public Queue<Exception> InvokeExceptions { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
|
|
/// </summary>
|
|
/// <param name="request">The OpenSessionRequest to process.</param>
|
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
|
public Task<OpenSessionReply> OpenSessionAsync(
|
|
OpenSessionRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
OpenSessionCalls.Add((request, callOptions));
|
|
if (OpenSessionExceptions.TryDequeue(out Exception? exception))
|
|
{
|
|
throw Translate(exception, callOptions);
|
|
}
|
|
|
|
return Task.FromResult(OpenSessionReply);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
|
|
/// </summary>
|
|
/// <param name="request">The CloseSessionRequest to process.</param>
|
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
|
public async Task<CloseSessionReply> CloseSessionAsync(
|
|
CloseSessionRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
CloseSessionCalls.Add((request, callOptions));
|
|
|
|
if (CloseSessionHook is not null)
|
|
{
|
|
await CloseSessionHook().ConfigureAwait(false);
|
|
}
|
|
|
|
if (CloseSessionExceptions.TryDequeue(out Exception? exception))
|
|
{
|
|
throw Translate(exception, callOptions);
|
|
}
|
|
|
|
return CloseSessionReply;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
|
|
/// </summary>
|
|
/// <param name="request">The MxCommandRequest to process.</param>
|
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
|
public Task<MxCommandReply> InvokeAsync(
|
|
MxCommandRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
InvokeCalls.Add((request, callOptions));
|
|
if (InvokeExceptions.TryDequeue(out Exception? exception))
|
|
{
|
|
throw Translate(exception, callOptions);
|
|
}
|
|
|
|
return Task.FromResult(_invokeReplies.Dequeue());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
|
|
/// </summary>
|
|
/// <param name="request">The StreamEventsRequest to process.</param>
|
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
StreamEventsRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
StreamEventsCalls.Add((request, callOptions));
|
|
|
|
foreach (MxEvent gatewayEvent in _events)
|
|
{
|
|
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
|
await Task.Yield();
|
|
yield return gatewayEvent;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enqueues a reply to be returned from the next InvokeAsync call.
|
|
/// </summary>
|
|
/// <param name="reply">The reply to enqueue.</param>
|
|
public void AddInvokeReply(MxCommandReply reply)
|
|
{
|
|
_invokeReplies.Enqueue(reply);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enqueues an event to be yielded from StreamEventsAsync.
|
|
/// </summary>
|
|
/// <param name="gatewayEvent">The event to enqueue.</param>
|
|
public void AddEvent(MxEvent gatewayEvent)
|
|
{
|
|
_events.Add(gatewayEvent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records the acknowledge call and returns the next enqueued reply (or default).
|
|
/// </summary>
|
|
public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
|
|
AcknowledgeAlarmRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
AcknowledgeAlarmCalls.Add((request, callOptions));
|
|
if (AcknowledgeAlarmExceptions.TryDequeue(out Exception? exception))
|
|
{
|
|
throw Translate(exception, callOptions);
|
|
}
|
|
|
|
return Task.FromResult(_acknowledgeReplies.Count > 0
|
|
? _acknowledgeReplies.Dequeue()
|
|
: new AcknowledgeAlarmReply
|
|
{
|
|
SessionId = request.SessionId,
|
|
CorrelationId = request.ClientCorrelationId,
|
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
Status = new MxStatusProxy { Success = 1, Category = MxStatusCategory.Ok },
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records the query call and yields each enqueued snapshot.
|
|
/// </summary>
|
|
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
|
|
QueryActiveAlarmsRequest request,
|
|
CallOptions callOptions)
|
|
{
|
|
QueryActiveAlarmsCalls.Add((request, callOptions));
|
|
|
|
foreach (ActiveAlarmSnapshot snapshot in _activeAlarmSnapshots)
|
|
{
|
|
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
|
await Task.Yield();
|
|
yield return snapshot;
|
|
}
|
|
}
|
|
|
|
/// <summary>Enqueues an acknowledge reply.</summary>
|
|
public void AddAcknowledgeReply(AcknowledgeAlarmReply reply)
|
|
{
|
|
_acknowledgeReplies.Enqueue(reply);
|
|
}
|
|
|
|
/// <summary>Enqueues a snapshot to be yielded from QueryActiveAlarmsAsync.</summary>
|
|
public void AddActiveAlarmSnapshot(ActiveAlarmSnapshot snapshot)
|
|
{
|
|
_activeAlarmSnapshots.Add(snapshot);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a queued exception the way the production gRPC transport does when
|
|
/// <see cref="MapTransportExceptions"/> is set; otherwise returns it unchanged.
|
|
/// </summary>
|
|
private Exception Translate(Exception exception, CallOptions callOptions)
|
|
{
|
|
if (MapTransportExceptions && exception is RpcException rpcException)
|
|
{
|
|
return RpcExceptionMapper.Map(rpcException, callOptions.CancellationToken);
|
|
}
|
|
|
|
return exception;
|
|
}
|
|
}
|