Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorTraceArmTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

246 lines
11 KiB
C#

using Akka.Actor;
using Akka.TestKit;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
using ZB.MOM.WW.ScadaBridge.TestSupport;
using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
/// <summary>
/// Flow Debugger design 2026-09-05 §5.3 — the Instance Actor trace arm: a TRACED write to a
/// data-sourced attribute stamps the next DCL echo for that attribute with the writer's
/// <c>SourceExecutionId</c> and <c>Trace</c>; the arm is consumed once and expires unconsumed.
/// Untraced writes never arm (the production-unchanged pin). A traced static write publishes
/// the flag directly.
/// </summary>
public class InstanceActorTraceArmTests : TestKit, IDisposable
{
private const string Tag = "ns=3;s=Recipe.Processed";
private readonly SiteStorageService _storage;
private readonly ScriptCompilationService _compilationService;
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly TestLocalDb _localDb;
/// <summary>Initializes the test with a temporary site database and the script services.</summary>
public InstanceActorTraceArmTests()
{
_localDb = TestLocalDb.CreateTemp("instance-tracearm-test");
_storage = new SiteStorageService(_localDb.Db, NullLogger<SiteStorageService>.Instance);
_storage.InitializeAsync().GetAwaiter().GetResult();
_compilationService = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
_sharedScriptLibrary = new SharedScriptLibrary(_compilationService, NullLogger<SharedScriptLibrary>.Instance);
}
/// <inheritdoc />
void IDisposable.Dispose()
{
Shutdown();
var path = _localDb.Path;
_localDb.Dispose();
TestLocalDb.DeleteFiles(path);
}
private static FlattenedConfiguration Config(string instance) => new()
{
InstanceUniqueName = instance,
Attributes =
[
new ResolvedAttribute
{
CanonicalName = "Processed", Value = "false", DataType = "Boolean",
DataSourceReference = Tag, BoundDataConnectionName = "PLC"
},
new ResolvedAttribute { CanonicalName = "Status", Value = "Idle", DataType = "String" },
]
};
private (IActorRef Actor, TestProbe Dcl, TestProbe Subscriber) Create(string instance, int armWindowSeconds = 30)
{
var options = new SiteRuntimeOptions { StreamBufferSize = 100, TraceArmWindowSeconds = armWindowSeconds };
var streamManager = new SiteStreamManager(options, NullLogger<SiteStreamManager>.Instance);
streamManager.Initialize(Sys);
var dcl = CreateTestProbe();
var actor = ActorOf(Props.Create(() => new InstanceActor(
instance, JsonSerializer.Serialize(Config(instance)), _storage, _compilationService,
_sharedScriptLibrary, streamManager, options, NullLogger<InstanceActor>.Instance, dcl.Ref)));
var subscriber = CreateTestProbe();
streamManager.Subscribe(instance, subscriber.Ref);
return (actor, dcl, subscriber);
}
private static WriteTagRequest ExpectWrite(TestProbe dcl)
=> dcl.FishForMessage<WriteTagRequest>(_ => true, TimeSpan.FromSeconds(5));
private void Write(IActorRef actor, TestProbe dcl, string instance, Guid? source, bool trace)
{
actor.Tell(new SetStaticAttributeCommand("c", instance, "Processed", "true", DateTimeOffset.UtcNow,
SourceExecutionId: source, Trace: trace));
var write = ExpectWrite(dcl);
Assert.Equal(Tag, write.TagPath);
dcl.Reply(new WriteTagResponse(write.CorrelationId, true, null, DateTimeOffset.UtcNow));
Assert.True(ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5)).Success);
}
/// <summary>Same as <see cref="Write"/> but the DCL rejects the write, so the arm must be dropped.</summary>
private void FailingWrite(IActorRef actor, TestProbe dcl, string instance, Guid? source, bool trace)
{
actor.Tell(new SetStaticAttributeCommand("c", instance, "Processed", "true", DateTimeOffset.UtcNow,
SourceExecutionId: source, Trace: trace));
var write = ExpectWrite(dcl);
Assert.Equal(Tag, write.TagPath);
dcl.Reply(new WriteTagResponse(write.CorrelationId, false, "rejected", DateTimeOffset.UtcNow));
Assert.False(ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5)).Success);
}
private static AttributeValueChanged Echo(IActorRef actor, TestProbe subscriber, object? value)
{
actor.Tell(new TagValueUpdate("PLC", Tag, value, QualityCode.Good, DateTimeOffset.UtcNow));
return subscriber.FishForMessage<AttributeValueChanged>(
m => m.AttributeName == "Processed" && Equals(m.Value, value), TimeSpan.FromSeconds(10));
}
/// <summary>Verifies that a traced data write followed by its echo publishes a change stamped with the source and trace.</summary>
[Fact]
public void TracedDataWrite_ThenEcho_PublishesChangeStampedWithSourceAndTrace()
{
var (actor, dcl, sub) = Create("P1");
var writer = Guid.NewGuid();
Write(actor, dcl, "P1", writer, trace: true);
// The device clamps/echoes whatever it wants — the stamp does not depend on the value.
var published = Echo(actor, sub, "clamped");
Assert.Equal(writer, published.SourceExecutionId);
Assert.True(published.Trace);
Assert.True(published.SourceExecutionInferred);
}
/// <summary>Verifies that an untraced data write followed by its echo publishes an unstamped change.</summary>
[Fact]
public void UntracedDataWrite_ThenEcho_PublishesUnstampedChange()
{
var (actor, dcl, sub) = Create("P2");
// Stamped writer but NOT traced — the ordinary script-write path.
Write(actor, dcl, "P2", Guid.NewGuid(), trace: false);
var published = Echo(actor, sub, true);
Assert.Null(published.SourceExecutionId);
Assert.False(published.Trace);
}
/// <summary>
/// Arch-review F6: the arm is recorded when the write is ISSUED, not when the DCL acknowledges
/// it. A write the device rejects must drop the arm, otherwise the next unrelated subscription
/// update on that attribute is stamped with the debugger's execution id for the rest of the
/// 30 s window — the Flow Debugger would draw a "device echo" of a write that never landed.
/// </summary>
[Fact]
public void TracedDataWrite_ThatFails_DisarmsTheAttribute_SoTheNextEchoIsUnstamped()
{
var (actor, dcl, sub) = Create("P6");
FailingWrite(actor, dcl, "P6", Guid.NewGuid(), trace: true);
var published = Echo(actor, sub, "unrelated");
Assert.Null(published.SourceExecutionId);
Assert.False(published.Trace);
}
/// <summary>
/// A failed write must only drop ITS OWN arm — a newer traced write that succeeded in the
/// meantime owns the arm and keeps it (the disarm is keyed on the writer's execution id).
/// </summary>
[Fact]
public void FailedWrite_DoesNotDisarm_AnArmTakenByALaterWriter()
{
var (actor, dcl, sub) = Create("P7");
var loser = Guid.NewGuid();
var winner = Guid.NewGuid();
// Issue the traced write that will fail, but hold its DCL reply back. The reply goes to the
// Ask's temporary ref, so capture it now — a later Reply() would answer the second write.
actor.Tell(new SetStaticAttributeCommand("c1", "P7", "Processed", "true", DateTimeOffset.UtcNow,
SourceExecutionId: loser, Trace: true));
var first = ExpectWrite(dcl);
var firstAsker = dcl.LastSender;
// A second traced write lands and succeeds — it now owns the arm.
Write(actor, dcl, "P7", winner, trace: true);
// Only now does the first write fail; its disarm must not steal the winner's arm.
firstAsker.Tell(new WriteTagResponse(first.CorrelationId, false, "rejected", DateTimeOffset.UtcNow), dcl.Ref);
Assert.False(ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5)).Success);
var published = Echo(actor, sub, "echo");
Assert.Equal(winner, published.SourceExecutionId);
Assert.True(published.Trace);
}
/// <summary>Verifies that the trace arm is consumed only once.</summary>
[Fact]
public void Arm_IsConsumedOnce()
{
var (actor, dcl, sub) = Create("P3");
var writer = Guid.NewGuid();
Write(actor, dcl, "P3", writer, trace: true);
var first = Echo(actor, sub, "one");
var second = Echo(actor, sub, "two");
Assert.Equal(writer, first.SourceExecutionId);
Assert.True(first.Trace);
Assert.Null(second.SourceExecutionId);
Assert.False(second.Trace);
}
/// <summary>Verifies that an expired trace arm is ignored.</summary>
[Fact]
[Trait("Category", "Slow")]
public void Arm_Expired_IsIgnored()
{
var (actor, dcl, sub) = Create("P4", armWindowSeconds: 1);
Write(actor, dcl, "P4", Guid.NewGuid(), trace: true);
Thread.Sleep(1100);
var published = Echo(actor, sub, true);
Assert.Null(published.SourceExecutionId);
Assert.False(published.Trace);
}
/// <summary>Verifies that a traced static write publishes the trace flag.</summary>
[Fact]
public void StaticWrite_Traced_PublishesTraceFlag()
{
var (actor, _, sub) = Create("P5");
var writer = Guid.NewGuid();
actor.Tell(new SetStaticAttributeCommand("c", "P5", "Status", "Running", DateTimeOffset.UtcNow,
SourceExecutionId: writer, Trace: true));
Assert.True(ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5)).Success);
var published = sub.FishForMessage<AttributeValueChanged>(
m => m.AttributeName == "Status" && Equals(m.Value, "Running"), TimeSpan.FromSeconds(10));
Assert.Equal(writer, published.SourceExecutionId);
Assert.True(published.Trace);
// And an untraced static write publishes Trace=false (pin).
actor.Tell(new SetStaticAttributeCommand("c2", "P5", "Status", "Idle2", DateTimeOffset.UtcNow, SourceExecutionId: writer));
ExpectMsg<SetStaticAttributeResponse>(TimeSpan.FromSeconds(5));
var plain = sub.FishForMessage<AttributeValueChanged>(
m => m.AttributeName == "Status" && Equals(m.Value, "Idle2"), TimeSpan.FromSeconds(10));
Assert.False(plain.Trace);
}
}