fca978de07
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
373 lines
17 KiB
C#
373 lines
17 KiB
C#
using ZB.MOM.WW.MxGateway.Contracts;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Server.Workers;
|
|
using ZB.MOM.WW.MxGateway.Tests.Gateway.Workers.Fakes;
|
|
using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Workers;
|
|
|
|
public sealed class FakeWorkerHarnessTests
|
|
{
|
|
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
|
|
|
|
/// <summary>Verifies that completing startup with hello and ready transitions the client to ready state.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CompleteStartupAsync_WithHelloAndReady_TransitionsClientToReady()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
|
|
Task startTask = client.StartAsync(CancellationToken.None);
|
|
WorkerEnvelope gatewayHello = await fakeWorker.CompleteStartupAsync();
|
|
await startTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.GatewayHello, gatewayHello.BodyCase);
|
|
Assert.Equal(FakeWorkerHarness.DefaultNonce, gatewayHello.GatewayHello.Nonce);
|
|
Assert.Equal(WorkerClientState.Ready, client.State);
|
|
Assert.Equal(FakeWorkerHarness.DefaultWorkerProcessId, client.ProcessId);
|
|
}
|
|
|
|
/// <summary>Verifies that a protocol version mismatch during startup fails the client.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task StartAsync_WithProtocolMismatch_FailsStartup()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
|
|
Task startTask = client.StartAsync(CancellationToken.None);
|
|
WorkerEnvelope gatewayHello = await fakeWorker.ReadGatewayEnvelopeAsync();
|
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.GatewayHello, gatewayHello.BodyCase);
|
|
await fakeWorker.SendWorkerHelloAsync(
|
|
workerProtocolVersion: GatewayContractInfo.WorkerProtocolVersion + 1);
|
|
|
|
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
|
|
async () => await startTask.WaitAsync(TestTimeout));
|
|
|
|
Assert.Equal(WorkerClientErrorCode.ProtocolViolation, exception.ErrorCode);
|
|
}
|
|
|
|
/// <summary>Verifies that a scripted reply completes a pending command invocation.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeAsync_WithScriptedReply_CompletesCommand()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.Ping),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
WorkerEnvelope commandEnvelope = await fakeWorker.ReadCommandAsync();
|
|
await fakeWorker.ReplyToCommandAsync(commandEnvelope);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(commandEnvelope.CorrelationId, reply.Reply.CorrelationId);
|
|
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
}
|
|
|
|
/// <summary>Verifies that scripted events are yielded in order through the event stream.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ReadEventsAsync_WithScriptedEvents_YieldsOrderedEvents()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
using CancellationTokenSource cancellationTokenSource = new(TestTimeout);
|
|
|
|
await using IAsyncEnumerator<WorkerEvent> events =
|
|
client.ReadEventsAsync(cancellationTokenSource.Token).GetAsyncEnumerator(cancellationTokenSource.Token);
|
|
|
|
await fakeWorker.EmitEventAsync(MxEventFamily.OnDataChange, cancellationTokenSource.Token);
|
|
await fakeWorker.EmitEventAsync(MxEventFamily.OperationComplete, cancellationTokenSource.Token);
|
|
|
|
Assert.True(await events.MoveNextAsync());
|
|
Assert.Equal((ulong)3, events.Current.Event.WorkerSequence);
|
|
Assert.Equal(MxEventFamily.OnDataChange, events.Current.Event.Family);
|
|
|
|
Assert.True(await events.MoveNextAsync());
|
|
Assert.Equal((ulong)4, events.Current.Event.WorkerSequence);
|
|
Assert.Equal(MxEventFamily.OperationComplete, events.Current.Event.Family);
|
|
}
|
|
|
|
/// <summary>Verifies that a scripted fault from the worker faults the client.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ReadLoop_WithScriptedFault_FaultsClient()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
await fakeWorker.EmitFaultAsync(
|
|
WorkerFaultCategory.MxaccessCommandFailed,
|
|
"scripted MXAccess command fault");
|
|
|
|
await WaitUntilAsync(
|
|
() => client.State == WorkerClientState.Faulted,
|
|
TestTimeout);
|
|
|
|
Assert.Equal(WorkerClientState.Faulted, client.State);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that sending a heartbeat updates the client heartbeat state. Uses a
|
|
/// <see cref="ManualTimeProvider"/> so the timestamp advance is deterministic rather
|
|
/// than relying on a wall-clock <c>Task.Delay</c> exceeding clock resolution.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SendHeartbeatAsync_UpdatesClientHeartbeatState()
|
|
{
|
|
ManualTimeProvider clock = new(DateTimeOffset.Parse("2026-05-18T12:00:00Z", System.Globalization.CultureInfo.InvariantCulture));
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient(timeProvider: clock);
|
|
await StartClientAsync(fakeWorker, client);
|
|
DateTimeOffset previousHeartbeat = client.LastHeartbeatAt;
|
|
|
|
clock.Advance(TimeSpan.FromSeconds(1));
|
|
await fakeWorker.SendHeartbeatAsync(
|
|
configureHeartbeat: heartbeat => heartbeat.WorkerProcessId = 2468);
|
|
|
|
await WaitUntilAsync(
|
|
() => client.ProcessId == 2468 && client.LastHeartbeatAt > previousHeartbeat,
|
|
TestTimeout);
|
|
|
|
Assert.Equal(WorkerClientState.Ready, client.State);
|
|
Assert.Equal(previousHeartbeat + TimeSpan.FromSeconds(1), client.LastHeartbeatAt);
|
|
}
|
|
|
|
/// <summary>Verifies that a hung worker times out pending command invocations.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task InvokeAsync_WithHungWorker_TimesOutPendingCommand()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.Ping),
|
|
TimeSpan.FromMilliseconds(50),
|
|
CancellationToken.None);
|
|
WorkerEnvelope commandEnvelope = await fakeWorker.ReadCommandAsync();
|
|
|
|
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
|
|
async () => await invokeTask.WaitAsync(TestTimeout));
|
|
|
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerCommand, commandEnvelope.BodyCase);
|
|
Assert.Equal(WorkerClientErrorCode.CommandTimeout, exception.ErrorCode);
|
|
}
|
|
|
|
/// <summary>Verifies that a malformed frame in the read loop faults the client.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ReadLoop_WithMalformedFrame_FaultsClient()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
await fakeWorker.WriteMalformedPayloadAsync(new byte[] { 0x08, 0x96, 0x01 });
|
|
|
|
await WaitUntilAsync(
|
|
() => client.State == WorkerClientState.Faulted,
|
|
TestTimeout);
|
|
|
|
Assert.Equal(WorkerClientState.Faulted, client.State);
|
|
}
|
|
|
|
/// <summary>Verifies that a shutdown acknowledgment from the worker closes the client.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ShutdownAsync_WithShutdownAck_ClosesClient()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task shutdownTask = client.ShutdownAsync(TestTimeout, CancellationToken.None);
|
|
WorkerEnvelope shutdownEnvelope = await fakeWorker.ReadShutdownAsync();
|
|
await fakeWorker.SendShutdownAckAsync();
|
|
await shutdownTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerShutdown, shutdownEnvelope.BodyCase);
|
|
Assert.Equal(WorkerClientState.Closed, client.State);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that RespondToControlCommandAsync echoes the Ping message back
|
|
/// in the DiagnosticMessage field, matching the real worker's ping reply shape.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task RespondToControlCommandAsync_Ping_EchoesMessageInDiagnostic()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.Ping, cmd => cmd.Ping = new PingCommand { Message = "hello-ping" }),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(MxCommandKind.Ping, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
Assert.Equal("hello-ping", reply.Reply.DiagnosticMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that RespondToControlCommandAsync returns a SessionStateReply
|
|
/// with state Ready for a GetSessionState command.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task RespondToControlCommandAsync_GetSessionState_ReturnsReadyState()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.GetSessionState),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(MxCommandKind.GetSessionState, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.Reply.SessionState);
|
|
Assert.Equal(SessionState.Ready, reply.Reply.SessionState.State);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that RespondToControlCommandAsync returns a WorkerInfoReply
|
|
/// with the fake worker's process ID, version, and MXAccess identifiers.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task RespondToControlCommandAsync_GetWorkerInfo_ReturnsFakeWorkerInfo()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.GetWorkerInfo),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(MxCommandKind.GetWorkerInfo, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.Reply.WorkerInfo);
|
|
Assert.Equal(FakeWorkerHarness.DefaultWorkerProcessId, reply.Reply.WorkerInfo.WorkerProcessId);
|
|
Assert.Equal("LMXProxy.LMXProxyServer.1", reply.Reply.WorkerInfo.MxaccessProgid);
|
|
Assert.Equal("{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}", reply.Reply.WorkerInfo.MxaccessClsid);
|
|
Assert.Equal("fake-worker", reply.Reply.WorkerInfo.WorkerVersion);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that RespondToControlCommandAsync returns an empty DrainEventsReply
|
|
/// for a DrainEvents command (the fake harness has no queued events).
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task RespondToControlCommandAsync_DrainEvents_ReturnsEmptyReply()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.DrainEvents, cmd => cmd.DrainEvents = new DrainEventsCommand { MaxEvents = 32 }),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(MxCommandKind.DrainEvents, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.Reply.DrainEvents);
|
|
Assert.Empty(reply.Reply.DrainEvents.Events);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that RespondToControlCommandAsync for ShutdownWorker sends an OK
|
|
/// reply followed by a WorkerShutdownAck, which closes the client.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task RespondToControlCommandAsync_ShutdownWorker_SendsReplyThenAck()
|
|
{
|
|
await using FakeWorkerHarness fakeWorker = await FakeWorkerHarness.CreateConnectedPairAsync();
|
|
await using WorkerClient client = fakeWorker.CreateClient();
|
|
await StartClientAsync(fakeWorker, client);
|
|
|
|
// ShutdownAsync triggers a WorkerShutdown envelope (not WorkerCommand),
|
|
// so we directly invoke ShutdownWorker as a control command via InvokeAsync.
|
|
Task<WorkerCommandReply> invokeTask = client.InvokeAsync(
|
|
CreateCommand(MxCommandKind.ShutdownWorker, cmd => cmd.ShutdownWorker = new ShutdownWorkerCommand()),
|
|
TestTimeout,
|
|
CancellationToken.None);
|
|
|
|
// The harness reads the ShutdownWorker WorkerCommand and replies with
|
|
// OK + ShutdownAck — the WorkerClient's read loop processes the ack and
|
|
// transitions to Closed.
|
|
await fakeWorker.RespondToControlCommandAsync().WaitAsync(TestTimeout);
|
|
|
|
WorkerCommandReply reply = await invokeTask.WaitAsync(TestTimeout);
|
|
|
|
Assert.Equal(MxCommandKind.ShutdownWorker, reply.Reply.Kind);
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.Reply.ProtocolStatus.Code);
|
|
|
|
await WaitUntilAsync(() => client.State == WorkerClientState.Closed, TestTimeout);
|
|
Assert.Equal(WorkerClientState.Closed, client.State);
|
|
}
|
|
|
|
private static async Task StartClientAsync(
|
|
FakeWorkerHarness fakeWorker,
|
|
WorkerClient client)
|
|
{
|
|
Task startTask = client.StartAsync(CancellationToken.None);
|
|
await fakeWorker.CompleteStartupAsync().ConfigureAwait(false);
|
|
await startTask.WaitAsync(TestTimeout).ConfigureAwait(false);
|
|
}
|
|
|
|
private static WorkerCommand CreateCommand(
|
|
MxCommandKind kind,
|
|
Action<MxCommand>? configure = null)
|
|
{
|
|
MxCommand command = new() { Kind = kind };
|
|
configure?.Invoke(command);
|
|
return new WorkerCommand { Command = command };
|
|
}
|
|
|
|
private static async Task WaitUntilAsync(
|
|
Func<bool> predicate,
|
|
TimeSpan timeout)
|
|
{
|
|
using CancellationTokenSource cancellationTokenSource = new(timeout);
|
|
while (!predicate())
|
|
{
|
|
await Task.Delay(TimeSpan.FromMilliseconds(10), cancellationTokenSource.Token);
|
|
}
|
|
}
|
|
|
|
}
|