fix(review): calc ReadOne reports an inputs-Bad publish; VT inputs-bad counter ticks only on a real report
This commit is contained in:
+74
@@ -1,8 +1,10 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using Akka.Actor;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
@@ -149,6 +151,35 @@ public sealed class VirtualTagActorInputQualityTests : RuntimeActorTestBase
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>inputs-bad</c> outcome counter must tick only when a tag actually <b>reports</b> Bad
|
||||
/// inputs — not on every warm-up trigger, where a declared dependency simply has not arrived yet.
|
||||
/// Metering above the readiness gate turns ordinary cold start into a stream of "inputs bad"
|
||||
/// measurements, which is exactly the noise that makes an operator stop trusting the counter.
|
||||
/// <para>The absence half is paired with a positive control in the same test — the counter DOES
|
||||
/// tick once the tag reports — so "no measurements" can never pass for want of a working listener.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void The_inputs_bad_counter_does_not_tick_during_warm_up()
|
||||
{
|
||||
var parent = CreateTestProbe();
|
||||
var actor = parent.ChildActorOf(VirtualTagActor.Props(
|
||||
"vt-1", "a + b", evaluator: new CountingSumEvaluator(), dependencyRefs: ["a", "b"]));
|
||||
using var meter = new OutcomeRecorder();
|
||||
|
||||
// Warm-up: "b" has never arrived, so this is cold start, not a fault.
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("a", null, DateTime.UtcNow, OpcUaStatus.BadNoCommunication));
|
||||
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
|
||||
|
||||
meter.Count("inputs-bad").ShouldBe(0, "warm-up is not an inputs-bad report");
|
||||
|
||||
// Positive control: once every dep has arrived, the tag reports — and the counter ticks.
|
||||
actor.Tell(new VirtualTagActor.DependencyValueChanged("b", 32, DateTime.UtcNow));
|
||||
parent.ExpectMsg<VirtualTagActor.EvaluationResult>(PresenceBudget)
|
||||
.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
AwaitCondition(() => meter.Count("inputs-bad") == 1, TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A deploy's <c>ReassertValue</c> re-emits the LAST PUBLISHED status, so a VT whose input is
|
||||
/// comms-lost re-asserts <c>BadNoCommunication</c> onto the freshly-materialised node rather than
|
||||
@@ -207,6 +238,49 @@ public sealed class VirtualTagActorInputQualityTests : RuntimeActorTestBase
|
||||
evaluator.Calls.ShouldBe(callsBefore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts <c>otopcua.virtualtag.eval</c> measurements by their <c>outcome</c> tag.
|
||||
/// <para>A <see cref="MeterListener"/> sees every measurement in the process, and this counter is
|
||||
/// hot across the assembly — so the assertions above are scoped to the <c>inputs-bad</c> outcome,
|
||||
/// which only this suite produces (nothing else drives a Bad dependency), and the absence
|
||||
/// assertion is paired with a positive control rather than standing alone.</para>
|
||||
/// </summary>
|
||||
private sealed class OutcomeRecorder : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly List<string> _outcomes = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public OutcomeRecorder()
|
||||
{
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == OtOpcUaTelemetry.MeterName
|
||||
&& instrument.Name == "otopcua.virtualtag.eval")
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
},
|
||||
};
|
||||
_listener.SetMeasurementEventCallback<long>((_, _, tags, _) =>
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (tag.Key != "outcome" || tag.Value is not string outcome) continue;
|
||||
lock (_gate) _outcomes.Add(outcome);
|
||||
}
|
||||
});
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
public int Count(string outcome)
|
||||
{
|
||||
lock (_gate) return _outcomes.Count(o => o == outcome);
|
||||
}
|
||||
|
||||
public void Dispose() => _listener.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sums the dependency values, counts its invocations, and records the status code the actor
|
||||
/// handed it for "a" — so a test can assert both that the script did NOT run and that, when it
|
||||
|
||||
Reference in New Issue
Block a user