diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs index e8ae7e92..0883ed57 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorTests.cs @@ -322,142 +322,4 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase AwaitCondition(() => driver.SubscribeCount >= 1, TimeSpan.FromSeconds(3)); } - private class StubDriver : IDriver - { - /// Gets or sets a value indicating whether initialization should throw. - public bool InitializeShouldThrow { get; set; } - /// Gets the number of times initialization was called. - public int InitializeCount; - /// Gets the number of times reinitialization was called. - public int ReinitializeCount; - - private readonly object _initConfigsLock = new(); - /// Every config string passed to , in call order. - public List InitConfigs { get; } = new(); - /// Optional per-config init behaviour. When set, it fully owns the init outcome for that - /// config (await/throw); is ignored. Null ⇒ legacy behaviour. - public Func? InitBehavior { get; set; } - - /// Gets the driver instance ID. - public string DriverInstanceId => "stub-driver-1"; - /// Gets the driver type. - public string DriverType => "Stub"; - - /// Initializes the driver with the specified configuration JSON. - /// The driver configuration JSON. - /// Cancellation token for the operation. - public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) - { - Interlocked.Increment(ref InitializeCount); - lock (_initConfigsLock) InitConfigs.Add(driverConfigJson); - if (InitBehavior is not null) { await InitBehavior(driverConfigJson); return; } - if (InitializeShouldThrow) throw new InvalidOperationException("stub-init-fail"); - } - - /// Reinitializes the driver with the specified configuration JSON. - /// The driver configuration JSON. - /// Cancellation token for the operation. - public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) - { - Interlocked.Increment(ref ReinitializeCount); - return Task.CompletedTask; - } - - /// Shuts down the driver. - /// Cancellation token for the operation. - public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask; - /// Gets the health status of the driver. - public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); - /// Gets the memory footprint of the driver. - public long GetMemoryFootprint() => 0; - /// Flushes optional caches in the driver. - /// Cancellation token for the operation. - public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } - - private sealed class WritableStubDriver : StubDriver, IWritable - { - /// Gets or sets the next status code to return from write operations. - public uint NextStatusCode { get; set; } = 0u; - /// Gets the list of write requests received. - public List Writes { get; } = new(); - - /// Writes the specified requests. - /// The write requests. - /// Cancellation token for the operation. - public Task> WriteAsync( - IReadOnlyList writes, CancellationToken cancellationToken) - { - Writes.AddRange(writes); - IReadOnlyList results = writes.Select(_ => new WriteResult(NextStatusCode)).ToList(); - return Task.FromResult(results); - } - } - - private sealed class SubscribableStubDriver : StubDriver, ISubscribable - { - /// Occurs when data changes. - public event EventHandler? OnDataChange; - - private readonly StubHandle _handle = new(); - - /// Gets the number of subscribers to OnDataChange. - public int OnDataChangeSubscriberCount => OnDataChange?.GetInvocationList().Length ?? 0; - - /// Number of times was called (re-subscribe asserts). - public int SubscribeCount; - /// The reference set passed to the most recent call. - public IReadOnlyList? LastSubscribedRefs; - - /// When true, genuinely yields (`await Task.Yield()`) - /// before completing, so a `ConfigureAwait(false)` continuation in the actor resumes off the - /// Akka ActorContext on a thread-pool thread — reproducing the no-ActorContext race that a - /// synchronously-completed stub task hides (the continuation otherwise runs inline). - public bool UnsubscribeYields { get; set; } - - /// Subscribes to the specified full references. - /// The full references to subscribe to. - /// The publishing interval. - /// Cancellation token for the operation. - public Task SubscribeAsync( - IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) - { - Interlocked.Increment(ref SubscribeCount); - LastSubscribedRefs = fullReferences; - return Task.FromResult(_handle); - } - - /// Unsubscribes from the specified subscription handle. - /// The subscription handle. - /// Cancellation token for the operation. - public async Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) - { - if (UnsubscribeYields) - { - // Complete the awaited task from a fresh background thread that has NO Akka actor - // cell on it, so the caller's `ConfigureAwait(false)` continuation resumes on a - // clean thread-pool thread where InternalCurrentActorCellKeeper.Current is null — - // a deterministic repro of the real async-backend no-ActorContext race. - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _ = Task.Run(() => tcs.SetResult()); - await tcs.Task.ConfigureAwait(false); - } - } - - /// Fires a data change event with the specified parameters. - /// The full reference of the data that changed. - /// The new value. - /// The OPC UA status code. - public void FireDataChange(string fullRef, object? value, uint statusCode) - { - var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow); - OnDataChange?.Invoke(this, new DataChangeEventArgs(_handle, fullRef, snapshot)); - } - - private sealed class StubHandle : ISubscriptionHandle - { - /// Gets the diagnostic ID of the subscription. - public string DiagnosticId => "stub-sub"; - } - } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorWriteAndSubscribeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorWriteAndSubscribeTests.cs index a7a5eb8a..4941eb45 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorWriteAndSubscribeTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorWriteAndSubscribeTests.cs @@ -173,42 +173,6 @@ public sealed class DriverInstanceActorWriteAndSubscribeTests : RuntimeActorTest deadLetterProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); } - // --- stub drivers (mirrors DriverInstanceActorTests) ------------------------------------------ - - private class StubDriver : IDriver - { - /// Gets or sets a value indicating whether initialization should throw. - public bool InitializeShouldThrow { get; set; } - /// Gets the number of times initialization was called. - public int InitializeCount; - - /// Gets the driver instance ID. - public string DriverInstanceId => "stub-driver-1"; - /// Gets the driver type. - public string DriverType => "Stub"; - - /// Initializes the driver with the specified configuration JSON. - public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) - { - Interlocked.Increment(ref InitializeCount); - if (InitializeShouldThrow) throw new InvalidOperationException("stub-init-fail"); - return Task.CompletedTask; - } - - /// Reinitializes the driver with the specified configuration JSON. - public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => - Task.CompletedTask; - - /// Shuts down the driver. - public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask; - /// Gets the health status of the driver. - public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); - /// Gets the memory footprint of the driver. - public long GetMemoryFootprint() => 0; - /// Flushes optional caches in the driver. - public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } - /// /// A standalone (and ) whose InitializeAsync /// never completes, parking the actor in Connecting. Implements the interface directly @@ -255,59 +219,4 @@ public sealed class DriverInstanceActorWriteAndSubscribeTests : RuntimeActorTest } } - private sealed class WritableStubDriver : StubDriver, IWritable - { - /// Gets or sets the next status code to return from write operations. - public uint NextStatusCode { get; set; } = 0u; - /// Gets the list of write requests received. - public List Writes { get; } = new(); - - /// Writes the specified requests. - public Task> WriteAsync( - IReadOnlyList writes, CancellationToken cancellationToken) - { - Writes.AddRange(writes); - IReadOnlyList results = writes.Select(_ => new WriteResult(NextStatusCode)).ToList(); - return Task.FromResult(results); - } - } - - private sealed class SubscribableStubDriver : StubDriver, ISubscribable - { - /// Occurs when data changes. - public event EventHandler? OnDataChange; - - private readonly StubHandle _handle = new(); - - /// Gets the number of subscribers to OnDataChange. - public int OnDataChangeSubscriberCount => OnDataChange?.GetInvocationList().Length ?? 0; - - /// Number of times was called. - public int SubscribeCount; - - /// Subscribes to the specified full references. - public Task SubscribeAsync( - IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) - { - Interlocked.Increment(ref SubscribeCount); - return Task.FromResult(_handle); - } - - /// Unsubscribes from the specified subscription handle. - public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) => - Task.CompletedTask; - - /// Fires a data change event with the specified parameters. - public void FireDataChange(string fullRef, object? value, uint statusCode) - { - var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow); - OnDataChange?.Invoke(this, new DataChangeEventArgs(_handle, fullRef, snapshot)); - } - - private sealed class StubHandle : ISubscriptionHandle - { - /// Gets the diagnostic ID of the subscription. - public string DiagnosticId => "stub-sub"; - } - } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/StubDrivers.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/StubDrivers.cs new file mode 100644 index 00000000..758b0069 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/StubDrivers.cs @@ -0,0 +1,158 @@ +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; +using ZB.MOM.WW.OtOpcUa.Commons.Types; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; + +/// +/// Shared stub harness used by DriverInstanceActorTests and +/// DriverInstanceActorWriteAndSubscribeTests. Promoted from the superset copy in +/// DriverInstanceActorTests so both suites compile against a single definition. +/// +internal class StubDriver : IDriver +{ + /// Gets or sets a value indicating whether initialization should throw. + public bool InitializeShouldThrow { get; set; } + /// Gets the number of times initialization was called. + public int InitializeCount; + /// Gets the number of times reinitialization was called. + public int ReinitializeCount; + + private readonly object _initConfigsLock = new(); + /// Every config string passed to , in call order. + public List InitConfigs { get; } = new(); + /// Optional per-config init behaviour. When set, it fully owns the init outcome for that + /// config (await/throw); is ignored. Null ⇒ legacy behaviour. + public Func? InitBehavior { get; set; } + + /// Gets the driver instance ID. + public string DriverInstanceId => "stub-driver-1"; + /// Gets the driver type. + public string DriverType => "Stub"; + + /// Initializes the driver with the specified configuration JSON. + /// The driver configuration JSON. + /// Cancellation token for the operation. + public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) + { + Interlocked.Increment(ref InitializeCount); + lock (_initConfigsLock) InitConfigs.Add(driverConfigJson); + if (InitBehavior is not null) { await InitBehavior(driverConfigJson); return; } + if (InitializeShouldThrow) throw new InvalidOperationException("stub-init-fail"); + } + + /// Reinitializes the driver with the specified configuration JSON. + /// The driver configuration JSON. + /// Cancellation token for the operation. + public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) + { + Interlocked.Increment(ref ReinitializeCount); + return Task.CompletedTask; + } + + /// Shuts down the driver. + /// Cancellation token for the operation. + public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask; + /// Gets the health status of the driver. + public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null); + /// Gets the memory footprint of the driver. + public long GetMemoryFootprint() => 0; + /// Flushes optional caches in the driver. + /// Cancellation token for the operation. + public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} + +/// +/// A that also implements , recording every +/// write call and returning a configurable status code. +/// +internal sealed class WritableStubDriver : StubDriver, IWritable +{ + /// Gets or sets the next status code to return from write operations. + public uint NextStatusCode { get; set; } = 0u; + /// Gets the list of write requests received. + public List Writes { get; } = new(); + + /// Writes the specified requests. + /// The write requests. + /// Cancellation token for the operation. + public Task> WriteAsync( + IReadOnlyList writes, CancellationToken cancellationToken) + { + Writes.AddRange(writes); + IReadOnlyList results = writes.Select(_ => new WriteResult(NextStatusCode)).ToList(); + return Task.FromResult(results); + } +} + +/// +/// A that also implements , firing +/// on demand and counting subscribe calls. +/// +internal sealed class SubscribableStubDriver : StubDriver, ISubscribable +{ + /// Occurs when data changes. + public event EventHandler? OnDataChange; + + private readonly StubHandle _handle = new(); + + /// Gets the number of subscribers to OnDataChange. + public int OnDataChangeSubscriberCount => OnDataChange?.GetInvocationList().Length ?? 0; + + /// Number of times was called (re-subscribe asserts). + public int SubscribeCount; + /// The reference set passed to the most recent call. + public IReadOnlyList? LastSubscribedRefs; + + /// When true, genuinely yields (await Task.Yield()) + /// before completing, so a ConfigureAwait(false) continuation in the actor resumes off the + /// Akka ActorContext on a thread-pool thread — reproducing the no-ActorContext race that a + /// synchronously-completed stub task hides (the continuation otherwise runs inline). + public bool UnsubscribeYields { get; set; } + + /// Subscribes to the specified full references. + /// The full references to subscribe to. + /// The publishing interval. + /// Cancellation token for the operation. + public Task SubscribeAsync( + IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) + { + Interlocked.Increment(ref SubscribeCount); + LastSubscribedRefs = fullReferences; + return Task.FromResult(_handle); + } + + /// Unsubscribes from the specified subscription handle. + /// The subscription handle. + /// Cancellation token for the operation. + public async Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) + { + if (UnsubscribeYields) + { + // Complete the awaited task from a fresh background thread that has NO Akka actor + // cell on it, so the caller's `ConfigureAwait(false)` continuation resumes on a + // clean thread-pool thread where InternalCurrentActorCellKeeper.Current is null — + // a deterministic repro of the real async-backend no-ActorContext race. + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _ = Task.Run(() => tcs.SetResult()); + await tcs.Task.ConfigureAwait(false); + } + } + + /// Fires a data change event with the specified parameters. + /// The full reference of the data that changed. + /// The new value. + /// The OPC UA status code. + public void FireDataChange(string fullRef, object? value, uint statusCode) + { + var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow); + OnDataChange?.Invoke(this, new DataChangeEventArgs(_handle, fullRef, snapshot)); + } +} + +/// Minimal for use by . +internal sealed class StubHandle : ISubscriptionHandle +{ + /// Gets the diagnostic ID of the subscription. + public string DiagnosticId => "stub-sub"; +}