Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Harness/RuntimeActorTestBase.cs
T
Joseph Doherty 55ae230fa4 ci(#606): Gitea-compatible trx upload + deterministic fixes for the CI-load test flakes
- v2-ci.yml: upload-artifact@v4 -> @v3 + continue-on-error (act_runner reports as GHES and
  v4 refuses GHES, failing every run); runner-capacity diagnostic step; AKKA_TEST_TIMEFACTOR=3
  on the test step.
- Runtime/ControlPlane harnesses: akka.test.timefactor from AKKA_TEST_TIMEFACTOR
  (TestTimeFactor), single-expect-default 15s; ControlPlane gains PresenceBudget.
- Leaked spinner threads: the infinite-loop timeout tests (Calculation, Host.IntegrationTests
  RoslynVirtualTagEvaluator, 3x Core.Scripting) pinned a core for the rest of the test process
  (abandoned, never stopped). Now a bounded 5 s wall-clock spin; outer compile-inclusive bound 30 s.
- Real test races fixed: ScriptedAlarmEngine push-during-drain (Inactive is also the start
  state + persist-before-update => wait for the Cleared save); DriverReconnectE2e (both nodes
  host the driver; Created[] is last-writer-wins and ForceReconnect is a no-op while
  Connecting); ControlPlaneStarterOrder (lost-race heal waited for a 60 s refresh tick).
- Fixed sleeps -> poll-for-condition: ReconnectSupervisor LastError, Modbus deadband steps
  (wait for observed reads), Modbus dispose hygiene precondition, FOCAS probe-loop log.
- Short presence budgets -> PresenceBudget in the Runtime/ControlPlane tests seen failing on
  Gitea; Calculation Settle 10 -> 30 s. No assertion removed, nothing skipped.
- infra/README.md §10 CI; stale TreatWarningsAsErrors comment in Directory.Build.props.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 19:44:31 -04:00

121 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
/// <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);
}
}
}