513fa1aa16
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.
237 lines
12 KiB
C#
237 lines
12 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
|
|
|
/// <summary>
|
|
/// Design 2026-09-10 §5: the reset-aware handshake runs its legs in a FIXED
|
|
/// order — reset write, reset-echo wait, batch + trigger, response wait — and never writes
|
|
/// the batch unless the reset echo was observed. The probe stands in for the InstanceActor
|
|
/// so the order and the "nothing after a failed reset" property are directly observable.
|
|
/// </summary>
|
|
public class WriteBatchResetAndWaitTests : TestKit
|
|
{
|
|
private const string Instance = "Plant.Reactor1";
|
|
private static readonly IReadOnlyDictionary<string, string?> Values =
|
|
new Dictionary<string, string?> { ["BatchID"] = "28950" };
|
|
|
|
private static ScriptRuntimeContext CreateContext(IActorRef instanceActor) =>
|
|
new(instanceActor,
|
|
new SharedScriptLibrary(
|
|
new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance),
|
|
NullLogger<SharedScriptLibrary>.Instance),
|
|
// 30s: the legs Ask the InstanceActor with their own bounds (the batch's internal 35s,
|
|
// the waits' own timeouts); a short ambient Ask timeout would race those and make the
|
|
// reset/response phases indistinguishable from an AskTimeoutException.
|
|
currentCallDepth: 0, maxCallDepth: 10, askTimeout: TimeSpan.FromSeconds(30),
|
|
instanceName: Instance, logger: NullLogger.Instance);
|
|
|
|
/// <summary>Verifies that the legs run in order and the result reports the response phase.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task Legs_RunInOrder_AndReportTheResponsePhase()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
// Leg 1: the reset write, and nothing else yet.
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
Assert.Equal("MoveOutCompleteFlag", reset.AttributeName);
|
|
Assert.Equal("False", reset.Value);
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", true, null, DateTimeOffset.UtcNow));
|
|
|
|
// Leg 2: the reset-echo wait targets the RESET value and the RESET timeout.
|
|
var resetWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
Assert.Equal("MoveOutCompleteFlag", resetWait.AttributeName);
|
|
Assert.Equal("False", resetWait.TargetValueEncoded);
|
|
Assert.Equal(TimeSpan.FromSeconds(10), resetWait.Timeout);
|
|
Assert.False(resetWait.RequireGoodQuality);
|
|
probe.Reply(new WaitForAttributeResponse(resetWait.CorrelationId, Matched: true, false, "Good", TimedOut: false));
|
|
|
|
// Leg 3: only now the batch + trigger.
|
|
var batch = probe.ExpectMsg<WriteAttributeBatchRequest>(TimeSpan.FromSeconds(5));
|
|
Assert.Equal("MoveOutFlag", batch.TriggerAttribute);
|
|
Assert.Equal("True", batch.TriggerEncodedValue);
|
|
Assert.Equal("28950", batch.AttributeEncodedValues["BatchID"]);
|
|
probe.Reply(new WriteAttributeBatchResponse(batch.CorrelationId, true, null));
|
|
|
|
// Leg 4: the response wait with the RESPONSE timeout.
|
|
var respWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
Assert.Equal("MoveOutCompleteFlag", respWait.AttributeName);
|
|
Assert.Equal("True", respWait.TargetValueEncoded);
|
|
Assert.Equal(TimeSpan.FromSeconds(25), respWait.Timeout);
|
|
probe.Reply(new WaitForAttributeResponse(respWait.CorrelationId, Matched: true, true, "Good", TimedOut: false));
|
|
|
|
var result = await call;
|
|
Assert.True(result.Matched);
|
|
Assert.Equal(HandshakePhase.Response, result.Phase);
|
|
Assert.False(result.TimedOut);
|
|
Assert.False(result.ResetFailed);
|
|
Assert.Equal(true, result.Value);
|
|
Assert.Equal("Good", result.Quality);
|
|
Assert.Null(result.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>Verifies that a reset echo timeout reports the reset phase and never writes the batch.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResetEchoTimeout_ReportsResetPhase_AndNeverWritesTheBatch()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", true, null, DateTimeOffset.UtcNow));
|
|
var resetWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(resetWait.CorrelationId, Matched: false, null, null, TimedOut: true));
|
|
|
|
var result = await call;
|
|
Assert.False(result.Matched);
|
|
Assert.True(result.TimedOut);
|
|
Assert.Equal(HandshakePhase.Reset, result.Phase);
|
|
Assert.True(result.ResetFailed);
|
|
Assert.Contains("MoveOutCompleteFlag", result.ErrorMessage);
|
|
|
|
// The property under test: no batch, no trigger, after a reset that was never echoed.
|
|
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
|
}
|
|
|
|
/// <summary>Verifies that a response timeout reports the response phase.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResponseTimeout_ReportsResponsePhase()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", true, null, DateTimeOffset.UtcNow));
|
|
var resetWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(resetWait.CorrelationId, Matched: true, false, "Good", TimedOut: false));
|
|
var batch = probe.ExpectMsg<WriteAttributeBatchRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WriteAttributeBatchResponse(batch.CorrelationId, true, null));
|
|
var respWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(respWait.CorrelationId, Matched: false, null, null, TimedOut: true));
|
|
|
|
var result = await call;
|
|
Assert.False(result.Matched);
|
|
Assert.True(result.TimedOut);
|
|
Assert.Equal(HandshakePhase.Response, result.Phase);
|
|
Assert.False(result.ResetFailed);
|
|
// The message names the attribute that did not answer and the bound it blew.
|
|
Assert.Contains("MoveOutCompleteFlag", result.ErrorMessage);
|
|
Assert.Contains("25", result.ErrorMessage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A reset wait that fails WITHOUT timing out (a predicate blew up in the waiter) surfaces the
|
|
/// actor's own message verbatim rather than a synthesized one, and still writes nothing further.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResetWaitError_PassesTheActorsMessageThrough()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", true, null, DateTimeOffset.UtcNow));
|
|
var resetWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(
|
|
resetWait.CorrelationId, Matched: false, null, null, TimedOut: false, ErrorMessage: "predicate threw"));
|
|
|
|
var result = await call;
|
|
Assert.False(result.Matched);
|
|
Assert.False(result.TimedOut);
|
|
Assert.True(result.ResetFailed);
|
|
Assert.Equal("predicate threw", result.ErrorMessage);
|
|
|
|
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The batch leg keeps throwing after a successful reset — a device-rejected batch is a hard
|
|
/// failure, not a <see cref="HandshakeResult"/> the script could mistake for a timeout.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task BatchFailureAfterASuccessfulReset_StillThrows()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", true, null, DateTimeOffset.UtcNow));
|
|
var resetWait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(resetWait.CorrelationId, Matched: true, false, "Good", TimedOut: false));
|
|
var batch = probe.ExpectMsg<WriteAttributeBatchRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WriteAttributeBatchResponse(batch.CorrelationId, false, "gateway rejected"));
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => call);
|
|
Assert.Contains("gateway rejected", ex.Message);
|
|
Assert.Contains("Batch write failed", ex.Message);
|
|
}
|
|
|
|
/// <summary>Verifies that a reset write failure throws, just like a failure of the batch leg.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task ResetWriteFailure_Throws_LikeTheBatchLeg()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchResetAndWait(
|
|
Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25),
|
|
"MoveOutCompleteFlag", "False", TimeSpan.FromSeconds(10));
|
|
|
|
var reset = probe.ExpectMsg<SetStaticAttributeCommand>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new SetStaticAttributeResponse(reset.CorrelationId, Instance, "MoveOutCompleteFlag", false, "device rejected", DateTimeOffset.UtcNow));
|
|
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => call);
|
|
Assert.Contains("device rejected", ex.Message);
|
|
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
|
|
/// <summary>The bool overload is untouched: batch first, then the wait, no reset leg.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task LegacyOverload_StillWritesTheBatchFirst()
|
|
{
|
|
var probe = CreateTestProbe();
|
|
var ctx = CreateContext(probe.Ref);
|
|
|
|
var call = ctx.WriteBatchAndWait(Values, "MoveOutFlag", "True", "MoveOutCompleteFlag", "True", TimeSpan.FromSeconds(25));
|
|
|
|
var batch = probe.ExpectMsg<WriteAttributeBatchRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WriteAttributeBatchResponse(batch.CorrelationId, true, null));
|
|
var wait = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
|
probe.Reply(new WaitForAttributeResponse(wait.CorrelationId, Matched: true, true, "Good", TimedOut: false));
|
|
|
|
Assert.True(await call);
|
|
}
|
|
}
|