b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask, SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass TestContext.Current.CancellationToken. Test counts unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
104 lines
5.5 KiB
C#
104 lines
5.5 KiB
C#
using System.Text.Json;
|
|
using Akka.Actor;
|
|
using Serilog;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
/// <summary>
|
|
/// Review I-2: under the factory Props a supervision restart of the scripted-alarm host builds a fresh, EMPTY
|
|
/// engine. Nothing re-sent <c>ApplyScriptedAlarms</c>, so until the next deploy the host had no mux interest
|
|
/// (inputs stopped) and its ownership filter dropped every operator ack/shelve as "not owned". The host now
|
|
/// tells its parent <see cref="ScriptedAlarms.ScriptedAlarmHostActor.ScriptedAlarmHostRestarted"/> from <c>PostRestart</c>,
|
|
/// and <see cref="DriverHostActor"/> re-sends the last plans it applied. This test never re-applies by hand.
|
|
/// </summary>
|
|
public sealed class DriverHostActorScriptedAlarmRestartTests : RuntimeActorTestBase
|
|
{
|
|
private static readonly NodeId TestNode = NodeId.Parse("driver-alarmhost-restart-test");
|
|
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('d', 64));
|
|
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
|
|
|
|
private static DeploymentId SeedAlarmDeployment(Microsoft.EntityFrameworkCore.IDbContextFactory<ZB.MOM.WW.OtOpcUa.Configuration.OtOpcUaConfigDbContext> db)
|
|
{
|
|
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
|
{
|
|
ScriptedAlarms = new[]
|
|
{
|
|
new
|
|
{
|
|
ScriptedAlarmId = "alm-1", EquipmentId = "eq-1", Name = "HighTemp", AlarmType = "AlarmCondition",
|
|
Severity = 800, MessageTemplate = "hot", PredicateScriptId = "s-1",
|
|
HistorizeToAveva = false, Retain = true, Enabled = true,
|
|
},
|
|
},
|
|
Scripts = new[] { new { ScriptId = "s-1", SourceCode = "return (int)ctx.GetTag(\"M.T\").Value > 90;" } },
|
|
});
|
|
var id = DeploymentId.NewId();
|
|
using var ctx = db.CreateDbContext();
|
|
ctx.Deployments.Add(new Deployment
|
|
{
|
|
DeploymentId = id.Value, RevisionHash = RevA.Value, Status = DeploymentStatus.Sealed,
|
|
CreatedBy = "test", SealedAtUtc = DateTime.UtcNow, ArtifactBlob = artifact,
|
|
});
|
|
ctx.SaveChanges();
|
|
return id;
|
|
}
|
|
|
|
[Fact]
|
|
public void After_a_supervision_restart_the_alarm_host_is_re_applied_and_inputs_and_acks_work_again()
|
|
{
|
|
var db = NewInMemoryDbFactory();
|
|
var deploymentId = SeedAlarmDeployment(db);
|
|
var publish = this.CreateCleanOutcomePublishProbe();
|
|
var mux = CreateTestProbe();
|
|
var coordinator = CreateTestProbe();
|
|
var host = Sys.ActorOf(DriverHostActor.Props(
|
|
db, TestNode, coordinator.Ref,
|
|
localRoles: new HashSet<string> { "driver" },
|
|
dependencyMux: mux.Ref,
|
|
opcUaPublishActor: publish.Ref,
|
|
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
|
|
virtualTagHostOverride: CreateTestProbe().Ref,
|
|
scriptRootLogger: new ScriptRootLogger(new LoggerConfiguration().CreateLogger()),
|
|
alarmStateStore: new InMemoryAlarmStateStore()));
|
|
V3ArtifactSeeder.Dispatch(host, coordinator, deploymentId, RevA);
|
|
|
|
var first = mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.T"), Timeout, cancellationToken: TestContext.Current.CancellationToken);
|
|
var alarmHost = first.Subscriber;
|
|
|
|
// The crash: a batch item with a null TagId makes the upstream Push throw (the documented unguarded
|
|
// path), so supervision restarts the alarm host. NO manual re-apply follows.
|
|
alarmHost.Tell(new VirtualTagActor.DependencyValueBatch(new[]
|
|
{
|
|
new VirtualTagActor.DependencyValueChanged(null!, 1, DateTime.UtcNow),
|
|
}));
|
|
mux.FishForMessage<DependencyMuxActor.UnregisterInterest>(u => u.Subscriber.Equals(alarmHost), Timeout, cancellationToken: TestContext.Current.CancellationToken);
|
|
|
|
// The parent re-applies on the restart signal: interest is registered again by the new incarnation.
|
|
mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.T"), Timeout, cancellationToken: TestContext.Current.CancellationToken);
|
|
|
|
// Inputs work: a dependency push activates the alarm.
|
|
alarmHost.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
|
|
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(
|
|
m => m.AlarmNodeId == "alm-1" && m.State.Active && !m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken);
|
|
|
|
// Acks work: the restarted engine owns alm-1 again, so the command is not filtered out as unowned.
|
|
alarmHost.Tell(new AlarmCommand(AlarmId: "alm-1", Operation: "Acknowledge", User: "alice", Comment: null, UnshelveAtUtc: null));
|
|
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(
|
|
m => m.AlarmNodeId == "alm-1" && m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken)
|
|
.State.Acknowledged.ShouldBeTrue();
|
|
}
|
|
}
|