fix(drivers): InitializeAsync releases previous device/session state before overwrite (#630)
FOCAS, AbLegacy, AbCip, TwinCAT, S7 and OpcUaClient orphaned the previous probe/recycle/fixed-tree loops and clients when re-initialised on a live driver. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #630 — <c>InitializeAsync</c> on a live driver (<c>DriverInstanceActor.AdoptConfigDuringInit</c> runs
|
||||
/// it with no teardown first) overwrote <c>_devices[host]</c> and orphaned the previous state's
|
||||
/// probe loop + client. It now releases them first.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FocasReinitializeReleasesStateTests
|
||||
{
|
||||
/// <summary>Initialize twice: the first state's probe loop has finished and its client is disposed.</summary>
|
||||
[Fact]
|
||||
public async Task SecondInitialize_StopsFirstStatesLoop_AndDisposesItsClient()
|
||||
{
|
||||
const string host = "focas://10.0.0.5:8193";
|
||||
var factory = new FakeFocasClientFactory();
|
||||
var options = new FocasDriverOptions
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(host)],
|
||||
RawTags = [FocasRawTags.Entry(new FocasTagDefinition("R", host, "R100", FocasDataType.Byte))],
|
||||
Probe = new FocasProbeOptions { Enabled = true, Interval = TimeSpan.FromMilliseconds(20), Timeout = TimeSpan.FromSeconds(2) },
|
||||
};
|
||||
var drv = new FocasDriver(options, "focas-reinit", factory);
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var first = drv.GetDeviceState(host)!;
|
||||
// Positive control: the first state's probe loop is demonstrably running and holds a live client.
|
||||
await WaitUntilAsync(() => factory.Clients.Count > 0 && factory.Clients[0].IsConnected);
|
||||
var firstLoop = first.ProbeTask;
|
||||
firstLoop.ShouldNotBeNull();
|
||||
firstLoop!.IsCompleted.ShouldBeFalse();
|
||||
var firstClient = factory.Clients[0];
|
||||
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
firstLoop.IsCompleted.ShouldBeTrue(); // joined
|
||||
firstClient.DisposeCount.ShouldBeGreaterThanOrEqualTo(1);
|
||||
firstClient.IsConnected.ShouldBeFalse();
|
||||
drv.GetDeviceState(host).ShouldNotBeSameAs(first);
|
||||
drv.GetDeviceState(host)!.ProbeTask.ShouldNotBeNull(); // the new state is running
|
||||
|
||||
await drv.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task WaitUntilAsync(Func<bool> cond)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
while (!cond())
|
||||
{
|
||||
if (DateTime.UtcNow > deadline) throw new TimeoutException("condition not met");
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user