rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Ipc;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Ipc;
|
||||
|
||||
public sealed class WorkerFrameProtocolTests
|
||||
{
|
||||
private const string SessionId = "session-1";
|
||||
private const string Nonce = "nonce-secret";
|
||||
|
||||
/// <summary>Verifies that valid envelopes round-trip through write and read.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new();
|
||||
WorkerEnvelope original = CreateGatewayHelloEnvelope();
|
||||
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
await writer.WriteAsync(original);
|
||||
stream.Position = 0;
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerEnvelope parsed = await reader.ReadAsync();
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that wrong protocol version throws mismatch error.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
|
||||
envelope.ProtocolVersion++;
|
||||
using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(envelope));
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.ProtocolVersionMismatch, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that wrong session ID throws mismatch error.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
|
||||
envelope.SessionId = "different-session";
|
||||
using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(envelope));
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.SessionMismatch, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a frame whose length prefix is zero is rejected before the
|
||||
/// payload buffer is allocated. <c>docs/WorkerFrameProtocol.md</c> states the
|
||||
/// reader rejects zero-length payloads as a malformed-length error. The
|
||||
/// length prefix is the leading four bytes of the stream, so a four-zero-byte
|
||||
/// stream is exactly a frame declaring a zero-length payload.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithZeroLengthPayload_ThrowsMalformedLength()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new(new byte[sizeof(uint)]);
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.MalformedLength, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a frame whose length prefix exceeds the configured maximum
|
||||
/// is rejected before the payload buffer is allocated. <c>docs/WorkerFrameProtocol.md</c>
|
||||
/// states the reader rejects oversized payloads as a message-too-large error.
|
||||
/// A small maximum is configured so the rejection is asserted without
|
||||
/// allocating a multi-megabyte buffer.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithPayloadAboveConfiguredMaximum_ThrowsMessageTooLarge()
|
||||
{
|
||||
const int maxMessageBytes = 64;
|
||||
WorkerFrameProtocolOptions options = new(
|
||||
SessionId,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce,
|
||||
maxMessageBytes);
|
||||
byte[] frame = new byte[sizeof(uint)];
|
||||
WorkerFrameTestHelpers.WriteUInt32LittleEndian(frame, maxMessageBytes + 1);
|
||||
using MemoryStream stream = new(frame);
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that malformed payload throws invalid envelope error.</summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new(WorkerFrameTestHelpers.CreateFrame(new byte[] { 0x80 }));
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker.Tests-021 (a): pins the <c>EndOfStream</c> branch of
|
||||
/// <c>WorkerFrameReader.ReadExactlyOrThrowAsync</c>. The gateway
|
||||
/// closing its end of the pipe during a partial-frame read is the
|
||||
/// most common production transport failure; the reader must
|
||||
/// surface this as <c>WorkerFrameProtocolErrorCode.EndOfStream</c>
|
||||
/// so the worker session can fault deterministically rather than
|
||||
/// spinning on a partial buffer. The stream here declares a 100-byte
|
||||
/// payload but only supplies 50 bytes, so the inner read loop sees
|
||||
/// <c>bytesRead == 0</c> mid-frame.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WhenStreamEndsMidFrame_ThrowsEndOfStream()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
byte[] frame = new byte[sizeof(uint) + 50];
|
||||
WorkerFrameTestHelpers.WriteUInt32LittleEndian(frame, 100);
|
||||
using MemoryStream stream = new(frame);
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await reader.ReadAsync());
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.EndOfStream, exception.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker.Tests-021 (b): pins the writer-side
|
||||
/// <c>MessageTooLarge</c> branch. A session that constructs an
|
||||
/// envelope whose serialised size exceeds <c>MaxMessageBytes</c>
|
||||
/// must be rejected by the writer before any bytes are sent down
|
||||
/// the pipe, so a misbehaving producer cannot push the receiver
|
||||
/// past its bounds. A small <c>MaxMessageBytes</c> is configured
|
||||
/// so a modest <c>GatewayHello</c> payload — with its nonce
|
||||
/// padded out to several hundred bytes — exceeds the limit
|
||||
/// without allocating anything large.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_WithEnvelopeAboveConfiguredMaximum_ThrowsMessageTooLarge()
|
||||
{
|
||||
const int maxMessageBytes = 64;
|
||||
WorkerFrameProtocolOptions options = new(
|
||||
SessionId,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce,
|
||||
maxMessageBytes);
|
||||
using MemoryStream stream = new();
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
|
||||
WorkerEnvelope envelope = CreateGatewayHelloEnvelope();
|
||||
envelope.GatewayHello.GatewayVersion = new string('x', 1024);
|
||||
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await writer.WriteAsync(envelope));
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode);
|
||||
Assert.Equal(0, stream.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker.Tests-021 (c): documents that the writer-side
|
||||
/// <c>InvalidEnvelope</c> branch (raised when
|
||||
/// <c>WorkerEnvelope.CalculateSize()</c> returns 0) is unreachable
|
||||
/// through public API. <c>WorkerEnvelopeValidator.Validate</c> (run
|
||||
/// before the size check in <c>WorkerFrameWriter.WriteAsync</c>)
|
||||
/// rejects any envelope whose <c>BodyCase</c> is <c>None</c> with
|
||||
/// <c>InvalidEnvelope</c>; a body-less envelope is therefore
|
||||
/// intercepted before the empty-payload branch can fire. Any
|
||||
/// envelope carrying a typed body serialises at least the field
|
||||
/// tag bytes, so <c>CalculateSize()</c> is strictly positive. This
|
||||
/// test exercises the body-less path and asserts the same
|
||||
/// <c>InvalidEnvelope</c> error code reaches the caller, pinning
|
||||
/// the contract that "no body" is rejected before any size check.
|
||||
/// The defensive zero-length branch in <c>WriteAsync</c> is left
|
||||
/// in place because the cost is one comparison and removing it
|
||||
/// would weaken the writer against future serialisation
|
||||
/// regressions; this test makes its rationale visible.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_WithEmptyEnvelope_ThrowsInvalidEnvelopeFromValidator()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new();
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
|
||||
WorkerEnvelope envelope = new()
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = SessionId,
|
||||
Sequence = 1,
|
||||
// No body — BodyCase == None, validator rejects.
|
||||
};
|
||||
|
||||
WorkerFrameProtocolException exception =
|
||||
await Assert.ThrowsAsync<WorkerFrameProtocolException>(
|
||||
async () => await writer.WriteAsync(envelope));
|
||||
|
||||
Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode);
|
||||
Assert.Equal(0, stream.Length);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent writes produce complete serialized frames.</summary>
|
||||
[Fact]
|
||||
public async Task WriteAsync_WithConcurrentCalls_SerializesCompleteFrames()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new();
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
|
||||
await Task.WhenAll(
|
||||
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 1)),
|
||||
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 2)),
|
||||
writer.WriteAsync(CreateGatewayHelloEnvelope(sequence: 3)));
|
||||
|
||||
stream.Position = 0;
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
|
||||
WorkerEnvelope first = await reader.ReadAsync();
|
||||
WorkerEnvelope second = await reader.ReadAsync();
|
||||
WorkerEnvelope third = await reader.ReadAsync();
|
||||
|
||||
Assert.Equal(new ulong[] { 1, 2, 3 }, new[] { first.Sequence, second.Sequence, third.Sequence }.OrderBy(sequence => sequence));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker-009 regression: the reader rents its payload buffer from a
|
||||
/// shared pool, so a rented buffer can be larger than the current frame
|
||||
/// and may carry bytes from a previous, larger frame. Reading frames of
|
||||
/// differing sizes back-to-back through one reader must parse each frame
|
||||
/// using only its own payload length, never trailing pooled bytes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReadAsync_WithVaryingFrameSizes_ParsesEachFrameExactly()
|
||||
{
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new();
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
|
||||
// A large-payload frame followed by a small-payload frame: if the
|
||||
// reader reused a pooled buffer without honouring the second frame's
|
||||
// length, the small frame would parse with stale trailing bytes.
|
||||
WorkerEnvelope large = CreateGatewayHelloEnvelope(sequence: 1);
|
||||
large.GatewayHello.GatewayVersion = new string('x', 4096);
|
||||
WorkerEnvelope small = CreateGatewayHelloEnvelope(sequence: 2);
|
||||
|
||||
await writer.WriteAsync(large);
|
||||
await writer.WriteAsync(small);
|
||||
stream.Position = 0;
|
||||
|
||||
WorkerFrameReader reader = new(stream, options);
|
||||
WorkerEnvelope firstParsed = await reader.ReadAsync();
|
||||
WorkerEnvelope secondParsed = await reader.ReadAsync();
|
||||
|
||||
Assert.Equal(large, firstParsed);
|
||||
Assert.Equal(small, secondParsed);
|
||||
}
|
||||
|
||||
private static WorkerFrameProtocolOptions CreateOptions()
|
||||
{
|
||||
return new WorkerFrameProtocolOptions(
|
||||
SessionId,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce);
|
||||
}
|
||||
|
||||
private static WorkerEnvelope CreateGatewayHelloEnvelope(ulong sequence = 1)
|
||||
{
|
||||
return new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = SessionId,
|
||||
Sequence = sequence,
|
||||
GatewayHello = new GatewayHello
|
||||
{
|
||||
SupportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce = Nonce,
|
||||
GatewayVersion = "test-gateway",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Bootstrap;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Ipc;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Ipc;
|
||||
|
||||
public sealed class WorkerPipeClientTests
|
||||
{
|
||||
/// <summary>Verifies that worker client connects and completes handshake.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_ConnectsToPipeAndCompletesHandshake()
|
||||
{
|
||||
string pipeName = $"mxaccess-gateway-test-{Guid.NewGuid():N}";
|
||||
WorkerOptions workerOptions = new(
|
||||
"session-1",
|
||||
pipeName,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
"nonce-secret");
|
||||
WorkerFrameProtocolOptions frameOptions = new(workerOptions);
|
||||
|
||||
using NamedPipeServerStream server = new(
|
||||
pipeName,
|
||||
PipeDirection.InOut,
|
||||
1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
WorkerPipeClient client = new(
|
||||
connectTimeoutMilliseconds: 5000,
|
||||
(stream, options) => CreateSession(stream, options));
|
||||
Task clientTask = client.RunAsync(workerOptions);
|
||||
|
||||
await Task.Factory.FromAsync(server.BeginWaitForConnection, server.EndWaitForConnection, null);
|
||||
|
||||
WorkerFrameReader reader = new(server, frameOptions);
|
||||
WorkerFrameWriter writer = new(server, frameOptions);
|
||||
|
||||
await writer.WriteAsync(new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 1,
|
||||
GatewayHello = new GatewayHello
|
||||
{
|
||||
SupportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce = "nonce-secret",
|
||||
GatewayVersion = "test-gateway",
|
||||
},
|
||||
});
|
||||
|
||||
WorkerEnvelope hello = await reader.ReadAsync();
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHello, hello.BodyCase);
|
||||
Assert.Equal("nonce-secret", hello.WorkerHello.Nonce);
|
||||
|
||||
WorkerEnvelope ready = await reader.ReadAsync();
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerReady, ready.BodyCase);
|
||||
|
||||
await writer.WriteAsync(new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 2,
|
||||
WorkerShutdown = new WorkerShutdown
|
||||
{
|
||||
GracePeriod = Duration.FromTimeSpan(TimeSpan.FromSeconds(1)),
|
||||
Reason = "test-complete",
|
||||
},
|
||||
});
|
||||
|
||||
WorkerEnvelope shutdownAck = await ReadUntilAsync(
|
||||
reader,
|
||||
WorkerEnvelope.BodyOneofCase.WorkerShutdownAck);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerShutdownAck, shutdownAck.BodyCase);
|
||||
await clientTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that worker client retries until pipe server becomes available.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_RetriesUntilPipeServerAppears()
|
||||
{
|
||||
string pipeName = $"mxaccess-gateway-test-{Guid.NewGuid():N}";
|
||||
WorkerOptions workerOptions = new(
|
||||
"session-1",
|
||||
pipeName,
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
"nonce-secret");
|
||||
WorkerFrameProtocolOptions frameOptions = new(workerOptions);
|
||||
|
||||
WorkerPipeClient client = new(
|
||||
logger: null,
|
||||
connectTimeoutMilliseconds: 1000,
|
||||
connectAttemptTimeoutMilliseconds: 50,
|
||||
(stream, options, _) => CreateSession(stream, options));
|
||||
Task clientTask = client.RunAsync(workerOptions);
|
||||
|
||||
await Task.Delay(150);
|
||||
|
||||
using NamedPipeServerStream server = new(
|
||||
pipeName,
|
||||
PipeDirection.InOut,
|
||||
1,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
await Task.Factory.FromAsync(server.BeginWaitForConnection, server.EndWaitForConnection, null);
|
||||
|
||||
WorkerFrameReader reader = new(server, frameOptions);
|
||||
WorkerFrameWriter writer = new(server, frameOptions);
|
||||
|
||||
await writer.WriteAsync(CreateGatewayHello());
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHello, (await reader.ReadAsync()).BodyCase);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerReady, (await reader.ReadAsync()).BodyCase);
|
||||
await writer.WriteAsync(CreateShutdown());
|
||||
|
||||
Assert.Equal(
|
||||
WorkerEnvelope.BodyOneofCase.WorkerShutdownAck,
|
||||
(await ReadUntilAsync(reader, WorkerEnvelope.BodyOneofCase.WorkerShutdownAck)).BodyCase);
|
||||
await clientTask;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that worker client throws timeout if pipe never appears.</summary>
|
||||
[Fact]
|
||||
public async Task RunAsync_WhenPipeNeverAppears_ThrowsTimeoutException()
|
||||
{
|
||||
WorkerOptions workerOptions = new(
|
||||
"session-1",
|
||||
$"mxaccess-gateway-test-{Guid.NewGuid():N}",
|
||||
GatewayContractInfo.WorkerProtocolVersion,
|
||||
"nonce-secret");
|
||||
|
||||
WorkerPipeClient client = new(
|
||||
logger: null,
|
||||
connectTimeoutMilliseconds: 100,
|
||||
connectAttemptTimeoutMilliseconds: 50,
|
||||
(stream, options, _) => CreateSession(stream, options));
|
||||
|
||||
await Assert.ThrowsAsync<TimeoutException>(async () => await client.RunAsync(workerOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads frames until one matching the expected body case is found,
|
||||
/// skipping interleaved heartbeats (the first heartbeat is emitted
|
||||
/// immediately on entering the heartbeat loop — see Worker-002).
|
||||
/// </summary>
|
||||
private static async Task<WorkerEnvelope> ReadUntilAsync(
|
||||
WorkerFrameReader reader,
|
||||
WorkerEnvelope.BodyOneofCase expectedBody)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
WorkerEnvelope envelope = await reader.ReadAsync();
|
||||
if (envelope.BodyCase == expectedBody)
|
||||
{
|
||||
return envelope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkerPipeSession CreateSession(
|
||||
Stream stream,
|
||||
WorkerFrameProtocolOptions options)
|
||||
{
|
||||
return new WorkerPipeSession(
|
||||
new WorkerFrameReader(stream, options),
|
||||
new WorkerFrameWriter(stream, options),
|
||||
options,
|
||||
() => 1234,
|
||||
new WorkerPipeSessionOptions
|
||||
{
|
||||
HeartbeatInterval = TimeSpan.FromSeconds(30),
|
||||
HeartbeatGrace = TimeSpan.FromSeconds(30),
|
||||
},
|
||||
() => new FakeRuntimeSession());
|
||||
}
|
||||
|
||||
private static WorkerEnvelope CreateGatewayHello()
|
||||
{
|
||||
return new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 1,
|
||||
GatewayHello = new GatewayHello
|
||||
{
|
||||
SupportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
Nonce = "nonce-secret",
|
||||
GatewayVersion = "test-gateway",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static WorkerEnvelope CreateShutdown()
|
||||
{
|
||||
return new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 2,
|
||||
WorkerShutdown = new WorkerShutdown
|
||||
{
|
||||
GracePeriod = Duration.FromTimeSpan(TimeSpan.FromSeconds(1)),
|
||||
Reason = "test-complete",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user