test(runtime): pin VT Bad-quality transition semantics — once-per-transition, same-value recovery, throw path (02/S13)
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
{
|
{
|
||||||
"id": "R2-03-T4",
|
"id": "R2-03-T4",
|
||||||
"subject": "S13 transition-semantics unit guards: second consecutive failure publishes nothing; same-value recovery republishes Good; evaluator-threw path also degrades",
|
"subject": "S13 transition-semantics unit guards: second consecutive failure publishes nothing; same-value recovery republishes Good; evaluator-threw path also degrades",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"R2-03-T3"
|
"R2-03-T3"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -87,6 +87,96 @@ public sealed class VirtualTagActorTests : RuntimeActorTestBase
|
|||||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>02/S13 once-per-transition: two consecutive failures publish exactly ONE Bad
|
||||||
|
/// EvaluationResult — the second failure is suppressed while already Bad.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Second_consecutive_failure_does_not_publish_a_second_Bad()
|
||||||
|
{
|
||||||
|
var parent = CreateTestProbe();
|
||||||
|
var evaluator = new QueuedEvaluator(
|
||||||
|
VirtualTagEvalResult.Failure("boom1"), VirtualTagEvalResult.Failure("boom2"));
|
||||||
|
var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||||
|
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
|
||||||
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>02/S13 same-value recovery: Ok(42) → fail → Ok(42) must republish Good with 42 even
|
||||||
|
/// though the value equals the pre-failure value (the dedup bypass — else the node stays Bad).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Recovery_to_the_same_value_republishes_Good_after_Bad()
|
||||||
|
{
|
||||||
|
var parent = CreateTestProbe();
|
||||||
|
var evaluator = new QueuedEvaluator(
|
||||||
|
VirtualTagEvalResult.Ok(42), VirtualTagEvalResult.Failure("boom"), VirtualTagEvalResult.Ok(42));
|
||||||
|
var actor = parent.ChildActorOf(VirtualTagActor.Props("vt-1", "expr", evaluator: evaluator));
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||||
|
var first = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
|
||||||
|
first.Quality.ShouldBe(OpcUaQuality.Good);
|
||||||
|
first.Value.ShouldBe(42);
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 2, DateTime.UtcNow));
|
||||||
|
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 3, DateTime.UtcNow));
|
||||||
|
var recovered = parent.ExpectMsg<VirtualTagActor.EvaluationResult>();
|
||||||
|
recovered.Quality.ShouldBe(OpcUaQuality.Good);
|
||||||
|
recovered.Value.ShouldBe(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>02/S13: an evaluator that THROWS (not just returns Failure) also degrades quality —
|
||||||
|
/// the try/catch path publishes a Bad EvaluationResult too.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Evaluator_throw_also_degrades_quality()
|
||||||
|
{
|
||||||
|
var parent = CreateTestProbe();
|
||||||
|
var actor = parent.ChildActorOf(VirtualTagActor.Props(
|
||||||
|
"vt-1", "expr", evaluator: new ThrowingEvaluator()));
|
||||||
|
|
||||||
|
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", 1, DateTime.UtcNow));
|
||||||
|
|
||||||
|
parent.ExpectMsg<VirtualTagActor.EvaluationResult>().Quality.ShouldBe(OpcUaQuality.Bad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Test evaluator returning a fixed sequence of results, one per Evaluate call; the last
|
||||||
|
/// result repeats once the queue is drained.</summary>
|
||||||
|
private sealed class QueuedEvaluator : IVirtualTagEvaluator
|
||||||
|
{
|
||||||
|
private readonly Queue<VirtualTagEvalResult> _results;
|
||||||
|
private VirtualTagEvalResult _last;
|
||||||
|
/// <summary>Initializes the evaluator with an ordered sequence of results.</summary>
|
||||||
|
/// <param name="results">The results returned in order, one per call.</param>
|
||||||
|
public QueuedEvaluator(params VirtualTagEvalResult[] results)
|
||||||
|
{
|
||||||
|
_results = new Queue<VirtualTagEvalResult>(results);
|
||||||
|
_last = results[^1];
|
||||||
|
}
|
||||||
|
/// <summary>Returns the next queued result (or the last one once drained).</summary>
|
||||||
|
/// <param name="id">The tag identifier.</param>
|
||||||
|
/// <param name="expr">The expression string.</param>
|
||||||
|
/// <param name="deps">The dependency values.</param>
|
||||||
|
/// <returns>The next queued result.</returns>
|
||||||
|
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
|
||||||
|
=> _results.Count > 0 ? _last = _results.Dequeue() : _last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Test evaluator that throws on every call — exercises the actor's try/catch degradation
|
||||||
|
/// path (distinct from a returned Failure).</summary>
|
||||||
|
private sealed class ThrowingEvaluator : IVirtualTagEvaluator
|
||||||
|
{
|
||||||
|
/// <summary>Always throws.</summary>
|
||||||
|
/// <param name="id">The tag identifier.</param>
|
||||||
|
/// <param name="expr">The expression string.</param>
|
||||||
|
/// <param name="deps">The dependency values.</param>
|
||||||
|
/// <returns>Never returns; throws.</returns>
|
||||||
|
public VirtualTagEvalResult Evaluate(string id, string expr, IReadOnlyDictionary<string, object?> deps)
|
||||||
|
=> throw new InvalidOperationException("kaboom");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Test evaluator that sums integer dependency values.</summary>
|
/// <summary>Test evaluator that sums integer dependency values.</summary>
|
||||||
private sealed class SumEvaluator : IVirtualTagEvaluator
|
private sealed class SumEvaluator : IVirtualTagEvaluator
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user