using Akka.Cluster;
using Akka.TestKit.Xunit2;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
///
/// Mirrors the ControlPlane test harness: single-node Akka cluster (self-join) + in-memory
/// EF Core via an .
///
public abstract class RuntimeActorTestBase : TestKit
{
///
/// Shared upper bound for a presence wait (AwaitAssert / AwaitCondition).
///
///
/// A budget, not a delay. These helpers poll and return the instant the condition holds,
/// so the value is how long to wait before giving up. Raising it costs nothing on the happy path and
/// cannot make a genuinely failing assertion pass — it only changes how quickly a real
/// breakage is reported. That is the opposite of an absence window (ExpectNoMsg,
/// settle-then-assert), where the elapsed time IS the assertion and every millisecond is spent on
/// every run. Absence windows keep their own short, individually-calibrated literals and must not be
/// routed through here.
/// Why it exists (Gitea #500). This assembly runs 44 Akka test classes roughly 14-way
/// parallel, each with its own ActorSystem. Budgets of 300–500 ms sized on an
/// idle machine are comfortable in isolation and marginal under that contention: three consecutive
/// 30-run verification rounds each surfaced a different test failing on one, in three
/// different files. Fixing them one at a time was chasing a distribution rather than a defect.
/// Public rather than protected so Harness helpers that are not themselves
/// subclasses — — budget their waits from the
/// same constant instead of re-deriving one.
///
public static readonly TimeSpan PresenceBudget = TimeSpan.FromSeconds(15);
/// Gets the Akka test HOCON configuration string for single-node cluster setup.
///
/// akka.test.single-expect-default is raised from Akka's 3 s to
/// (#606): it is the budget of every duration-less ExpectMsg/ExpectTerminated/
/// AwaitAssert, all presence waits. No test here 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 = [""driver""]
min-nr-of-members = 1
run-coordinated-shutdown-when-down = off
}
}";
///
/// The same single-node HOCON at a caller-chosen loglevel.
///
///
/// A log event below akka.loglevel is never published to the EventStream at all, so
/// neither EventFilter nor an EventStream-subscribed probe can see it. A test whose
/// subject IS a log line (e.g. "this Debug line must be emitted exactly once, not in a spin")
/// therefore has to raise the level for its own ActorSystem; the default stays
/// WARNING so the other 40-odd classes keep their quiet output.
///
/// An Akka log level — DEBUG, INFO, WARNING, ERROR.
/// The base test HOCON with its loglevel replaced.
protected static string AkkaTestHoconAtLevel(string level) =>
AkkaTestHocon.Replace(@"loglevel = ""WARNING""", $@"loglevel = ""{level}""", StringComparison.Ordinal);
/// Initializes a new instance of the RuntimeActorTestBase class with Akka test setup.
protected RuntimeActorTestBase() : this(AkkaTestHocon)
{
}
/// Initializes a new instance of the RuntimeActorTestBase class with caller-supplied HOCON.
/// The Akka configuration for this class's ActorSystem — usually
/// .
protected RuntimeActorTestBase(string hocon) : base(hocon)
{
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 factory for testing.
/// The database name, or null to generate a unique name.
/// An IDbContextFactory for the in-memory database.
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);
}
}
}