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
97 lines
4.4 KiB
C#
97 lines
4.4 KiB
C#
using Akka.Cluster;
|
||
using Akka.TestKit.Xunit2;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
|
||
|
||
/// <summary>
|
||
/// Akka TestKit fixture for ControlPlane actor tests. Provides:
|
||
/// - A test ActorSystem (xunit2 TestKit) wired with PubSub/Cluster extensions.
|
||
/// - A fresh in-memory <see cref="OtOpcUaConfigDbContext"/> per harness instance.
|
||
/// - An <see cref="IDbContextFactory{TContext}"/> the actors can hold.
|
||
///
|
||
/// One harness per test fact — InMemory provider gives strong isolation when the database
|
||
/// name is unique (random Guid).
|
||
/// </summary>
|
||
public abstract class ControlPlaneActorTestBase : TestKit
|
||
{
|
||
/// <summary>
|
||
/// Shared upper bound for a <b>presence</b> wait (<c>ExpectMsg</c> / <c>ExpectTerminated</c> /
|
||
/// <c>AwaitAssert</c>) — the same doctrine as <c>RuntimeActorTestBase.PresenceBudget</c>.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// A budget, not a delay: the helpers return the instant the condition holds, so raising it costs
|
||
/// nothing on the happy path and cannot make a genuinely failing assertion pass. Several actors here
|
||
/// do their first EF Core (InMemory) work inside the waited-for step — model build is a multi-second
|
||
/// cold cost on a loaded CI runner — and 3–5 s budgets sized on an idle machine failed there (#606).
|
||
/// Absence windows (<c>ExpectNoMsg</c>) keep their own short literals and must not use this.
|
||
/// </remarks>
|
||
protected static readonly TimeSpan PresenceBudget = TimeSpan.FromSeconds(15);
|
||
|
||
/// <summary>Gets the Akka configuration for test actors, including cluster and pub-sub extensions.</summary>
|
||
/// <remarks>
|
||
/// <c>akka.test.single-expect-default</c> is raised from Akka's 3 s to <see cref="PresenceBudget"/>:
|
||
/// it is the budget every duration-less <c>ExpectMsg</c>/<c>ExpectTerminated</c>/<c>AwaitAssert</c>
|
||
/// uses, all of which are presence waits. Nothing in this assembly 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 = [""admin""]
|
||
min-nr-of-members = 1
|
||
run-coordinated-shutdown-when-down = off
|
||
pub-sub.role = """"
|
||
}
|
||
}";
|
||
|
||
/// <summary>Initializes a new instance of the test harness; joins the cluster and waits for up status.</summary>
|
||
protected ControlPlaneActorTestBase() : base(AkkaTestHocon)
|
||
{
|
||
// Self-join so the cluster transitions to Up and DistributedPubSub forms.
|
||
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 context factory with an optional database name.</summary>
|
||
/// <param name="dbName">Optional database name; defaults to a random GUID if not provided.</param>
|
||
/// <returns>A context factory for creating in-memory config database contexts.</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);
|
||
}
|
||
}
|
||
}
|