test: pass TestContext.Current.CancellationToken at xUnit1051 call sites (analyzer code fix, 451 sites) (lmxopcua#725)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 13:19:34 -05:00
parent 5db01053e8
commit 1d7a4b7a53
77 changed files with 452 additions and 457 deletions
@@ -21,9 +21,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -46,9 +46,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -71,9 +71,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -91,13 +91,13 @@ public class AlarmsCommandTests
var command = new AlarmsCommand(factory) { Url = "opc.tcp://localhost:4840" };
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
fakeService.RaiseAlarmEvent(new Client.Shared.Models.AlarmEventArgs(
"src", "cond", 500, "msg", true, true, false, DateTime.UtcNow,
eventId: [0xAB, 0x01, 0xFF], conditionNodeId: "ns=2;s=Plant/cond"));
await Task.Delay(500);
await Task.Delay(500, TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -123,9 +123,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -146,9 +146,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -168,9 +168,9 @@ public class AlarmsCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -49,9 +49,9 @@ public class EventHandlerLifecycleTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => await command.ExecuteAsync(console));
var task = Task.Run(async () => await command.ExecuteAsync(console), TestContext.Current.CancellationToken);
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeAlarmsInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -55,7 +55,7 @@ public class SubscribeCommandSummaryTests
};
using var console = TestConsoleHelper.CreateConsole();
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
var runTask = Task.Run(async () => await command.ExecuteAsync(console), TestContext.Current.CancellationToken);
// Wait until the subscription is registered by the fake.
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
@@ -86,7 +86,7 @@ public class SubscribeCommandSummaryTests
};
using var console = TestConsoleHelper.CreateConsole();
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
var runTask = Task.Run(async () => await command.ExecuteAsync(console), TestContext.Current.CancellationToken);
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
@@ -143,7 +143,7 @@ public class SubscribeCommandSummaryTests
};
using var console = TestConsoleHelper.CreateConsole();
var runTask = Task.Run(async () => await command.ExecuteAsync(console));
var runTask = Task.Run(async () => await command.ExecuteAsync(console), TestContext.Current.CancellationToken);
await WaitForAsync(() => fakeService.SubscribeCalls.Count == 1);
fakeService.RaiseDataChanged(
@@ -180,7 +180,7 @@ public class SubscribeCommandSummaryTests
await command.ExecuteAsync(console);
File.Exists(tempFile).ShouldBeTrue();
var contents = await File.ReadAllTextAsync(tempFile);
var contents = await File.ReadAllTextAsync(tempFile, TestContext.Current.CancellationToken);
contents.ShouldContain("SUMMARY");
contents.ShouldContain("Total subscribed: 1");
}
@@ -24,10 +24,10 @@ public class SubscribeCommandTests
// The subscribe command waits for cancellation. We need to cancel it.
// Use the console's cancellation to trigger stop.
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
// Wait until the command has actually subscribed (deterministic — no fixed-sleep race), then cancel
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -50,9 +50,9 @@ public class SubscribeCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -73,9 +73,9 @@ public class SubscribeCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -98,9 +98,9 @@ public class SubscribeCommandTests
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
var task = Task.Run(async () => { await command.ExecuteAsync(console); }, TestContext.Current.CancellationToken);
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10));
await fakeService.SubscribeInvoked.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
console.RequestCancellation();
await task;
@@ -58,12 +58,12 @@ public sealed class FailoverEpochTests : IDisposable
_factory.EnqueueSession(userSession); // dequeued by the user's Connect (the failover is still held)
_factory.EnqueueSession(failoverSession); // dequeued by the failover once released
await _service.DisconnectAsync();
await _service.ConnectAsync(Settings());
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
lock (_states) _states.Clear();
gate.SetResult();
await failover.WaitAsync(Wait);
await failover.WaitAsync(Wait, TestContext.Current.CancellationToken);
failoverSession.Disposed.ShouldBeTrue();
userSession.Disposed.ShouldBeFalse();
@@ -79,14 +79,14 @@ public sealed class FailoverEpochTests : IDisposable
var (_, failover, gate) = await StartHeldFailoverAsync();
_factory.EnqueueSession(new FakeSessionAdapter());
await _service.DisconnectAsync();
await _service.ConnectAsync(Settings());
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
lock (_states) _states.Clear();
var createsBefore = _factory.CreateCallCount;
_factory.ThrowOnCreate = true;
gate.SetResult();
await failover.WaitAsync(Wait);
await failover.WaitAsync(Wait, TestContext.Current.CancellationToken);
_service.IsConnected.ShouldBeTrue();
lock (_states) _states.ShouldBeEmpty();
@@ -94,8 +94,8 @@ public sealed class FailoverEpochTests : IDisposable
_factory.CreateCallCount.ShouldBe(createsBefore);
// And the user can still disconnect / reconnect normally afterwards.
_factory.ThrowOnCreate = false;
await _service.DisconnectAsync();
await _service.ConnectAsync(Settings());
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
_service.IsConnected.ShouldBeTrue();
}
@@ -107,7 +107,7 @@ public sealed class FailoverEpochTests : IDisposable
_factory.EnqueueSession(new FakeSessionAdapter());
gate.SetResult();
await failover.WaitAsync(Wait);
await failover.WaitAsync(Wait, TestContext.Current.CancellationToken);
_service.IsConnected.ShouldBeTrue();
_service.CurrentConnectionInfo!.EndpointUrl.ShouldBe(Backup);
@@ -123,15 +123,15 @@ public sealed class FailoverEpochTests : IDisposable
{
var s1 = new FakeSessionAdapter();
_factory.EnqueueSession(s1);
await _service.ConnectAsync(Settings());
await _service.DisconnectAsync();
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
_factory.EnqueueSession(new FakeSessionAdapter());
await _service.ConnectAsync(Settings());
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
var creates = _factory.CreateCallCount;
lock (_states) _states.Clear();
s1.SimulateKeepAlive(false);
if (_service.LastFailover is { } t) await t.WaitAsync(Wait);
if (_service.LastFailover is { } t) await t.WaitAsync(Wait, TestContext.Current.CancellationToken);
_factory.CreateCallCount.ShouldBe(creates);
lock (_states) _states.ShouldBeEmpty();
@@ -150,7 +150,7 @@ public sealed class FailoverEpochTests : IDisposable
failover.IsCompleted.ShouldBeFalse();
_factory.EnqueueSession(new FakeSessionAdapter());
gate.SetResult();
await failover.WaitAsync(Wait);
await failover.WaitAsync(Wait, TestContext.Current.CancellationToken);
}
/// <summary>
@@ -163,15 +163,15 @@ public sealed class FailoverEpochTests : IDisposable
{
var sub = new FakeSubscriptionAdapter();
_factory.EnqueueSession(new FakeSessionAdapter { NextSubscription = sub });
await _service.ConnectAsync(Settings());
await _service.ConnectAsync(Settings(), TestContext.Current.CancellationToken);
var node = new Opc.Ua.NodeId("ns=2;s=X");
await _service.SubscribeAsync(node);
await _service.SubscribeAsync(node, ct: TestContext.Current.CancellationToken);
sub.ThrowOnRemove = true;
await Should.ThrowAsync<Opc.Ua.ServiceResultException>(() => _service.UnsubscribeAsync(node));
sub.ThrowOnRemove = false;
await _service.SubscribeAsync(node);
await _service.SubscribeAsync(node, ct: TestContext.Current.CancellationToken);
sub.AddDataChangeCount.ShouldBe(2);
}
}
@@ -47,7 +47,7 @@ public class OpcUaClientServiceTests : IDisposable
[Fact]
public async Task ConnectAsync_ValidSettings_ReturnsConnectionInfo()
{
var info = await _service.ConnectAsync(ValidSettings());
var info = await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
info.ShouldNotBeNull();
info.EndpointUrl.ShouldBe("opc.tcp://localhost:4840");
@@ -84,7 +84,7 @@ public class OpcUaClientServiceTests : IDisposable
};
_sessionFactory.EnqueueSession(session);
var info = await _service.ConnectAsync(ValidSettings());
var info = await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
info.ServerName.ShouldBe("MyServer");
info.SecurityMode.ShouldBe("Sign");
@@ -102,7 +102,7 @@ public class OpcUaClientServiceTests : IDisposable
var events = new List<ConnectionStateChangedEventArgs>();
_service.ConnectionStateChanged += (_, e) => events.Add(e);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
events.Count.ShouldBe(2);
events[0].OldState.ShouldBe(ConnectionState.Disconnected);
@@ -137,7 +137,7 @@ public class OpcUaClientServiceTests : IDisposable
settings.Username = "admin";
settings.Password = "secret";
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
_configFactory.LastSettings!.Username.ShouldBe("admin");
_configFactory.LastSettings!.Password.ShouldBe("secret");
@@ -151,10 +151,10 @@ public class OpcUaClientServiceTests : IDisposable
[Fact]
public async Task DisconnectAsync_WhenConnected_ClosesSession()
{
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var session = _sessionFactory.CreatedSessions[0];
await _service.DisconnectAsync();
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
session.Closed.ShouldBeTrue();
_service.IsConnected.ShouldBeFalse();
@@ -167,7 +167,7 @@ public class OpcUaClientServiceTests : IDisposable
[Fact]
public async Task DisconnectAsync_WhenNotConnected_IsIdempotent()
{
await _service.DisconnectAsync(); // Should not throw
await _service.DisconnectAsync(TestContext.Current.CancellationToken); // Should not throw
_service.IsConnected.ShouldBeFalse();
}
@@ -177,9 +177,9 @@ public class OpcUaClientServiceTests : IDisposable
[Fact]
public async Task DisconnectAsync_CalledTwice_IsIdempotent()
{
await _service.ConnectAsync(ValidSettings());
await _service.DisconnectAsync();
await _service.DisconnectAsync(); // Should not throw
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
await _service.DisconnectAsync(TestContext.Current.CancellationToken); // Should not throw
}
// --- Read tests ---
@@ -195,9 +195,9 @@ public class OpcUaClientServiceTests : IDisposable
ReadResponse = new DataValue(new Variant(42), StatusCodes.Good)
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ReadValueAsync(new NodeId("ns=2;s=MyNode"));
var result = await _service.ReadValueAsync(new NodeId("ns=2;s=MyNode"), TestContext.Current.CancellationToken);
result.Value.ShouldBe(42);
session.ReadCount.ShouldBe(1);
@@ -221,7 +221,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter { ThrowOnRead = true };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await Should.ThrowAsync<ServiceResultException>(() =>
_service.ReadValueAsync(new NodeId("ns=2;s=MyNode")));
@@ -241,9 +241,9 @@ public class OpcUaClientServiceTests : IDisposable
WriteResponse = StatusCodes.Good
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), 42);
var result = await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), 42, TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.WriteCount.ShouldBe(1);
@@ -260,9 +260,9 @@ public class OpcUaClientServiceTests : IDisposable
ReadResponse = new DataValue(new Variant(0), StatusCodes.Good) // int type
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), "42");
await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), "42", TestContext.Current.CancellationToken);
session.WriteCount.ShouldBe(1);
session.ReadCount.ShouldBe(1); // Read for type inference
@@ -276,9 +276,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), 42);
await _service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), 42, TestContext.Current.CancellationToken);
session.WriteCount.ShouldBe(1);
session.ReadCount.ShouldBe(0); // No read for non-string values
@@ -306,7 +306,7 @@ public class OpcUaClientServiceTests : IDisposable
ReadResponse = new DataValue(StatusCodes.BadNodeIdUnknown) // Bad status, null Value
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
_service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), "42"));
@@ -326,7 +326,7 @@ public class OpcUaClientServiceTests : IDisposable
ReadResponse = new DataValue(StatusCodes.BadNotReadable)
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
_service.WriteValueAsync(new NodeId("ns=2;s=MyNode"), "42"));
@@ -355,9 +355,9 @@ public class OpcUaClientServiceTests : IDisposable
]
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var results = await _service.BrowseAsync();
var results = await _service.BrowseAsync(ct: TestContext.Current.CancellationToken);
results.Count.ShouldBe(1);
results[0].DisplayName.ShouldBe("Child1");
@@ -376,9 +376,9 @@ public class OpcUaClientServiceTests : IDisposable
BrowseResponse = []
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.BrowseAsync();
await _service.BrowseAsync(ct: TestContext.Current.CancellationToken);
session.BrowseCount.ShouldBe(1);
}
@@ -416,9 +416,9 @@ public class OpcUaClientServiceTests : IDisposable
]
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var results = await _service.BrowseAsync();
var results = await _service.BrowseAsync(ct: TestContext.Current.CancellationToken);
results.Select(r => r.HasChildren).ShouldBe([true, true, false]);
session.BrowseCount.ShouldBe(1);
@@ -455,9 +455,9 @@ public class OpcUaClientServiceTests : IDisposable
BrowseNextContinuationPoint = null
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var results = await _service.BrowseAsync();
var results = await _service.BrowseAsync(ct: TestContext.Current.CancellationToken);
results.Count.ShouldBe(2);
session.BrowseNextCount.ShouldBe(1);
@@ -482,9 +482,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), 500);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), 500, TestContext.Current.CancellationToken);
session.CreatedSubscriptions.Count.ShouldBe(1);
session.CreatedSubscriptions[0].AddDataChangeCount.ShouldBe(1);
@@ -498,10 +498,10 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"));
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode")); // duplicate
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), ct: TestContext.Current.CancellationToken);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), ct: TestContext.Current.CancellationToken); // duplicate
session.CreatedSubscriptions[0].AddDataChangeCount.ShouldBe(1);
}
@@ -515,12 +515,12 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
DataChangedEventArgs? received = null;
_service.DataChanged += (_, e) => received = e;
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), 500);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), 500, TestContext.Current.CancellationToken);
// Simulate data change
var handle = fakeSub.ActiveHandles.First();
@@ -540,10 +540,10 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"));
await _service.UnsubscribeAsync(new NodeId("ns=2;s=MyNode"));
await _service.SubscribeAsync(new NodeId("ns=2;s=MyNode"), ct: TestContext.Current.CancellationToken);
await _service.UnsubscribeAsync(new NodeId("ns=2;s=MyNode"), TestContext.Current.CancellationToken);
fakeSub.RemoveCount.ShouldBe(1);
}
@@ -556,9 +556,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.UnsubscribeAsync(new NodeId("ns=2;s=NotSubscribed"));
await _service.UnsubscribeAsync(new NodeId("ns=2;s=NotSubscribed"), TestContext.Current.CancellationToken);
// Should not throw
}
@@ -582,9 +582,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
session.CreatedSubscriptions.Count.ShouldBe(1);
session.CreatedSubscriptions[0].AddEventCount.ShouldBe(1);
@@ -598,10 +598,10 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(); // duplicate
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken); // duplicate
session.CreatedSubscriptions.Count.ShouldBe(1);
}
@@ -615,12 +615,12 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
AlarmEventArgs? received = null;
_service.AlarmEvent += (_, e) => received = e;
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
// Simulate alarm event with proper field count
var handle = fakeSub.ActiveHandles.First();
@@ -663,10 +663,10 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync();
await _service.UnsubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
await _service.UnsubscribeAlarmsAsync(TestContext.Current.CancellationToken);
fakeSub.Deleted.ShouldBeTrue();
}
@@ -679,9 +679,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.UnsubscribeAlarmsAsync(); // Should not throw
await _service.UnsubscribeAlarmsAsync(TestContext.Current.CancellationToken); // Should not throw
}
/// <summary>
@@ -693,10 +693,10 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync();
await _service.RequestConditionRefreshAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
await _service.RequestConditionRefreshAsync(TestContext.Current.CancellationToken);
fakeSub.ConditionRefreshCalled.ShouldBeTrue();
}
@@ -709,7 +709,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await Should.ThrowAsync<InvalidOperationException>(() =>
_service.RequestConditionRefreshAsync());
@@ -740,10 +740,9 @@ public class OpcUaClientServiceTests : IDisposable
};
var session = new FakeSessionAdapter { HistoryReadRawResponse = expectedValues };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var results = await _service.HistoryReadRawAsync(
new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow);
var results = await _service.HistoryReadRawAsync(new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, ct: TestContext.Current.CancellationToken);
results.Count.ShouldBe(2);
session.HistoryReadRawCount.ShouldBe(1);
@@ -767,7 +766,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter { ThrowOnHistoryReadRaw = true };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await Should.ThrowAsync<ServiceResultException>(() =>
_service.HistoryReadRawAsync(new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow));
@@ -785,11 +784,9 @@ public class OpcUaClientServiceTests : IDisposable
};
var session = new FakeSessionAdapter { HistoryReadAggregateResponse = expectedValues };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var results = await _service.HistoryReadAggregateAsync(
new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow,
AggregateType.Average);
var results = await _service.HistoryReadAggregateAsync(new NodeId("ns=2;s=Temp"), DateTime.UtcNow.AddHours(-1), DateTime.UtcNow, AggregateType.Average, ct: TestContext.Current.CancellationToken);
results.Count.ShouldBe(1);
session.HistoryReadAggregateCount.ShouldBe(1);
@@ -815,7 +812,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter { ThrowOnHistoryReadAggregate = true };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await Should.ThrowAsync<ServiceResultException>(() =>
_service.HistoryReadAggregateAsync(
@@ -847,9 +844,9 @@ public class OpcUaClientServiceTests : IDisposable
}
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var info = await _service.GetRedundancyInfoAsync();
var info = await _service.GetRedundancyInfoAsync(TestContext.Current.CancellationToken);
info.Mode.ShouldBe("Warm");
info.ServiceLevel.ShouldBe((byte)200);
@@ -877,9 +874,9 @@ public class OpcUaClientServiceTests : IDisposable
}
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var info = await _service.GetRedundancyInfoAsync();
var info = await _service.GetRedundancyInfoAsync(TestContext.Current.CancellationToken);
info.Mode.ShouldBe("None");
info.ServiceLevel.ShouldBe((byte)100);
@@ -917,9 +914,9 @@ public class OpcUaClientServiceTests : IDisposable
}
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var info = await _service.GetRedundancyInfoAsync();
var info = await _service.GetRedundancyInfoAsync(TestContext.Current.CancellationToken);
info.Mode.ShouldBe("Warm");
info.ServiceLevel.ShouldBe((byte)200);
@@ -944,9 +941,9 @@ public class OpcUaClientServiceTests : IDisposable
}
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var info = await _service.GetRedundancyInfoAsync();
var info = await _service.GetRedundancyInfoAsync(TestContext.Current.CancellationToken);
info.Mode.ShouldBe("None");
info.ServiceLevel.ShouldBe((byte)0);
@@ -964,12 +961,12 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
AlarmEventArgs? received = null;
_service.AlarmEvent += (_, e) => received = e;
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
var handle = fakeSub.ActiveHandles.First();
// Only 3 fields — EventId, EventType, SourceName — fewer than the old < 6 threshold
@@ -998,12 +995,12 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var eventCount = 0;
_service.AlarmEvent += (_, _) => eventCount++;
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
var handle = fakeSub.ActiveHandles.First();
fakeSub.SimulateEvent(handle, new EventFieldList { EventFields = [] });
@@ -1022,9 +1019,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.AcknowledgeAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "acked");
var result = await _service.AcknowledgeAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "acked", TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1045,9 +1042,9 @@ public class OpcUaClientServiceTests : IDisposable
StatusCodes.BadConditionAlreadyEnabled, "already acked")
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.AcknowledgeAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "acked");
var result = await _service.AcknowledgeAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "acked", TestContext.Current.CancellationToken);
StatusCode.IsBad(result).ShouldBeTrue();
result.Code.ShouldBe(StatusCodes.BadConditionAlreadyEnabled);
@@ -1062,10 +1059,10 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.AcknowledgeAlarmAsync("ns=2;s=Plant/Alarm1", new byte[] { 1, 2 }, "acked");
await _service.ConfirmAlarmAsync("ns=2;s=Plant/Alarm1", new byte[] { 1, 2 }, "confirmed");
await _service.AcknowledgeAlarmAsync("ns=2;s=Plant/Alarm1", new byte[] { 1, 2 }, "acked", TestContext.Current.CancellationToken);
await _service.ConfirmAlarmAsync("ns=2;s=Plant/Alarm1", new byte[] { 1, 2 }, "confirmed", TestContext.Current.CancellationToken);
session.CallMethodObjectIds.ShouldAllBe(id => id == NodeId.Parse("ns=2;s=Plant/Alarm1"));
session.CallMethodMethodIds[0].ShouldBe(MethodIds.AcknowledgeableConditionType_Acknowledge);
@@ -1083,9 +1080,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ConfirmAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "confirmed");
var result = await _service.ConfirmAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "confirmed", TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1104,9 +1101,9 @@ public class OpcUaClientServiceTests : IDisposable
StatusCodes.BadConditionAlreadyEnabled, "already confirmed")
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ConfirmAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "confirmed");
var result = await _service.ConfirmAlarmAsync("ns=2;s=Cond", new byte[] { 1, 2 }, "confirmed", TestContext.Current.CancellationToken);
StatusCode.IsBad(result).ShouldBeTrue();
result.Code.ShouldBe(StatusCodes.BadConditionAlreadyEnabled);
@@ -1122,9 +1119,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.OneShot);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.OneShot, ct: TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1142,10 +1139,10 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
const double durationSeconds = 30.0;
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.Timed, durationSeconds);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.Timed, durationSeconds, TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1161,9 +1158,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.Unshelve);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.Unshelve, ct: TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1185,9 +1182,9 @@ public class OpcUaClientServiceTests : IDisposable
StatusCodes.BadConditionAlreadyShelved, "already shelved")
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.OneShot);
var result = await _service.ShelveAlarmAsync("ns=2;s=Cond", ShelveKind.OneShot, ct: TestContext.Current.CancellationToken);
StatusCode.IsBad(result).ShouldBeTrue();
result.Code.ShouldBe(StatusCodes.BadConditionAlreadyShelved);
@@ -1204,9 +1201,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.EnableAsync("ns=2;s=Cond");
var result = await _service.EnableAsync("ns=2;s=Cond", TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1224,9 +1221,9 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.DisableAsync("ns=2;s=Cond");
var result = await _service.DisableAsync("ns=2;s=Cond", TestContext.Current.CancellationToken);
result.ShouldBe(StatusCodes.Good);
session.CallMethodCount.ShouldBe(1);
@@ -1248,9 +1245,9 @@ public class OpcUaClientServiceTests : IDisposable
StatusCodes.BadConditionAlreadyEnabled, "already enabled")
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.EnableAsync("ns=2;s=Cond");
var result = await _service.EnableAsync("ns=2;s=Cond", TestContext.Current.CancellationToken);
StatusCode.IsBad(result).ShouldBeTrue();
result.Code.ShouldBe(StatusCodes.BadConditionAlreadyEnabled);
@@ -1269,9 +1266,9 @@ public class OpcUaClientServiceTests : IDisposable
StatusCodes.BadConditionAlreadyDisabled, "already disabled")
};
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var result = await _service.DisableAsync("ns=2;s=Cond");
var result = await _service.DisableAsync("ns=2;s=Cond", TestContext.Current.CancellationToken);
StatusCode.IsBad(result).ShouldBeTrue();
result.Code.ShouldBe(StatusCodes.BadConditionAlreadyDisabled);
@@ -1290,12 +1287,12 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
AlarmEventArgs? received = null;
_service.AlarmEvent += (_, e) => received = e;
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
var handle = fakeSub.ActiveHandles.First();
var fields = new EventFieldList
@@ -1348,13 +1345,13 @@ public class OpcUaClientServiceTests : IDisposable
var stateChanges = new List<ConnectionStateChangedEventArgs>();
_service.ConnectionStateChanged += (_, e) => stateChanges.Add(e);
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
// Simulate keep-alive failure
session1.SimulateKeepAlive(false);
// Give async failover time to complete
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
// Should have reconnected
stateChanges.ShouldContain(e => e.NewState == ConnectionState.Reconnecting);
@@ -1380,9 +1377,9 @@ public class OpcUaClientServiceTests : IDisposable
var settings = ValidSettings("opc.tcp://primary:4840");
settings.FailoverUrls = ["opc.tcp://backup:4840"];
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
session1.SimulateKeepAlive(false);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
_service.CurrentConnectionInfo!.EndpointUrl.ShouldBe("opc.tcp://backup:4840");
_service.CurrentConnectionInfo.ServerName.ShouldBe("BackupServer");
@@ -1397,12 +1394,12 @@ public class OpcUaClientServiceTests : IDisposable
var session1 = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session1);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
// After the first session, make factory fail
_sessionFactory.ThrowOnCreate = true;
session1.SimulateKeepAlive(false);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
_service.IsConnected.ShouldBeFalse();
}
@@ -1423,7 +1420,7 @@ public class OpcUaClientServiceTests : IDisposable
var settings = ValidSettings("opc.tcp://primary:4840");
settings.FailoverUrls = ["opc.tcp://backup:4840"];
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
var createCountAfterConnect = _sessionFactory.CreateCallCount; // 1
// Hold the failover's session creation open so it stays in-flight.
@@ -1439,7 +1436,7 @@ public class OpcUaClientServiceTests : IDisposable
// Release the gate so the in-flight failover completes.
gate.SetResult();
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
// Exactly one extra session created by the single failover loop.
_sessionFactory.CreateCallCount.ShouldBe(createCountAfterConnect + 1);
@@ -1455,7 +1452,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session1 = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session1);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await Should.ThrowAsync<InvalidOperationException>(() => _service.ConnectAsync(ValidSettings()));
@@ -1463,8 +1460,8 @@ public class OpcUaClientServiceTests : IDisposable
session1.Disposed.ShouldBeFalse();
_service.IsConnected.ShouldBeTrue();
await _service.DisconnectAsync();
await _service.ConnectAsync(ValidSettings());
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
_sessionFactory.CreateCallCount.ShouldBe(2);
_service.IsConnected.ShouldBeTrue();
}
@@ -1484,26 +1481,26 @@ public class OpcUaClientServiceTests : IDisposable
_sessionFactory.EnqueueSession(session2);
var settings = ValidSettings("opc.tcp://primary:4840");
settings.FailoverUrls = ["opc.tcp://backup:4840"];
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
var gate = new TaskCompletionSource();
session1.CreateSubscriptionGate = gate;
var subscribe = _service.SubscribeAsync(new NodeId("ns=2;s=Racer"), 1000);
await session1.CreateSubscriptionEntered.Task.WaitAsync(TimeSpan.FromSeconds(10));
var subscribe = _service.SubscribeAsync(new NodeId("ns=2;s=Racer"), 1000, TestContext.Current.CancellationToken);
await session1.CreateSubscriptionEntered.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
session1.SimulateKeepAlive(false);
var failover = _service.LastFailover.ShouldNotBeNull();
gate.SetResult();
await subscribe.WaitAsync(TimeSpan.FromSeconds(10));
await failover.WaitAsync(TimeSpan.FromSeconds(10));
await subscribe.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
await failover.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
_service.CurrentConnectionInfo!.EndpointUrl.ShouldBe("opc.tcp://backup:4840");
var replayed = session2.CreatedSubscriptions.ShouldHaveSingleItem();
replayed.AddDataChangeCount.ShouldBe(1);
// And it is the live subscription: unsubscribing removes it from session2's subscription, not session1's.
await _service.UnsubscribeAsync(new NodeId("ns=2;s=Racer"));
await _service.UnsubscribeAsync(new NodeId("ns=2;s=Racer"), TestContext.Current.CancellationToken);
replayed.RemoveCount.ShouldBe(1);
replayed.ActiveHandles.ShouldBeEmpty();
}
@@ -1522,16 +1519,16 @@ public class OpcUaClientServiceTests : IDisposable
_sessionFactory.EnqueueSession(session2);
var settings = ValidSettings("opc.tcp://primary:4840");
settings.FailoverUrls = ["opc.tcp://backup:4840"];
await _service.ConnectAsync(settings);
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
var gate = new TaskCompletionSource();
_sessionFactory.CreateGate = gate;
session1.SimulateKeepAlive(false);
var failover = _service.LastFailover.ShouldNotBeNull();
await _service.DisconnectAsync();
await _service.DisconnectAsync(TestContext.Current.CancellationToken);
gate.SetResult();
await failover.WaitAsync(TimeSpan.FromSeconds(10));
await failover.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
_service.IsConnected.ShouldBeFalse();
session2.Disposed.ShouldBeTrue();
@@ -1547,7 +1544,7 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
var tasks = new List<Task>();
for (var i = 0; i < 50; i++)
@@ -1574,7 +1571,7 @@ public class OpcUaClientServiceTests : IDisposable
{
var session = new FakeSessionAdapter();
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
_service.Dispose();
@@ -1645,14 +1642,14 @@ public class OpcUaClientServiceTests : IDisposable
// Second CreateSubscription call should return the alarm fake.
// We wire it by overriding the factory queue after the first subscribe.
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
// Subscribe data (uses dataFakeSub from session.NextSubscription).
await _service.SubscribeAsync(new NodeId("ns=2;s=Node1"));
await _service.SubscribeAsync(new NodeId("ns=2;s=Node1"), ct: TestContext.Current.CancellationToken);
// Now set NextSubscription on the session so SubscribeAlarmsAsync gets the alarm fake.
session.NextSubscription = alarmFakeSub;
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
// Both subscriptions are now active. Disconnect with a cancellable token.
using var cts = new CancellationTokenSource();
@@ -1681,7 +1678,7 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
// Fire many concurrent SubscribeAlarmsAsync calls — only one should "win".
var tasks = Enumerable.Range(0, 20).Select(_ =>
@@ -1706,9 +1703,9 @@ public class OpcUaClientServiceTests : IDisposable
var fakeSub = new FakeSubscriptionAdapter();
var session = new FakeSessionAdapter { NextSubscription = fakeSub };
_sessionFactory.EnqueueSession(session);
await _service.ConnectAsync(ValidSettings());
await _service.ConnectAsync(ValidSettings(), TestContext.Current.CancellationToken);
await _service.SubscribeAlarmsAsync();
await _service.SubscribeAlarmsAsync(ct: TestContext.Current.CancellationToken);
using var cts = new CancellationTokenSource();
await _service.UnsubscribeAlarmsAsync(cts.Token);
@@ -106,7 +106,7 @@ public class BrowseTreeViewModelTests
parent.IsExpanded = true;
// Allow async operation to complete
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
_service.BrowseCallCount.ShouldBe(initialBrowseCount + 1);
parent.Children.Count.ShouldBe(1);
@@ -131,13 +131,13 @@ public class BrowseTreeViewModelTests
var parent = _vm.RootNodes[0];
parent.IsExpanded = true;
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
var browseCountAfterFirst = _service.BrowseCallCount;
parent.IsExpanded = false;
parent.IsExpanded = true;
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
_service.BrowseCallCount.ShouldBe(browseCountAfterFirst);
}
@@ -157,7 +157,7 @@ public class BrowseTreeViewModelTests
var parent = _vm.RootNodes[0];
parent.IsExpanded = true;
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
// After completion, IsLoading should be false
parent.IsLoading.ShouldBeFalse();
@@ -67,9 +67,9 @@ public sealed class ClusterAuditQueryTests : IDisposable
// Noise that must NOT appear: other cluster's structured row + an orphan NodeId.
SeedAudit("Published", clusterId: null, nodeId: "OTHER-A", ts: t0.AddMinutes(3));
SeedAudit("Published", clusterId: null, nodeId: "UNKNOWN-NODE", ts: t0.AddMinutes(4));
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200, ct: TestContext.Current.CancellationToken);
rows.Select(r => r.EventType).ShouldBe(
["NodeApplied", "DraftEdited", "Published"], // newest first
@@ -83,9 +83,9 @@ public sealed class ClusterAuditQueryTests : IDisposable
SeedNode("LINE3-OPCUA", "LINE3-OPCUA-A");
var t0 = new DateTime(2026, 6, 2, 10, 0, 0, DateTimeKind.Utc);
SeedAudit("Published", clusterId: "OTHER-CLUSTER", nodeId: null, ts: t0);
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 200, ct: TestContext.Current.CancellationToken);
rows.ShouldBeEmpty();
}
@@ -98,9 +98,9 @@ public sealed class ClusterAuditQueryTests : IDisposable
var t0 = new DateTime(2026, 6, 2, 10, 0, 0, DateTimeKind.Utc);
for (var i = 0; i < 5; i++)
SeedAudit("DraftEdited", clusterId: null, nodeId: "LINE3-OPCUA-A", ts: t0.AddMinutes(i));
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 3);
var rows = await ClusterAuditQuery.ForClusterAsync(_db, "LINE3-OPCUA", pageSize: 3, ct: TestContext.Current.CancellationToken);
rows.Count.ShouldBe(3);
rows.First().Timestamp.ShouldBe(t0.AddMinutes(4)); // newest
@@ -35,16 +35,16 @@ public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixtu
// GrpcPort is deliberately nullable: nothing listens on it until Phase 5, and a non-null
// default would assert a port that does not exist.
ctx.ClusterNodes.Add(NewNode(clusterId, "defaulted"));
await ctx.SaveChangesAsync();
await ctx.SaveChangesAsync(TestContext.Current.CancellationToken);
await using var read = NewContext();
var explicitNode = await read.ClusterNodes.AsNoTracking()
.SingleAsync(n => n.NodeId == $"{clusterId}-explicit:4053");
.SingleAsync(n => n.NodeId == $"{clusterId}-explicit:4053", cancellationToken: TestContext.Current.CancellationToken);
explicitNode.AkkaPort.ShouldBe(4055);
explicitNode.GrpcPort.ShouldBe(5223);
var defaulted = await read.ClusterNodes.AsNoTracking()
.SingleAsync(n => n.NodeId == $"{clusterId}-defaulted:4053");
.SingleAsync(n => n.NodeId == $"{clusterId}-defaulted:4053", cancellationToken: TestContext.Current.CancellationToken);
defaulted.AkkaPort.ShouldBe(4053);
defaulted.GrpcPort.ShouldBeNull();
}
@@ -74,12 +74,12 @@ public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixtu
cmd.Parameters.AddWithValue("@clusterId", clusterId);
cmd.Parameters.AddWithValue("@host", $"{clusterId}-raw");
cmd.Parameters.AddWithValue("@uri", $"urn:OtOpcUa:{clusterId}-raw");
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
await using var read = NewContext();
var row = await read.ClusterNodes.AsNoTracking()
.SingleAsync(n => n.NodeId == $"{clusterId}-raw:4053");
.SingleAsync(n => n.NodeId == $"{clusterId}-raw:4053", cancellationToken: TestContext.Current.CancellationToken);
row.AkkaPort.ShouldBe(4053);
row.GrpcPort.ShouldBeNull();
}
@@ -96,13 +96,13 @@ public sealed class ClusterNodeTransportPortsTests(SchemaComplianceFixture fixtu
var clusterId = await SeedClusterAsync(ctx);
ctx.ClusterNodes.Add(NewNode(clusterId, "a", n => n.GrpcPort = 5223));
ctx.ClusterNodes.Add(NewNode(clusterId, "b"));
await ctx.SaveChangesAsync();
await ctx.SaveChangesAsync(TestContext.Current.CancellationToken);
await using var read = NewContext();
var contacts = await read.ClusterNodes.AsNoTracking()
.Where(n => n.ClusterId == clusterId && n.GrpcPort != null)
.Select(n => new { n.Host, n.AkkaPort, n.GrpcPort })
.ToListAsync();
.ToListAsync(cancellationToken: TestContext.Current.CancellationToken);
contacts.Count.ShouldBe(1);
contacts[0].AkkaPort.ShouldBe(4053);
@@ -52,7 +52,7 @@ public sealed class DraftSnapshotFactorySqlServerTests
ClusterId = clusterId, Name = "snapshot-read", Enterprise = "zb", Site = "test",
NodeCount = 1, RedundancyMode = RedundancyMode.None, CreatedBy = "test",
});
await seed.SaveChangesAsync();
await seed.SaveChangesAsync(TestContext.Current.CancellationToken);
}
try
@@ -64,7 +64,7 @@ public sealed class DraftSnapshotFactorySqlServerTests
await using (var db = new OtOpcUaConfigDbContext(options))
{
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(db);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(db, TestContext.Current.CancellationToken);
// The interceptor committed a Script while ServerClusters (the FIRST table read) was being
// materialised. Scripts is read later in the same transaction and must not see it.
snapshot.ServerClusters.ShouldContain(c => c.ClusterId == clusterId);
@@ -74,16 +74,16 @@ public sealed class DraftSnapshotFactorySqlServerTests
await using (var verify = NewContext())
{
// Positive control: the edit really landed, and a read started afterwards sees it.
(await verify.Scripts.AnyAsync(s => s.ScriptId == scriptId)).ShouldBeTrue();
var later = await DraftSnapshotFactory.FromConfigDbAsync(verify);
(await verify.Scripts.AnyAsync(s => s.ScriptId == scriptId, cancellationToken: TestContext.Current.CancellationToken)).ShouldBeTrue();
var later = await DraftSnapshotFactory.FromConfigDbAsync(verify, TestContext.Current.CancellationToken);
later.Scripts.ShouldContain(s => s.ScriptId == scriptId);
}
}
finally
{
await using var cleanup = NewContext();
await cleanup.Scripts.Where(s => s.ScriptId == scriptId).ExecuteDeleteAsync();
await cleanup.ServerClusters.Where(c => c.ClusterId == clusterId).ExecuteDeleteAsync();
await cleanup.Scripts.Where(s => s.ScriptId == scriptId).ExecuteDeleteAsync(cancellationToken: TestContext.Current.CancellationToken);
await cleanup.ServerClusters.Where(c => c.ClusterId == clusterId).ExecuteDeleteAsync(cancellationToken: TestContext.Current.CancellationToken);
}
}
@@ -39,9 +39,9 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
_db.Tags.Add(BuildTag(name: "speed"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "a"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed", suffix: "b"));
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db, TestContext.Current.CancellationToken);
snapshot.Tags.Count.ShouldBe(1);
snapshot.VirtualTags.Count.ShouldBe(2);
@@ -56,9 +56,9 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
SeedEquipment("eq-1");
_db.Tags.Add(BuildTag(name: "speed"));
_db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "temperature"));
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db, TestContext.Current.CancellationToken);
snapshot.Tags.Count.ShouldBe(1);
snapshot.VirtualTags.Count.ShouldBe(1);
@@ -94,9 +94,9 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
ReleasedBy = "test",
ReleaseReason = "retired",
});
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db, TestContext.Current.CancellationToken);
snapshot.ActiveReservations.Count.ShouldBe(1);
snapshot.ActiveReservations[0].Value.ShouldBe("ZT-001");
@@ -124,9 +124,9 @@ public sealed class DraftSnapshotFactoryTests : IDisposable
{
ApiKeyId = "k1", Name = "scada", SecretHash = "hash", Principal = "scadabridge", Enabled = true,
});
await _db.SaveChangesAsync();
await _db.SaveChangesAsync(TestContext.Current.CancellationToken);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db);
var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db, TestContext.Current.CancellationToken);
snapshot.ServerClusters.ShouldHaveSingleItem().ClusterId.ShouldBe("SITE-A");
snapshot.ClusterNodes.ShouldHaveSingleItem().NodeId.ShouldBe("site-a-1:4053");
@@ -107,7 +107,7 @@ public sealed class ScriptedAlarmEngineRunGuardTests
await WaitForAsync(() => eng.RunGuard.StartedRunCount > before);
}
eng.RunGuard.StartedRunCount.ShouldBe(1 + 1 + pushes, "one runaway dispatch, then healthy-only dispatches");
await Task.Delay(300); // absence window for trailing emissions
await Task.Delay(300, TestContext.Current.CancellationToken); // absence window for trailing emissions
ScriptedAlarmEvent[] snapshot;
lock (events) snapshot = events.ToArray();
@@ -215,7 +215,7 @@ public sealed class ScriptedAlarmEngineTests
eng.OnEvent += (_, e) => events.Add(e);
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Any(e => e.Emission == EmissionKind.Activated).ShouldBeFalse(
"OneShot shelve suppresses activation emission");
@@ -252,7 +252,7 @@ public sealed class ScriptedAlarmEngineTests
await eng.DisableAsync("HighTemp", "alice", TestContext.Current.CancellationToken);
up.Push("Temp", 150);
await Task.Delay(100);
await Task.Delay(100, TestContext.Current.CancellationToken);
eng.GetState("HighTemp")!.Active.ShouldBe(AlarmActiveState.Inactive,
"disabled alarm ignores predicate");
@@ -353,7 +353,7 @@ public sealed class ScriptedAlarmEngineTests
up.Push($"Temp{round % alarmCount}", round % 2 == 0 ? 150 : 50);
round++;
}
});
}, TestContext.Current.CancellationToken);
// Readers: continuously touch the three unguarded read paths.
var readers = Enumerable.Range(0, 4).Select(_reader => Task.Run(() =>
@@ -839,7 +839,7 @@ public sealed class ScriptedAlarmEngineTests
// Dispose must wait for the in-flight reevaluation to complete rather
// than returning while a background task still runs.
var disposeTask = Task.Run(() => eng.Dispose());
var disposeTask = Task.Run(() => eng.Dispose(), TestContext.Current.CancellationToken);
// Verify Dispose does NOT complete immediately — it should block waiting
// for the in-flight task. Without the -006 fix Dispose returns straight
@@ -1003,7 +1003,7 @@ public sealed class ScriptedAlarmEngineTests
// load could keep firing between the failed second load and Dispose.
// After the fix, it is stopped at the START of every LoadAsync attempt
// (successful or not), so Dispose always finds a clean timer state.
var disposeTask = Task.Run(() => eng.Dispose());
var disposeTask = Task.Run(() => eng.Dispose(), TestContext.Current.CancellationToken);
await disposeTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
// If Dispose threw or hung, the WaitAsync would surface it.
}
@@ -1748,12 +1748,12 @@ public sealed class ScriptedAlarmEngineTests
eng.TrackBackgroundTaskForTest(() => gate.Task);
eng.InFlightCountForTest.ShouldBe(1);
var disposeTask = Task.Run(() => eng.Dispose());
var disposeTask = Task.Run(() => eng.Dispose(), TestContext.Current.CancellationToken);
var raced = await Task.WhenAny(disposeTask, Task.Delay(300));
raced.ShouldNotBe(disposeTask, "Dispose must not return while registered background work is still running");
gate.SetResult();
await disposeTask.WaitAsync(TimeSpan.FromSeconds(5));
await disposeTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
eng.InFlightCountForTest.ShouldBe(0, "completed work must unregister itself");
}
@@ -1785,7 +1785,7 @@ public sealed class ScriptedAlarmEngineTests
() => throw new InvalidOperationException("factory failed (test)")));
eng.InFlightCountForTest.ShouldBe(0, "work that never started must not be left registered");
await Task.Run(() => eng.Dispose()).WaitAsync(TimeSpan.FromSeconds(5));
await Task.Run(() => eng.Dispose(), TestContext.Current.CancellationToken).WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
}
/// <summary>Verifies a faulted background task is still drained by Dispose and its failure is
@@ -1801,11 +1801,11 @@ public sealed class ScriptedAlarmEngineTests
// Fault it while Dispose is actually blocked on the drain, so the WhenAll observes
// the failure rather than finding an already-unregistered task.
var disposeTask = Task.Run(() => Should.NotThrow(() => eng.Dispose()));
await Task.Delay(200);
var disposeTask = Task.Run(() => Should.NotThrow(() => eng.Dispose()), TestContext.Current.CancellationToken);
await Task.Delay(200, TestContext.Current.CancellationToken);
gate.SetException(new InvalidOperationException("background failed (test)"));
await disposeTask.WaitAsync(TimeSpan.FromSeconds(5));
await disposeTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
eng.InFlightCountForTest.ShouldBe(0);
}
@@ -46,7 +46,7 @@ public sealed class ScriptedAlarmSourceLoggingTests
if (sinkCapture.Events.Any(e => e.Level == LogEventLevel.Warning && e.Exception?.Message == "subscriber broke"))
break;
}
await Task.Delay(25);
await Task.Delay(25, TestContext.Current.CancellationToken);
}
LogEvent warning;
@@ -53,7 +53,7 @@ public sealed class ScriptedAlarmSourceTests
var handle = await source.SubscribeAlarmsAsync([], TestContext.Current.CancellationToken);
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Count.ShouldBe(1);
events[0].ConditionId.ShouldBe("Plant/Line1::HighTemp");
@@ -80,7 +80,7 @@ public sealed class ScriptedAlarmSourceTests
var handle = await source.SubscribeAlarmsAsync(["Plant/Line1"], TestContext.Current.CancellationToken);
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Count.ShouldBe(1);
events[0].SourceNodeId.ShouldBe("Plant/Line1");
@@ -102,7 +102,7 @@ public sealed class ScriptedAlarmSourceTests
await source.UnsubscribeAlarmsAsync(handle, TestContext.Current.CancellationToken);
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Count.ShouldBe(0);
}
@@ -124,7 +124,7 @@ public sealed class ScriptedAlarmSourceTests
// Drive HighTemp's input Bad: the predicate freezes (no transition), but the worst-quality bucket
// moves Good→Bad → the engine emits QualityChanged. The source must swallow it.
up.Push("Temp", 50, 0x80000000u);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.ShouldBeEmpty();
}
@@ -138,7 +138,7 @@ public sealed class ScriptedAlarmSourceTests
using var _s = source;
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
engine.GetState("Plant/Line1::HighTemp")!.Acked.ShouldBe(AlarmAckedState.Unacknowledged);
await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
@@ -165,7 +165,7 @@ public sealed class ScriptedAlarmSourceTests
// Supplied principal -> honored.
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
"Plant/Line1", "Plant/Line1::HighTemp", "ack as bob", "bob")],
TestContext.Current.CancellationToken);
@@ -173,9 +173,9 @@ public sealed class ScriptedAlarmSourceTests
// No principal -> default audit identity.
up.Push("Temp", 50);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
up.Push("Temp", 150);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
await source.AcknowledgeAsync([new AlarmAcknowledgeRequest(
"Plant/Line1", "Plant/Line1::HighTemp", "ack anon", null)],
TestContext.Current.CancellationToken);
@@ -36,7 +36,7 @@ public sealed class ScriptCancellationTokenTests
{
using var eval = ScriptEvaluator<FakeScriptContext, int>.Compile("return 1;");
var ctx = new FakeScriptContext();
(await eval.RunAsync(ctx)).ShouldBe(1);
(await eval.RunAsync(ctx, TestContext.Current.CancellationToken)).ShouldBe(1);
ctx.CancellationToken.ShouldBe(CancellationToken.None, "the token is scoped to the run, not left on the context");
}
@@ -26,7 +26,7 @@ public sealed class DriverResiliencePipelineBuilderTests
attempts++;
if (attempts < 3) throw new InvalidOperationException("transient");
await Task.Yield();
});
}, TestContext.Current.CancellationToken);
attempts.ShouldBe(3);
}
@@ -141,7 +141,7 @@ public sealed class DriverResiliencePipelineBuilderTests
{
liveAttempts++;
await Task.Yield();
});
}, TestContext.Current.CancellationToken);
liveAttempts.ShouldBe(1, "healthy sibling host must not be affected by dead peer");
}
@@ -200,7 +200,7 @@ public sealed class DriverResiliencePipelineBuilderTests
{
await Task.Yield();
ran = true;
});
}, TestContext.Current.CancellationToken);
ran.ShouldBeTrue("the breaker must stay closed after refusals");
}
@@ -341,7 +341,7 @@ public sealed class DriverResiliencePipelineBuilderTests
// Elapse the break duration (15 s) so the breaker half-opens, then let a probe succeed → OnClosed.
clock.Advance(TimeSpan.FromSeconds(16));
await pipeline.ExecuteAsync(async _ => await Task.Yield());
await pipeline.ExecuteAsync(async _ => await Task.Yield(), TestContext.Current.CancellationToken);
var snap = tracker.TryGet("drv-trk", "host-c")!;
snap.LastBreakerClosedUtc.ShouldNotBeNull("OnClosed must stamp the close time on recovery");
@@ -404,7 +404,7 @@ public sealed class DriverResiliencePipelineBuilderTests
await Task.Yield();
throw new InvalidOperationException("dead");
}));
await live.ExecuteAsync(async _ => await Task.Yield());
await live.ExecuteAsync(async _ => await Task.Yield(), TestContext.Current.CancellationToken);
tracker.TryGet("drv-trk", "dead")!.ConsecutiveFailures.ShouldBeGreaterThan(0);
tracker.TryGet("drv-trk", "live").ShouldBeNull();
@@ -428,7 +428,7 @@ public sealed class DriverResiliencePipelineBuilderTests
attempts++;
if (attempts < 3) throw new InvalidOperationException("transient");
await Task.Yield();
});
}, TestContext.Current.CancellationToken);
attempts.ShouldBe(3);
logger.Entries.ShouldContain(e =>
@@ -392,7 +392,7 @@ public sealed class AbCipAlarmProjectionTests
int Reads() => factory.Tags.Values.Sum(t => t.ReadCount); // the loop may re-create its runtime on the new device state
var frozen = Reads();
await Task.Delay(300); // ~15 poll intervals; a live loop would advance the count
await Task.Delay(300, TestContext.Current.CancellationToken); // ~15 poll intervals; a live loop would advance the count
Reads().ShouldBe(frozen);
await drv.ShutdownAsync(CancellationToken.None);
@@ -357,7 +357,7 @@ public sealed class AbCipAlmaProjectionTests
await Next(r, x => x.Kind == AlarmTransitionKind.Raise);
r.Factory.Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.NotFound };
r.Factory.Tags[$"{Tag}.HHSeverity"].Status = (int)AbStatus.NotFound;
await Task.Delay(150);
await Task.Delay(150, TestContext.Current.CancellationToken);
await Settled(r);
await r.StopAsync();
Snapshot(r).Count.ShouldBe(1, "no Retrigger/update from a Bad severity read");
@@ -394,7 +394,7 @@ public sealed class AbCipAlmaProjectionTests
factory.Tags[$"{Tag}.HInAlarm"].Value = true;
await WaitFor(() => { lock (events) return events.Count >= 2; });
await Task.Delay(100);
await Task.Delay(100, TestContext.Current.CancellationToken);
await drv.UnsubscribeAlarmsAsync(handle, CancellationToken.None);
await drv.ShutdownAsync(CancellationToken.None);
List<AlarmEventArgs> evs; lock (events) evs = [.. events];
@@ -89,7 +89,7 @@ public sealed class AbCipDeviceRefusalTests
var (drv, factory) = Build(p => new RefusingTag(p) { ReadThrows = p.TagName == "Arr[999]" ? status : null, Value = 1 });
await drv.InitializeAsync("{}", CancellationToken.None);
var before = drv.GetHealth().LastSuccessfulRead!.Value;
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
for (var i = 0; i < 3; i++)
{
@@ -239,7 +239,7 @@ public sealed class AbCipDeviceRefusalTests
: new RefusingTag(p) { ReadThrows = AbStatus.Unsupported });
await drv.InitializeAsync("{}", CancellationToken.None);
var before = drv.GetHealth().LastSuccessfulRead!.Value;
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
(await drv.ReadAsync(["Bad"], CancellationToken.None))[0].StatusCode.ShouldBe(AbCipStatusMapper.BadNotSupported);
drv.GetHealth().LastSuccessfulRead.ShouldBe(before);
@@ -278,7 +278,7 @@ public sealed class AbCipDeviceRefusalTests
}, "abcip-refusal", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var before = drv.GetHealth().LastSuccessfulRead!.Value;
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
(await drv.WriteAsync([new WriteRequest("Ro", 5)], CancellationToken.None))[0].StatusCode
.ShouldBe(AbCipStatusMapper.BadNotWritable);
@@ -104,14 +104,14 @@ public sealed class AbCipLifecycleConcurrencyTests
// The second init's own release waits on the slow first probe loop; the shutdown overtakes it there.
var init = drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
await drv.ShutdownAsync(CancellationToken.None);
var superseded = await Should.ThrowAsync<AbCipInitializationSupersededException>(() => init);
superseded.ShouldNotBeAssignableTo<OperationCanceledException>(); // M4: distinct from a caller cancellation
drv.GetDeviceState(Host).ShouldBeNull();
drv.GetHostStatuses().ShouldBeEmpty();
await Task.Delay(100);
await Task.Delay(100, TestContext.Current.CancellationToken);
factory.Tags["ProbeTag"].ShouldBeSameAs(firstProbe); // no second probe loop ever created a runtime
drv.GetHealth().State.ShouldNotBe(DriverState.Healthy);
@@ -164,7 +164,7 @@ public sealed class AbCipLifecycleConcurrencyTests
}
return reads;
});
await started.Task.WaitAsync(TimeSpan.FromSeconds(5));
await started.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
for (var i = 0; i < 200; i++)
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -204,7 +204,7 @@ public sealed class AbCipLifecycleConcurrencyTests
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitUntilAsync(() => factory.Tags.ContainsKey("ProbeTag"), "probe never started");
var loop = drv.GetDeviceState(Host)!.ProbeTask!;
await Task.Delay(50); // the probe is inside its slow, cancellation-deaf read
await Task.Delay(50, TestContext.Current.CancellationToken); // the probe is inside its slow, cancellation-deaf read
var sw = System.Diagnostics.Stopwatch.StartNew();
((IDisposable)drv).Dispose();
@@ -225,10 +225,10 @@ public sealed class AbCipLifecycleConcurrencyTests
var (drv, factory) = Build(probeReadDelay: TimeSpan.FromMilliseconds(400));
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitUntilAsync(() => factory.Tags.ContainsKey("ProbeTag"), "probe never started");
await Task.Delay(30); // inside the slow probe read
await Task.Delay(30, TestContext.Current.CancellationToken); // inside the slow probe read
var shutdown = drv.ShutdownAsync(CancellationToken.None);
await Task.Delay(30);
await Task.Delay(30, TestContext.Current.CancellationToken);
await drv.InitializeAsync("{}", CancellationToken.None); // newer: publishes while the shutdown still joins
await shutdown;
@@ -59,7 +59,7 @@ public sealed class AbCipProbeRefusalTests
var (drv, logger) = Build(new AbTransportException(AbStatus.NotFound, "NotFound"));
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitUntilAsync(() => drv.GetHostStatuses().Single().State == HostState.Running);
await Task.Delay(100); // many probe ticks
await Task.Delay(100, TestContext.Current.CancellationToken); // many probe ticks
var r = await drv.ReadAsync(["Speed"], CancellationToken.None);
@@ -84,7 +84,7 @@ public sealed class AbCipReadWindowTests
await read;
throw new ShouldAssertException("device A's read waited behind device B's blocked cold create");
}
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
}
read.IsCompleted.ShouldBeFalse(); // B is still blocked — A was read regardless
@@ -84,7 +84,7 @@ public sealed class AbCipWriteHealthTests
await drv.ReadAsync(["Broken"], CancellationToken.None);
var degraded = drv.GetHealth();
degraded.State.ShouldBe(DriverState.Degraded);
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
var r = await drv.WriteAsync([new WriteRequest("D", 5)], CancellationToken.None);
@@ -173,7 +173,7 @@ public sealed class AbLegacyAnsweredStatusTests
var healthyAt = drv.GetHealth().LastSuccessfulRead; // positive control: Good reads DID advance it
healthyAt.ShouldNotBeNull();
foreach (var t in factory.Tags.Values) t.Status = RemoteErr;
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
for (var poll = 0; poll < 3; poll++)
(await drv.ReadAsync(["X", "Y"], CancellationToken.None)).ShouldAllBe(s => s.StatusCode == AbLegacyStatusMapper.BadCommunicationError);
@@ -203,7 +203,7 @@ public sealed class AbLegacyAnsweredStatusTests
var healthyAt = drv.GetHealth().LastSuccessfulRead;
drv.GetDeviceState(Host)!.Runtimes.TryRemove("X", out _);
factory.Customise = p => new ThrowingTag(p, status);
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
var r = await drv.ReadAsync(["X"], CancellationToken.None);
@@ -220,7 +220,7 @@ public sealed class AbLegacyAnsweredStatusTests
var drv = NewDriver(factory, coalesce: false, new AbLegacyTagDefinition("X", Host, "N7:0", AbLegacyDataType.Int));
await drv.InitializeAsync("{}", CancellationToken.None);
var before = drv.GetHealth().LastSuccessfulRead;
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
(await drv.ReadAsync(["X"], CancellationToken.None))[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
@@ -36,7 +36,7 @@ public sealed class AbLegacyBatchSubscribeTests
var handle = await drv.SubscribeAsync(["A", "B"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
var deadline = DateTime.UtcNow.AddSeconds(5);
while (batches.IsEmpty && DateTime.UtcNow < deadline) await Task.Delay(10);
while (batches.IsEmpty && DateTime.UtcNow < deadline) await Task.Delay(10, TestContext.Current.CancellationToken);
batches.TryPeek(out var first).ShouldBeTrue();
first!.SubscriptionHandle.ShouldBe(handle);
@@ -101,7 +101,7 @@ public sealed class AbLegacyDeviceIsolationTests
new AbLegacyTagDefinition("B", HostB, "N7:0", AbLegacyDataType.Int));
await drv.InitializeAsync("{}", CancellationToken.None);
var r = await drv.ReadAsync(["A", "B"], CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
var r = await drv.ReadAsync(["A", "B"], CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
r[0].Value.ShouldBe(1);
r[1].Value.ShouldBe(2);
@@ -157,7 +157,7 @@ public sealed class AbLegacyDeviceIsolationTests
}, "drv-p7", factory, logger);
await drv.InitializeAsync("{}", CancellationToken.None);
var initHealth = drv.GetHealth();
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
var r1 = await drv.ReadAsync(["X"], CancellationToken.None);
var r2 = await drv.ReadAsync(["X"], CancellationToken.None);
@@ -289,7 +289,7 @@ public sealed class AbLegacyFamilyAddressingTests
await drv.InitializeAsync("{}", CancellationToken.None);
var deadline = DateTime.UtcNow.AddSeconds(5);
while (!factory.Tags.ContainsKey("I:15") && DateTime.UtcNow < deadline) await Task.Delay(20);
while (!factory.Tags.ContainsKey("I:15") && DateTime.UtcNow < deadline) await Task.Delay(20, TestContext.Current.CancellationToken);
await drv.ShutdownAsync(CancellationToken.None);
factory.Tags.ContainsKey("I:15").ShouldBeTrue();
@@ -53,7 +53,7 @@ public sealed class AbLegacyHealthEvidenceTests
var drv = await DegradedDriverAsync(f, new AbLegacyTagDefinition("X", Host, "N7:0", AbLegacyDataType.Int));
var before = drv.GetHealth().LastSuccessfulRead;
f.Make = p => new FakeAbLegacyTag(p) { Status = (int)AbStatus.NotFound };
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
var r = await drv.ReadAsync(["X"], CancellationToken.None);
@@ -75,7 +75,7 @@ public sealed class AbLegacyHealthEvidenceTests
new AbLegacyTagDefinition("W", Host, "N7:5", AbLegacyDataType.Int));
var before = drv.GetHealth().LastSuccessfulRead;
f.Make = p => new FakeAbLegacyTag(p) { WriteStatusOverride = writeStatus };
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
var w = await drv.WriteAsync([new WriteRequest("W", 3)], CancellationToken.None);
@@ -113,7 +113,7 @@ public sealed class AbLegacyHealthEvidenceTests
new AbLegacyTagDefinition("Bad", Host, "N7:6", AbLegacyDataType.Int));
await drv.InitializeAsync("{}", CancellationToken.None);
var before = drv.GetHealth().LastSuccessfulRead;
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
var w = await drv.WriteAsync([new WriteRequest("Good", 1), new WriteRequest("Bad", 2)], CancellationToken.None);
@@ -207,7 +207,7 @@ public sealed class AbLegacyHealthEvidenceTests
await entered.Task;
var shutdown = drv.ShutdownAsync(CancellationToken.None); // parks joining the wedged probe loop
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
shutdown.IsCompleted.ShouldBeFalse();
await drv.InitializeAsync("{}", CancellationToken.None); // a newer initialise wins
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
@@ -242,13 +242,13 @@ public sealed class AbLegacyHealthEvidenceTests
var first = f.Tags["N7:0"];
var gate = state.GetRuntimeLock("X");
await gate.WaitAsync();
await gate.WaitAsync(TestContext.Current.CancellationToken);
// Signalled at exactly "handle fetched, about to wait on the gate" — no sleep, so the eviction below always
// lands between the reader's fetch and its use (review 07 r1 test nit: the sleep could pass vacuously).
var atGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
state.BeforeOperationGate = key => { if (key == "X") atGate.TrySetResult(); };
var read = drv.ReadAsync(["X"], CancellationToken.None);
await atGate.Task.WaitAsync(TimeSpan.FromSeconds(5));
await atGate.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
state.Runtimes.TryRemove("X", out var evicted).ShouldBeTrue();
evicted!.Dispose();
gate.Release();
@@ -285,11 +285,11 @@ public sealed class AbLegacyHealthEvidenceTests
state.Runtimes.ContainsKey(key).ShouldBeTrue();
var gate = state.GetRuntimeLock(key);
await gate.WaitAsync();
await gate.WaitAsync(TestContext.Current.CancellationToken);
var atGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
state.BeforeOperationGate = k => { if (k == key) atGate.TrySetResult(); };
var read = drv.ReadAsync(["Ind"], CancellationToken.None);
await atGate.Task.WaitAsync(TimeSpan.FromSeconds(5));
await atGate.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
// What the sweep does when the index moves: evict the resolution, then forget its gate.
state.Runtimes.TryRemove(key, out var swept).ShouldBeTrue();
swept!.Dispose();
@@ -189,7 +189,7 @@ public sealed class AbLegacyLifecycleConcurrencyTests
await probe!.Entered.Task;
var shutdown = drv.ShutdownAsync(CancellationToken.None); // parks joining the wedged probe loop
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
shutdown.IsCompleted.ShouldBeFalse();
Should.NotThrow(() => drv.Dispose());
@@ -232,14 +232,14 @@ public sealed class AbLegacyLifecycleConcurrencyTests
await first!.Entered.Task;
var abandoned = drv.InitializeAsync("{}", CancellationToken.None); // parks joining the wedged loop
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
abandoned.IsCompleted.ShouldBeFalse();
await drv.ShutdownAsync(CancellationToken.None);
release.SetResult();
await Should.ThrowAsync<OperationCanceledException>(() => abandoned);
var createdAfter = probes.Count;
await Task.Delay(150); // several probe intervals
await Task.Delay(150, TestContext.Current.CancellationToken); // several probe intervals
drv.DeviceCount.ShouldBe(0);
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
@@ -38,16 +38,16 @@ public sealed class AbLegacyShutdownJoinTests
Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), ProbeAddress = "S:0" },
}, "drv-join", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var shutdown = drv.ShutdownAsync(CancellationToken.None);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
shutdown.IsCompleted.ShouldBeFalse("shutdown must join the probe loop parked in runtime init");
// Positive control: releasing the parked call lets shutdown finish, with the probe runtime released.
release.SetResult();
await shutdown.WaitAsync(TimeSpan.FromSeconds(5));
await shutdown.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
factory.Tags["S:0"].Disposed.ShouldBeTrue();
}
@@ -68,16 +68,16 @@ public sealed class AbLegacyShutdownJoinTests
Probe = new AbLegacyProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(50), ProbeAddress = "S:0" },
}, "drv-join-dispose", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var dispose = Task.Run(drv.Dispose);
var dispose = Task.Run(drv.Dispose, TestContext.Current.CancellationToken);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
dispose.IsCompleted.ShouldBeFalse("Dispose must join the probe loop parked in runtime init");
// Positive control: releasing the parked call lets Dispose finish, with the probe runtime released.
release.SetResult();
await dispose.WaitAsync(TimeSpan.FromSeconds(5));
await dispose.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
factory.Tags["S:0"].Disposed.ShouldBeTrue();
}
@@ -61,7 +61,7 @@ public sealed class CalculationDriverRemediationTests
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
driver.OnDependencyValue("src/a", 1, StatusGood, DateTime.UtcNow);
var deadline = DateTime.UtcNow.AddSeconds(10);
while (driver.IsEvaluationIdle && DateTime.UtcNow < deadline) await Task.Delay(5);
while (driver.IsEvaluationIdle && DateTime.UtcNow < deadline) await Task.Delay(5, TestContext.Current.CancellationToken);
driver.IsEvaluationIdle.ShouldBeFalse("positive control: a run is in flight when Dispose lands");
var sw = System.Diagnostics.Stopwatch.StartNew();
@@ -74,7 +74,7 @@ public sealed class CalculationDriverTests
{
var (driver, published, _) = Build(
CalcTag("calc/e/sum", "return (double)ctx.GetTag(\"src/a\").Value + (double)ctx.GetTag(\"src/b\").Value;"));
await driver.InitializeAsync("{}", default);
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Only one of two deps arrived → the change-gate holds.
driver.OnDependencyValue("src/a", 10.0, StatusGood, DateTime.UtcNow);
@@ -197,7 +197,7 @@ public sealed class CalculationDriverTests
{
var (driver, published, _) = Build(
CalcTag("calc/e/dbl", "return (double)ctx.GetTag(\"src/a\").Value * 2.0;"));
await driver.InitializeAsync("{}", default);
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
// Each arrival is queued with the dependency snapshot that triggered it, so the three evaluations
// see 5 / 5 / 7 in that order however far the pump lags behind them.
@@ -216,7 +216,7 @@ public sealed class CalculationDriverTests
{
var (driver, published, logs) = Build(
CalcTag("calc/e/div", "return 100 / (int)ctx.GetTag(\"src/a\").Value;"));
await driver.InitializeAsync("{}", default);
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
// a = 0 → DivideByZero → Failure → Bad (carrying last-known = null) + one script-log.
driver.OnDependencyValue("src/a", 0, StatusGood, DateTime.UtcNow);
@@ -254,7 +254,7 @@ public sealed class CalculationDriverTests
// No dependencies (constant script) so only the timer can drive it.
var (driver, published, _) = Build(
CalcTag("calc/e/const", "return 42.0;", changeTriggered: false, timerMs: 50));
await driver.InitializeAsync("{}", default);
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var fired = await WaitUntil(() => !published.IsEmpty, TimeSpan.FromSeconds(3));
fired.ShouldBeTrue("the timer trigger should have computed the tag within 3s");
@@ -262,7 +262,7 @@ public sealed class CalculationDriverTests
e!.FullReference.ShouldBe("calc/e/const");
e.Snapshot.Value.ShouldBe(42.0);
await driver.ShutdownAsync(default);
await driver.ShutdownAsync(TestContext.Current.CancellationToken);
}
// ── IReadable ─────────────────────────────────────────────────────────────────────────────
@@ -271,14 +271,14 @@ public sealed class CalculationDriverTests
public async Task Read_returns_BadWaitingForInitialData_before_first_eval_then_the_computed_value()
{
var (driver, _, _) = Build(CalcTag("calc/e/x", "return (double)ctx.GetTag(\"src/a\").Value * 2.0;"));
await driver.InitializeAsync("{}", default);
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
var before = await driver.ReadAsync(new[] { "calc/e/x" }, default);
var before = await driver.ReadAsync(new[] { "calc/e/x" }, TestContext.Current.CancellationToken);
before[0].StatusCode.ShouldBe(StatusBadWaitingForInitialData);
driver.OnDependencyValue("src/a", 3.0, StatusGood, DateTime.UtcNow);
await Settle(driver);
var after = await driver.ReadAsync(new[] { "calc/e/x" }, default);
var after = await driver.ReadAsync(new[] { "calc/e/x" }, TestContext.Current.CancellationToken);
after[0].StatusCode.ShouldBe(StatusGood);
after[0].Value.ShouldBe(6.0);
}
@@ -111,7 +111,7 @@ public sealed class FocasCapabilityTests
await drv.ShutdownAsync(CancellationToken.None);
var afterShutdown = events.Count;
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Count.ShouldBe(afterShutdown);
}
@@ -60,7 +60,7 @@ public sealed class FocasConcurrentConnectTests
// Deterministic: the first caller is now parked INSIDE its connect (client created, not yet connected) —
// the exact window the race needs. The second caller then runs synchronously up to its first real await:
// pre-fix that is "dispose the first client, Create() a second"; with the gate it parks on ConnectGate.
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var second = drv.ReadAsync(["B"], CancellationToken.None);
release.SetResult();
var results = await Task.WhenAll(first, second);
@@ -89,9 +89,9 @@ public sealed class FocasConcurrentConnectTests
(await drv.ReadAsync(["A"], CancellationToken.None)).Single().StatusCode
.ShouldNotBe(FocasStatusMapper.Good);
// Past the 1 s initial connect backoff window, the next caller must be able to connect.
await Task.Delay(TimeSpan.FromMilliseconds(1100));
await Task.Delay(TimeSpan.FromMilliseconds(1100), TestContext.Current.CancellationToken);
var read = drv.ReadAsync(["A"], CancellationToken.None);
(await read.WaitAsync(TimeSpan.FromSeconds(5))).Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
(await read.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)).Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
}
/// <summary>
@@ -120,12 +120,12 @@ public sealed class FocasConcurrentConnectTests
using var cts = new CancellationTokenSource();
var cancelled = drv.ReadAsync(["A"], cts.Token);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
cts.Cancel();
try { await cancelled; } catch (OperationCanceledException) { /* either surfaced or mapped — both fine */ }
// Immediately — well inside the 1 s initial backoff window a recorded failure would have opened.
var next = await drv.ReadAsync(["A"], CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
var next = await drv.ReadAsync(["A"], CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
next.Single().StatusCode.ShouldBe(FocasStatusMapper.Good, "a caller's own cancellation must not throttle the next connect");
factory.Clients.Count.ShouldBe(2);
}
@@ -67,11 +67,11 @@ public sealed class FocasDeviceTableLifecycleTests
}
catch (Exception ex) { lock (errors) errors.Add(ex); }
}
});
}, TestContext.Current.CancellationToken);
for (var i = 0; i < 100; i++)
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(100);
await Task.Delay(100, TestContext.Current.CancellationToken);
stop.Cancel();
await reader;
await drv.ShutdownAsync(CancellationToken.None);
@@ -105,13 +105,13 @@ public sealed class FocasDeviceTableLifecycleTests
await drv.InitializeAsync("{}", CancellationToken.None);
var read = drv.ReadAsync(["raw/R"], CancellationToken.None);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var shutdown = drv.ShutdownAsync(CancellationToken.None);
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
release.SetResult();
await shutdown.WaitAsync(TimeSpan.FromSeconds(10));
var snaps = await read.WaitAsync(TimeSpan.FromSeconds(5));
await shutdown.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
var snaps = await read.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
factory.Clients.Count.ShouldBe(1);
factory.Clients[0].IsConnected.ShouldBeFalse("a connect finishing on a retired device must dispose its client");
@@ -137,13 +137,13 @@ public sealed class FocasDeviceTableLifecycleTests
return new FocasDriverBinding(options, factory);
});
var init = Task.Run(() => drv.InitializeAsync("{\"devices\":[]}", CancellationToken.None));
await inRebind.Task.WaitAsync(TimeSpan.FromSeconds(5));
var init = Task.Run(() => drv.InitializeAsync("{\"devices\":[]}", CancellationToken.None), TestContext.Current.CancellationToken);
await inRebind.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
await drv.ShutdownAsync(CancellationToken.None);
gate.Set();
await Should.ThrowAsync<OperationCanceledException>(() => init.WaitAsync(TimeSpan.FromSeconds(10)));
await Task.Delay(200); // several probe intervals — a started loop would have connected by now
await Task.Delay(200, TestContext.Current.CancellationToken); // several probe intervals — a started loop would have connected by now
drv.DeviceCount.ShouldBe(0);
factory.Clients.ShouldBeEmpty();
drv.GetHealth().State.ShouldNotBe(DriverState.Healthy);
@@ -180,11 +180,11 @@ public sealed class FocasDeviceTableLifecycleTests
var shutdown = drv.ShutdownAsync(CancellationToken.None);
var dispose = drv.DisposeAsync().AsTask();
await dispose.WaitAsync(TimeSpan.FromSeconds(10));
await dispose.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
factory.Clients.ShouldAllBe(c => !c.IsConnected);
factory.Clients.ShouldAllBe(c => c.DisposeCount >= 1);
await shutdown.WaitAsync(TimeSpan.FromSeconds(10));
await shutdown.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
drv.DeviceCount.ShouldBe(0);
}
@@ -85,11 +85,11 @@ public sealed class FocasFixedTreeConcurrencyTests
var tasks = new List<Task>
{
Task.Run(() => Writer(0)),
Task.Run(() => Writer(1)),
Task.Run(() => Writer(2)),
Task.Run(Reader),
Task.Run(Reader),
Task.Run(() => Writer(0), TestContext.Current.CancellationToken),
Task.Run(() => Writer(1), TestContext.Current.CancellationToken),
Task.Run(() => Writer(2), TestContext.Current.CancellationToken),
Task.Run(Reader, TestContext.Current.CancellationToken),
Task.Run(Reader, TestContext.Current.CancellationToken),
};
await Task.WhenAll(tasks);
@@ -53,7 +53,7 @@ public sealed class FocasHandleRecycleTests
await drv.InitializeAsync("{}", CancellationToken.None);
await drv.ReadAsync(["R"], CancellationToken.None);
await Task.Delay(150);
await Task.Delay(150, TestContext.Current.CancellationToken);
// With recycle off the same client stays live — no Dispose during the window.
factory.Clients.Count.ShouldBe(1);
@@ -119,7 +119,7 @@ public sealed class FocasHandleRecycleTests
var addr = FocasAddress.TryParse("R100")!;
var read = sync.ReadAsync(addr, FocasDataType.Byte, CancellationToken.None);
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
sync.PendingOperations.ShouldBe(1);
var dispose = sync.DisposeWhenIdleAsync(TimeSpan.FromSeconds(5));
@@ -71,7 +71,7 @@ public sealed class FocasProgramInfoQualityTests
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
while (DateTime.UtcNow < deadline && drv.GetDeviceState(Host)?.LastProgramInfo is null)
await Task.Delay(15);
await Task.Delay(15, TestContext.Current.CancellationToken);
var read = await drv.ReadAsync(
[$"{Host}/Program/Name", $"{Host}/Program/BlockCount", $"{Host}/Program/ONumber",
@@ -67,7 +67,7 @@ public sealed class FocasReadHealthContractTests
for (var i = 0; i < 3; i++)
{
await Task.Delay(5);
await Task.Delay(5, TestContext.Current.CancellationToken);
var snap = (await drv.ReadAsync(["raw/R100"], CancellationToken.None))[0];
snap.StatusCode.ShouldBe(refusal);
}
@@ -170,16 +170,16 @@ public sealed class FocasReadHealthContractTests
await drv.InitializeAsync("{}", CancellationToken.None);
var stale = drv.ReadAsync(["raw/R100"], CancellationToken.None);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
await drv.InitializeAsync("{}", CancellationToken.None); // a new generation, Healthy
release.SetResult();
(await stale.WaitAsync(TimeSpan.FromSeconds(5)))[0].StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
(await stale.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken))[0].StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
drv.GetHealth().State.ShouldBe(DriverState.Healthy, "the old generation's failure must not land on the new one");
// Positive control: a failure under the live generation degrades.
var live = drv.ReadAsync(["raw/R100"], CancellationToken.None);
(await live.WaitAsync(TimeSpan.FromSeconds(5)))[0].StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
(await live.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken))[0].StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
await drv.ShutdownAsync(CancellationToken.None);
}
@@ -39,16 +39,16 @@ public sealed class FocasShutdownJoinTests
};
var drv = new FocasDriver(options, "focas-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var shutdown = drv.ShutdownAsync(CancellationToken.None);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
shutdown.IsCompleted.ShouldBeFalse("shutdown must join the probe loop parked in connect");
// Positive control: releasing the parked connect lets shutdown finish.
release.SetResult();
await shutdown.WaitAsync(TimeSpan.FromSeconds(5));
await shutdown.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
factory.Clients.ShouldNotBeEmpty();
factory.Clients.ShouldAllBe(c => c.DisposeCount >= 1);
@@ -66,7 +66,7 @@ public sealed class FocasWireProtocolTests
var (client, server) = await ConnectedPairAsync();
try
{
await server.GetStream().WriteAsync(pdu);
await server.GetStream().WriteAsync(pdu, TestContext.Current.CancellationToken);
var read = await FocasWireProtocol.ReadPduAsync(client.GetStream(), CancellationToken.None);
read.Type.ShouldBe(FocasWireProtocol.TypeData);
@@ -84,7 +84,7 @@ public sealed class FocasWireProtocolTests
var (client, server) = await ConnectedPairAsync();
try
{
await server.GetStream().WriteAsync(bad);
await server.GetStream().WriteAsync(bad, TestContext.Current.CancellationToken);
await Should.ThrowAsync<FocasWireException>(async () =>
await FocasWireProtocol.ReadPduAsync(client.GetStream(), CancellationToken.None));
}
@@ -100,7 +100,7 @@ public sealed class FocasWireProtocolTests
var (client, server) = await ConnectedPairAsync();
try
{
await server.GetStream().WriteAsync(header);
await server.GetStream().WriteAsync(header, TestContext.Current.CancellationToken);
await Should.ThrowAsync<FocasWireException>(async () =>
await FocasWireProtocol.ReadPduAsync(client.GetStream(), CancellationToken.None));
}
@@ -127,7 +127,7 @@ public sealed class FocasWireProtocolTests
var (client, server) = await ConnectedPairAsync();
try
{
await server.GetStream().WriteAsync(pdu);
await server.GetStream().WriteAsync(pdu, TestContext.Current.CancellationToken);
var read = await FocasWireProtocol.ReadPduAsync(client.GetStream(), CancellationToken.None);
read.Type.ShouldBe(FocasWireProtocol.TypeData);
@@ -147,7 +147,7 @@ public sealed class FocasWireV3Tests
try
{
// Send only 5 of the 10 header bytes, then stall forever — the cnc_rdsvmeter hang shape.
await server.GetStream().WriteAsync(new byte[] { 0xa0, 0xa0, 0xa0, 0xa0, 0x00 });
await server.GetStream().WriteAsync(new byte[] { 0xa0, 0xa0, 0xa0, 0xa0, 0x00 }, TestContext.Current.CancellationToken);
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
var sw = Stopwatch.StartNew();
@@ -131,11 +131,11 @@ public sealed class DeployWatcherTests
await watcher.StartAsync(CancellationToken.None);
// Push only the bootstrap event.
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.Parse("2026-01-01T00:00:00Z")));
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.Parse("2026-01-01T00:00:00Z")), TestContext.Current.CancellationToken);
// Give the loop a moment to consume + ack.
await WaitUntilAsync(() => source.CallCount > 0 && channel.Reader.Count == 0, TimeSpan.FromSeconds(2));
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
captured.ShouldBeEmpty();
@@ -156,8 +156,8 @@ public sealed class DeployWatcherTests
await watcher.StartAsync(CancellationToken.None);
await channel.Writer.WriteAsync(Event(0, t0)); // bootstrap
await channel.Writer.WriteAsync(Event(1, t1)); // real change
await channel.Writer.WriteAsync(Event(0, t0), TestContext.Current.CancellationToken); // bootstrap
await channel.Writer.WriteAsync(Event(1, t1), TestContext.Current.CancellationToken); // real change
await WaitUntilAsync(() => captured.Count >= 1, TimeSpan.FromSeconds(2));
@@ -183,11 +183,11 @@ public sealed class DeployWatcherTests
await watcher.StartAsync(CancellationToken.None);
await channel.Writer.WriteAsync(Event(0, t0)); // bootstrap
await channel.Writer.WriteAsync(Event(2, t0)); // duplicate state — gateway re-sent
await channel.Writer.WriteAsync(Event(0, t0), TestContext.Current.CancellationToken); // bootstrap
await channel.Writer.WriteAsync(Event(2, t0), TestContext.Current.CancellationToken); // duplicate state — gateway re-sent
await WaitUntilAsync(() => channel.Reader.Count == 0, TimeSpan.FromSeconds(2));
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
captured.ShouldBeEmpty();
@@ -208,9 +208,9 @@ public sealed class DeployWatcherTests
await watcher.StartAsync(CancellationToken.None);
// Bootstrap with absent deploy time (Galaxy never deployed).
await channel.Writer.WriteAsync(Event(0, deployTime: null));
await channel.Writer.WriteAsync(Event(0, deployTime: null), TestContext.Current.CancellationToken);
// Now a deploy lands and the present flag flips.
await channel.Writer.WriteAsync(Event(1, t1));
await channel.Writer.WriteAsync(Event(1, t1), TestContext.Current.CancellationToken);
await WaitUntilAsync(() => captured.Count >= 1, TimeSpan.FromSeconds(2));
@@ -232,7 +232,7 @@ public sealed class DeployWatcherTests
await watcher.StartAsync(CancellationToken.None);
// Push bootstrap so the loop enters its enumeration body before stop.
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.UtcNow));
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.UtcNow), TestContext.Current.CancellationToken);
await WaitUntilAsync(() => source.CallCount > 0, TimeSpan.FromSeconds(2));
// StopAsync should complete without throwing and within a reasonable window.
@@ -251,11 +251,11 @@ public sealed class DeployWatcherTests
var watcher = new DeployWatcher(source);
await watcher.StartAsync(CancellationToken.None);
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.UtcNow));
await channel.Writer.WriteAsync(Event(0, DateTimeOffset.UtcNow), TestContext.Current.CancellationToken);
await WaitUntilAsync(() => source.CallCount > 0, TimeSpan.FromSeconds(2));
// Should not throw, should not hang.
var disposeTask = Task.Run(watcher.Dispose);
var disposeTask = Task.Run(watcher.Dispose, TestContext.Current.CancellationToken);
var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(5)));
completed.ShouldBe(disposeTask);
await disposeTask;
@@ -295,8 +295,8 @@ public sealed class DeployWatcherTests
await WaitUntilAsync(() => source.CallCount >= 2, TimeSpan.FromSeconds(2));
// Now feed bootstrap + real event into the second channel.
await secondChannel.Writer.WriteAsync(Event(0, t0));
await secondChannel.Writer.WriteAsync(Event(1, t1));
await secondChannel.Writer.WriteAsync(Event(0, t0), TestContext.Current.CancellationToken);
await secondChannel.Writer.WriteAsync(Event(1, t1), TestContext.Current.CancellationToken);
await WaitUntilAsync(() => captured.Count >= 1, TimeSpan.FromSeconds(2));
@@ -30,7 +30,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
[Fact]
public async Task Literal_string_is_returned_unchanged()
{
(await GalaxySecretRef.ResolveApiKeyAsync("plain-text-key", FakeResolver()))
(await GalaxySecretRef.ResolveApiKeyAsync("plain-text-key", FakeResolver(), ct: TestContext.Current.CancellationToken))
.ShouldBe("plain-text-key");
}
@@ -42,7 +42,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
Environment.SetEnvironmentVariable(name, "key-from-env");
try
{
(await GalaxySecretRef.ResolveApiKeyAsync($"env:{name}", FakeResolver()))
(await GalaxySecretRef.ResolveApiKeyAsync($"env:{name}", FakeResolver(), ct: TestContext.Current.CancellationToken))
.ShouldBe("key-from-env");
}
finally
@@ -72,7 +72,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
File.WriteAllText(path, " key-from-file \n");
try
{
(await GalaxySecretRef.ResolveApiKeyAsync($"file:{path}", FakeResolver()))
(await GalaxySecretRef.ResolveApiKeyAsync($"file:{path}", FakeResolver(), ct: TestContext.Current.CancellationToken))
.ShouldBe("key-from-file");
}
finally
@@ -98,7 +98,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
[Fact]
public async Task Secret_prefix_resolves_through_secret_resolver()
{
(await GalaxySecretRef.ResolveApiKeyAsync($"secret:{KnownSecretName}", FakeResolver()))
(await GalaxySecretRef.ResolveApiKeyAsync($"secret:{KnownSecretName}", FakeResolver(), ct: TestContext.Current.CancellationToken))
.ShouldBe(KnownSecretValue);
}
@@ -122,7 +122,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
public async Task Secret_prefix_does_not_emit_literal_warning()
{
var logger = new CaptureLogger();
await GalaxySecretRef.ResolveApiKeyAsync($"secret:{KnownSecretName}", FakeResolver(), logger);
await GalaxySecretRef.ResolveApiKeyAsync($"secret:{KnownSecretName}", FakeResolver(), logger, TestContext.Current.CancellationToken);
logger.Entries.ShouldNotContain(e => e.Level == LogLevel.Warning);
}
@@ -136,7 +136,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
// in the DriverConfig JSON. The resolver must surface a warning so an
// operator who committed one by accident sees it at startup.
var logger = new CaptureLogger();
var key = await GalaxySecretRef.ResolveApiKeyAsync("plain-text-key", FakeResolver(), logger);
var key = await GalaxySecretRef.ResolveApiKeyAsync("plain-text-key", FakeResolver(), logger, TestContext.Current.CancellationToken);
key.ShouldBe("plain-text-key");
logger.Entries.ShouldContain(e =>
@@ -151,7 +151,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
// key (dev / parity rig). The resolver must accept it AND suppress the
// warning so production logs aren't polluted on a deliberate dev choice.
var logger = new CaptureLogger();
var key = await GalaxySecretRef.ResolveApiKeyAsync("dev:plain-text-key", FakeResolver(), logger);
var key = await GalaxySecretRef.ResolveApiKeyAsync("dev:plain-text-key", FakeResolver(), logger, TestContext.Current.CancellationToken);
key.ShouldBe("plain-text-key");
logger.Entries.ShouldNotContain(e => e.Level == LogLevel.Warning);
@@ -166,7 +166,7 @@ public sealed class GalaxyDriverApiKeyResolverTests
try
{
var logger = new CaptureLogger();
await GalaxySecretRef.ResolveApiKeyAsync($"env:{name}", FakeResolver(), logger);
await GalaxySecretRef.ResolveApiKeyAsync($"env:{name}", FakeResolver(), logger, TestContext.Current.CancellationToken);
logger.Entries.ShouldNotContain(e => e.Level == LogLevel.Warning);
}
finally
@@ -222,7 +222,7 @@ public sealed class GalaxyDriverFactoryTests
var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value;
// Force a measurable clock delta so the comparison is stable on fast machines.
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
await driver.ReinitializeAsync(equivalentConfig, CancellationToken.None);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
@@ -140,7 +140,7 @@ public sealed class HostStatusAggregatorTests
}
})).ToArray();
Task.WaitAll(tasks);
Task.WaitAll(tasks, TestContext.Current.CancellationToken);
agg.Snapshot().Count.ShouldBeLessThanOrEqualTo(32);
}
}
@@ -82,7 +82,7 @@ public sealed class EventPumpBoundedChannelTests
// Stage 1 — one event, then wait for the dispatcher to take it and block. After this the
// channel is provably empty and exactly one event is in flight inside the handler.
await subscriber.EmitAsync(itemHandle: 7, value: 0);
await handlerEntered.Task.WaitAsync(Budget);
await handlerEntered.Task.WaitAsync(Budget, TestContext.Current.CancellationToken);
// Stage 2 — the rest. `channelCapacity` are accepted; the remainder must be dropped.
for (var i = 1; i < totalEvents; i++)
@@ -97,7 +97,7 @@ public sealed class EventPumpStreamFaultTests
oldPump.OnDataChange += (_, _) => staleObserved = true;
oldPump.Start();
faulted.FaultStream(new IOException("simulated gateway transport drop"));
await Task.Delay(100);
await Task.Delay(100, TestContext.Current.CancellationToken);
// In-place Start() after a fault is a no-op — the loop task is non-null but done.
oldPump.Start();
@@ -324,7 +324,7 @@ public sealed class GalaxyDriverFixRound1Tests
// Never leave the teardown parked: it holds the lifecycle gate the await-using dispose needs.
sub.ReleaseUnsubscribe();
}
await shutdown.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await shutdown.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
driver.HasEventPumpForTests.ShouldBeFalse("no pump may survive the teardown");
driver.TrackedSubscriptionCountForTests.ShouldBe(0);
@@ -354,7 +354,7 @@ public sealed class GalaxyDriverFixRound1Tests
};
var sw = System.Diagnostics.Stopwatch.StartNew();
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
sw.Stop();
closeEntered.Task.IsCompleted.ShouldBeTrue("positive control: the close was attempted");
@@ -398,7 +398,7 @@ public sealed class GalaxyDriverFixRound1Tests
driver.TrackedSubscriptionCountForTests.ShouldBe(0);
sub.ReleaseSubscribe();
await replay.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await replay.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
driver.TrackedSubscriptionCountForTests.ShouldBe(0, "a Rebind after the teardown would resurrect dead handles");
}
@@ -84,14 +84,14 @@ public sealed class GalaxyDriverFixRound2Tests
try
{
var sw = System.Diagnostics.Stopwatch.StartNew();
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
sw.Stop();
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
await Entered(platforms.Entered, "positive control: the seed fetch was started");
sw.Elapsed.ShouldBeLessThan(InitBudget);
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
var token = await platforms.ParkedToken.Task;
token.IsCancellationRequested.ShouldBeTrue("the teardown must cancel a seed still in flight");
}
@@ -109,7 +109,7 @@ public sealed class GalaxyDriverFixRound2Tests
var platforms = new ThrowingPlatformSource(new OperationCanceledException("gateway deadline"));
await using var driver = new GalaxyDriver("drv", Opts(), hierarchySource: null, subscriber: sub, platformSource: platforms);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
await WaitUntilAsync(() => platforms.Calls == 1, "positive control: the seed fetch ran");
@@ -136,7 +136,7 @@ public sealed class GalaxyDriverFixRound2Tests
try
{
initCts.CancelAfter(TimeSpan.FromMilliseconds(300)); // the Polly Initialize timeout, shrunk
await driver.InitializeAsync("{}", initCts.Token).WaitAsync(InitBudget);
await driver.InitializeAsync("{}", initCts.Token).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
await Entered(platforms.Entered, "the seed fetch must be in flight");
@@ -162,7 +162,7 @@ public sealed class GalaxyDriverFixRound2Tests
await using var driver = new GalaxyDriver("drv", Opts(), hierarchySource: null, subscriber: sub, platformSource: platforms);
try
{
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
await Entered(platforms.Entered, "the seed must be parked");
driver.ProbeWatcherForTests.ShouldNotBeNull().WatchedPlatforms.ShouldBeEmpty();
}
@@ -197,11 +197,11 @@ public sealed class GalaxyDriverFixRound2Tests
await using var driver = new GalaxyDriver("drv", Opts(), hierarchySource: null, subscriber: sub, platformSource: platforms);
try
{
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
await Entered(platforms.Entered, "runtime 1's seed must be parked");
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget);
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
await driver.InitializeAsync("{}", CancellationToken.None).WaitAsync(InitBudget, TestContext.Current.CancellationToken);
}
finally
{
@@ -242,7 +242,7 @@ public sealed class GalaxyDriverFixRound2Tests
await watcher.StopAsync(CancellationToken.None);
sub.ReleaseSubscribe();
await sync.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await sync.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
registry.Contains(SubscriptionRegistry.ProbeSubscriptionId).ShouldBeFalse("a stopped watcher must not re-register");
watcher.WatchedPlatforms.ShouldBeEmpty();
@@ -271,7 +271,7 @@ public sealed class GalaxyDriverFixRound2Tests
try
{
await Entered(sub.SubscribeEntered, "the subscribe must be parked in SubscribeBulk");
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
driver.TrackedSubscriptionCountForTests.ShouldBe(0);
}
finally
@@ -299,7 +299,7 @@ public sealed class GalaxyDriverFixRound2Tests
try
{
await Entered(sub.SubscribeEntered, "the read must be parked in SubscribeBulk");
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
await driver.ShutdownAsync(CancellationToken.None).WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
}
finally
{
@@ -197,7 +197,7 @@ public sealed class GalaxyDriverProbeWiringTests
};
await driver.InitializeAsync("{}", CancellationToken.None);
var initStamp = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
var delivered = 0;
driver.OnDataChange += (_, _) => Interlocked.Increment(ref delivered);
@@ -209,7 +209,7 @@ public sealed class GalaxyDriverProbeWiringTests
var first = driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
first.ShouldBeGreaterThan(initStamp, "data on the stream is liveness evidence");
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
await sub.EmitDoubleAsync(handle, 2.0);
await WaitUntilAsync(() => Volatile.Read(ref delivered) == 2, "second event must be delivered");
driver.GetHealth().LastSuccessfulRead.ShouldBe(first, "inside the throttle window the stamp holds still");
@@ -109,7 +109,7 @@ public sealed class GalaxyDriverReadTests
var readTask = driver.ReadAsync(["Tank.Level"], CancellationToken.None);
// Push the "initial value" event the gw would emit immediately after SubscribeBulk.
await Task.Delay(50); // give SubscribeBulk a beat to register + handler to attach
await Task.Delay(50, TestContext.Current.CancellationToken); // give SubscribeBulk a beat to register + handler to attach
var itemHandle = subscriber.Map["Tank.Level"];
await subscriber.EmitOnDataChangeAsync(itemHandle, 42.0);
@@ -131,7 +131,7 @@ public sealed class GalaxyDriverReadTests
"g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber);
var readTask = driver.ReadAsync(["Good", "Bad"], CancellationToken.None);
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
await subscriber.EmitOnDataChangeAsync(subscriber.Map["Good"], 1.0);
var result = await readTask;
@@ -197,7 +197,7 @@ public sealed class GalaxyDriverReadTests
};
var result = await driver.ReadAsync(["Slow.A", "Slow.B"], CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(10));
.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
result.Count.ShouldBe(2);
result.ShouldAllBe(r => r.StatusCode == StatusCodeMap.BadTimeout);
@@ -205,7 +205,7 @@ public sealed class GalaxyDriverReadTests
// The RPC finally completes after the read gave up; its handles must not leak.
subscriber.Gate.SetResult();
var deadline = DateTime.UtcNow.AddSeconds(10);
while (subscriber.UnsubscribedHandles.Count < 2 && DateTime.UtcNow < deadline) await Task.Delay(20);
while (subscriber.UnsubscribedHandles.Count < 2 && DateTime.UtcNow < deadline) await Task.Delay(20, TestContext.Current.CancellationToken);
subscriber.UnsubscribedHandles.OrderBy(h => h).ShouldBe([101, 102]);
}
@@ -257,7 +257,7 @@ public sealed class GalaxyDriverShutdownTests
await source.PushAsync(1, new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
await source.PushAsync(2, new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero));
await WaitUntilAsync(() => Volatile.Read(ref raised) >= 1, "positive control: a deploy change must raise rediscovery");
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
Volatile.Read(ref raised).ShouldBe(1, "stacked watchers would each raise for the same deploy");
await driver.ShutdownAsync(CancellationToken.None);
@@ -195,7 +195,7 @@ public sealed class GalaxyDriverSubscribeTests
// Good has an itemHandle; Bad doesn't (item handle 0). An event with handle 0
// must NOT be dispatched (no subscribers registered against it).
await subscriber.EmitOnDataChangeAsync(itemHandle: 0, value: 999.0);
await Task.Delay(50); // give the pump a chance
await Task.Delay(50, TestContext.Current.CancellationToken); // give the pump a chance
captured.ShouldBeEmpty();
}
@@ -219,7 +219,7 @@ public sealed class GalaxyDriverSubscribeTests
var captured = new List<DataChangeEventArgs>();
driver.OnDataChange += (_, args) => captured.Add(args);
await subscriber.EmitOnDataChangeAsync(itemHandle, 11.0);
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
captured.ShouldBeEmpty();
}
@@ -26,7 +26,7 @@ public sealed class GalaxyMxSessionCloseTests
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
var sw = System.Diagnostics.Stopwatch.StartNew();
await session.CloseAsync(cts.Token).WaitAsync(TimeSpan.FromSeconds(10));
await session.CloseAsync(cts.Token).WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
sw.Stop();
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5));
@@ -88,7 +88,7 @@ public sealed class GatewayGalaxyDataWriterConcurrencyTests
var writes = Enumerable.Range(0, 16).Select(i => new WriteRequest($"Tag{i}.PV", (double)i)).ToArray();
var results = await NewWriter().WriteBatchForTestAsync(ops, writes, Free, CancellationToken.None)
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
ops.Peak.ShouldBe(GatewayGalaxyDataWriter.MaxConcurrentWriteLanes,
"first-touch lanes must overlap (serial = 1) and stay bounded (unbounded > 8)");
@@ -112,7 +112,7 @@ public sealed class GatewayGalaxyDataWriterConcurrencyTests
};
var results = await NewWriter().WriteBatchForTestAsync(ops, writes, Free, CancellationToken.None)
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
results.Count.ShouldBe(4);
results.ShouldAllBe(r => r.StatusCode == StatusCodeMap.Good);
@@ -146,7 +146,7 @@ public sealed class GatewayGalaxyDataWriterConcurrencyTests
};
var results = await NewWriter().WriteBatchForTestAsync(ops, writes, Free, CancellationToken.None)
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
results.Select(r => r.StatusCode).ShouldBe(
[StatusCodeMap.Good, StatusCodeMap.BadCommunicationError, StatusCodeMap.Good],
@@ -161,7 +161,7 @@ public sealed class GatewayGalaxyDataWriterConcurrencyTests
var writes = new[] { new WriteRequest("X.PV", 1.0), new WriteRequest("X.PV", 2.0) };
var results = await NewWriter().WriteBatchForTestAsync(ops, writes, Free, CancellationToken.None)
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs));
.WaitAsync(TimeSpan.FromMilliseconds(WaitMs), TestContext.Current.CancellationToken);
results.Count.ShouldBe(2);
ops.Peak.ShouldBe(1);
@@ -163,7 +163,7 @@ public sealed class ReconnectSupervisorTests
options: FastBackoff());
sup.ReportTransportFailure(new IOException("kick off"));
await Task.Delay(50); // let the loop start the long reopen
await Task.Delay(50, TestContext.Current.CancellationToken); // let the loop start the long reopen
Should.NotThrow(() => sup.Dispose());
cancelled.ShouldBeTrue();
}
@@ -33,11 +33,11 @@ public sealed class AlarmWriterTests(SqlHistorianFixture fx)
(await w.WriteBatchAsync([Ev(kind: "Cleared")], CancellationToken.None)).ShouldBe([HistorianWriteOutcome.Ack]);
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await c.OpenAsync(TestContext.Current.CancellationToken);
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT SourceName, AlarmId, AlarmName, AlarmTypeName, Severity, EventKind, Message, UserName, Comment, EventTimeUtc, ReceivedTimeUtc FROM [{s}].Event WHERE EventKind='Activated'";
await using var r = await cmd.ExecuteReaderAsync();
(await r.ReadAsync()).ShouldBeTrue();
await using var r = await cmd.ExecuteReaderAsync(TestContext.Current.CancellationToken);
(await r.ReadAsync(TestContext.Current.CancellationToken)).ShouldBeTrue();
r.GetString(0).ShouldBe("Plant/Line/Eq");
r.GetString(1).ShouldBe("alarm-1");
r.GetString(2).ShouldBe("HiTemp");
@@ -48,10 +48,10 @@ public sealed class AlarmWriterTests(SqlHistorianFixture fx)
r.GetString(8).ShouldBe("c");
SqlHistorianConnection.GetUtc(r, 9).Ticks.ShouldBe(T.Ticks);
(DateTime.UtcNow - SqlHistorianConnection.GetUtc(r, 10)).ShouldBeLessThan(TimeSpan.FromMinutes(1));
(await r.ReadAsync()).ShouldBeFalse();
(await r.ReadAsync(TestContext.Current.CancellationToken)).ShouldBeFalse();
await r.CloseAsync();
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Event";
Convert.ToInt32(await cmd.ExecuteScalarAsync()).ShouldBe(2);
Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)).ShouldBe(2);
}
[Fact]
@@ -72,10 +72,10 @@ public sealed class AlarmWriterTests(SqlHistorianFixture fx)
var rs = await Task.WhenAll(Enumerable.Range(0, 12).Select(_ => Task.Run(() => w.WriteBatchAsync([Ev()], CancellationToken.None))));
rs.ShouldAllBe(x => x[0] == HistorianWriteOutcome.Ack);
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await c.OpenAsync(TestContext.Current.CancellationToken);
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Event WHERE EventKind='Activated'";
Convert.ToInt32(await cmd.ExecuteScalarAsync()).ShouldBe(1);
Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)).ShouldBe(1);
}
[Fact]
@@ -90,10 +90,10 @@ public sealed class ProvisioningTests(SqlHistorianFixture fx)
}
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await c.OpenAsync(TestContext.Current.CancellationToken);
await using var cmd = c.CreateCommand();
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Tag";
Convert.ToInt32(await cmd.ExecuteScalarAsync()).ShouldBe(20 * 6 + 1);
Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)).ShouldBe(20 * 6 + 1);
}
[Fact]
@@ -117,11 +117,11 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
// Hold the same session applock the purger takes, on a separate connection.
await using var holder = new SqlConnection(fx.ConnectionString);
await holder.OpenAsync();
await holder.OpenAsync(TestContext.Current.CancellationToken);
await using (var cmd = holder.CreateCommand())
{
cmd.CommandText = $"EXEC sp_getapplock @Resource = N'OtOpcUa.Historian.Retention.{s}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = 0;";
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
var purger = new SqlServerHistorianRetention(c, b, TimeSpan.FromHours(1), 30, 4000, time: new FixedTime(Now));
@@ -131,7 +131,7 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
await using (var cmd = holder.CreateCommand())
{
cmd.CommandText = $"EXEC sp_releaseapplock @Resource = N'OtOpcUa.Historian.Retention.{s}', @LockOwner = 'Session';";
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
(await purger.PurgeAsync(CancellationToken.None)).ShouldNotBeEmpty();
(await CountAsync($"SELECT COUNT(*) FROM [{s}].Sample")).ShouldBe(6);
@@ -187,20 +187,20 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
// A second session holds an exclusive table lock, so the purger's first DELETE blocks server-side.
await using var holder = new SqlConnection(fx.ConnectionString);
await holder.OpenAsync();
await using var tx = (SqlTransaction)await holder.BeginTransactionAsync();
await holder.OpenAsync(TestContext.Current.CancellationToken);
await using var tx = (SqlTransaction)await holder.BeginTransactionAsync(TestContext.Current.CancellationToken);
await using (var cmd = holder.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = $"SELECT COUNT(*) FROM [{s}].Sample WITH (TABLOCKX, HOLDLOCK)";
await cmd.ExecuteScalarAsync();
await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken);
}
short holderSpid;
await using (var cmd = holder.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText = "SELECT CAST(@@SPID AS smallint)";
holderSpid = (short)(await cmd.ExecuteScalarAsync())!;
holderSpid = (short)(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken))!;
}
using var cts = new CancellationTokenSource();
@@ -215,7 +215,7 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
{
purge.IsCompleted.ShouldBeFalse("the purge finished without ever blocking on the holder's table lock");
DateTime.UtcNow.ShouldBeLessThan(deadline, "the purge's DELETE never blocked behind the holder");
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
}
cts.Cancel();
@@ -223,7 +223,7 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
var ex = await Should.ThrowAsync<Exception>(() => purge);
(ex is OperationCanceledException or SqlException).ShouldBeTrue($"unexpected {ex.GetType().Name}: {ex.Message}");
await tx.RollbackAsync();
await tx.RollbackAsync(TestContext.Current.CancellationToken);
(await LockIsFreeAsync(s)).ShouldBeTrue();
(await CountAsync($"SELECT COUNT(*) FROM [{s}].Sample")).ShouldBe(21);
}
@@ -301,11 +301,11 @@ public sealed class RetentionTests(SqlHistorianFixture fx)
await SeedAsync(b, s);
await using var holder = new SqlConnection(fx.ConnectionString);
await holder.OpenAsync();
await holder.OpenAsync(TestContext.Current.CancellationToken);
await using (var cmd = holder.CreateCommand())
{
cmd.CommandText = $"EXEC sp_getapplock @Resource = N'OtOpcUa.Historian.Retention.{s}', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = 0;";
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
var log = new CapturingLogger();
@@ -44,12 +44,12 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
await Boot(Opts(s)).EnsureAsync(CancellationToken.None);
var script = new StreamReader(typeof(SqlServerSchemaBootstrapper).Assembly.GetManifestResourceStream("Schema.001_initial.sql")!).ReadToEnd();
await using var c = new SqlConnection(fx.ConnectionString);
await c.OpenAsync();
await c.OpenAsync(TestContext.Current.CancellationToken);
foreach (var b in SqlServerSchemaBootstrapper.SplitBatches(script.Replace("$(Schema)", s)))
{
await using var cmd = c.CreateCommand();
cmd.CommandText = b;
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
(await ScalarAsync($"SELECT COUNT(*) FROM [{s}].SchemaVersion")).ShouldBe(SqlServerSchemaBootstrapper.ModifiedTrailVersion);
}
@@ -68,17 +68,17 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
var v1 = new StreamReader(typeof(SqlServerSchemaBootstrapper).Assembly.GetManifestResourceStream("Schema.001_initial.sql")!).ReadToEnd();
await using (var c = new SqlConnection(fx.ConnectionString))
{
await c.OpenAsync();
await c.OpenAsync(TestContext.Current.CancellationToken);
foreach (var b in SqlServerSchemaBootstrapper.SplitBatches(v1.Replace("$(Schema)", s)))
{
await using var cmd = c.CreateCommand();
cmd.CommandText = b;
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
await using var seed = c.CreateCommand();
seed.CommandText = $"INSERT [{s}].Tag (Name, DataType, CreatedUtc, UpdatedUtc) VALUES (N'old', 1, SYSUTCDATETIME(), SYSUTCDATETIME()); " +
$"INSERT [{s}].Sample (TagId, SourceTimeUtc, ValueKind, NumValue, StatusCode, ServerTimeUtc) SELECT TagId, '2026-01-01', 1, 4.0, 0, SYSUTCDATETIME() FROM [{s}].Tag";
await seed.ExecuteNonQueryAsync();
await seed.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
(await ScalarAsync($"SELECT COUNT(*) FROM sys.tables WHERE name = 'SampleModified' AND schema_id = SCHEMA_ID('{s}')")).ShouldBe(0);
@@ -178,7 +178,7 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
start.Wait();
return await cat.GetOrCreateTagIdAsync(name, 1, CancellationToken.None);
})).ToArray();
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
start.Set();
(await Task.WhenAll(ids)).Distinct().Count().ShouldBe(1);
(await ScalarAsync($"SELECT COUNT(*) FROM [{s}].Tag WHERE Name = N'{name}'")).ShouldBe(1);
@@ -203,10 +203,10 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
cmd.Parameters.AddWithValue("@id", tagId);
conn.AddStructured(cmd, "@rows", "SampleRow",
SampleRowMapper.ToSampleRecords([new HistorianSampleRow(t, ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistorianValue.FromDouble(1), 0u)]));
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
await using var q = conn.CreateCommand(c, $"SELECT SourceTimeUtc FROM {conn.Q("Sample")}");
await using var r = await q.ExecuteReaderAsync();
(await r.ReadAsync()).ShouldBeTrue();
await using var r = await q.ExecuteReaderAsync(TestContext.Current.CancellationToken);
(await r.ReadAsync(TestContext.Current.CancellationToken)).ShouldBeTrue();
SqlHistorianConnection.GetUtc(r, 0).Ticks.ShouldBe(t.Ticks);
}
}
@@ -74,14 +74,14 @@ public sealed class MTConnectDriverProbeTests
public async Task Probe_on_unreachable_agent_returns_not_ok_and_does_not_throw()
{
var probe = new MTConnectDriverProbe();
var r = await probe.ProbeAsync("{\"agentUri\":\"http://127.0.0.1:1/\"}", TimeSpan.FromMilliseconds(300), default);
var r = await probe.ProbeAsync("{\"agentUri\":\"http://127.0.0.1:1/\"}", TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
r.Ok.ShouldBeFalse();
r.Message.ShouldNotBeNull();
}
[Fact]
public async Task Probe_on_blank_config_returns_not_ok()
=> (await new MTConnectDriverProbe().ProbeAsync("{}", TimeSpan.FromSeconds(1), default)).Ok.ShouldBeFalse();
=> (await new MTConnectDriverProbe().ProbeAsync("{}", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken)).Ok.ShouldBeFalse();
// ── unparseable JSON ──────────────────────────────────────────────────────
@@ -165,7 +165,7 @@ public sealed class ModbusBatchSubscriptionTests
// it as a batch item, but the driver's deadband suppresses the publish — so neither
// event fires and no batch is raised for that cycle at all.
fake.HoldingRegisters[0] = 102;
await Task.Delay(600);
await Task.Delay(600, TestContext.Current.CancellationToken);
perTag.Count.ShouldBe(1, "under-threshold change is suppressed on the per-tag event");
batches.Count.ShouldBe(1, "a cycle whose only change is deadband-suppressed raises no batch");
@@ -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)).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken).ConfigureAwait(false);
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)).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken).ConfigureAwait(false);
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)).ConfigureAwait(false);
await serverTask.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken).ConfigureAwait(false);
result.Ok.ShouldBeTrue();
result.Message.ShouldNotBeNull();
@@ -245,7 +245,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
// new sends. Tolerate at most one in-flight send straddling the cancel.
fake.DisposeCount.ShouldBe(1);
var sendsAfterDispose = Interlocked.CompareExchange(ref fake.SendCount, 0, 0);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
var sendsAtRest = Interlocked.CompareExchange(ref fake.SendCount, 0, 0);
(sendsAtRest - sendsAfterDispose).ShouldBeLessThanOrEqualTo(1, "background loops must stop after DisposeAsync");
}
@@ -267,7 +267,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
// Spin up a polled subscription; the PollGroupEngine schedules a background Task that
// will keep issuing SendAsync until either Unsubscribe or DisposeAsync stops it.
var handle = await drv.SubscribeAsync(["A"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
await Task.Delay(250);
await Task.Delay(250, TestContext.Current.CancellationToken);
var beforeDispose = Interlocked.CompareExchange(ref fake.SendCount, 0, 0);
beforeDispose.ShouldBeGreaterThan(0);
@@ -275,7 +275,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
await drv.DisposeAsync();
var atDispose = Interlocked.CompareExchange(ref fake.SendCount, 0, 0);
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
var atRest = Interlocked.CompareExchange(ref fake.SendCount, 0, 0);
(atRest - atDispose).ShouldBeLessThanOrEqualTo(1,
@@ -353,7 +353,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
var drv = new ModbusDriver(opts, "modbus-1", _ => Interlocked.Increment(ref handed) == 1 ? first : second);
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
Interlocked.CompareExchange(ref first.SendCount, 0, 0).ShouldBeGreaterThan(0,
"precondition: the first initialize's probe loop must actually be sending");
@@ -361,7 +361,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
// Tolerate one in-flight send straddling the cancel; a leaked loop produces ~16 in 400ms.
var atReinit = Interlocked.CompareExchange(ref first.SendCount, 0, 0);
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
var atRest = Interlocked.CompareExchange(ref first.SendCount, 0, 0);
(atRest - atReinit).ShouldBeLessThanOrEqualTo(1,
"the previous initialize's probe loop must be cancelled by the next initialize");
@@ -442,7 +442,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
Activator.CreateInstance(stateType, nonPublic: true)!);
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
Interlocked.CompareExchange(ref first.SendCount, 0, 0).ShouldBeGreaterThan(0,
"precondition: the seeded prohibition must make the re-probe loop actually send");
@@ -464,7 +464,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
prohibitions.Count.ShouldBe(1, "a re-initialize must not clear the prohibition set");
var atReinit = Interlocked.CompareExchange(ref second.SendCount, 0, 0);
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
var atRest = Interlocked.CompareExchange(ref second.SendCount, 0, 0);
(atRest - atReinit).ShouldBeLessThanOrEqualTo(1,
"the previous initialize's re-probe loop must be cancelled by the next initialize");
@@ -723,7 +723,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
{
try { while (!cts.IsCancellationRequested) await drv.ReadAsync(["A"], CancellationToken.None); }
catch (Exception ex) { faults.Enqueue(ex); }
});
}, TestContext.Current.CancellationToken);
var reader = Task.Run(() =>
{
try
@@ -737,7 +737,7 @@ public sealed class ModbusLifecycleHygieneTests(ITestOutputHelper output)
}
}
catch (Exception ex) { faults.Enqueue(ex); }
});
}, TestContext.Current.CancellationToken);
await Task.WhenAll(writer, reader);
faults.ShouldBeEmpty();
@@ -89,11 +89,11 @@ public sealed class ModbusProbeTests
// Wait for the first probe to complete.
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (fake.ProbeCount == 0 && DateTime.UtcNow < deadline) await Task.Delay(25);
while (fake.ProbeCount == 0 && DateTime.UtcNow < deadline) await Task.Delay(25, TestContext.Current.CancellationToken);
// Then wait for the event to actually arrive.
deadline = DateTime.UtcNow + TimeSpan.FromSeconds(1);
while (transitions.Count == 0 && DateTime.UtcNow < deadline) await Task.Delay(25);
while (transitions.Count == 0 && DateTime.UtcNow < deadline) await Task.Delay(25, TestContext.Current.CancellationToken);
transitions.Count.ShouldBeGreaterThanOrEqualTo(1);
transitions.TryDequeue(out var t).ShouldBeTrue();
@@ -169,7 +169,7 @@ public sealed class ModbusProbeTests
await drv.InitializeAsync("{}", CancellationToken.None);
await WaitForStateAsync(drv, HostState.Running, TimeSpan.FromSeconds(2));
await Task.Delay(500); // several more probe ticks, all successful — state shouldn't thrash
await Task.Delay(500, TestContext.Current.CancellationToken); // several more probe ticks, all successful — state shouldn't thrash
transitions.Count.ShouldBe(1); // only the initial Unknown→Running
await drv.ShutdownAsync(CancellationToken.None);
@@ -184,7 +184,7 @@ public sealed class ModbusProbeTests
drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e);
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
transitions.Count.ShouldBe(0);
drv.GetHostStatuses()[0].State.ShouldBe(HostState.Unknown);
@@ -206,7 +206,7 @@ public sealed class ModbusProbeTests
var before = fake.ProbeCount;
await drv.ShutdownAsync(CancellationToken.None);
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
// A handful of in-flight ticks may complete after shutdown in a narrow race; the
// contract is that the loop stops scheduling new ones. Tolerate ≤1 extra.
@@ -86,7 +86,7 @@ public sealed class ModbusSubscriptionTests
drv.OnDataChange += (_, e) => events.Enqueue(e);
var handle = await drv.SubscribeAsync(["Level"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
await Task.Delay(500); // ~5 poll cycles at 100ms, value stable the whole time
await Task.Delay(500, TestContext.Current.CancellationToken); // ~5 poll cycles at 100ms, value stable the whole time
await drv.UnsubscribeAsync(handle, CancellationToken.None);
events.Count.ShouldBe(1); // only the initial-data push, no change events after
@@ -129,7 +129,7 @@ public sealed class ModbusSubscriptionTests
var countAfterUnsub = events.Count;
fake.HoldingRegisters[0] = 999; // would trigger a change if still polling
await Task.Delay(400);
await Task.Delay(400, TestContext.Current.CancellationToken);
events.Count.ShouldBe(countAfterUnsub);
}
@@ -147,7 +147,7 @@ public sealed class ModbusSubscriptionTests
drv.OnDataChange += (_, e) => events.Enqueue(e);
var handle = await drv.SubscribeAsync(["Level"], TimeSpan.FromMilliseconds(10), CancellationToken.None);
await Task.Delay(300);
await Task.Delay(300, TestContext.Current.CancellationToken);
await drv.UnsubscribeAsync(handle, CancellationToken.None);
events.Count.ShouldBe(1);
@@ -227,7 +227,7 @@ public sealed class ModbusSubscriptionTests
{
for (var i = 0; i < tagCount; i++)
fake.HoldingRegisters[i] = (ushort)(fake.HoldingRegisters[i] + 5);
await Task.Delay(20);
await Task.Delay(20, TestContext.Current.CancellationToken);
}
foreach (var h in handles)
@@ -268,8 +268,8 @@ public sealed class MqttDriverDiscoveryTests
await release.Task;
};
var initialize = Task.Run(() => driver.InitializeAsync("", CancellationToken.None));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
var initialize = Task.Run(() => driver.InitializeAsync("", CancellationToken.None), TestContext.Current.CancellationToken);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
var dispose = driver.DisposeAsync().AsTask();
@@ -282,7 +282,7 @@ public sealed class MqttDriverDiscoveryTests
release.SetResult();
await Should.ThrowAsync<Exception>(() => initialize); // connect refused on the closed port
await dispose.WaitAsync(TimeSpan.FromSeconds(10));
await dispose.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
dispose.IsCompletedSuccessfully.ShouldBeTrue();
}
@@ -267,7 +267,7 @@ public sealed class MqttDriverSessionLifecycleTests
await release.Task;
};
var initialize = Task.Run(() => driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken));
var initialize = Task.Run(() => driver.InitializeAsync(Json(options), TestContext.Current.CancellationToken), TestContext.Current.CancellationToken);
await entered.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
// Times out on the gate (1 s) and tears down ungated.
@@ -41,7 +41,7 @@ public sealed class MqttDriverWriteTests
public async Task An_unknown_reference_is_refused_in_its_own_slot()
{
var results = await PlainDriver(PlainTag(Path, "oven/set"))
.WriteAsync([new WriteRequest("Plant/Mqtt/dev1/NotATag", 1.0)], default);
.WriteAsync([new WriteRequest("Plant/Mqtt/dev1/NotATag", 1.0)], TestContext.Current.CancellationToken);
results.Count.ShouldBe(1);
results[0].StatusCode.ShouldBe(0x80340000u); // BadNodeIdUnknown
@@ -54,7 +54,7 @@ public sealed class MqttDriverWriteTests
// the operator simply never said where a write should go. The status has to say that, or the
// only diagnosis available is "writes don't work".
var results = await PlainDriver(PlainTag(ReadOnlyPath, writeTopic: null))
.WriteAsync([new WriteRequest(ReadOnlyPath, 1.0)], default);
.WriteAsync([new WriteRequest(ReadOnlyPath, 1.0)], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0x803B0000u); // BadNotWritable
}
@@ -66,7 +66,7 @@ public sealed class MqttDriverWriteTests
// replay hazard WriteIdempotent exists to prevent — and this would be the driver deciding it on
// the operator's behalf.
var results = await PlainDriver(PlainTag(Path, "oven/set"))
.WriteAsync([new WriteRequest(Path, 42.5)], default);
.WriteAsync([new WriteRequest(Path, 42.5)], TestContext.Current.CancellationToken);
results[0].StatusCode.ShouldBe(0x80050000u); // BadCommunicationError
}
@@ -78,13 +78,11 @@ public sealed class MqttDriverWriteTests
// write in the same OPC UA request.
var driver = PlainDriver(PlainTag(Path, "oven/set"), PlainTag(ReadOnlyPath, writeTopic: null));
var results = await driver.WriteAsync(
[
var results = await driver.WriteAsync([
new WriteRequest("Plant/Mqtt/dev1/NotATag", 1.0),
new WriteRequest(ReadOnlyPath, 1.0),
new WriteRequest(Path, 42.5),
],
default);
], TestContext.Current.CancellationToken);
// Order-preserving, one status per request, each naming its own distinct cause.
results.Count.ShouldBe(3);
@@ -95,7 +93,7 @@ public sealed class MqttDriverWriteTests
[Fact]
public async Task An_empty_batch_is_answered_with_an_empty_result()
=> (await PlainDriver().WriteAsync([], default)).ShouldBeEmpty();
=> (await PlainDriver().WriteAsync([], TestContext.Current.CancellationToken)).ShouldBeEmpty();
[Fact]
public async Task WriteAsync_rejects_a_null_batch()
@@ -109,7 +107,7 @@ public sealed class MqttDriverWriteTests
{
var builder = new WriteCapturingBuilder();
await PlainDriver(PlainTag(Path, "oven/set")).DiscoverAsync(builder, default);
await PlainDriver(PlainTag(Path, "oven/set")).DiscoverAsync(builder, TestContext.Current.CancellationToken);
builder.Variables.ShouldHaveSingleItem();
builder.Variables[0].SecurityClass.ShouldBe(SecurityClassification.Operate);
@@ -125,7 +123,7 @@ public sealed class MqttDriverWriteTests
// refuses every write would be the silent-no-op failure in a new costume.
var builder = new WriteCapturingBuilder();
await PlainDriver(PlainTag(ReadOnlyPath, writeTopic: null)).DiscoverAsync(builder, default);
await PlainDriver(PlainTag(ReadOnlyPath, writeTopic: null)).DiscoverAsync(builder, TestContext.Current.CancellationToken);
builder.Variables[0].SecurityClass.ShouldBe(SecurityClassification.ViewOnly);
}
@@ -476,7 +476,7 @@ public sealed class S7DriverReconnectTests
await WaitUntilAsync(() => factory.Created.Count >= 2, TimeSpan.FromSeconds(5));
factory.Created.Count.ShouldBeGreaterThanOrEqualTo(2);
var atFirstAttempt = factory.Created.Count;
await Task.Delay(TimeSpan.FromMilliseconds(900));
await Task.Delay(TimeSpan.FromMilliseconds(900), TestContext.Current.CancellationToken);
// Without the fix this is ~10+ (one 80 ms-cut attempt per tick); with it the window suppresses them.
(factory.Created.Count - atFirstAttempt).ShouldBeLessThanOrEqualTo(1);
@@ -96,7 +96,7 @@ public sealed class TwinCATCapabilityTests
await drv.ShutdownAsync(CancellationToken.None);
var afterShutdown = events.Count;
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
events.Count.ShouldBe(afterShutdown);
}
@@ -185,7 +185,7 @@ public sealed class TwinCATCapabilityTests
Probe = new TwinCATProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
await Task.Delay(200);
await Task.Delay(200, TestContext.Current.CancellationToken);
drv.GetHostStatuses().Single().State.ShouldBe(HostState.Unknown);
await drv.ShutdownAsync(CancellationToken.None);
@@ -186,7 +186,7 @@ public sealed class TwinCATDiagnosticsTests
IReadOnlyDictionary<string, string> bag = new Dictionary<string, string>();
for (var i = 0; i < 100 && bag.Count == 0; i++)
{
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
bag = ((IDriverDiagnostics)drv).GetDiagnostics();
}
bag["TwinCAT.OnlineChangeCnt"].ShouldBe("7");
@@ -218,8 +218,8 @@ public sealed class TwinCATDiagnosticsTests
try
{
for (var i = 0; i < 100 && drv.GetHostStatuses().All(h => h.State != HostState.Running); i++)
await Task.Delay(20);
await Task.Delay(100); // several more probe ticks
await Task.Delay(20, TestContext.Current.CancellationToken);
await Task.Delay(100, TestContext.Current.CancellationToken); // several more probe ticks
}
finally { await drv.ShutdownAsync(CancellationToken.None); }
@@ -187,7 +187,7 @@ public sealed class TwinCATDriverProbeTests
catch (OperationCanceledException) { /* accept cancelled — expected on teardown */ }
catch (ObjectDisposedException) { /* listener stopped — expected on teardown */ }
catch (SocketException) { /* listener stopped — expected on teardown */ }
});
}, TestContext.Current.CancellationToken);
try
{
@@ -314,7 +314,7 @@ public sealed class TwinCATReconnectReplayTests
factory.Clients[0].SimulateWireDrop();
var readTask = Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None));
await WaitForAsync(() => factory.Clients.Count >= 2, TimeSpan.FromSeconds(2));
await Task.Delay(50); // let the replay reach the blocked AddNotification
await Task.Delay(50, TestContext.Current.CancellationToken); // let the replay reach the blocked AddNotification
// Unsubscribe mid-replay (gate-free) — removes the registration from the device registry.
await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
@@ -56,7 +56,7 @@ public sealed class TwinCATSumChunkWarningTests
while (!factory.Clients.Any(c => c.IsConnected) && DateTime.UtcNow < deadline)
{
await drv.ReadAsync(["A", "B"], CancellationToken.None);
await Task.Delay(50);
await Task.Delay(50, TestContext.Current.CancellationToken);
}
factory.Clients.Any(c => c.IsConnected).ShouldBeTrue("the device must reconnect for the second half to mean anything");
await drv.ReadAsync(["A", "B"], CancellationToken.None);