test: guarantee driver fixture cleanup without masking failures
This commit is contained in:
+76
-10
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using ZB.MOM.WW.OtOpcUa.Tests;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
@@ -14,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
||||
/// unit test alongside them per Driver.Modbus-012.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ModbusLifecycleHygieneTests
|
||||
public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
|
||||
{
|
||||
private sealed class FakeTransport : IModbusTransport
|
||||
{
|
||||
@@ -528,7 +529,7 @@ public sealed class ModbusLifecycleHygieneTests
|
||||
|
||||
/// <summary>Verifies that DecodeBitArray rejects an empty bitmap with InvalidDataException.</summary>
|
||||
[Fact]
|
||||
public void DecodeBitArray_rejects_an_empty_bitmap_with_InvalidDataException()
|
||||
public async Task DecodeBitArray_rejects_an_empty_bitmap_with_InvalidDataException()
|
||||
{
|
||||
var decode = typeof(ModbusDriver).GetMethod(
|
||||
"DecodeBitArray", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
@@ -542,11 +543,49 @@ public sealed class ModbusLifecycleHygieneTests
|
||||
RawTags = ModbusRawTags.Entries([new ModbusTagDefinition("Coil", ModbusRegion.Coils, 0, ModbusDataType.Bool)]),
|
||||
};
|
||||
var drv = new ModbusDriver(opts, "modbus-1", _ => fake);
|
||||
drv.InitializeAsync("{}", CancellationToken.None).GetAwaiter().GetResult();
|
||||
await FixtureCleanup.RunAsync(async () =>
|
||||
{
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var r = drv.ReadAsync(["Coil"], CancellationToken.None).GetAwaiter().GetResult();
|
||||
// The empty-bitmap guard surfaces via the BadCommunicationError catch-all.
|
||||
r[0].StatusCode.ShouldBe(0x80050000u);
|
||||
var r = await drv.ReadAsync(["Coil"], CancellationToken.None);
|
||||
// The empty-bitmap guard surfaces via the BadCommunicationError catch-all.
|
||||
r[0].StatusCode.ShouldBe(0x80050000u);
|
||||
}, () => drv.ShutdownAsync(CancellationToken.None),
|
||||
failure => output.WriteLine($"Modbus empty bitmap cleanup failed: {failure}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Body_failure_after_probe_request_cancels_it_and_disposes_transport()
|
||||
{
|
||||
var fake = new EmptyBitTransport();
|
||||
var opts = new ModbusDriverOptions
|
||||
{
|
||||
Host = "fake",
|
||||
RawTags = ModbusRawTags.Entries([new ModbusTagDefinition("Coil", ModbusRegion.Coils, 0, ModbusDataType.Bool)]),
|
||||
};
|
||||
var drv = new ModbusDriver(opts, "modbus-1", _ => fake);
|
||||
var sentinel = new InvalidOperationException("body sentinel");
|
||||
try
|
||||
{
|
||||
var actual = await Should.ThrowAsync<InvalidOperationException>(() => FixtureCleanup.RunAsync(async () =>
|
||||
{
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var result = await drv.ReadAsync(["Coil"], CancellationToken.None);
|
||||
result[0].StatusCode.ShouldBe(0x80050000u);
|
||||
fake.CoilRequestCount.ShouldBe(1);
|
||||
await fake.ProbeEntered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
fake.ProbeToken.IsCancellationRequested.ShouldBeFalse();
|
||||
fake.DisposeCount.ShouldBe(0);
|
||||
throw sentinel;
|
||||
}, () => drv.ShutdownAsync(CancellationToken.None),
|
||||
failure => output.WriteLine($"Modbus probe fixture cleanup failed: {failure}")));
|
||||
actual.ShouldBeSameAs(sentinel);
|
||||
fake.ProbeToken.IsCancellationRequested.ShouldBeTrue();
|
||||
await fake.ProbeCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
await fake.ProbeRequestCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
fake.DisposeCount.ShouldBe(1);
|
||||
}
|
||||
finally { fake.ReleaseProbe.TrySetResult(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -556,6 +595,14 @@ public sealed class ModbusLifecycleHygieneTests
|
||||
/// </summary>
|
||||
private sealed class EmptyBitTransport : IModbusTransport
|
||||
{
|
||||
public TaskCompletionSource ProbeEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource ProbeCancelled { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource ProbeRequestCompleted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource ReleaseProbe { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public CancellationToken ProbeToken { get; private set; }
|
||||
public int DisposeCount { get; private set; }
|
||||
public int CoilRequestCount { get; private set; }
|
||||
|
||||
/// <summary>Returns a completed task without performing any connection.</summary>
|
||||
/// <param name="ct">The cancellation token for the operation.</param>
|
||||
public Task ConnectAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
@@ -563,10 +610,29 @@ public sealed class ModbusLifecycleHygieneTests
|
||||
/// <param name="unitId">The Modbus unit ID.</param>
|
||||
/// <param name="pdu">The protocol data unit being sent.</param>
|
||||
/// <param name="ct">The cancellation token for the operation.</param>
|
||||
public Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
=> Task.FromResult(new byte[] { pdu[0], 0 });
|
||||
/// <summary>Completes the disposal without doing any work.</summary>
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
public async Task<byte[]> SendAsync(byte unitId, byte[] pdu, CancellationToken ct)
|
||||
{
|
||||
if (pdu[0] == 0x03)
|
||||
{
|
||||
ProbeToken = ct;
|
||||
ProbeEntered.TrySetResult();
|
||||
try { await ReleaseProbe.Task.WaitAsync(ct); }
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
ProbeCancelled.TrySetResult();
|
||||
throw;
|
||||
}
|
||||
finally { ProbeRequestCompleted.TrySetResult(); }
|
||||
}
|
||||
if (pdu[0] == 0x01) CoilRequestCount++;
|
||||
return new byte[] { pdu[0], 0 };
|
||||
}
|
||||
/// <summary>Records transport disposal.</summary>
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
DisposeCount++;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Finding -003 (volatile _health) --------------------
|
||||
|
||||
Reference in New Issue
Block a user