fix(runtime): mux adapter forwards the dependency's real status code (05/STAB-19)
This commit is contained in:
@@ -731,3 +731,13 @@ test could only pass once the production change landed.
|
||||
- The identical-value rule is honoured: the driver pushes **5.0 Good** before and after the outage, so the
|
||||
recovery assertion cannot be satisfied by a value change. The control (`InitialPush = null`) asserts
|
||||
`ExpectNoMsg` — no Good ever arrives — and that the audit fires with exactly 1 latched ref.
|
||||
|
||||
### T12 — `DependencyConsumerMuxAdapter` forwards the real status code
|
||||
|
||||
- RED: `--filter "…DependencyConsumerMuxAdapterTests"` ⇒ **2 failed**, both
|
||||
`Shouldly.ShouldAssertException : consumer.Received[…].StatusCode should be 2150694912u` (0x80310000)
|
||||
— the consumer received `0` (Good) for a `BadNoCommunication` input, on both the single and the batch
|
||||
path. That is the finding, reproduced at the seam.
|
||||
- GREEN: 11 passed (`DependencyConsumerMuxAdapterTests` + `DependencyMuxActorTests`).
|
||||
- The false "quality is stripped at the mux boundary" comment is deleted and replaced with why it was
|
||||
false (theme 6: the contract moved at #478/#519 and the comment outlived the fact).
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
{
|
||||
"id": "T12",
|
||||
"subject": "DependencyConsumerMuxAdapter forwards the real StatusCode; delete the false 'quality is stripped' comment (05/STAB-19)",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"blockedBy": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,10 +66,23 @@ public sealed class DependencyConsumerMuxAdapter : ReceiveActor
|
||||
_log.Debug("DependencyConsumerMuxAdapter: registered {Count} dependency ref(s)", refs.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards one dependency value to the consumer, <b>status code included</b> (archreview
|
||||
/// 05/STAB-19).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This used to pass a hard-coded <c>0u</c> behind a comment saying quality was stripped at the
|
||||
/// mux boundary. That was true when the adapter was written and stopped being true at #478/#519 —
|
||||
/// <see cref="VirtualTagActor.DependencyValueChanged"/> has carried the source driver's full status
|
||||
/// code, sub-code and all, ever since. The stale comment outliving the fact is what kept a
|
||||
/// comms-lost input arriving at the Calculation driver as <i>arrived, Good, null</i>:
|
||||
/// indistinguishable from a genuine Good null, so the warm-up gate was satisfied by a value that
|
||||
/// never arrived and the calc output stayed Good over a dead source.
|
||||
/// </remarks>
|
||||
/// <param name="msg">The dependency value, as the mux delivered it.</param>
|
||||
private void OnDependencyValueChanged(VirtualTagActor.DependencyValueChanged msg)
|
||||
{
|
||||
// Quality is stripped at the mux boundary (value + timestamp only), so forward Good (0).
|
||||
_consumer.OnDependencyValue(msg.TagId, msg.Value, statusCode: 0u, msg.TimestampUtc);
|
||||
_consumer.OnDependencyValue(msg.TagId, msg.Value, msg.StatusCode, msg.TimestampUtc);
|
||||
}
|
||||
|
||||
/// <summary>The mux's batched delivery: one message carrying this adapter's whole matched slice of a
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
using Akka.Actor;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.VirtualTags;
|
||||
|
||||
/// <summary>
|
||||
/// Archreview <b>05/STAB-19</b> — the adapter that carries dependency values from the per-node
|
||||
/// dependency mux into an <see cref="IDependencyConsumer"/> (today: the Calculation driver).
|
||||
///
|
||||
/// <para><b>The defect these pin.</b> The adapter forwarded a hard-coded <c>statusCode: 0u</c> behind
|
||||
/// a comment claiming "quality is stripped at the mux boundary". That was true once; since #478/#519
|
||||
/// <see cref="VirtualTagActor.DependencyValueChanged"/> carries the source driver's full status code,
|
||||
/// sub-code included. The consequence was that a comms-lost input reached the Calculation driver as
|
||||
/// <em>arrived, Good, null</em> — indistinguishable from a real Good reading of null, which is how a
|
||||
/// calc output kept publishing Good over a dead input.</para>
|
||||
/// </summary>
|
||||
public sealed class DependencyConsumerMuxAdapterTests : RuntimeActorTestBase
|
||||
{
|
||||
private const string TagA = "Plant/Modbus/dev1/a";
|
||||
private const string TagB = "Plant/Modbus/dev1/b";
|
||||
|
||||
/// <summary>Single delivery: the status code reaches the consumer verbatim, sub-code and all.</summary>
|
||||
[Fact]
|
||||
public void A_single_dependency_value_carries_its_status_code_to_the_consumer()
|
||||
{
|
||||
var (consumer, adapter) = Spawn();
|
||||
|
||||
adapter.Tell(new VirtualTagActor.DependencyValueChanged(
|
||||
TagA, Value: null, DateTime.UtcNow, OpcUaStatus.BadNoCommunication));
|
||||
|
||||
AwaitCondition(() => consumer.Received.Count == 1, PresenceBudget);
|
||||
consumer.Received[0].Ref.ShouldBe(TagA);
|
||||
consumer.Received[0].StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
consumer.Received[0].StatusCode.ShouldNotBe(OpcUaStatus.Good,
|
||||
"a Bad input forwarded as Good is the laundering 05/STAB-19 is about");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batched delivery goes through the same seam, so it must carry status too — the batch path is
|
||||
/// what a real driver burst uses, and a fix applied only to the single-value path would be inert
|
||||
/// in production.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void A_batched_delivery_carries_each_items_status_code()
|
||||
{
|
||||
var (consumer, adapter) = Spawn();
|
||||
var ts = DateTime.UtcNow;
|
||||
|
||||
adapter.Tell(new VirtualTagActor.DependencyValueBatch(new[]
|
||||
{
|
||||
new VirtualTagActor.DependencyValueChanged(TagA, 1.0, ts, OpcUaStatus.Good),
|
||||
new VirtualTagActor.DependencyValueChanged(TagB, null, ts, OpcUaStatus.BadNoCommunication),
|
||||
}));
|
||||
|
||||
AwaitCondition(() => consumer.Received.Count == 2, PresenceBudget);
|
||||
consumer.Received[0].StatusCode.ShouldBe(OpcUaStatus.Good);
|
||||
consumer.Received[1].StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
||||
}
|
||||
|
||||
private (RecordingConsumer Consumer, IActorRef Adapter) Spawn()
|
||||
{
|
||||
var consumer = new RecordingConsumer();
|
||||
var mux = CreateTestProbe();
|
||||
var adapter = Sys.ActorOf(DependencyConsumerMuxAdapter.Props(consumer, mux.Ref));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(PresenceBudget);
|
||||
return (consumer, adapter);
|
||||
}
|
||||
|
||||
private sealed class RecordingConsumer : IDependencyConsumer
|
||||
{
|
||||
private readonly List<(string Ref, object? Value, uint StatusCode)> _received = new();
|
||||
private readonly object _gate = new();
|
||||
|
||||
public IReadOnlyList<(string Ref, object? Value, uint StatusCode)> Received
|
||||
{
|
||||
get { lock (_gate) return _received.ToArray(); }
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> DependencyRefs { get; } = [TagA, TagB];
|
||||
|
||||
public void OnDependencyValue(string fullReference, object? value, uint statusCode, DateTime timestampUtc)
|
||||
{
|
||||
lock (_gate) _received.Add((fullReference, value, statusCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user