b211911f98
v2-ci / unit-tests (push) Failing after 13m22s
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
160 lines
9.1 KiB
C#
160 lines
9.1 KiB
C#
using Akka.Cluster;
|
||
using Akka.TestKit;
|
||
using Akka.TestKit.Xunit;
|
||
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 within one poll interval of the
|
||
/// condition holding (<c>ExpectMsg</c> the instant the message arrives; <c>AwaitAssert</c> every 100 ms,
|
||
/// Akka's default; <c>AwaitCondition</c> every <see cref="PollInterval"/> through this class's overloads —
|
||
/// Akka's own default there is <c>max / 10</c> of the dilated budget, 4.5 s in CI, lmxopcua#775), 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>
|
||
/// Poll interval for every <c>AwaitCondition</c> in this class's subclasses that does not pass its own
|
||
/// (lmxopcua#775). Akka's own default is <c>max / 10</c> of the DILATED budget — 1.5 s for a 15 s budget
|
||
/// locally, 4.5 s under CI's <c>AKKA_TEST_TIMEFACTOR=3</c> — so a wait could overshoot the moment its
|
||
/// condition held by seconds. That wasted CI time everywhere, and failed tests where an undilated
|
||
/// deadline in the code under test elapsed during the overshoot (lmxopcua 342a825d). <c>AwaitAssert</c>
|
||
/// is unaffected: its default interval is a fixed 100 ms. Public, like <see cref="PresenceBudget"/>, for
|
||
/// test classes that are not subclasses (a bare <see cref="TestKit"/>) to pass explicitly.
|
||
/// </summary>
|
||
public static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(20);
|
||
|
||
/// <summary>
|
||
/// <see cref="TestKitBase.AwaitCondition(Func{bool}, TimeSpan?, TimeSpan?, string, CancellationToken)"/> with
|
||
/// <paramref name="interval"/> defaulting to <see cref="PollInterval"/> instead of Akka's <c>max / 10</c>.
|
||
/// Hides the TestKit overloads, so every subclass call without an interval binds here.
|
||
/// </summary>
|
||
/// <param name="conditionIsFulfilled">The condition to poll.</param>
|
||
/// <param name="max">The budget (dilated by Akka); null takes the enclosing <c>Within</c> or the default.</param>
|
||
/// <param name="interval">The poll interval; null means <see cref="PollInterval"/>.</param>
|
||
/// <param name="message">The failure message.</param>
|
||
/// <param name="cancellationToken">Cancels the wait.</param>
|
||
protected new void AwaitCondition(Func<bool> conditionIsFulfilled, TimeSpan? max = null, TimeSpan? interval = null,
|
||
string? message = null, CancellationToken cancellationToken = default) =>
|
||
base.AwaitCondition(conditionIsFulfilled, max, interval ?? PollInterval, message!, cancellationToken);
|
||
|
||
/// <summary>The message-carrying overload of <see cref="AwaitCondition(Func{bool}, TimeSpan?, TimeSpan?, string?, CancellationToken)"/>,
|
||
/// polled every <see cref="PollInterval"/>.</summary>
|
||
/// <param name="conditionIsFulfilled">The condition to poll.</param>
|
||
/// <param name="max">The budget (dilated by Akka).</param>
|
||
/// <param name="message">The failure message.</param>
|
||
/// <param name="cancellationToken">Cancels the wait.</param>
|
||
protected new void AwaitCondition(Func<bool> conditionIsFulfilled, TimeSpan? max, string message,
|
||
CancellationToken cancellationToken = default) =>
|
||
base.AwaitCondition(conditionIsFulfilled, max, PollInterval, message, cancellationToken);
|
||
|
||
/// <summary>Gets the Akka test HOCON configuration string for single-node cluster setup.</summary>
|
||
/// <remarks>
|
||
/// <c>akka.test.single-expect-default</c> is raised from Akka's 3 s to <see cref="PresenceBudget"/>
|
||
/// (#606): it is the budget of every duration-less <c>ExpectMsg</c>/<c>ExpectTerminated</c>/
|
||
/// <c>AwaitAssert</c>, all presence waits. No test here calls a duration-less <c>ExpectNoMsg()</c>,
|
||
/// the one absence wait that would also read it.
|
||
/// <para><c>akka.test.timefactor</c> comes from <see cref="TestTimeFactor"/>.</para>
|
||
/// </remarks>
|
||
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
|
||
}
|
||
}";
|
||
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|