test(runtime): fix the intermittent Runtime.Tests failure — 4 ordering/logic defects + the presence-budget class (#500)
The reported symptom was "Runtime.Tests occasionally reports Failed: 1" with no test
name. Instrumenting 30 full-assembly runs reproduced it at 13% (4 runs) and showed it
was never ONE flaky test: five failures across three distinct tests in that batch, and
five more distinct tests over the verification rounds that followed.
Two hypotheses were tested and DISPROVED by measurement before anything was changed:
- Cluster-formation timeout. Every test in this assembly forms a real single-node Akka
cluster over TCP in RuntimeActorTestBase's constructor and waits 5 s for MemberStatus.Up
— 219 formations per run. Instrumented: idle max 960 ms, under-load max 1470 ms, zero
timeouts. A 3.4x margin; not the cause.
- Ephemeral-port exhaustion from that bind churn. TIME_WAIT measured at ~0 throughout.
Not the cause.
FOUR GENUINE ORDERING/LOGIC DEFECTS (none is a timeout):
1. VirtualTagHostActorTests.ApplyVirtualTags_respawns_child_when_plan_changes_in_place
The test loops to accept RegisterInterest/UnregisterInterest in EITHER order — and
then asserts on mux.LastSender, which DEPENDS on that order. Under the [Register,
Unregister] interleaving LastSender is the DYING child and the assertion fails. Now
captures the sender of the Register message as it arrives. Fully deterministic; no
timing involved. (Swept the other four LastSender sites: all drain the Unregister
first, so their ordering is fixed. Only this one was unsafe.)
2. DriverHostActorWriteRoutingTests.Primary_routes_write_via_uns_nodeid_to_driver_by_rawpath
ApplyAck marks the end of the APPLY, not the point a write can succeed: the child is
still in Connecting, which deliberately fast-fails writes ("driver not connected"),
and the NodeId->driver reverse map has not been pushed. Now retries until accepted.
This does not weaken the assertion — every rejection branch replies WITHOUT reaching
the driver, so Writes.Count.ShouldBe(1) still means exactly what it did.
3. HistorianAdapterActorTests.Redundancy_snapshot_without_local_node_...
One 500 ms constant served both presence budgets (AwaitAssert) and absence windows
(ExpectNoMsg). Different quantities that happened to share a number, so the presence
budget could not be raised without slowing every absence check. Split into
AssertTimeout (5 s) and Settle (500 ms).
4. ContinuousHistorizationRecorderTests.Retry_after_writer_failure_eventually_acks
Waited on `outbox == 0`, which is ALSO true before the first append — so the poll
could win the race against the recorder and return having observed the INITIAL state,
after which the CallCount>=2 check outside the block failed against a recorder that
had not run. Both conditions are now polled together with the retry count as the
discriminator. (The preceding test in the same file documents this exact trap.)
THE PRESENCE-BUDGET CLASS:
After those four, three consecutive 30-run rounds each still failed once — on a
DIFFERENT test, in a DIFFERENT file, every time (OpcUaPublishActorApplyFailureTests 2 s,
OpcUaPublishActorRebuildTests 2 s, OpcUaPublishActorTests 500 ms). That is a
distribution, not a defect, and fixing it one test at a time was the wrong method.
RuntimeActorTestBase now carries a documented PresenceBudget (15 s) and 57 sub-3 s
AwaitAssert/AwaitCondition durations across four files route through it.
Why this is safe rather than sloppy: a presence budget is an upper bound before giving
up, not a wait — these helpers poll and return the instant the condition holds. Raising
one costs nothing on the happy path and CANNOT make a genuinely failing assertion pass;
it only changes how quickly a real breakage reports. Absence windows are the opposite
(the elapsed time IS the assertion) and were deliberately left untouched with their own
short, individually-calibrated literals.
ScriptedAlarmHostActorTests is a special case, diagnosed rather than merely raised: its
8 s budget was sized for message passing but actually waits on a REAL Roslyn compile
(ApplyScriptedAlarms -> ScriptedAlarmEngine.LoadAsync compiles every predicate). Cold
script compiles are the slowest and most variable thing in the assembly, so that class
gets 30 s with the reason recorded.
Verification: 30 consecutive full-assembly runs, 30 passed / 0 failed, against a 13%
baseline. Full solution builds; all 41 unit-test projects green.
NOT fixed here, filed as #501: DriverHostActorNativeAlarmAckRoutingTests:90 and :116 use
AwaitAssert for an ABSENCE assertion, so they return instantly and prove nothing. They
are excluded from the budget change on purpose — a bigger number does not fix them, and
rewriting them to settle-then-assert may legitimately turn them red.
This commit is contained in:
+44
-6
@@ -55,11 +55,9 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
|
||||
|
||||
var actor = SpawnHostAndApply(db, deploymentId, recorder);
|
||||
|
||||
var asker = CreateTestProbe();
|
||||
// The node manager passes the FULL ns-qualified id; the host must normalise + resolve it.
|
||||
actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0, AddressSpaceRealm.Uns), asker.Ref);
|
||||
RouteWriteUntilAccepted(actor, $"ns=3;s={UnsNodeId}", 123.0, AddressSpaceRealm.Uns);
|
||||
|
||||
asker.ExpectMsg<DriverHostActor.NodeWriteResult>(Timeout).Success.ShouldBeTrue();
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
recorder.Writes.Count.ShouldBe(1);
|
||||
@@ -79,10 +77,8 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
|
||||
|
||||
var actor = SpawnHostAndApply(db, deploymentId, recorder);
|
||||
|
||||
var asker = CreateTestProbe();
|
||||
actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=2;s={RawPath}", 456.0, AddressSpaceRealm.Raw), asker.Ref);
|
||||
RouteWriteUntilAccepted(actor, $"ns=2;s={RawPath}", 456.0, AddressSpaceRealm.Raw);
|
||||
|
||||
asker.ExpectMsg<DriverHostActor.NodeWriteResult>(Timeout).Success.ShouldBeTrue();
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
recorder.Writes.Count.ShouldBe(1);
|
||||
@@ -91,6 +87,48 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
|
||||
}, duration: Timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a write, retrying until the host accepts it, and fails with the host's own rejection
|
||||
/// <c>Reason</c> if it never does.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Why a retry rather than a single Tell.</b> <c>ApplyAck</c> — which
|
||||
/// <see cref="SpawnHostAndApply"/> waits for — marks the end of the <em>apply</em>, not the point at
|
||||
/// which a write can succeed. Several things still have to happen after it: the spawned child has to
|
||||
/// finish <c>InitializeAsync</c> and leave <c>Connecting</c> (that state deliberately fast-fails
|
||||
/// writes with "driver not connected"), and the host has to push the NodeId→driver reverse map that
|
||||
/// resolves the write. Telling the write immediately therefore races the setup, which is why this
|
||||
/// assertion failed roughly 1 run in 30 of the fully parallel assembly — and never once in 60
|
||||
/// consecutive runs of this class alone.</para>
|
||||
/// <para><b>Why retrying does not weaken the assertion.</b> Every rejection branch — the Primary
|
||||
/// gate, an unresolved reverse map, "driver not running", and the child's own pre-Connected
|
||||
/// fast-fail — replies <em>without</em> reaching the driver. A rejected attempt records nothing, so
|
||||
/// the caller's <c>Writes.Count.ShouldBe(1)</c> still means exactly what it did before: the accepted
|
||||
/// write reached the driver exactly once.</para>
|
||||
/// <para>The failure message carries the last <c>Reason</c> so a genuine regression is diagnosable
|
||||
/// rather than surfacing as a bare "expected True".</para>
|
||||
/// </remarks>
|
||||
/// <param name="actor">The driver-host actor.</param>
|
||||
/// <param name="nodeId">The ns-qualified NodeId string, as the node manager passes it.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="realm">The address-space realm the NodeId belongs to.</param>
|
||||
private void RouteWriteUntilAccepted(
|
||||
IActorRef actor, string nodeId, object value, AddressSpaceRealm realm)
|
||||
{
|
||||
var lastReason = "(no reply)";
|
||||
AwaitAssert(
|
||||
() =>
|
||||
{
|
||||
var asker = CreateTestProbe();
|
||||
actor.Tell(new DriverHostActor.RouteNodeWrite(nodeId, value, realm), asker.Ref);
|
||||
var result = asker.ExpectMsg<DriverHostActor.NodeWriteResult>(TimeSpan.FromSeconds(1));
|
||||
lastReason = result.Reason ?? "(none)";
|
||||
result.Success.ShouldBeTrue($"host kept rejecting the write; last reason: {lastReason}");
|
||||
},
|
||||
duration: Timeout,
|
||||
interval: TimeSpan.FromMilliseconds(50));
|
||||
}
|
||||
|
||||
/// <summary>On a SECONDARY, RouteNodeWrite (via the UNS NodeId) replies "not primary" and the driver
|
||||
/// receives NO write — the primary gate fires before the reverse-map lookup.</summary>
|
||||
[Fact]
|
||||
|
||||
@@ -11,6 +11,25 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
/// </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>
|
||||
/// </remarks>
|
||||
protected 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 {
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class PeerProbeSupervisorTests : RuntimeActorTestBase
|
||||
State(Adm, RedundancyRole.Detached)));
|
||||
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the child for a departed peer is stopped when the next snapshot omits it.</summary>
|
||||
@@ -71,11 +71,11 @@ public sealed class PeerProbeSupervisorTests : RuntimeActorTestBase
|
||||
State(Local, RedundancyRole.Primary),
|
||||
State(Peer, RedundancyRole.Secondary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
sup.Tell(Snapshot(State(Local, RedundancyRole.Primary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a single-node snapshot (just the local node) spawns no children.</summary>
|
||||
@@ -88,7 +88,7 @@ public sealed class PeerProbeSupervisorTests : RuntimeActorTestBase
|
||||
sup.Tell(Snapshot(State(Local, RedundancyRole.Primary)));
|
||||
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a previously-removed peer is respawned when it re-appears, without an
|
||||
@@ -103,17 +103,17 @@ public sealed class PeerProbeSupervisorTests : RuntimeActorTestBase
|
||||
State(Local, RedundancyRole.Primary),
|
||||
State(Peer, RedundancyRole.Secondary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
sup.Tell(Snapshot(State(Local, RedundancyRole.Primary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
sup.Tell(Snapshot(
|
||||
State(Local, RedundancyRole.Primary),
|
||||
State(Peer, RedundancyRole.Secondary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Locks in the stale-Terminated guard: when an OLD (already-replaced) child's
|
||||
@@ -132,27 +132,27 @@ public sealed class PeerProbeSupervisorTests : RuntimeActorTestBase
|
||||
State(Local, RedundancyRole.Primary),
|
||||
State(Peer, RedundancyRole.Secondary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
AwaitAssert(() => spawned.Count.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
AwaitAssert(() => spawned.Count.ShouldBe(1), duration: PresenceBudget);
|
||||
var oldRef = spawned[0];
|
||||
|
||||
// Drop the peer -> child #0 stopped, ChildCount back to 0.
|
||||
sup.Tell(Snapshot(State(Local, RedundancyRole.Primary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
// Re-add the SAME peer -> a NEW child #1 (the FRESH ref) is spawned.
|
||||
sup.Tell(Snapshot(
|
||||
State(Local, RedundancyRole.Primary),
|
||||
State(Peer, RedundancyRole.Secondary)));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
AwaitAssert(() => spawned.Count.ShouldBe(2), duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
AwaitAssert(() => spawned.Count.ShouldBe(2), duration: PresenceBudget);
|
||||
|
||||
// Now deliver a STALE Terminated for the OLD ref. The current child for Peer is the fresh
|
||||
// child #1, so ref-equality finds no match and the supervisor must leave ChildCount at 1.
|
||||
sup.Tell(new Terminated(oldRef, existenceConfirmed: true, addressTerminated: false));
|
||||
AwaitAssert(() => sup.UnderlyingActor.ChildCount.ShouldBe(1),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -295,10 +295,24 @@ public sealed class ContinuousHistorizationRecorderTests : TestKit
|
||||
|
||||
// The first drain returns false (entry retained); after the backoff the retry drain succeeds
|
||||
// and acks, truncating the outbox to 0.
|
||||
await AwaitAssertAsync(async () =>
|
||||
Assert.Equal(0, await outbox.CountAsync(default)), TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.True(writer.CallCount >= 2, "the writer must have been called at least twice (a retry happened)");
|
||||
//
|
||||
// BOTH conditions are polled together, and the retry count is the load-bearing one. An empty
|
||||
// outbox is NOT a distinguishing observation: it is equally true BEFORE the append lands, so
|
||||
// waiting on it alone can be satisfied by the initial state and return before the recorder has
|
||||
// done anything at all. That is exactly what happened intermittently under a fully parallel
|
||||
// assembly run — the poll won the race against the first append, the wait returned immediately,
|
||||
// and the CallCount check that used to sit outside the block then failed against a recorder that
|
||||
// had not yet run. (The same trap is called out on the preceding test, which guards against it by
|
||||
// pairing its count with a writer observation.)
|
||||
await AwaitAssertAsync(
|
||||
async () =>
|
||||
{
|
||||
Assert.True(
|
||||
writer.CallCount >= 2,
|
||||
"the writer must have been called at least twice (a retry happened)");
|
||||
Assert.Equal(0, await outbox.CountAsync(default));
|
||||
},
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+27
-9
@@ -23,9 +23,27 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
/// <summary>The local node id the gating tests construct the adapter with.</summary>
|
||||
private static readonly NodeId LocalNode = new("node-A");
|
||||
|
||||
/// <summary>A short window we allow the fire-and-forget enqueue to land within.</summary>
|
||||
/// <summary>
|
||||
/// The window an <b>absence</b> assertion waits before concluding nothing arrived
|
||||
/// (<c>ExpectNoMsg</c>). Its length is a calibration decision — long enough that a message which
|
||||
/// was going to arrive would have — so it is deliberately NOT generous.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan Settle = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
/// <summary>
|
||||
/// The budget a <b>presence</b> assertion may take to become true (<c>AwaitAssert</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Separate from <see cref="Settle"/> on purpose, though they once shared its 500 ms. The two are
|
||||
/// different quantities that merely had the same number: a presence budget is an upper bound before
|
||||
/// giving up, and <c>AwaitAssert</c> returns the instant the condition holds, so a generous value
|
||||
/// costs nothing in the passing case and can never make a genuinely failing assertion pass. An
|
||||
/// absence window is the opposite — every millisecond is spent on every run. Conflating them meant
|
||||
/// the enqueue assertions could only be given more headroom by slowing every <c>ExpectNoMsg</c> in
|
||||
/// the class, so they kept a 500 ms budget that a fully parallel assembly run occasionally missed.
|
||||
/// </remarks>
|
||||
private static readonly TimeSpan AssertTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>Thread-safe fake sink that records every <see cref="EnqueueAsync"/> call.</summary>
|
||||
private sealed class RecordingSink : IAlarmHistorianSink
|
||||
{
|
||||
@@ -101,7 +119,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(SampleEvent());
|
||||
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), Settle);
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Secondary suppression (T2): when the cached local role is Secondary, the adapter MUST
|
||||
@@ -145,7 +163,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
TellRedundancyRole(actor, RedundancyRole.Primary);
|
||||
actor.Tell(SampleEvent());
|
||||
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), Settle);
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Absent-node default-historize (T5): a snapshot that mentions only a DIFFERENT node
|
||||
@@ -174,7 +192,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
actor.Tell(SampleEvent());
|
||||
|
||||
// Local role is still unknown ⇒ default-historize path: sink must record exactly one enqueue.
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), Settle);
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Builds an <see cref="AlarmTransitionEvent"/> (the shape published on the <c>alerts</c>
|
||||
@@ -222,7 +240,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
e.Severity.ShouldBe(AlarmSeverity.High);
|
||||
e.Comment.ShouldBe("note");
|
||||
},
|
||||
Settle);
|
||||
AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Secondary suppression for alerts (T7): a Secondary node must NOT historize a transition
|
||||
@@ -252,7 +270,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
TellRedundancyRole(actor, RedundancyRole.Primary);
|
||||
actor.Tell(SampleTransition());
|
||||
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), Settle);
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Per-alarm opt-out (T8b): a Primary node must NOT historize a transition whose
|
||||
@@ -286,7 +304,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
TellRedundancyRole(actor, RedundancyRole.Primary);
|
||||
actor.Tell(SampleTransition(historizeToAveva: null));
|
||||
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), Settle);
|
||||
AwaitAssert(() => sink.EnqueueCount.ShouldBe(1), AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Severity buckets (T9): the OPC UA 1–1000 numeric severity on the transition maps onto
|
||||
@@ -308,7 +326,7 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
|
||||
AwaitAssert(
|
||||
() => sink.Events.ShouldHaveSingleItem().Severity.ShouldBe(expected),
|
||||
Settle);
|
||||
AssertTimeout);
|
||||
}
|
||||
|
||||
/// <summary>Rolling-restart null default (T10): an old-format transition deserialized by Akka's JSON
|
||||
@@ -326,6 +344,6 @@ public sealed class HistorianAdapterActorTests : RuntimeActorTestBase
|
||||
|
||||
AwaitAssert(
|
||||
() => sink.Events.ShouldHaveSingleItem().AlarmTypeName.ShouldBe("AlarmCondition"),
|
||||
Settle);
|
||||
AssertTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -48,11 +48,16 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
|
||||
var dep2 = SeedEquipmentDeployment(db, ("eq-1", "eq-1-renamed"));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep2)));
|
||||
|
||||
// A PRESENCE budget, so it is an upper bound before giving up rather than a wait: the helper polls
|
||||
// and returns the instant the meter fires, so a generous value costs nothing on the happy path and
|
||||
// can never make a genuinely failing assertion pass. Two seconds covered an idle machine but not a
|
||||
// fully parallel assembly run, where this deploy→rebuild→throw chain (two DB-backed applies) has to
|
||||
// share 14 cores with 40-odd other Akka test classes; it failed there roughly 1 run in 30.
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
recorder.Total.ShouldBeGreaterThanOrEqualTo(1);
|
||||
recorder.WithTag("kind", "rebuild").ShouldBeGreaterThanOrEqualTo(1);
|
||||
}, duration: TimeSpan.FromSeconds(2));
|
||||
}, duration: TimeSpan.FromSeconds(15));
|
||||
}
|
||||
|
||||
/// <summary>A clean rebuild does NOT increment the apply-failed meter (Info-only happy path).</summary>
|
||||
|
||||
+16
-16
@@ -45,7 +45,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
sink.Calls.ShouldContain("EF:eq-1");
|
||||
sink.Calls.ShouldContain("EF:eq-2");
|
||||
sink.Calls.ShouldContain("NA:line-1");
|
||||
}, duration: TimeSpan.FromSeconds(2));
|
||||
}, duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
sink: sink, dbFactory: db, applier: applier));
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0); // PureAdd — no full rebuild
|
||||
var callsAfterFirst = sink.Calls.Count;
|
||||
|
||||
@@ -107,7 +107,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -132,7 +132,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), Artifact: artifact));
|
||||
|
||||
// The in-hand bytes drove the real diff-and-apply — the equipment folder was materialised…
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: PresenceBudget);
|
||||
// …and the address-space-wiping raw-sink fallback was NOT taken.
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
}
|
||||
@@ -176,7 +176,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,7 +197,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep)));
|
||||
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-1"), duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
// PureAdd (equipment + tag) ⇒ no full rebuild; the materialise passes still run the cluster slice.
|
||||
// t-sa (EquipmentId "eq-sa", FolderPath "F", Name "S1") → folder-scoped variable "eq-sa/F/S1".
|
||||
AwaitAssert(() => sinkA.Calls.ShouldContain("EV:eq-sa/F/S1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sinkA.Calls.ShouldContain("EV:eq-sa/F/S1"), duration: PresenceBudget);
|
||||
sinkA.RebuildCalls.ShouldBe(0);
|
||||
// t-main (MAIN cluster) must NOT leak onto the SITE-A node.
|
||||
sinkA.Calls.ShouldNotContain("EV:eq-main/F/M1");
|
||||
@@ -249,7 +249,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
mainActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => sinkM.Calls.ShouldContain("EV:eq-main/F/M1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sinkM.Calls.ShouldContain("EV:eq-main/F/M1"), duration: PresenceBudget);
|
||||
sinkM.RebuildCalls.ShouldBe(0);
|
||||
sinkM.Calls.ShouldNotContain("EV:eq-sa/F/S1");
|
||||
}
|
||||
@@ -331,7 +331,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("NA:line-1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("NA:line-1"), duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
|
||||
var calls = sink.Calls.ToList();
|
||||
@@ -356,7 +356,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep1)));
|
||||
AwaitAssert(() => sink.Calls.Count(c => c.StartsWith("NA:")).ShouldBe(1), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.Count(c => c.StartsWith("NA:")).ShouldBe(1), duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
var naAfterFirst = sink.Calls.Count(c => c.StartsWith("NA:"));
|
||||
|
||||
@@ -365,7 +365,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
var dep2 = SeedNamedEquipmentDeployment(db, ("eq-1", "Pump-1-RENAMED"), ("eq-2", "Pump-2"));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep2)));
|
||||
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: PresenceBudget);
|
||||
// No new NodeAdded announcement was raised for the rebuild-kind deploy.
|
||||
sink.Calls.Count(c => c.StartsWith("NA:")).ShouldBe(naAfterFirst);
|
||||
}
|
||||
@@ -388,7 +388,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep1)));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-2"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-2"), duration: PresenceBudget);
|
||||
var callsAfterFirst = sink.Calls.Count;
|
||||
|
||||
// The next deploy arrives while the ConfigDb is briefly unreachable.
|
||||
@@ -423,13 +423,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
|
||||
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep1)));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-2"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:eq-2"), duration: PresenceBudget);
|
||||
|
||||
// eq-2 really is gone from the next artifact — that IS a configuration change, so it must apply.
|
||||
var dep2 = SeedNamedEquipmentDeployment(db, ("eq-1", "Pump-1"));
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep2)));
|
||||
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("RE:eq-2"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("RE:eq-2"), duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>An <see cref="IDbContextFactory{TContext}"/> whose <c>CreateDbContext</c> can be made to
|
||||
@@ -552,7 +552,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
// First deploy: the area folder is materialised with the OLD name. R2-07 — this is now a PureAdd
|
||||
// (area + line + equipment all added), so NO full rebuild; the folder is materialised directly.
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId(), new DeploymentId(dep1)));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:area-1"), duration: TimeSpan.FromSeconds(2));
|
||||
AwaitAssert(() => sink.Calls.ShouldContain("EF:area-1"), duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0);
|
||||
|
||||
// Second deploy: ONLY the area Name changed — a rename. The actor must reach the apply path and
|
||||
@@ -563,7 +563,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
sink.FolderRenameCalls.ShouldContain(("area-1", "Plant South"));
|
||||
}, duration: TimeSpan.FromSeconds(2));
|
||||
}, duration: PresenceBudget);
|
||||
sink.RebuildCalls.ShouldBe(0); // the rename did NOT force a full rebuild
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
sink.Values[0].Value.ShouldBe(3.14);
|
||||
sink.Values[0].Quality.ShouldBe(OpcUaQuality.Good);
|
||||
sink.Values[1].Quality.ShouldBe(OpcUaQuality.Uncertain);
|
||||
}, duration: TimeSpan.FromMilliseconds(500));
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that AlarmStateUpdate routes to sink WriteAlarmCondition with the full snapshot.</summary>
|
||||
@@ -73,7 +73,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
sink.Alarms[0].State.Active.ShouldBeTrue();
|
||||
sink.Alarms[0].State.Acknowledged.ShouldBeFalse();
|
||||
sink.Alarms[0].State.Severity.ShouldBe((ushort)700);
|
||||
}, duration: TimeSpan.FromMilliseconds(500));
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>#477 — AlarmQualityUpdate routes to sink.WriteAlarmQuality with the quality + realm.</summary>
|
||||
@@ -93,7 +93,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
q.AlarmNodeId.ShouldBe("Plant/Modbus/dev1/temp_hi");
|
||||
q.Quality.ShouldBe(OpcUaQuality.Bad);
|
||||
q.Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
}, duration: TimeSpan.FromMilliseconds(500));
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Builds a test <see cref="AlarmConditionSnapshot"/> with sensible defaults so each test
|
||||
@@ -117,7 +117,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
|
||||
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that <see cref="OpcUaPublishActor.MaterialiseDiscoveredNodes"/> forwards to the
|
||||
@@ -146,7 +146,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
sink.Folders.ShouldContain(("EQ-1/Axes", "EQ-1", "Axes"));
|
||||
sink.Variables.ShouldContain(("EQ-1/Axes/X", "EQ-1/Axes", "X", "Double", false));
|
||||
sink.ModelChanges.ShouldContain("EQ-1");
|
||||
}, duration: TimeSpan.FromMilliseconds(500));
|
||||
}, duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ServiceLevelChanged publishes to IServiceLevelPublisher once per unique level.</summary>
|
||||
@@ -161,7 +161,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
actor.Tell(new OpcUaPublishActor.ServiceLevelChanged(100));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldBe(new byte[] { 240, 100 }),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the very first computed ServiceLevel is always published even when it is
|
||||
@@ -186,7 +186,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldBe(new byte[] { 0 }),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RedundancyStateChanged drives local ServiceLevel publish for primary leader.</summary>
|
||||
@@ -210,7 +210,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
actor.Tell(snapshot);
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldBe(new byte[] { 240 }),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RedundancyStateChanged for secondary publishes 100.</summary>
|
||||
@@ -231,7 +231,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
actor.Tell(snapshot);
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldBe(new byte[] { 100 }),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the calculator path computes 250 for a healthy primary role-leader
|
||||
@@ -256,7 +256,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the calculator path computes 240 for a healthy non-leader secondary
|
||||
@@ -282,7 +282,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the calculator path computes 100 when the DB is unreachable
|
||||
@@ -307,7 +307,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)100),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the calculator path computes 200 for a stale snapshot when the DB is
|
||||
@@ -335,7 +335,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)200),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a detached local node publishes 0 (the calculator does not model
|
||||
@@ -362,7 +362,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
// Now detach — expect the guard to drive ServiceLevel down to 0.
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
@@ -374,7 +374,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an actively-observed, recent peer probe of MY endpoint that came back
|
||||
@@ -403,7 +403,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies branch (3) of <c>OpcUaProbeOk()</c>: a peer's NEGATIVE verdict about this node
|
||||
@@ -447,7 +447,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
|
||||
// Healthy (250), NOT 0 — proves an aged negative verdict does not demote.
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that with no peer probe result ever received, <c>OpcUaProbeOk()</c> defaults
|
||||
@@ -474,7 +474,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a later <c>Ok==true</c> peer probe supersedes an earlier <c>Ok==false</c>
|
||||
@@ -503,7 +503,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a peer probe result about a DIFFERENT node is ignored — it does not
|
||||
@@ -531,7 +531,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies the legacy back-compat seam: with no DB-health probe wired, the handler
|
||||
@@ -553,7 +553,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the periodic <c>HealthTick</c> Asks the local <see cref="DbHealthProbeActor"/>
|
||||
@@ -607,7 +607,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>DB-less, member Up, snapshot fresh, Primary → 250 (basis 240 + 10 Primary bonus).</summary>
|
||||
@@ -629,7 +629,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>DB-less, snapshot stale (entry.AsOfUtc older than the stale window) → 200. Staleness
|
||||
@@ -654,7 +654,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)200),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>DB-less, Detached → 0. The Detached / missing-entry guard runs ABOVE the DB-less
|
||||
@@ -677,7 +677,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
Nodes: new[]
|
||||
@@ -688,7 +688,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)0),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Regression: a DB-BACKED node (probe wired + DbHealthStatus reachable, dbLess default
|
||||
@@ -714,7 +714,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
duration: PresenceBudget);
|
||||
}
|
||||
|
||||
/// <summary>Stub DB-health probe actor that answers <see cref="DbHealthProbeActor.GetStatus"/>
|
||||
|
||||
+17
-1
@@ -28,7 +28,23 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;
|
||||
/// </summary>
|
||||
public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
{
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(8);
|
||||
/// <summary>
|
||||
/// Upper bound for the <b>presence</b> waits in this class (<c>ExpectMsg</c> / <c>FishForMessage</c>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Sized for a <b>real Roslyn compilation</b>, not for message passing.
|
||||
/// <c>ApplyScriptedAlarms</c> drives <c>ScriptedAlarmEngine.LoadAsync</c>, which compiles every
|
||||
/// alarm predicate through <c>ScriptEvaluator.Compile</c>; the <c>RegisterInterest</c> these tests
|
||||
/// wait on is only sent once that finishes. A cold C# script compile is slow and highly variable —
|
||||
/// far more so than anything else in this assembly — and Roslyn's caches are per-process, so the
|
||||
/// first class to compile pays the worst of it. Under a fully parallel assembly run, sharing 14
|
||||
/// cores with 40-odd other Akka test classes, 8 s was occasionally missed (Gitea #500).</para>
|
||||
/// <para>Raising it is free on the happy path: these waits return the instant the message arrives,
|
||||
/// so the value is an upper bound before giving up, not a delay, and it cannot make a genuinely
|
||||
/// failing expectation pass. The <b>absence</b> assertions in this class deliberately keep their own
|
||||
/// short literals — there the elapsed time is the point.</para>
|
||||
/// </remarks>
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>Plan whose predicate compares the single tag "M.T" against 90 — enabled by default.</summary>
|
||||
private static EquipmentScriptedAlarmPlan Plan(
|
||||
|
||||
@@ -79,7 +79,7 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
||||
entry.Message.ShouldContain("syntax error");
|
||||
entry.ScriptId.ShouldBe("script-7");
|
||||
entry.VirtualTagId.ShouldBe("vt-1");
|
||||
}, duration: TimeSpan.FromMilliseconds(500));
|
||||
}, duration: PresenceBudget);
|
||||
|
||||
// 02/S13: the failure now ALSO degrades the node — with no declared dependencyRefs the
|
||||
// inputs-ready gate is vacuously satisfied, so a Bad EvaluationResult reaches the parent (this
|
||||
|
||||
+11
-1
@@ -260,7 +260,15 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
// The old child is stopped (PostStop ⇒ UnregisterInterest) and a new one spawned
|
||||
// (PreStart ⇒ RegisterInterest on "B"). Both messages arrive at the mux probe; order between
|
||||
// the dying child's PostStop and the new child's PreStart is not guaranteed, so accept either.
|
||||
//
|
||||
// Because the order is genuinely undefined, the SENDER of the RegisterInterest must be captured
|
||||
// as that message is received. Reading mux.LastSender after the loop instead would read the
|
||||
// sender of whichever message happened to arrive SECOND — the dying child when the interleaving
|
||||
// is [Register, Unregister] — so the identity assertion below silently depended on the very
|
||||
// ordering this loop exists to tolerate. That made it fail roughly 10% of the time under a
|
||||
// fully parallel assembly run, and never in isolation.
|
||||
DependencyMuxActor.RegisterInterest? reg2 = null;
|
||||
IActorRef? secondChild = null;
|
||||
var sawUnregister = false;
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
@@ -269,6 +277,7 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
{
|
||||
case DependencyMuxActor.RegisterInterest r:
|
||||
reg2 = r;
|
||||
secondChild = mux.LastSender; // sender OF THIS message, not of the last one seen
|
||||
break;
|
||||
case DependencyMuxActor.UnregisterInterest:
|
||||
sawUnregister = true;
|
||||
@@ -282,7 +291,8 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
|
||||
reg2.TagRefs.ShouldNotContain("A");
|
||||
|
||||
// The replacement is a different actor ref than the original (auto-named, so no collision).
|
||||
mux.LastSender.ShouldNotBe(firstChild);
|
||||
secondChild.ShouldNotBeNull();
|
||||
secondChild.ShouldNotBe(firstChild);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user