docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -8,18 +8,21 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
public sealed class DeploymentArtifactTests
{
/// <summary>Verifies that empty blob returns empty list.</summary>
[Fact]
public void Empty_blob_returns_empty_list()
{
DeploymentArtifact.ParseDriverInstances(ReadOnlySpan<byte>.Empty).ShouldBeEmpty();
}
/// <summary>Verifies that malformed JSON returns empty list.</summary>
[Fact]
public void Malformed_json_returns_empty_list()
{
DeploymentArtifact.ParseDriverInstances(Encoding.UTF8.GetBytes("not json")).ShouldBeEmpty();
}
/// <summary>Verifies that snapshot without DriverInstances returns empty.</summary>
[Fact]
public void Snapshot_without_DriverInstances_returns_empty()
{
@@ -27,6 +30,7 @@ public sealed class DeploymentArtifactTests
DeploymentArtifact.ParseDriverInstances(blob).ShouldBeEmpty();
}
/// <summary>Verifies that driver instances are parsed from composer-shaped blob.</summary>
[Fact]
public void Parses_driver_instances_from_composer_shaped_blob()
{
@@ -69,6 +73,7 @@ public sealed class DeploymentArtifactTests
specs[1].Enabled.ShouldBeFalse();
}
/// <summary>Verifies that ParseComposition returns empty for empty blob.</summary>
[Fact]
public void ParseComposition_returns_empty_for_empty_blob()
{
@@ -78,6 +83,7 @@ public sealed class DeploymentArtifactTests
c.ScriptedAlarmPlans.ShouldBeEmpty();
}
/// <summary>Verifies that ParseComposition reads all three entity classes sorted by ID.</summary>
[Fact]
public void ParseComposition_reads_all_three_entity_classes_sorted_by_id()
{
@@ -111,6 +117,7 @@ public sealed class DeploymentArtifactTests
c.ScriptedAlarmPlans.Single().ScriptedAlarmId.ShouldBe("alarm-1");
}
/// <summary>Verifies that specs missing required fields are dropped.</summary>
[Fact]
public void Spec_missing_required_fields_is_dropped()
{
@@ -21,6 +21,7 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
/// <summary>Verifies that applying a deployment with driver instances spawns one child per enabled row.</summary>
[Fact]
public void Apply_with_driver_instances_in_artifact_spawns_one_child_per_enabled_row()
{
@@ -43,6 +44,7 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
AwaitAssert(() => factory.CreateCount.ShouldBe(2), duration: TimeSpan.FromSeconds(3));
}
/// <summary>Verifies that applying a deployment with unsupported driver type falls back to stub.</summary>
[Fact]
public void Apply_with_unsupported_driver_type_falls_back_to_stub()
{
@@ -72,6 +74,7 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
snap.Drivers[0].State.ShouldBe("Stubbed");
}
/// <summary>Verifies that Galaxy driver on non-Windows is stubbed by ShouldStub check.</summary>
[Fact]
public void Galaxy_on_non_windows_is_stubbed_by_ShouldStub_check()
{
@@ -102,6 +105,7 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
}
}
/// <summary>Verifies that a second apply with removed driver stops the child.</summary>
[Fact]
public void Second_apply_with_removed_driver_stops_the_child()
{
@@ -161,12 +165,20 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
return id;
}
/// <summary>Test double for IDriverFactory that counts driver creation attempts.</summary>
private sealed class CountingDriverFactory : IDriverFactory
{
private readonly string _supportedType;
/// <summary>Gets the number of times TryCreate was called and returned a driver.</summary>
public int CreateCount;
/// <summary>Initializes a new instance with the specified supported driver type.</summary>
/// <param name="supportedType">The driver type this factory supports.</param>
public CountingDriverFactory(string supportedType) { _supportedType = supportedType; }
/// <summary>Attempts to create a driver if the type is supported.</summary>
/// <param name="driverType">The driver type to create.</param>
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
/// <param name="driverConfigJson">The driver configuration in JSON format.</param>
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
{
if (!string.Equals(driverType, _supportedType, StringComparison.Ordinal)) return null;
@@ -174,19 +186,32 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
return new TestDriver(driverInstanceId, driverType);
}
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedTypes => new[] { _supportedType };
}
/// <summary>Test double for IDriver with minimal implementation.</summary>
private sealed class TestDriver : IDriver
{
/// <inheritdoc />
public string DriverInstanceId { get; }
/// <inheritdoc />
public string DriverType { get; }
/// <summary>Initializes a new test driver with the specified ID and type.</summary>
/// <param name="id">The driver instance ID.</param>
/// <param name="type">The driver type.</param>
public TestDriver(string id, string type) { DriverInstanceId = id; DriverType = type; }
/// <inheritdoc />
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, LastError: null);
/// <inheritdoc />
public long GetMemoryFootprint() => 0;
/// <inheritdoc />
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
@@ -17,6 +17,7 @@ public sealed class DriverHostActorTests : RuntimeActorTestBase
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
/// <summary>Verifies that bootstrap with no prior state enters the Steady state.</summary>
[Fact]
public void Bootstrap_with_no_prior_state_enters_Steady()
{
@@ -33,6 +34,7 @@ public sealed class DriverHostActorTests : RuntimeActorTestBase
ack.NodeId.ShouldBe(TestNode);
}
/// <summary>Verifies that dispatching the same revision is acked immediately without apply work.</summary>
[Fact]
public void Same_revision_dispatch_is_acked_immediately_with_no_apply_work()
{
@@ -66,6 +68,7 @@ public sealed class DriverHostActorTests : RuntimeActorTestBase
verify.NodeDeploymentStates.Count(s => s.NodeId == TestNode.Value).ShouldBe(1);
}
/// <summary>Verifies that a new revision dispatch writes an Applied NodeDeploymentState.</summary>
[Fact]
public void New_revision_dispatch_writes_Applied_NodeDeploymentState()
{
@@ -89,6 +92,7 @@ public sealed class DriverHostActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(3));
}
/// <summary>Verifies that an orphaned Applying row on bootstrap is replayed.</summary>
[Fact]
public void Orphan_Applying_row_on_bootstrap_replays_apply()
{
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
public sealed class DriverInstanceActorTests : RuntimeActorTestBase
{
/// <summary>Verifies that ApplyDelta calls ReinitializeAsync when connected and replies success.</summary>
[Fact]
public async Task ApplyDelta_when_Connected_calls_ReinitializeAsync_and_replies_success()
{
@@ -32,6 +33,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
driver.ReinitializeCount.ShouldBe(1);
}
/// <summary>Verifies that initialize failure keeps the actor in Reconnecting state.</summary>
[Fact]
public void Initialize_failure_keeps_actor_in_Reconnecting_state()
{
@@ -45,6 +47,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
AwaitCondition(() => driver.InitializeCount >= 3, TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that writing to a non-IWritable driver returns failure.</summary>
[Fact]
public async Task Write_against_non_IWritable_driver_returns_failure()
{
@@ -62,6 +65,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
reply.Reason!.ShouldContain("IWritable");
}
/// <summary>Verifies that writing to an IWritable driver returns success when status is Good.</summary>
[Fact]
public async Task Write_against_IWritable_returns_success_when_status_is_Good()
{
@@ -80,6 +84,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
driver.Writes.Single().Value.ShouldBe(42);
}
/// <summary>Verifies that write propagates status code on Bad result.</summary>
[Fact]
public async Task Write_propagates_status_code_on_Bad_result()
{
@@ -98,6 +103,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
reply.Reason!.ShouldContain("80340000");
}
/// <summary>Verifies that subscribing to an ISubscribable driver forwards OnDataChange to parent.</summary>
[Fact]
public async Task Subscribe_against_ISubscribable_forwards_OnDataChange_to_parent()
{
@@ -122,6 +128,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
published.Quality.ShouldBe(OpcUaQuality.Good);
}
/// <summary>Verifies that subscribe translates OPC UA status severity bits to OpcUaQuality.</summary>
[Fact]
public async Task Subscribe_translates_OPC_UA_status_severity_bits_to_OpcUaQuality()
{
@@ -145,6 +152,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().Quality.ShouldBe(OpcUaQuality.Bad);
}
/// <summary>Verifies that subscribing to a non-ISubscribable driver replies with failure.</summary>
[Fact]
public async Task Subscribe_against_non_ISubscribable_replies_with_failure()
{
@@ -161,6 +169,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
reply.Reason.ShouldContain("ISubscribable");
}
/// <summary>Verifies that DisconnectObserved detaches subscription handler so late events are dropped.</summary>
[Fact]
public async Task DisconnectObserved_detaches_subscription_handler_so_late_events_are_dropped()
{
@@ -185,13 +194,21 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
private class StubDriver : IDriver
{
/// <summary>Gets or sets a value indicating whether initialization should throw.</summary>
public bool InitializeShouldThrow { get; set; }
/// <summary>Gets the number of times initialization was called.</summary>
public int InitializeCount;
/// <summary>Gets the number of times reinitialization was called.</summary>
public int ReinitializeCount;
/// <summary>Gets the driver instance ID.</summary>
public string DriverInstanceId => "stub-driver-1";
/// <summary>Gets the driver type.</summary>
public string DriverType => "Stub";
/// <summary>Initializes the driver with the specified configuration JSON.</summary>
/// <param name="driverConfigJson">The driver configuration JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
Interlocked.Increment(ref InitializeCount);
@@ -199,23 +216,37 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
return Task.CompletedTask;
}
/// <summary>Reinitializes the driver with the specified configuration JSON.</summary>
/// <param name="driverConfigJson">The driver configuration JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
Interlocked.Increment(ref ReinitializeCount);
return Task.CompletedTask;
}
/// <summary>Shuts down the driver.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>Gets the health status of the driver.</summary>
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
/// <summary>Gets the memory footprint of the driver.</summary>
public long GetMemoryFootprint() => 0;
/// <summary>Flushes optional caches in the driver.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
private sealed class WritableStubDriver : StubDriver, IWritable
{
/// <summary>Gets or sets the next status code to return from write operations.</summary>
public uint NextStatusCode { get; set; } = 0u;
/// <summary>Gets the list of write requests received.</summary>
public List<WriteRequest> Writes { get; } = new();
/// <summary>Writes the specified requests.</summary>
/// <param name="writes">The write requests.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
@@ -227,19 +258,32 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
private sealed class SubscribableStubDriver : StubDriver, ISubscribable
{
/// <summary>Occurs when data changes.</summary>
public event EventHandler<DataChangeEventArgs>? OnDataChange;
private readonly StubHandle _handle = new();
/// <summary>Gets the number of subscribers to OnDataChange.</summary>
public int OnDataChangeSubscriberCount => OnDataChange?.GetInvocationList().Length ?? 0;
/// <summary>Subscribes to the specified full references.</summary>
/// <param name="fullReferences">The full references to subscribe to.</param>
/// <param name="publishingInterval">The publishing interval.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken)
=> Task.FromResult<ISubscriptionHandle>(_handle);
/// <summary>Unsubscribes from the specified subscription handle.</summary>
/// <param name="handle">The subscription handle.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
=> Task.CompletedTask;
/// <summary>Fires a data change event with the specified parameters.</summary>
/// <param name="fullRef">The full reference of the data that changed.</param>
/// <param name="value">The new value.</param>
/// <param name="statusCode">The OPC UA status code.</param>
public void FireDataChange(string fullRef, object? value, uint statusCode)
{
var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow);
@@ -248,6 +292,7 @@ public sealed class DriverInstanceActorTests : RuntimeActorTestBase
private sealed class StubHandle : ISubscriptionHandle
{
/// <summary>Gets the diagnostic ID of the subscription.</summary>
public string DiagnosticId => "stub-sub";
}
}
@@ -9,6 +9,7 @@ public sealed class DriverSpawnPlannerTests
private static DriverInstanceSpec Spec(string id, string type = "Modbus", string config = "{\"host\":\"127.0.0.1\"}", bool enabled = true) =>
new(Guid.NewGuid(), id, id, type, enabled, config);
/// <summary>Verifies that all new drivers are placed in ToSpawn when current is empty.</summary>
[Fact]
public void All_new_drivers_go_into_ToSpawn_when_current_is_empty()
{
@@ -22,6 +23,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToStop.ShouldBeEmpty();
}
/// <summary>Verifies that the same configuration yields an empty plan.</summary>
[Fact]
public void Same_config_yields_empty_plan()
{
@@ -38,6 +40,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToStop.ShouldBeEmpty();
}
/// <summary>Verifies that different configuration is routed to ApplyDelta.</summary>
[Fact]
public void Different_config_routes_to_ApplyDelta()
{
@@ -54,6 +57,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToStop.ShouldBeEmpty();
}
/// <summary>Verifies that removed drivers are routed to ToStop.</summary>
[Fact]
public void Removed_driver_routes_to_ToStop()
{
@@ -71,6 +75,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToApplyDelta.ShouldBeEmpty();
}
/// <summary>Verifies that disabled drivers with running children are routed to ToStop.</summary>
[Fact]
public void Disabled_driver_with_running_child_routes_to_ToStop()
{
@@ -87,6 +92,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToApplyDelta.ShouldBeEmpty();
}
/// <summary>Verifies that disabled new drivers are not spawned.</summary>
[Fact]
public void Disabled_new_driver_is_not_spawned()
{
@@ -100,6 +106,7 @@ public sealed class DriverSpawnPlannerTests
plan.ToStop.ShouldBeEmpty();
}
/// <summary>Verifies that driver type changes trigger stop followed by respawn.</summary>
[Fact]
public void Driver_type_change_triggers_stop_plus_respawn()
{
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
/// </summary>
public abstract class RuntimeActorTestBase : TestKit
{
/// <summary>Gets the Akka test HOCON configuration string for single-node cluster setup.</summary>
protected static string AkkaTestHocon => @"
akka {
loglevel = ""WARNING""
@@ -32,6 +33,7 @@ akka {
}
}";
/// <summary>Initializes a new instance of the RuntimeActorTestBase class with Akka test setup.</summary>
protected RuntimeActorTestBase() : base(AkkaTestHocon)
{
var cluster = Akka.Cluster.Cluster.Get(Sys);
@@ -40,6 +42,9 @@ akka {
TimeSpan.FromSeconds(5));
}
/// <summary>Creates a new in-memory database factory for testing.</summary>
/// <param name="dbName">The database name, or null to generate a unique name.</param>
/// <returns>An IDbContextFactory for the in-memory database.</returns>
protected static IDbContextFactory<OtOpcUaConfigDbContext> NewInMemoryDbFactory(string? dbName = null)
{
dbName ??= Guid.NewGuid().ToString("N");
@@ -48,6 +53,8 @@ akka {
private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
{
/// <summary>Creates a new in-memory database context.</summary>
/// <returns>A new OtOpcUaConfigDbContext instance.</returns>
public OtOpcUaConfigDbContext CreateDbContext()
{
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Health;
public sealed class HealthProbeActorTests : RuntimeActorTestBase
{
/// <summary>Verifies that the DB health probe actor returns reachable status against an in-memory database.</summary>
[Fact]
public async Task DbHealthProbeActor_returns_reachable_against_in_memory_db()
{
@@ -28,6 +29,7 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
status.LastError.ShouldBeNull();
}
/// <summary>Verifies that the peer OPC UA probe actor reports Ok true against a live listener.</summary>
[Fact]
public void PeerOpcUaProbeActor_reports_Ok_true_against_a_live_listener()
{
@@ -47,6 +49,7 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
TimeSpan.FromSeconds(3));
}
/// <summary>Verifies that the peer OPC UA probe actor reports Ok false against an unreachable endpoint.</summary>
[Fact]
public void PeerOpcUaProbeActor_reports_Ok_false_against_an_unreachable_endpoint()
{
@@ -63,6 +66,7 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
TimeSpan.FromSeconds(3));
}
/// <summary>Verifies that the historian adapter actor forwards events to the injected sink.</summary>
[Fact]
public void HistorianAdapterActor_forwards_events_to_injected_sink()
{
@@ -87,6 +91,7 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
new[] { "alm-0", "alm-1", "alm-2", "alm-3", "alm-4" });
}
/// <summary>Verifies that the historian adapter actor returns sink status via GetStatus.</summary>
[Fact]
public async Task HistorianAdapterActor_returns_sink_status_via_GetStatus()
{
@@ -107,14 +112,19 @@ public sealed class HealthProbeActorTests : RuntimeActorTestBase
private sealed class RecordingSink : IAlarmHistorianSink
{
/// <summary>Gets the list of enqueued alarm historian events.</summary>
public ConcurrentBag<AlarmHistorianEvent> Enqueued { get; } = [];
/// <summary>Enqueues an alarm historian event.</summary>
/// <param name="evt">The event to enqueue.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken)
{
Enqueued.Add(evt);
return Task.CompletedTask;
}
/// <summary>Gets the current status of the historian sink.</summary>
public HistorianSinkStatus GetStatus() => new(
QueueDepth: Enqueued.Count,
DeadLetterDepth: 0,
@@ -19,6 +19,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Observability;
/// </summary>
public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
{
/// <summary>Verifies that VirtualTagActor evaluation emits the otopcua_virtualtag_eval counter.</summary>
[Fact]
public void VirtualTagActor_evaluation_emits_otopcua_virtualtag_eval_counter()
{
@@ -34,6 +35,7 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
recorder.WithTag("outcome", "ok").ShouldBeGreaterThanOrEqualTo(1);
}
/// <summary>Verifies that OpcUaPublishActor AttributeValueUpdate emits the sink write counter.</summary>
[Fact]
public void OpcUaPublishActor_AttributeValueUpdate_emits_sink_write_counter()
{
@@ -58,6 +60,7 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
});
}
/// <summary>Verifies that RebuildAddressSpace starts an address space rebuild span.</summary>
[Fact]
public void RebuildAddressSpace_starts_an_address_space_rebuild_span()
{
@@ -95,6 +98,8 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
private readonly List<KeyValuePair<string, object?>[]> _tagSets = new();
private readonly object _gate = new();
/// <summary>Initializes the recorder for the specified instrument name.</summary>
/// <param name="instrumentName">Name of the instrument to listen for.</param>
public MeterRecorder(string instrumentName)
{
_name = instrumentName;
@@ -117,8 +122,12 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
_listener.Start();
}
/// <summary>Gets the total value recorded.</summary>
public long Total { get { lock (_gate) return _total; } }
/// <summary>Gets count of measurements with the specified tag key-value pair.</summary>
/// <param name="key">Tag key.</param>
/// <param name="value">Tag value.</param>
public int WithTag(string key, string value)
{
lock (_gate)
@@ -127,7 +136,8 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
}
}
public void Dispose() => _listener.Dispose();
/// <summary>Releases the listener.</summary>
public void Dispose() => _listener.Dispose();
}
/// <summary>Listens to a single ActivitySource by name and stores started Activities.</summary>
@@ -138,6 +148,8 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
private readonly List<Activity> _activities = new();
private readonly object _gate = new();
/// <summary>Initializes the recorder for the specified operation name.</summary>
/// <param name="operationName">Name of the operation to listen for.</param>
public ActivityRecorder(string operationName)
{
_operationName = operationName;
@@ -156,23 +168,45 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase
ActivitySource.AddActivityListener(_listener);
}
/// <summary>Gets the list of recorded activities.</summary>
public IReadOnlyList<Activity> Activities { get { lock (_gate) return _activities.ToArray(); } }
/// <summary>Releases the listener.</summary>
public void Dispose() => _listener.Dispose();
}
private sealed class ConstEval(object? value) : IVirtualTagEvaluator
{
/// <summary>Evaluates the virtual tag with a constant value.</summary>
/// <param name="virtualTagId">Virtual tag ID.</param>
/// <param name="expression">Expression to evaluate.</param>
/// <param name="dependencies">Dependency values.</param>
public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
=> VirtualTagEvalResult.Ok(value);
}
private sealed class RecordingSink : IOpcUaAddressSpaceSink
{
/// <summary>Gets the write count.</summary>
public int Writes { get; private set; }
/// <summary>Records a value write.</summary>
/// <param name="nodeId">The OPC UA node identifier.</param>
/// <param name="value">The value being written.</param>
/// <param name="quality">The OPC UA quality status.</param>
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => Writes++;
/// <summary>Records an alarm state write.</summary>
/// <param name="alarmNodeId">The alarm node identifier.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm is acknowledged.</param>
/// <param name="occurredUtc">The time the alarm occurred in UTC.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime occurredUtc) => Writes++;
/// <summary>Ensures folder exists (stub implementation).</summary>
/// <param name="folderNodeId">The folder node identifier.</param>
/// <param name="parentNodeId">The parent folder node identifier.</param>
/// <param name="displayName">The display name for the folder.</param>
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
/// <summary>Rebuilds address space (recorded via span).</summary>
public void RebuildAddressSpace() { /* recorded via span */ }
}
}
@@ -18,6 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{
/// <summary>Tests that RebuildAddressSpace with dbFactory loads artifact, composes, and applies.</summary>
[Fact]
public void RebuildAddressSpace_with_dbFactory_loads_artifact_composes_and_applies()
{
@@ -42,6 +43,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(2));
}
/// <summary>Tests that rebuild with no artifact is idempotent no-op.</summary>
[Fact]
public void Rebuild_with_no_artifact_is_idempotent_no_op()
{
@@ -61,6 +63,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
sink.RebuildCalls.ShouldBe(0);
}
/// <summary>Tests that second rebuild with same artifact is empty plan no-op.</summary>
[Fact]
public void Second_rebuild_with_same_artifact_is_empty_plan_no_op()
{
@@ -81,6 +84,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
sink.RebuildCalls.ShouldBe(1);
}
/// <summary>Tests that rebuild without dbFactory falls back to raw sink rebuild.</summary>
[Fact]
public void Rebuild_without_dbFactory_falls_back_to_raw_sink_rebuild()
{
@@ -133,14 +137,31 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
private sealed class RecordingSink : IOpcUaAddressSpaceSink
{
/// <summary>Gets the list of recorded sink calls.</summary>
public ConcurrentQueue<string> Calls { get; } = new();
/// <summary>Gets or sets the count of rebuild address space calls.</summary>
public int RebuildCalls;
/// <summary>Records a value write call.</summary>
/// <param name="nodeId">The OPC UA node ID.</param>
/// <param name="value">The value to write.</param>
/// <param name="quality">The OPC UA quality code.</param>
/// <param name="ts">The timestamp of the write.</param>
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts)
=> Calls.Enqueue($"WV:{nodeId}");
/// <summary>Records an alarm state write call.</summary>
/// <param name="alarmNodeId">The alarm node ID.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm is acknowledged.</param>
/// <param name="ts">The timestamp of the state change.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime ts)
=> Calls.Enqueue($"WA:{alarmNodeId}");
/// <summary>Records a folder ensure call.</summary>
/// <param name="folderNodeId">The folder node ID.</param>
/// <param name="parentNodeId">The parent node ID, or null if this is a root folder.</param>
/// <param name="displayName">The display name of the folder.</param>
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
=> Calls.Enqueue($"EF:{folderNodeId}");
/// <summary>Records a rebuild address space call.</summary>
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
}
}
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
{
/// <summary>Verifies that message contracts are accepted without a pinned dispatcher in tests.</summary>
[Fact]
public void Accepts_message_contracts_without_pinned_dispatcher_in_tests()
{
@@ -24,6 +25,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies that production props target the OPC UA synchronized dispatcher.</summary>
[Fact]
public void Production_Props_targets_opcua_synchronized_dispatcher()
{
@@ -31,6 +33,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
props.Dispatcher.ShouldBe(OpcUaPublishActor.DispatcherId);
}
/// <summary>Verifies that AttributeValueUpdate routes to sink WriteValue.</summary>
[Fact]
public void AttributeValueUpdate_routes_to_sink_WriteValue()
{
@@ -50,6 +53,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that AlarmStateUpdate routes to sink WriteAlarmState.</summary>
[Fact]
public void AlarmStateUpdate_routes_to_sink_WriteAlarmState()
{
@@ -67,6 +71,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that RebuildAddressSpace calls sink Rebuild.</summary>
[Fact]
public void RebuildAddressSpace_calls_sink_Rebuild()
{
@@ -78,6 +83,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that ServiceLevelChanged publishes to IServiceLevelPublisher once per unique level.</summary>
[Fact]
public void ServiceLevelChanged_publishes_to_IServiceLevelPublisher_once_per_unique_level()
{
@@ -92,6 +98,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that RedundancyStateChanged drives local ServiceLevel publish for primary leader.</summary>
[Fact]
public void RedundancyStateChanged_drives_local_ServiceLevel_publish_for_primary_leader()
{
@@ -115,6 +122,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that RedundancyStateChanged for secondary publishes 100.</summary>
[Fact]
public void RedundancyStateChanged_for_secondary_publishes_100()
{
@@ -135,32 +143,57 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Test implementation of IOpcUaAddressSpaceSink that records calls.</summary>
private sealed class RecordingSink : IOpcUaAddressSpaceSink
{
/// <summary>Gets the queue of recorded value updates.</summary>
public ConcurrentQueue<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> ValueQueue { get; } = new();
/// <summary>Gets the queue of recorded alarm state updates.</summary>
public ConcurrentQueue<(string AlarmNodeId, bool Active, bool Acknowledged, DateTime Ts)> AlarmQueue { get; } = new();
/// <summary>Count of rebuild calls.</summary>
public int RebuildCalls;
/// <summary>Gets the list of recorded value updates.</summary>
public List<(string NodeId, object? Value, OpcUaQuality Quality, DateTime Ts)> Values =>
ValueQueue.ToList();
/// <summary>Gets the list of recorded alarm state updates.</summary>
public List<(string AlarmNodeId, bool Active, bool Acknowledged, DateTime Ts)> Alarms =>
AlarmQueue.ToList();
/// <summary>Records a value update.</summary>
/// <param name="nodeId">The OPC UA node identifier.</param>
/// <param name="value">The attribute value.</param>
/// <param name="quality">The OPC UA quality code.</param>
/// <param name="ts">The timestamp of the update.</param>
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime ts) =>
ValueQueue.Enqueue((nodeId, value, quality, ts));
/// <summary>Records an alarm state update.</summary>
/// <param name="alarmNodeId">The OPC UA alarm node identifier.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm is acknowledged.</param>
/// <param name="ts">The timestamp of the update.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime ts) =>
AlarmQueue.Enqueue((alarmNodeId, active, acknowledged, ts));
/// <summary>Ensures a folder exists (no-op in test).</summary>
/// <param name="folderNodeId">The OPC UA folder node identifier.</param>
/// <param name="parentNodeId">The parent folder node identifier, or null for root.</param>
/// <param name="displayName">The display name of the folder.</param>
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) { }
/// <summary>Records a rebuild call.</summary>
public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls);
}
/// <summary>Test implementation of IServiceLevelPublisher that records publishes.</summary>
private sealed class RecordingPublisher : IServiceLevelPublisher
{
private readonly ConcurrentQueue<byte> _q = new();
/// <summary>Gets the recorded service levels.</summary>
public byte[] Levels => _q.ToArray();
/// <summary>Records a service level publish.</summary>
/// <param name="serviceLevel">The service level value to publish.</param>
public void Publish(byte serviceLevel) => _q.Enqueue(serviceLevel);
}
}
@@ -24,6 +24,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
{
private static CancellationToken Ct => CancellationToken.None;
/// <summary>Verifies that the primary cluster leader sets Server ServiceLevel to 240.</summary>
[Fact]
public async Task Primary_leader_drives_Server_ServiceLevel_to_240()
{
@@ -63,6 +64,7 @@ public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
}
}
/// <summary>Verifies that the secondary node sets Server ServiceLevel to 100.</summary>
[Fact]
public async Task Secondary_drives_Server_ServiceLevel_to_100()
{
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;
public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
{
/// <summary>Verifies that full state cycle publishes StateChanged messages to parent at each transition.</summary>
[Fact]
public void Full_state_cycle_publishes_StateChanged_to_parent_at_each_transition()
{
@@ -33,6 +34,7 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
t3.State.ShouldBe(ScriptedAlarmActorState.Inactive);
}
/// <summary>Verifies that duplicate ConditionMet messages in Active state are ignored.</summary>
[Fact]
public void Duplicate_ConditionMet_in_Active_is_ignored()
{
@@ -46,6 +48,7 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies that active transition publishes AlarmTransitionEvent to the alerts topic.</summary>
[Fact]
public void Engine_active_transition_publishes_AlarmTransitionEvent_to_alerts_topic()
{
@@ -82,6 +85,7 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(1));
}
/// <summary>Verifies that clear transition publishes Cleared event.</summary>
[Fact]
public void Engine_clear_transition_publishes_Cleared_event()
{
@@ -107,6 +111,7 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(1));
}
/// <summary>Verifies that manual acknowledge emits Acknowledged transition with the user.</summary>
[Fact]
public void Manual_acknowledge_emits_Acknowledged_transition_with_user()
{
@@ -132,10 +137,16 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(1));
}
/// <summary>A threshold-based alarm evaluator for testing.</summary>
private sealed class ThresholdEvaluator : IScriptedAlarmEvaluator
{
private readonly double _threshold;
/// <summary>Initializes a new instance of the ThresholdEvaluator class.</summary>
/// <param name="threshold">The threshold value to compare against.</param>
public ThresholdEvaluator(double threshold) { _threshold = threshold; }
/// <inheritdoc />
public ScriptedAlarmEvalResult Evaluate(string id, string predicate, IReadOnlyDictionary<string, object?> deps)
{
if (!deps.TryGetValue("temp", out var raw) || raw is null)
@@ -144,10 +155,18 @@ public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
}
}
/// <summary>A test publisher that captures published messages.</summary>
private sealed class CapturingPublisher
{
/// <summary>Gets the topics that messages were published to.</summary>
public ConcurrentBag<string> Topics { get; } = new();
/// <summary>Gets the payloads that were published.</summary>
public ConcurrentBag<object> Payloads { get; } = new();
/// <summary>Publishes a message to the specified topic.</summary>
/// <param name="topic">The topic name.</param>
/// <param name="payload">The message payload.</param>
public void Publish(string topic, object payload)
{
Topics.Add(topic);
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;
public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
{
/// <summary>Verifies that alarm state transitions write to the state store with the correct lastAckUser value.</summary>
[Fact]
public async Task Transition_writes_to_state_store_with_lastAckUser()
{
@@ -37,6 +38,7 @@ public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(2));
}
/// <summary>Verifies that actor restart restores persisted state so pending acknowledgment is not dropped.</summary>
[Fact]
public async Task PreStart_restores_persisted_state_so_restart_does_not_drop_pending_ack()
{
@@ -61,6 +63,7 @@ public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
}, duration: TimeSpan.FromSeconds(3));
}
/// <summary>Verifies that alarm boots to inactive state when no persisted state exists.</summary>
[Fact]
public async Task PreStart_with_no_persisted_state_boots_inactive()
{
@@ -76,6 +79,7 @@ public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
}
/// <summary>Verifies that EF-based alarm actor state store correctly persists and restores state through the config database.</summary>
[Fact]
public async Task EfAlarmActorStateStore_round_trip_persists_via_ConfigDb()
{
@@ -113,6 +117,7 @@ public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
}
}
/// <summary>Verifies that loading an alarm state for a missing ID returns null.</summary>
[Fact]
public async Task EfAlarmActorStateStore_load_for_missing_id_returns_null()
{
@@ -128,11 +133,20 @@ public sealed class ScriptedAlarmStatePersistenceTests : RuntimeActorTestBase
private readonly ConcurrentDictionary<string, AlarmActorStateSnapshot> _byId = new(StringComparer.Ordinal);
private readonly ConcurrentQueue<AlarmActorStateSnapshot> _saves = new();
/// <summary>Gets all saved alarm state snapshots in order.</summary>
public List<AlarmActorStateSnapshot> Snapshots => _saves.ToList();
/// <summary>Loads the alarm state snapshot for the specified alarm ID.</summary>
/// <param name="alarmId">The alarm ID.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>The alarm state snapshot if found, null otherwise.</returns>
public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct)
=> Task.FromResult(_byId.TryGetValue(alarmId, out var v) ? v : null);
/// <summary>Saves the alarm state snapshot.</summary>
/// <param name="snapshot">The alarm state snapshot to save.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>A completed task.</returns>
public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct)
{
_byId[snapshot.AlarmId] = snapshot;
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests;
/// </summary>
public sealed class ServiceCollectionExtensionsTests
{
/// <summary>Verifies that WithOtOpcUaRuntimeActors spawns driver host and DB health probe actors.</summary>
[Fact]
public async Task WithOtOpcUaRuntimeActors_spawns_driver_host_and_db_health_probe()
{
@@ -67,8 +68,10 @@ public sealed class ServiceCollectionExtensionsTests
}
}
/// <summary>In-memory database factory for testing.</summary>
private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
{
/// <summary>Creates a new in-memory database context.</summary>
public OtOpcUaConfigDbContext CreateDbContext()
{
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
@@ -78,13 +81,25 @@ public sealed class ServiceCollectionExtensionsTests
}
}
/// <summary>Fake cluster role information for testing.</summary>
private sealed class FakeClusterRoleInfo : IClusterRoleInfo
{
/// <summary>Gets the local node ID.</summary>
public NodeId LocalNode { get; } = NodeId.Parse("test-node");
/// <summary>Gets the local roles.</summary>
public IReadOnlySet<string> LocalRoles { get; } = new HashSet<string>(["driver"]);
/// <summary>Determines whether the local node has the specified role.</summary>
/// <param name="role">The role to check.</param>
public bool HasRole(string role) => LocalRoles.Contains(role);
/// <summary>Gets the members with the specified role.</summary>
/// <param name="role">The role to query.</param>
public IReadOnlyList<NodeId> MembersWithRole(string role) => Array.Empty<NodeId>();
/// <summary>Gets the leader node for the specified role.</summary>
/// <param name="role">The role to query.</param>
public NodeId? RoleLeader(string role) => null;
/// <summary>Raised when the role leader changes.</summary>
public event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged
{
add { _ = value; }
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags;
public sealed class DependencyMuxActorTests : RuntimeActorTestBase
{
/// <summary>Verifies that AttributeValuePublished messages are forwarded only to subscribers interested in the specific reference.</summary>
[Fact]
public void AttributeValuePublished_is_forwarded_only_to_subscribers_interested_in_that_ref()
{
@@ -44,6 +45,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
subB.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
}
/// <summary>Verifies that publishes for unregistered references are silently dropped.</summary>
[Fact]
public void Publish_for_unregistered_ref_is_silently_dropped()
{
@@ -56,6 +58,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies that unregistering interest stops forwarding to that subscriber.</summary>
[Fact]
public void UnregisterInterest_stops_forwarding()
{
@@ -71,6 +74,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies that re-registering a subscriber replaces the prior interest set.</summary>
[Fact]
public void Re_register_replaces_prior_interest_set()
{
@@ -87,6 +91,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("tag-2");
}
/// <summary>Verifies that a virtual tag actor registers dependencies with the mux during PreStart and that evaluation fires end-to-end.</summary>
[Fact]
public void VirtualTagActor_PreStart_registers_deps_with_mux_and_eval_fires_end_to_end()
{
@@ -119,6 +124,7 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies that the DriverHostActor forwards AttributeValuePublished messages through the dependency mux.</summary>
[Fact]
public void DriverHostActor_forwards_AttributeValuePublished_through_to_mux()
{
@@ -145,6 +151,10 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
private sealed class EchoSumEvaluator : ZB.MOM.WW.OtOpcUa.Commons.Engines.IVirtualTagEvaluator
{
/// <summary>Evaluates the expression by summing all dependency values as integers.</summary>
/// <param name="id">The identifier of the virtual tag.</param>
/// <param name="expression">The expression to evaluate.</param>
/// <param name="deps">A dictionary of dependency values keyed by reference.</param>
public ZB.MOM.WW.OtOpcUa.Commons.Engines.VirtualTagEvalResult Evaluate(
string id, string expression, IReadOnlyDictionary<string, object?> deps)
{
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags;
public sealed class VirtualTagActorTests : RuntimeActorTestBase
{
/// <summary>Verifies the actor accepts dependency value changes and remains alive.</summary>
[Fact]
public void DependencyValueChanged_is_accepted_and_actor_stays_alive()
{
@@ -21,6 +22,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies evaluator results flow to parent as EvaluationResult.</summary>
[Fact]
public void Evaluator_result_flows_to_parent_as_EvaluationResult()
{
@@ -38,6 +40,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
second.Value.ShouldBe(42);
}
/// <summary>Verifies repeated same values do not emit a second EvaluationResult.</summary>
[Fact]
public void Repeated_same_value_does_not_emit_a_second_EvaluationResult()
{
@@ -53,6 +56,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>Verifies evaluator failure publishes ScriptLogEntry warning.</summary>
[Fact]
public void Evaluator_failure_publishes_ScriptLogEntry_warning()
{
@@ -79,8 +83,14 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
}
/// <summary>Test evaluator that sums integer dependency values.</summary>
private sealed class SumEvaluator : IVirtualTagEvaluator
{
/// <summary>Evaluates the expression by summing integer dependencies.</summary>
/// <param name="id">The tag identifier.</param>
/// <param name="expr">The expression string.</param>
/// <param name="deps">The dependency values.</param>
/// <returns>The sum of integer values in the dependencies.</returns>
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
{
var sum = deps.Values.OfType<int>().Sum();
@@ -88,26 +98,48 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
}
}
/// <summary>Test evaluator that always returns a constant value.</summary>
private sealed class ConstEvaluator : IVirtualTagEvaluator
{
private readonly object _value;
/// <summary>Initializes the constant evaluator with a fixed value.</summary>
/// <param name="value">The constant value to return.</param>
public ConstEvaluator(object value) { _value = value; }
/// <summary>Evaluates the expression by returning the constant value.</summary>
/// <param name="id">The tag identifier.</param>
/// <param name="expr">The expression string.</param>
/// <param name="deps">The dependency values.</param>
/// <returns>The constant value.</returns>
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
=> VirtualTagEvalResult.Ok(_value);
}
/// <summary>Test evaluator that always returns a failure.</summary>
private sealed class FailingEvaluator : IVirtualTagEvaluator
{
private readonly string _reason;
/// <summary>Initializes the failing evaluator with a failure reason.</summary>
/// <param name="reason">The reason for the failure.</param>
public FailingEvaluator(string reason) { _reason = reason; }
/// <summary>Evaluates the expression and returns a failure.</summary>
/// <param name="id">The tag identifier.</param>
/// <param name="expr">The expression string.</param>
/// <param name="deps">The dependency values.</param>
/// <returns>A failure result with the specified reason.</returns>
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
=> VirtualTagEvalResult.Failure(_reason);
}
/// <summary>Test publisher that captures published messages.</summary>
private sealed class CapturingPublisher
{
/// <summary>Gets the list of published topics.</summary>
public ConcurrentBag<string> Topics { get; } = new();
/// <summary>Gets the list of published payloads.</summary>
public ConcurrentBag<object> Payloads { get; } = new();
/// <summary>Publishes a message with a topic and payload.</summary>
/// <param name="topic">The message topic.</param>
/// <param name="payload">The message payload.</param>
public void Publish(string topic, object payload)
{
Topics.Add(topic);