111 lines
5.5 KiB
C#
111 lines
5.5 KiB
C#
using Akka.Cluster;
|
||
using Akka.TestKit.Xunit2;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||
|
||
/// <summary>
|
||
/// Mirrors the ControlPlane test harness: single-node Akka cluster (self-join) + in-memory
|
||
/// EF Core <see cref="OtOpcUaConfigDbContext"/> via an <see cref="IDbContextFactory{TContext}"/>.
|
||
/// </summary>
|
||
public abstract class RuntimeActorTestBase : TestKit
|
||
{
|
||
/// <summary>
|
||
/// Shared upper bound for a <b>presence</b> wait (<c>AwaitAssert</c> / <c>AwaitCondition</c>).
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para><b>A budget, not a delay.</b> 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
|
||
/// <em>cannot</em> make a genuinely failing assertion pass — it only changes how quickly a real
|
||
/// breakage is reported. That is the opposite of an <b>absence</b> window (<c>ExpectNoMsg</c>,
|
||
/// 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.</para>
|
||
/// <para><b>Why it exists (Gitea #500).</b> This assembly runs 44 Akka test classes roughly 14-way
|
||
/// parallel, each with its own <see cref="TestKit"/> 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 <em>different</em> test failing on one, in three
|
||
/// different files. Fixing them one at a time was chasing a distribution rather than a defect.</para>
|
||
/// <para>Public rather than protected so Harness helpers that are not themselves
|
||
/// <see cref="TestKit"/> subclasses — <see cref="V3ArtifactSeeder"/> — budget their waits from the
|
||
/// same constant instead of re-deriving one.</para>
|
||
/// </remarks>
|
||
public static readonly TimeSpan PresenceBudget = TimeSpan.FromSeconds(15);
|
||
|
||
/// <summary>Gets the Akka test HOCON configuration string for single-node cluster setup.</summary>
|
||
protected static string AkkaTestHocon => @"
|
||
akka {
|
||
loglevel = ""WARNING""
|
||
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
|
||
}
|
||
}";
|
||
|
||
/// <summary>
|
||
/// The same single-node HOCON at a caller-chosen <c>loglevel</c>.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// A log event below <c>akka.loglevel</c> is never published to the EventStream at all, so
|
||
/// neither <c>EventFilter</c> 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
|
||
/// <c>WARNING</c> so the other 40-odd classes keep their quiet output.
|
||
/// </remarks>
|
||
/// <param name="level">An Akka log level — <c>DEBUG</c>, <c>INFO</c>, <c>WARNING</c>, <c>ERROR</c>.</param>
|
||
/// <returns>The base test HOCON with its <c>loglevel</c> replaced.</returns>
|
||
protected static string AkkaTestHoconAtLevel(string level) =>
|
||
AkkaTestHocon.Replace(@"loglevel = ""WARNING""", $@"loglevel = ""{level}""", StringComparison.Ordinal);
|
||
|
||
/// <summary>Initializes a new instance of the RuntimeActorTestBase class with Akka test setup.</summary>
|
||
protected RuntimeActorTestBase() : this(AkkaTestHocon)
|
||
{
|
||
}
|
||
|
||
/// <summary>Initializes a new instance of the RuntimeActorTestBase class with caller-supplied HOCON.</summary>
|
||
/// <param name="hocon">The Akka configuration for this class's ActorSystem — usually
|
||
/// <see cref="AkkaTestHoconAtLevel"/>.</param>
|
||
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));
|
||
}
|
||
|
||
/// <summary>Creates a new in-memory database factory for testing.</summary>
|
||
/// <param name="dbName">The database name, or null to generate a unique name.</param>
|
||
/// <returns>An IDbContextFactory for the in-memory database.</returns>
|
||
protected static IDbContextFactory<OtOpcUaConfigDbContext> NewInMemoryDbFactory(string? dbName = null)
|
||
{
|
||
dbName ??= Guid.NewGuid().ToString("N");
|
||
return new InMemoryConfigDbFactory(dbName);
|
||
}
|
||
|
||
private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
|
||
{
|
||
/// <summary>Creates a new in-memory database context.</summary>
|
||
/// <returns>A new OtOpcUaConfigDbContext instance.</returns>
|
||
public OtOpcUaConfigDbContext CreateDbContext()
|
||
{
|
||
var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
||
.UseInMemoryDatabase(dbName)
|
||
.Options;
|
||
return new OtOpcUaConfigDbContext(opts);
|
||
}
|
||
}
|
||
}
|