Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/NativeAlarmFeedSurvivesDeployTests.cs
T
Joseph Doherty b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
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
2026-11-10 08:57:33 -05:00

88 lines
4.5 KiB
C#

using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary>
/// Gitea #662 — a native-alarm feed must survive a deploy that does NOT respawn its driver, end to end
/// through the real <see cref="Runtime.Drivers.DriverHostActor"/> and its real
/// <see cref="Runtime.Drivers.DriverInstanceActor"/> child.
/// </summary>
/// <remarks>
/// Found live on the docker-dev rig: a deploy that only disabled ANOTHER driver re-pushed the desired set to
/// an unchanged OpcUaClient driver, whose data re-subscribe tore down the alarm event handler too. The
/// condition then froze while upstream kept emitting, and only a respawn brought it back. Every existing
/// end-to-end alarm test drives the driver directly (the host's driver is <c>Enabled=false</c>), so none of
/// them could see the instance actor drop the feed.
/// </remarks>
public sealed class NativeAlarmFeedSurvivesDeployTests : RuntimeActorTestBase
{
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
private static readonly NodeId TestNode = NodeId.Parse("node-1:4053");
private const string DriverId = "DI-1";
private const string AlarmXPath = "Plant/Modbus/dev1/alarmX";
private const string AlarmConfig = """{"alarm":{"alarmType":"AlarmCondition","severity":500}}""";
private static readonly (string Driver, string DriverName, string Device, string Tag, string DataType, bool Writable)[]
RawTags =
[
(DriverId, "Modbus", "dev1", "speed", "Float", false),
(DriverId, "Modbus", "dev1", "alarmX", "Float", false),
];
private static readonly Dictionary<string, string> TagConfigs = new() { ["alarmX"] = AlarmConfig };
[Fact]
public void An_upstream_alarm_still_reaches_its_condition_after_a_non_respawning_deploy()
{
var db = NewInMemoryDbFactory();
var factory = new RecordingSubscribableDriverFactory("Modbus");
var revA = V3ArtifactSeeder.SeedV3Deployment(
db, RevA, RawTags, Array.Empty<(string, string, string, string, string?)>(),
enabled: true, tagConfigByTag: TagConfigs);
var (host, publish, _) = V3ArtifactSeeder.SpawnHostAndApply(this, db, TestNode, revA, RevA, factory);
RecordingSubscribableDriver driver = null!;
AwaitAssert(() =>
{
factory.Created.Count.ShouldBe(1);
driver = factory.Created[0];
driver.SubscribeCount.ShouldBeGreaterThanOrEqualTo(1);
driver.LastSubscribedAlarmRefs.ShouldBe(new[] { AlarmXPath });
driver.AlarmSubscriberCount.ShouldBe(1);
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
var dataSubscribesBefore = driver.SubscribeCount;
// Revision B only ADDS another driver: DI-1's merged DriverConfig is byte-identical, so it is kept,
// and the bulk push re-sends its (unchanged) desired set — which re-subscribes its data.
var revB = V3ArtifactSeeder.SeedV3Deployment(
db, RevB, RawTags, Array.Empty<(string, string, string, string, string?)>(),
enabled: true, tagConfigByTag: TagConfigs, taglessDrivers: ["DI-2"]);
host.Tell(new DispatchDeployment(revB, RevB, CorrelationId.NewId()));
AwaitAssert(() =>
{
driver.SubscribeCount.ShouldBeGreaterThan(dataSubscribesBefore,
"precondition: the deploy must re-subscribe DI-1's data, or this test exercises nothing");
}, duration: PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
driver.InitializeCount.ShouldBe(1, "precondition: DI-1 must NOT have been respawned or re-initialised");
driver.ShutdownCount.ShouldBe(0);
factory.Created[0].ShouldBeSameAs(driver);
driver.AlarmSubscriberCount.ShouldBe(1, "the alarm event handler must survive the data re-subscribe");
driver.FireAlarm(AlarmXPath, AlarmTransitionKind.Raise);
var update = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(
m => m.AlarmNodeId == AlarmXPath && m.State.Active, PresenceBudget, cancellationToken: TestContext.Current.CancellationToken);
update.State.Acknowledged.ShouldBeFalse();
}
}