Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/WriteBatchResetAndWaitTests.cs
T

165 lines
8.1 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>
/// Gitea #36 / 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),
currentCallDepth: 0, maxCallDepth: 10, askTimeout: TimeSpan.FromSeconds(5),
instanceName: Instance, logger: NullLogger.Instance);
[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);
}
[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 that fixes #36: no batch, no trigger, after a reset that was never echoed.
probe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
}
[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);
}
[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>
[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);
}
}