using Akka.Cluster; using Akka.TestKit.Xunit2; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness; /// /// Akka TestKit fixture for ControlPlane actor tests. Provides: /// - A test ActorSystem (xunit2 TestKit) wired with PubSub/Cluster extensions. /// - A fresh in-memory per harness instance. /// - An the actors can hold. /// /// One harness per test fact — InMemory provider gives strong isolation when the database /// name is unique (random Guid). /// public abstract class ControlPlaneActorTestBase : TestKit { /// /// Shared upper bound for a presence wait (ExpectMsg / ExpectTerminated / /// AwaitAssert) — the same doctrine as RuntimeActorTestBase.PresenceBudget. /// /// /// A budget, not a delay: the helpers return the instant the condition holds, so raising it costs /// nothing on the happy path and cannot make a genuinely failing assertion pass. Several actors here /// do their first EF Core (InMemory) work inside the waited-for step — model build is a multi-second /// cold cost on a loaded CI runner — and 3–5 s budgets sized on an idle machine failed there (#606). /// Absence windows (ExpectNoMsg) keep their own short literals and must not use this. /// protected static readonly TimeSpan PresenceBudget = TimeSpan.FromSeconds(15); /// Gets the Akka configuration for test actors, including cluster and pub-sub extensions. /// /// akka.test.single-expect-default is raised from Akka's 3 s to : /// it is the budget every duration-less ExpectMsg/ExpectTerminated/AwaitAssert /// uses, all of which are presence waits. Nothing in this assembly calls a duration-less /// ExpectNoMsg(), the one absence wait that would also read it. /// akka.test.timefactor comes from . /// protected static string AkkaTestHocon => BaseHocon + TestTimeFactor.Hocon; private const string BaseHocon = @" akka { loglevel = ""WARNING"" test.single-expect-default = 15s extensions = [ ""Akka.Cluster.Tools.PublishSubscribe.DistributedPubSubExtensionProvider, Akka.Cluster.Tools"" ] actor { provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" } remote.dot-netty.tcp { hostname = ""127.0.0.1"" port = 0 } cluster { seed-nodes = [] roles = [""admin""] min-nr-of-members = 1 run-coordinated-shutdown-when-down = off pub-sub.role = """" } }"; /// Initializes a new instance of the test harness; joins the cluster and waits for up status. protected ControlPlaneActorTestBase() : base(AkkaTestHocon) { // Self-join so the cluster transitions to Up and DistributedPubSub forms. var cluster = Akka.Cluster.Cluster.Get(Sys); cluster.Join(cluster.SelfAddress); AwaitCondition(() => cluster.State.Members.Any(m => m.Status == MemberStatus.Up), TimeSpan.FromSeconds(5)); } /// Creates a new in-memory database context factory with an optional database name. /// Optional database name; defaults to a random GUID if not provided. /// A context factory for creating in-memory config database contexts. protected static IDbContextFactory NewInMemoryDbFactory(string? dbName = null) { dbName ??= Guid.NewGuid().ToString("N"); return new InMemoryConfigDbFactory(dbName); } private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory { /// Creates a new in-memory database context. /// A new OtOpcUaConfigDbContext instance. public OtOpcUaConfigDbContext CreateDbContext() { var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase(dbName) .Options; return new OtOpcUaConfigDbContext(opts); } } }