test: remaining xUnit1051/1031/1030, CS8602/8604, CS0618, CS0219, CS0649, NU1510 fixes (lmxopcua#725)
v2-ci / unit-tests (push) Has been cancelled

Awaited deadline oracles get the runner token; sync-over-async tests made async;
nullable child models asserted via ShouldNotBeNull() return values; obsolete SDK
ctors replaced with the telemetry overloads; dead locals/fixture and the redundant
DI PackageReference removed. No suppressions.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 13:22:34 -05:00
parent 1d7a4b7a53
commit b8202e1147
27 changed files with 84 additions and 92 deletions
@@ -15,7 +15,7 @@ public class DefaultSubscriptionAdapterTests
[Fact]
public async Task DeleteAsync_detaches_the_subscription_from_its_session_once()
{
var subscription = new Subscription();
var subscription = new Subscription(ClientTelemetry.Context);
var detached = new List<Subscription>();
var adapter = new DefaultSubscriptionAdapter(subscription, (s, _) =>
{
@@ -34,7 +34,7 @@ public class DefaultSubscriptionAdapterTests
public void Dispose_detaches_the_subscription_from_its_session()
{
var detached = 0;
var adapter = new DefaultSubscriptionAdapter(new Subscription(), (_, _) =>
var adapter = new DefaultSubscriptionAdapter(new Subscription(ClientTelemetry.Context), (_, _) =>
{
Interlocked.Increment(ref detached);
return Task.CompletedTask;
@@ -49,7 +49,7 @@ public class DefaultSubscriptionAdapterTests
[Fact]
public async Task A_failing_detach_does_not_throw_from_DeleteAsync()
{
var adapter = new DefaultSubscriptionAdapter(new Subscription(),
var adapter = new DefaultSubscriptionAdapter(new Subscription(ClientTelemetry.Context),
(_, _) => throw new InvalidOperationException("boom"));
await Should.NotThrowAsync(() => adapter.DeleteAsync(TestContext.Current.CancellationToken));
@@ -860,7 +860,6 @@ public class OpcUaClientServiceTests : IDisposable
[Fact]
public async Task GetRedundancyInfoAsync_MissingOptionalArrays_ReturnsGracefully()
{
var readCallIndex = 0;
var session = new FakeSessionAdapter
{
ReadResponseFunc = nodeId =>
@@ -1554,7 +1553,7 @@ public class OpcUaClientServiceTests : IDisposable
{
await _service.SubscribeAsync(nodeId);
await _service.UnsubscribeAsync(nodeId);
}));
}, TestContext.Current.CancellationToken));
}
// No InvalidOperationException from concurrent Dictionary mutation.
@@ -1634,7 +1633,6 @@ public class OpcUaClientServiceTests : IDisposable
var alarmFakeSub = new FakeSubscriptionAdapter();
// First CreateSubscription call → data subscription; second → alarm subscription.
var callIndex = 0;
var session = new FakeSessionAdapter
{
NextSubscription = dataFakeSub
@@ -94,7 +94,7 @@ public class MainWindowViewModelTests
{
await _vm.ConnectCommand.ExecuteAsync(null);
_vm.BrowseTree.RootNodes.Count.ShouldBe(1);
_vm.BrowseTree.ShouldNotBeNull().RootNodes.Count.ShouldBe(1);
_vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root");
}
@@ -148,7 +148,7 @@ public class MainWindowViewModelTests
_vm.SessionLabel.ShouldBe(string.Empty);
_vm.RedundancyInfo.ShouldBeNull();
_vm.BrowseTree.RootNodes.ShouldBeEmpty();
_vm.BrowseTree.ShouldNotBeNull().RootNodes.ShouldBeEmpty();
_vm.SubscriptionCount.ShouldBe(0);
}
@@ -176,11 +176,11 @@ public class MainWindowViewModelTests
{
await _vm.ConnectCommand.ExecuteAsync(null);
var node = _vm.BrowseTree.RootNodes[0];
var node = _vm.BrowseTree.ShouldNotBeNull().RootNodes[0];
_vm.SelectedTreeNode = node;
_vm.ReadWrite.SelectedNodeId.ShouldBe(node.NodeId);
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
_vm.ReadWrite.ShouldNotBeNull().SelectedNodeId.ShouldBe(node.NodeId);
_vm.History.ShouldNotBeNull().SelectedNodeId.ShouldBe(node.NodeId);
}
/// <summary>
@@ -191,10 +191,10 @@ public class MainWindowViewModelTests
{
await _vm.ConnectCommand.ExecuteAsync(null);
_vm.ReadWrite.IsConnected.ShouldBeTrue();
_vm.Subscriptions.IsConnected.ShouldBeTrue();
_vm.Alarms.IsConnected.ShouldBeTrue();
_vm.History.IsConnected.ShouldBeTrue();
_vm.ReadWrite.ShouldNotBeNull().IsConnected.ShouldBeTrue();
_vm.Subscriptions.ShouldNotBeNull().IsConnected.ShouldBeTrue();
_vm.Alarms.ShouldNotBeNull().IsConnected.ShouldBeTrue();
_vm.History.ShouldNotBeNull().IsConnected.ShouldBeTrue();
}
/// <summary>
@@ -206,10 +206,10 @@ public class MainWindowViewModelTests
await _vm.ConnectCommand.ExecuteAsync(null);
await _vm.DisconnectCommand.ExecuteAsync(null);
_vm.ReadWrite.IsConnected.ShouldBeFalse();
_vm.Subscriptions.IsConnected.ShouldBeFalse();
_vm.Alarms.IsConnected.ShouldBeFalse();
_vm.History.IsConnected.ShouldBeFalse();
_vm.ReadWrite.ShouldNotBeNull().IsConnected.ShouldBeFalse();
_vm.Subscriptions.ShouldNotBeNull().IsConnected.ShouldBeFalse();
_vm.Alarms.ShouldNotBeNull().IsConnected.ShouldBeFalse();
_vm.History.ShouldNotBeNull().IsConnected.ShouldBeFalse();
}
/// <summary>
@@ -355,7 +355,7 @@ public class MainWindowViewModelTests
await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null);
_vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty();
_vm.Subscriptions.ShouldNotBeNull().ActiveSubscriptions.ShouldBeEmpty();
}
/// <summary>
@@ -366,12 +366,12 @@ public class MainWindowViewModelTests
{
await _vm.ConnectCommand.ExecuteAsync(null);
var node = _vm.BrowseTree.RootNodes[0];
var node = _vm.BrowseTree.ShouldNotBeNull().RootNodes[0];
_vm.SelectedTreeNodes.Add(node);
_vm.ViewHistoryForSelectedNodeCommand.Execute(null);
_vm.History.SelectedNodeId.ShouldBe(node.NodeId);
_vm.History.ShouldNotBeNull().SelectedNodeId.ShouldBe(node.NodeId);
_vm.SelectedTabIndex.ShouldBe(3);
}
@@ -506,7 +506,7 @@ public class MainWindowViewModelTests
await _vm.ConnectCommand.ExecuteAsync(null);
// Add a subscription
_vm.Subscriptions.IsConnected = true;
_vm.Subscriptions.ShouldNotBeNull().IsConnected = true;
await _vm.Subscriptions.AddSubscriptionForNodeAsync("ns=2;s=TestSub");
// Disconnect saves settings including subscriptions
@@ -554,7 +554,7 @@ public class MainWindowViewModelTests
await vm.ConnectCommand.ExecuteAsync(null);
vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(2);
vm.Subscriptions.ShouldNotBeNull().ActiveSubscriptions.Count.ShouldBe(2);
vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Restored1");
vm.Subscriptions.ActiveSubscriptions[1].NodeId.ShouldBe("ns=2;s=Restored2");
vm.SubscriptionCount.ShouldBe(2);
@@ -128,7 +128,7 @@ public class ReadWriteViewModelTests
// because auto-read catches the exception too
_vm.SelectedNodeId = "ns=2;s=SomeNode";
_vm.CurrentStatus.ShouldContain("Error");
_vm.CurrentStatus.ShouldNotBeNull().ShouldContain("Error");
}
/// <summary>Verifies that clear resets all properties.</summary>
@@ -739,7 +739,7 @@ public sealed class ScriptedAlarmEngineTests
up.Push("Temp", 150);
var winner = await Task.WhenAny(ackDone.Task, Task.Delay(TimeSpan.FromSeconds(3)));
var winner = await Task.WhenAny(ackDone.Task, Task.Delay(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken));
winner.ShouldBe(ackDone.Task,
"subscriber re-entering the engine must not deadlock against _evalGate");
await ackDone.Task; // surface any inner exception
@@ -753,7 +753,7 @@ public sealed class ScriptedAlarmEngineTests
/// <summary>Verifies that OnEvent emission occurs after the evaluation gate is released, so subscribers can re-enter the engine without deadlocking (regression for Core.ScriptedAlarms-003).</summary>
[Fact]
public void OnEvent_emission_happens_outside_evalGate(/* -003 */)
public async Task OnEvent_emission_happens_outside_evalGate(/* -003 */)
{
// Direct white-box check on the gate-release ordering: AcknowledgeAsync
// emits the Acknowledged event AFTER releasing the gate. We assert that
@@ -764,8 +764,8 @@ public sealed class ScriptedAlarmEngineTests
var eng = Build(up, out _);
try
{
eng.LoadAsync([Alarm("HighTemp", """return (int)ctx.GetTag("Temp").Value > 100;""")],
TestContext.Current.CancellationToken).GetAwaiter().GetResult();
await eng.LoadAsync([Alarm("HighTemp", """return (int)ctx.GetTag("Temp").Value > 100;""")],
TestContext.Current.CancellationToken);
// Drive to Active so Acknowledge has something to ack.
up.Push("Temp", 150);
// Use the same WaitForAsync that other tests use — synchronously
@@ -796,8 +796,7 @@ public sealed class ScriptedAlarmEngineTests
}
};
eng.AcknowledgeAsync("HighTemp", "alice", null, CancellationToken.None)
.GetAwaiter().GetResult();
await eng.AcknowledgeAsync("HighTemp", "alice", null, CancellationToken.None);
gateFreeInsideEmission.ShouldBeTrue(
"_evalGate must be released before OnEvent fires so subscribers " +
@@ -844,7 +843,7 @@ public sealed class ScriptedAlarmEngineTests
// Verify Dispose does NOT complete immediately — it should block waiting
// for the in-flight task. Without the -006 fix Dispose returns straight
// away and the background reevaluation can outlive the engine.
var prematureFinish = await Task.WhenAny(disposeTask, Task.Delay(200));
var prematureFinish = await Task.WhenAny(disposeTask, Task.Delay(200, TestContext.Current.CancellationToken));
prematureFinish.ShouldNotBe(disposeTask,
"Dispose must block until in-flight background tasks complete");
@@ -1749,7 +1748,7 @@ public sealed class ScriptedAlarmEngineTests
eng.InFlightCountForTest.ShouldBe(1);
var disposeTask = Task.Run(() => eng.Dispose(), TestContext.Current.CancellationToken);
var raced = await Task.WhenAny(disposeTask, Task.Delay(300));
var raced = await Task.WhenAny(disposeTask, Task.Delay(300, TestContext.Current.CancellationToken));
raced.ShouldNotBe(disposeTask, "Dispose must not return while registered background work is still running");
gate.SetResult();
@@ -13,11 +13,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
[Trait("Category", "Unit")]
public sealed class CompiledScriptCacheTests
{
private sealed class CompileCountingGate
{
public int Count;
}
/// <summary>Verifies that the first call to GetOrCompile compiles the script and caches the evaluator.</summary>
[Fact]
public void First_call_compiles_and_caches()
@@ -112,7 +107,7 @@ public sealed class CompiledScriptCacheTests
/// <summary>Verifies that concurrent compiles of the same source deduplicate to a single compilation.</summary>
[Fact]
public void Concurrent_compiles_of_the_same_source_deduplicate()
public async Task Concurrent_compiles_of_the_same_source_deduplicate()
{
// LazyThreadSafetyMode.ExecutionAndPublication guarantees only one compile
// even when multiple threads race GetOrCompile against an empty cache.
@@ -122,13 +117,13 @@ public sealed class CompiledScriptCacheTests
const string src = """return 99;""";
var tasks = Enumerable.Range(0, 20)
.Select(_ => Task.Run(() => cache.GetOrCompile(src)))
.Select(_ => Task.Run(() => cache.GetOrCompile(src), TestContext.Current.CancellationToken))
.ToArray();
Task.WhenAll(tasks).GetAwaiter().GetResult();
var evaluators = await Task.WhenAll(tasks);
var firstInstance = tasks[0].Result;
foreach (var t in tasks)
ReferenceEquals(t.Result, firstInstance).ShouldBeTrue();
var firstInstance = evaluators[0];
foreach (var e in evaluators)
ReferenceEquals(e, firstInstance).ShouldBeTrue();
cache.Count.ShouldBe(1);
}
@@ -129,7 +129,7 @@ public sealed class FocasLowFindingsTests
// timeout fix, this never completes (Probe runs forever).
var cancelled = await Task.WhenAny(
hangSignal.Task,
Task.Delay(TimeSpan.FromSeconds(2))) == hangSignal.Task;
Task.Delay(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken)) == hangSignal.Task;
await drv.ShutdownAsync(CancellationToken.None);
@@ -237,7 +237,7 @@ public sealed class DeployWatcherTests
// StopAsync should complete without throwing and within a reasonable window.
var stopTask = watcher.StopAsync();
var completed = await Task.WhenAny(stopTask, Task.Delay(TimeSpan.FromSeconds(5)));
var completed = await Task.WhenAny(stopTask, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken));
completed.ShouldBe(stopTask);
await stopTask; // observe (no) exception
}
@@ -256,7 +256,7 @@ public sealed class DeployWatcherTests
// Should not throw, should not hang.
var disposeTask = Task.Run(watcher.Dispose, TestContext.Current.CancellationToken);
var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(5)));
var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken));
completed.ShouldBe(disposeTask);
await disposeTask;
}
@@ -124,7 +124,7 @@ public sealed class HostStatusAggregatorTests
/// <summary>Verifies that concurrent updates do not corrupt the internal dictionary.</summary>
[Fact]
public void ConcurrentUpdates_DoNotCorruptDictionary()
public async Task ConcurrentUpdates_DoNotCorruptDictionary()
{
var agg = new HostStatusAggregator();
const int threadCount = 8;
@@ -138,9 +138,9 @@ public sealed class HostStatusAggregatorTests
var state = i % 2 == 0 ? HostState.Running : HostState.Stopped;
agg.Update(Status(hostName, state));
}
})).ToArray();
}, TestContext.Current.CancellationToken)).ToArray();
Task.WaitAll(tasks, TestContext.Current.CancellationToken);
await Task.WhenAll(tasks).WaitAsync(TestContext.Current.CancellationToken);
agg.Snapshot().Count.ShouldBeLessThanOrEqualTo(32);
}
}
@@ -66,7 +66,7 @@ public sealed class EventPumpDispatchFaultTests
await subscriber.EmitAsync(7);
(await Task.WhenAny(fault.Task, Task.Delay(WaitMs))).ShouldBe(fault.Task,
(await Task.WhenAny(fault.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(fault.Task,
"a dispatch-loop exit must be reported as a stream fault");
(await fault.Task).ShouldBeOfType<InvalidDataException>();
}
@@ -89,7 +89,7 @@ public sealed class EventPumpDispatchFaultTests
await subscriber.EmitAsync(7);
(await Task.WhenAny(delivered.Task, Task.Delay(WaitMs))).ShouldBe(delivered.Task);
(await Task.WhenAny(delivered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(delivered.Task);
(await delivered.Task).FullReference.ShouldBe("Tank01.Level");
faulted.ShouldBeFalse();
}
@@ -57,7 +57,7 @@ public sealed class EventPumpReplayGapTests
await subscriber.EmitAsync(GapSentinel(requestedAfter: 100, oldestAvailable: 250));
(await Task.WhenAny(observed.Task, Task.Delay(WaitMs))).ShouldBe(observed.Task,
(await Task.WhenAny(observed.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(observed.Task,
"a replay-gap sentinel must reach the driver so it can re-snapshot");
var gap = await observed.Task;
gap.RequestedAfterSequence.ShouldBe(100UL);
@@ -93,7 +93,7 @@ public sealed class EventPumpReplayGapTests
pump.Start();
await subscriber.EmitAsync(GapSentinel(requestedAfter: 1, oldestAvailable: 2));
(await Task.WhenAny(gapSeen.Task, Task.Delay(WaitMs))).ShouldBe(gapSeen.Task);
(await Task.WhenAny(gapSeen.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(gapSeen.Task);
// A positive control: follow the sentinel with a real event on the same handle. Without
// it, "nothing was dispatched" would also pass on a pump that dispatches nothing at all.
@@ -106,7 +106,7 @@ public sealed class EventPumpReplayGapTests
SourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
});
(await Task.WhenAny(realValue.Task, Task.Delay(WaitMs))).ShouldBe(realValue.Task,
(await Task.WhenAny(realValue.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(realValue.Task,
"the pump must keep dispatching real events after a sentinel");
(await realValue.Task).Snapshot.Value.ShouldBe(12.5);
Volatile.Read(ref dispatched).ShouldBe(1, "only the real event may be dispatched, never the sentinel");
@@ -149,7 +149,7 @@ public sealed class EventPumpReplayGapTests
await subscriber.EmitAsync(GapSentinel(requestedAfter: 5, oldestAvailable: 900));
(await Task.WhenAny(observed.Task, Task.Delay(WaitMs))).ShouldBe(observed.Task,
(await Task.WhenAny(observed.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(observed.Task,
"the sentinel must not be droppable by channel back-pressure");
(await observed.Task).OldestAvailableSequence.ShouldBe(900UL);
@@ -187,7 +187,7 @@ public sealed class EventPumpReplayGapTests
Quality = 192,
});
(await Task.WhenAny(resumed.Task, Task.Delay(WaitMs))).ShouldBe(resumed.Task,
(await Task.WhenAny(resumed.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(resumed.Task,
"a failed re-snapshot must not stop the stream that is still delivering");
faulted.ShouldBeFalse("a throwing gap callback is not a transport fault");
}
@@ -217,7 +217,7 @@ public sealed class EventPumpReplayGapTests
subscriber.CompleteStream();
(await Task.WhenAny(faultObserved.Task, Task.Delay(WaitMs))).ShouldBe(faultObserved.Task,
(await Task.WhenAny(faultObserved.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(faultObserved.Task,
"a server-completed stream is as fatal as a faulted one and must reach the supervisor");
(await faultObserved.Task).Message.ShouldContain("closed or evicted");
}
@@ -277,7 +277,7 @@ public sealed class EventPumpReplayGapTests
SourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
});
(await Task.WhenAny(bothSeen.Task, Task.Delay(WaitMs))).ShouldBe(bothSeen.Task);
(await Task.WhenAny(bothSeen.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(bothSeen.Task);
lock (received)
{
@@ -35,7 +35,7 @@ public sealed class EventPumpStreamFaultTests
// Drop the gw stream — RunAsync's await foreach throws.
subscriber.FaultStream(new IOException("simulated gateway transport drop"));
var completed = await Task.WhenAny(faultObserved.Task, Task.Delay(WaitMs));
var completed = await Task.WhenAny(faultObserved.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken));
completed.ShouldBe(faultObserved.Task,
"EventPump must invoke onStreamFault when the gw StreamEvents stream faults");
(await faultObserved.Task).ShouldBeOfType<IOException>();
@@ -69,9 +69,9 @@ public sealed class EventPumpStreamFaultTests
subscriber.FaultStream(new IOException("simulated gateway transport drop"));
(await Task.WhenAny(reopenRan.Task, Task.Delay(WaitMs))).ShouldBe(reopenRan.Task,
(await Task.WhenAny(reopenRan.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(reopenRan.Task,
"stream fault must trigger the supervisor's reopen path");
(await Task.WhenAny(replayRan.Task, Task.Delay(WaitMs))).ShouldBe(replayRan.Task,
(await Task.WhenAny(replayRan.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(replayRan.Task,
"stream fault must trigger the supervisor's replay path");
await supervisor.WaitForHealthyAsync(new CancellationTokenSource(WaitMs).Token);
@@ -114,7 +114,7 @@ public sealed class EventPumpStreamFaultTests
await resubscribed.EmitAsync(itemHandle: 7, value: 123.0);
var completed = await Task.WhenAny(resumed.Task, Task.Delay(WaitMs));
var completed = await Task.WhenAny(resumed.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken));
completed.ShouldBe(resumed.Task,
"a fresh EventPump created after a fault must resume dispatching OnDataChange");
(await resumed.Task).FullReference.ShouldBe("Tank.Level");
@@ -251,7 +251,7 @@ public sealed class GalaxyDriverFixRound1Tests
await WaitUntilAsync(() => Volatile.Read(ref deploys.Opened) == 1, "the deploy watch must open");
await deploys.PushAsync(1, new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); // bootstrap
await deploys.PushAsync(2, new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero));
(await Task.WhenAny(platforms.Entered.Task, Task.Delay(WaitMs))).ShouldBe(platforms.Entered.Task,
(await Task.WhenAny(platforms.Entered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(platforms.Entered.Task,
"the first rediscovery pass must be parked mid-fetch");
await deploys.PushAsync(3, new DateTimeOffset(2026, 1, 3, 0, 0, 0, TimeSpan.Zero));
@@ -308,7 +308,7 @@ public sealed class GalaxyDriverFixRound1Tests
var subscribeCallsBefore = sub.SubscribeCalls;
try
{
(await Task.WhenAny(sub.UnsubscribeEntered.Task, Task.Delay(WaitMs))).ShouldBe(sub.UnsubscribeEntered.Task,
(await Task.WhenAny(sub.UnsubscribeEntered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(sub.UnsubscribeEntered.Task,
"the teardown must be parked inside the probe watcher's unsubscribe");
shutdown.IsCompleted.ShouldBeFalse();
@@ -391,7 +391,7 @@ public sealed class GalaxyDriverFixRound1Tests
await driver.SubscribeAsync(["Tank01.Level"], TimeSpan.Zero, CancellationToken.None);
var replay = driver.InvokeReplayForTestAsync(CancellationToken.None);
(await Task.WhenAny(sub.SubscribeEntered.Task, Task.Delay(WaitMs))).ShouldBe(sub.SubscribeEntered.Task,
(await Task.WhenAny(sub.SubscribeEntered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(sub.SubscribeEntered.Task,
"the replay must be parked inside its SubscribeBulk");
await driver.ShutdownAsync(CancellationToken.None);
@@ -172,7 +172,7 @@ public sealed class GalaxyDriverReadTests
};
var readTask = driver.ReadAsync(["Silent.Tag"], CancellationToken.None);
var finished = await Task.WhenAny(readTask, Task.Delay(TimeSpan.FromSeconds(10)));
var finished = await Task.WhenAny(readTask, Task.Delay(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken));
finished.ShouldBe(readTask);
var result = await readTask;
@@ -174,14 +174,14 @@ public sealed class GalaxyDriverShutdownTests
driver.HasEventPumpForTests.ShouldBeTrue();
await sub.EmitAsync(sub.HandleByRef["Tank01.Level"], 42.0);
(await Task.WhenAny(delivered.Task, Task.Delay(WaitMs))).ShouldBe(delivered.Task,
(await Task.WhenAny(delivered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(delivered.Task,
"positive control: the stream must deliver before the shutdown");
await driver.ShutdownAsync(CancellationToken.None);
// Bounded wait, not an instantaneous IsCompleted check: the pump's dispose does not promise the
// enumerator's cancellation registration has run by the time it returns (observed flaky).
(await Task.WhenAny(sub.StreamCancelled.Task, Task.Delay(WaitMs))).ShouldBe(sub.StreamCancelled.Task,
(await Task.WhenAny(sub.StreamCancelled.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(sub.StreamCancelled.Task,
"ShutdownAsync must cancel the StreamEvents stream");
driver.HasEventPumpForTests.ShouldBeFalse();
driver.TrackedSubscriptionCountForTests.ShouldBe(0, "handles died with the session; the host re-subscribes after re-init");
@@ -53,7 +53,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
};
feed.Start();
(await Task.WhenAny(got3.Task, Task.Delay(TimeSpan.FromSeconds(2))))
(await Task.WhenAny(got3.Task, Task.Delay(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken)))
.ShouldBe(got3.Task, "snapshot + transition should dispatch within 2s");
observed.Count.ShouldBe(3);
@@ -109,7 +109,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
};
feed.Start();
(await Task.WhenAny(got2.Task, Task.Delay(TimeSpan.FromSeconds(2)))).ShouldBe(got2.Task);
(await Task.WhenAny(got2.Task, Task.Delay(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken))).ShouldBe(got2.Task);
observed[0].TransitionKind.ShouldBe(GalaxyAlarmTransitionKind.Clear);
observed[0].IsRestatement.ShouldBeTrue();
@@ -145,7 +145,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
};
feed.Start();
(await Task.WhenAny(gotOne.Task, Task.Delay(TimeSpan.FromSeconds(2))))
(await Task.WhenAny(gotOne.Task, Task.Delay(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken)))
.ShouldBe(gotOne.Task);
// Only the well-formed Raise survives; the Unspecified + empty messages drop.
@@ -187,7 +187,7 @@ public sealed class GatewayGalaxyAlarmFeedTests
};
feed.Start();
(await Task.WhenAny(gotOne.Task, Task.Delay(TimeSpan.FromSeconds(3))))
(await Task.WhenAny(gotOne.Task, Task.Delay(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken)))
.ShouldBe(gotOne.Task, "the feed should reopen the stream and deliver after a fault");
calls.ShouldBeGreaterThanOrEqualTo(2);
@@ -47,7 +47,7 @@ public sealed class ReconnectSupervisorCycleCtsTests
// Positive control: a cycle parked inside reopen still holds its CTS.
Volatile.Write(ref hold[0], true);
sup.ReportTransportFailure(new IOException("drop 3"));
(await Task.WhenAny(reopenEntered.Task, Task.Delay(WaitMs))).ShouldBe(reopenEntered.Task);
(await Task.WhenAny(reopenEntered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(reopenEntered.Task);
sup.CyclesStarted.ShouldBe(4);
sup.CyclesReleased.ShouldBe(3, "an in-flight cycle's CTS is live until its loop exits");
@@ -71,10 +71,10 @@ public sealed class ReconnectSupervisorCycleCtsTests
options: FastBackoff());
sup.ReportTransportFailure(new IOException("drop"));
(await Task.WhenAny(reopenEntered.Task, Task.Delay(WaitMs))).ShouldBe(reopenEntered.Task);
(await Task.WhenAny(reopenEntered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(reopenEntered.Task);
var stop = sup.StopAsync();
(await Task.WhenAny(stop, Task.Delay(WaitMs))).ShouldBe(stop, "StopAsync must cancel the parked reopen and return");
(await Task.WhenAny(stop, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(stop, "StopAsync must cancel the parked reopen and return");
sup.CyclesReleased.ShouldBe(1);
await Should.NotThrowAsync(sup.StopAsync); // idempotent
Should.NotThrow(sup.Dispose);
@@ -13,8 +13,8 @@
<PackageReference Include="xunit.v3"/>
<PackageReference Include="Shouldly"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<!-- ServiceCollection + BuildServiceProvider for the CreateDataSource factory test. -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
<!-- ServiceCollection + BuildServiceProvider (CreateDataSource factory test) come from the Microsoft.AspNetCore.App
FrameworkReference below, so no explicit Microsoft.Extensions.DependencyInjection package (NU1510). -->
<!-- In-process gRPC server capturing the published client's ReadEvents request (#675 bounds pin). -->
<PackageReference Include="Grpc.AspNetCore"/>
<PackageReference Include="xunit.runner.visualstudio">
@@ -51,6 +51,6 @@ public sealed class ModbusDriverProbeParityTests
var (target, error) = ModbusDriverProbe.TryParseProbeTarget(
"not valid json {{", fallbackTimeout: TimeSpan.FromSeconds(5));
target.ShouldBeNull();
error.ShouldContain("invalid");
error.ShouldNotBeNull().ShouldContain("invalid");
}
}
@@ -181,7 +181,7 @@ public sealed class ModbusDriverProbeTests
var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);
// The server task should have completed (or close enough).
await serverTask.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
result.Ok.ShouldBeFalse();
result.Message.ShouldNotBeNull();
@@ -211,7 +211,7 @@ public sealed class ModbusDriverProbeTests
var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Message.ShouldBe("Modbus FC03 OK");
@@ -241,7 +241,7 @@ public sealed class ModbusDriverProbeTests
var result = await _probe.ProbeAsync(Config(port), QuickTimeout, CancellationToken.None);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
result.Ok.ShouldBeTrue();
result.Message.ShouldNotBeNull();
@@ -497,8 +497,8 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
});
await Task.WhenAll(
Task.Run(() => drv.InitializeAsync("{}", CancellationToken.None)),
Task.Run(() => drv.InitializeAsync("{}", CancellationToken.None)));
Task.Run(() => drv.InitializeAsync("{}", CancellationToken.None), TestContext.Current.CancellationToken),
Task.Run(() => drv.InitializeAsync("{}", CancellationToken.None), TestContext.Current.CancellationToken));
var built = handed.ToArray();
built.Length.ShouldBe(2);
@@ -276,7 +276,7 @@ public sealed class MqttDriverDiscoveryTests
// The gate is held by the parked initialize. Dispose must still be waiting. (The gate wait is
// bounded by ConnectTimeoutSeconds = 2 s, so this 300 ms window is comfortably inside it —
// an ungated dispose returns essentially instantly.)
var raced = await Task.WhenAny(dispose, Task.Delay(TimeSpan.FromMilliseconds(300)));
var raced = await Task.WhenAny(dispose, Task.Delay(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken));
raced.ShouldNotBe(dispose, "DisposeAsync returned while a lifecycle operation held the gate");
release.SetResult();
@@ -20,7 +20,7 @@ public sealed class OpcUaClientBrowseSessionAttributesTests
private static Mock<ISession> SessionReturning(NodeClass nodeClass, NodeId dataType, int valueRank, byte userAccess)
{
var mock = new Mock<ISession>(MockBehavior.Loose);
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext());
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
mock.Setup(s => s.ReadAsync(
It.IsAny<RequestHeader>(), It.IsAny<double>(), It.IsAny<TimestampsToReturn>(),
@@ -20,7 +20,7 @@ public sealed class OpcUaClientBrowseSessionPaginationTests
private static Mock<ISession> BuildSession(bool cancelNext)
{
var mock = new Mock<ISession>(MockBehavior.Loose);
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext());
mock.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
mock.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
var firstRef = new ReferenceDescription
@@ -120,7 +120,7 @@ public sealed class OpcUaClientDriverBrowserTests
// A loose mock answers the attribute read with no response, so nothing is known about the node.
var mockSession = new Mock<ISession>(MockBehavior.Loose);
mockSession.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
mockSession.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext());
mockSession.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext(BrowseTelemetry.Context));
var nsMap = NamespaceMap.FromTable(new NamespaceTable());
var sut = new OpcUaClientBrowseSession(mockSession.Object, nsMap, ObjectIds.ObjectsFolder);
@@ -48,7 +48,7 @@ public sealed class SqlCatalogGateDriverTests
driver.GetHealth().LastError.ShouldBeNull();
// …and the one that contradicts it.
var report = ((ISkippedTagReporter)driver).GetSkippedTags();
var report = ((ISkippedTagReporter)driver).GetSkippedTags().ShouldNotBeNull();
report.AuthoredCount.ShouldBe(2);
report.SkippedCount.ShouldBe(1, "the gate rejected one tag; the blob mapped both");
report.Sample.Single().RawPath.ShouldBe("Bogus");
@@ -64,7 +64,7 @@ public sealed class SqlCatalogGateDriverTests
await driver.InitializeAsync(ConfigJson, CancellationToken.None);
var report = ((ISkippedTagReporter)driver).GetSkippedTags();
var report = ((ISkippedTagReporter)driver).GetSkippedTags().ShouldNotBeNull();
report.AuthoredCount.ShouldBe(1);
report.HasSkips.ShouldBeFalse();
}
@@ -121,7 +121,7 @@ public sealed class TwinCATLowFindingsRegressionTests
/// <summary>Verifies that Dispose does not block on async operations in the default synchronization context.</summary>
[Fact]
public void Dispose_does_not_block_on_async_in_default_synchronization_context()
public async Task Dispose_does_not_block_on_async_in_default_synchronization_context()
{
// Sync-over-async on a single-threaded sync context (like the OPC UA stack thread) can
// deadlock. Dispose() must complete without scheduling continuations through a captured
@@ -132,7 +132,7 @@ public sealed class TwinCATLowFindingsRegressionTests
Devices = [new TwinCATDeviceOptions(DeviceA)],
Probe = new TwinCATProbeOptions { Enabled = false },
}, "drv-1");
drv.InitializeAsync("{}", CancellationToken.None).GetAwaiter().GetResult();
await drv.InitializeAsync("{}", CancellationToken.None);
var ctx = new SingleThreadedSyncContext();
var prev = SynchronizationContext.Current;