55ae230fa4
- 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
121 lines
6.1 KiB
C#
121 lines
6.1 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>
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|