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";
+}