Retain fake worker fault diagnostics in test fixtures
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m20s
ci / java (push) Successful in 2m19s
ci / windows-x86 (pull_request) Successful in 1m20s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m23s
ci / portable (push) Successful in 9m10s
ci / portable (pull_request) Successful in 9m2s
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m20s
ci / java (push) Successful in 2m19s
ci / windows-x86 (pull_request) Successful in 1m20s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m23s
ci / portable (push) Successful in 9m10s
ci / portable (pull_request) Successful in 9m2s
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.MxGateway.Server.Workers;
|
||||
using ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Workers;
|
||||
|
||||
/// <summary>Protects bounded teardown, fault observation, and safe diagnostic output.</summary>
|
||||
public sealed class FakeWorkerDiagnosticsTests
|
||||
{
|
||||
/// <summary>Repeated gateway logs remain bounded and never format sensitive state or messages.</summary>
|
||||
[Fact]
|
||||
public void Log_WhenFaultsRepeat_ReportsOnlyBoundedMetadata()
|
||||
{
|
||||
using FakeWorkerDiagnostics diagnostics = new();
|
||||
List<string> output = [];
|
||||
diagnostics.SetOutput(output.Add);
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
diagnostics.Log(LogLevel.Warning, new EventId(1), "secret-command",
|
||||
new WorkerFrameProtocolException(WorkerFrameProtocolErrorCode.InvalidEnvelope, "secret-item"),
|
||||
(state, exception) => $"{state} {exception}");
|
||||
}
|
||||
|
||||
Assert.NotEmpty(output);
|
||||
Assert.InRange(output.Count, 1, 16);
|
||||
Assert.All(output, line =>
|
||||
{
|
||||
Assert.Contains("type=WorkerFrameProtocolException", line);
|
||||
Assert.Contains("code=InvalidEnvelope", line);
|
||||
Assert.DoesNotContain("secret", line);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>A late fault is observed after timeout without calling the completed output sink.</summary>
|
||||
/// <returns>The asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DrainAsync_WhenWorkerOutlivesBudget_ObservesLateFaultWithoutLateOutput()
|
||||
{
|
||||
FakeWorkerDiagnostics diagnostics = new();
|
||||
List<string> output = [];
|
||||
diagnostics.SetOutput(output.Add);
|
||||
TaskCompletionSource worker = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
diagnostics.Track(worker.Task);
|
||||
await diagnostics.DrainAsync(TimeSpan.Zero);
|
||||
Assert.Contains(output, line => line.Contains("source=teardown") && line.Contains("type=TimeoutException"));
|
||||
int beforeFault = output.Count;
|
||||
worker.SetException(new WorkerFrameProtocolException(WorkerFrameProtocolErrorCode.InvalidEnvelope, "secret-item"));
|
||||
await diagnostics.DrainAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal(beforeFault, output.Count);
|
||||
}
|
||||
|
||||
/// <summary>Expected cancellation does not add fault diagnostics.</summary>
|
||||
/// <returns>The asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DrainAsync_WhenWorkerIsCancelled_DoesNotReportFault()
|
||||
{
|
||||
FakeWorkerDiagnostics diagnostics = new();
|
||||
List<string> output = [];
|
||||
diagnostics.SetOutput(output.Add);
|
||||
diagnostics.Track(Task.FromCanceled(new CancellationToken(true)));
|
||||
await diagnostics.DrainAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
/// <summary>An unavailable output helper cannot fault the observer or replace the primary failure.</summary>
|
||||
/// <returns>The asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task DrainAsync_WhenOutputThrows_PreservesPrimaryFailure()
|
||||
{
|
||||
InvalidOperationException primary = new("primary");
|
||||
FakeWorkerDiagnostics diagnostics = new();
|
||||
diagnostics.SetOutput(_ => throw new InvalidOperationException("completed output"));
|
||||
Exception? failure = await Record.ExceptionAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
throw primary;
|
||||
}
|
||||
finally
|
||||
{
|
||||
diagnostics.Track(Task.FromException(new WorkerFrameProtocolException(
|
||||
WorkerFrameProtocolErrorCode.InvalidEnvelope, "secret-item")));
|
||||
await diagnostics.DrainAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
});
|
||||
Assert.Same(primary, failure);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,11 @@ public sealed class GatedWorkerEnvelopeReaderTests
|
||||
{
|
||||
/// <summary>A gate wakeup must preserve a partially consumed frame and the following frame.</summary>
|
||||
/// <returns>The asynchronous test operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReadOrGateAsync_WhenGateOpensDuringSegmentedFrame_PreservesBothFrames()
|
||||
/// <param name="watchGate">Whether the emission gate is watched (false before Advise).</param>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task ReadOrGateAsync_WhenGateOpensDuringSegmentedFrame_PreservesBothFrames(bool watchGate)
|
||||
{
|
||||
WorkerFrameProtocolOptions options = new("segmented-session");
|
||||
WorkerEnvelope first = new()
|
||||
@@ -38,15 +41,27 @@ public sealed class GatedWorkerEnvelopeReaderTests
|
||||
|
||||
// The length and first payload byte are consumed before the remaining bytes exist.
|
||||
await pipe.Writer.WriteAsync(bytes.AsMemory(0, 5));
|
||||
Task<WorkerEnvelope?> pending = selector.ReadOrGateAsync();
|
||||
Task<WorkerEnvelope?> pending = selector.ReadOrGateAsync(watchGate);
|
||||
Assert.False(pending.IsCompleted);
|
||||
gate.Release();
|
||||
Assert.Null(await pending);
|
||||
if (watchGate)
|
||||
{
|
||||
Assert.Null(await pending.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Explicit deadline probe: pre-Advise framing must remain pending beyond the old
|
||||
// 50ms cancellation budget, even with the emission gate already open.
|
||||
Task oldReadBudget = Task.Delay(TimeSpan.FromMilliseconds(100), lifetime.Token);
|
||||
Assert.Same(oldReadBudget, await Task.WhenAny(pending, oldReadBudget));
|
||||
await oldReadBudget;
|
||||
}
|
||||
|
||||
await pipe.Writer.WriteAsync(bytes.AsMemory(5));
|
||||
await pipe.Writer.CompleteAsync();
|
||||
|
||||
Assert.Equal(first, await selector.ReadOrGateAsync());
|
||||
Assert.Equal(second, await selector.ReadOrGateAsync());
|
||||
Assert.Equal(first, watchGate ? await selector.ReadOrGateAsync() : await pending.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
Assert.Equal(second, await selector.ReadOrGateAsync(watchGate));
|
||||
}
|
||||
|
||||
/// <summary>Disposal or lifetime cancellation drains the retained read and gate wait.</summary>
|
||||
|
||||
Reference in New Issue
Block a user