using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; /// /// Review 02 L2: DriverHostActor._lastReprovisionUtc was never pruned — a driver removed by a deploy /// after a supervision crash kept its stamp, so the map grew with every id ever reprovisioned-then-removed, /// and (the observable half) a driver removed and later re-added inherited the OLD stamp: its first crash /// after the re-add was deferred by the backoff as if it were crash-looping, although the new incarnation had /// never crashed. StopChild now drops the stamp. /// public sealed class DriverHostActorReprovisionStampTests : 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 RevisionHash RevC = RevisionHash.Parse(new string('c', 64)); private static readonly NodeId TestNode = NodeId.Parse("node-1:4053"); private const string DriverId = "DI-1"; private const string SpeedPath = "Plant/Modbus/dev1/speed"; [Fact] public void A_driver_removed_and_re_added_does_not_inherit_the_old_incarnations_reprovision_stamp() { var db = NewInMemoryDbFactory(); // Far longer than the presence budget below: an inherited stamp would defer the re-added driver's // reprovision past it, which is the failure this test reports. var backoff = TimeSpan.FromSeconds(60); var factory = new RecordingSubscribableDriverFactory("Modbus"); var withDriver = new[] { (DriverId, "Modbus", "dev1", "speed", "Float", false) }; var none = Array.Empty<(string, string, string, string, string?)>(); var revA = V3ArtifactSeeder.SeedV3Deployment(db, RevA, withDriver, none, enabled: true); var (host, _, _, coordinator) = V3ArtifactSeeder.SpawnHostAndApplyWithCoordinator( this, db, TestNode, revA, RevA, factory, reprovisionBackoff: backoff); AwaitAssert(() => factory.Created.Count.ShouldBe(1), PresenceBudget); AwaitAssert(() => factory.Created[0].SubscribeCount.ShouldBe(1), PresenceBudget); // Crash 1 on the first incarnation: reprovisioned immediately, which stamps the id. factory.Created[0].FireMalformedDataChange(SpeedPath); AwaitAssert(() => factory.Created[0].InitializeCount.ShouldBe(2), PresenceBudget); // Deploy B removes the driver (StopChild), deploy C re-adds it: a brand-new driver object. var revB = V3ArtifactSeeder.SeedV3Deployment( db, RevB, Array.Empty<(string, string, string, string, string, bool)>(), none, enabled: true); V3ArtifactSeeder.Dispatch(host, coordinator, revB, RevB); var revC = V3ArtifactSeeder.SeedV3Deployment(db, RevC, withDriver, none, enabled: true); V3ArtifactSeeder.Dispatch(host, coordinator, revC, RevC); AwaitAssert(() => factory.Created.Count.ShouldBe(2), PresenceBudget); var reAdded = factory.Created[1]; AwaitAssert(() => reAdded.SubscribeCount.ShouldBe(1), PresenceBudget); // Its FIRST crash must reprovision immediately — it has no history of its own. reAdded.FireMalformedDataChange(SpeedPath); AwaitAssert( () => reAdded.InitializeCount.ShouldBe( 2, "the re-added driver's first reprovision was deferred by the REMOVED incarnation's stamp"), duration: TimeSpan.FromSeconds(5)); } }