test(vtags): thread-safe CapturingHistoryWriter + drop redundant wait (H5c review follow-up)

This commit is contained in:
Joseph Doherty
2026-06-15 10:33:14 -04:00
parent 0c6d4c5491
commit 2f30c54dc1
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Akka.Actor;
using Shouldly;
using Xunit;
@@ -191,7 +192,8 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
// Wait for the history record to land (delivered on the same actor turn as the publish).
AwaitAssert(() => writer.Calls.Count.ShouldBe(1));
var (path, value) = writer.Calls[0];
writer.Calls.TryPeek(out var captured).ShouldBeTrue();
var (path, value) = captured;
path.ShouldBe("eq-1/speed-rpm");
value.Value.ShouldBe(42.0);
value.StatusCode.ShouldBe(0u); // OPC UA Good
@@ -213,11 +215,10 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
host.Tell(new VirtualTagActor.EvaluationResult("vt-1", 42.0, DateTime.UtcNow, CorrelationId.NewId()));
// The value is still published
// The value is still published — and the publish completing means OnResult's turn is done,
// so the historian (which would have been called on the same turn) was never touched.
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>();
// …but the historian was never touched.
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
writer.Calls.ShouldBeEmpty();
writer.Calls.IsEmpty.ShouldBeTrue();
}
/// <summary>A plan with an explicit Expression + DependencyRefs (the H1b in-place-change case).</summary>
@@ -348,12 +349,14 @@ public sealed class VirtualTagHostActorTests : RuntimeActorTestBase
/// assert the host historizes (or does not) and with what path + snapshot.</summary>
private sealed class CapturingHistoryWriter : IHistoryWriter
{
public readonly List<(string Path, DataValueSnapshot Value)> Calls = new();
// ConcurrentQueue: Record runs on the actor thread, the test asserts on the test thread —
// a plain List would be a cross-thread data race under load.
public readonly ConcurrentQueue<(string Path, DataValueSnapshot Value)> Calls = new();
/// <summary>Captures the path + snapshot of a Record call.</summary>
/// <param name="path">The virtual tag path.</param>
/// <param name="value">The data value snapshot.</param>
public void Record(string path, DataValueSnapshot value) => Calls.Add((path, value));
public void Record(string path, DataValueSnapshot value) => Calls.Enqueue((path, value));
}
/// <summary>Deterministic no-op evaluator: keeps spawned children inert so tests drive the host's