feat(driver-calc): B2-WP7 Calculation driver — IDependencyConsumer + triggers + deploy cycle gate
Adds the Calculation pseudo-driver (tags computed by C# scripts over other tags' live values) plus the host seam that feeds it: - IDependencyConsumer capability interface (Core.Abstractions) + DriverTypeNames.Calculation. - CalculationDriver : IDriver, ISubscribable, IReadable, IDependencyConsumer — change-gate + dedupe (VirtualTagActor parity), timer-group scheduler, Bad-transition + script-log emission, Good recovery; reuses the T0-4 CalculationEvaluator. - DriverHostActor spawns a DependencyConsumerMuxAdapter per dependency-consuming driver, registering its refs on the per-node DependencyMuxActor and forwarding DependencyValueChanged → OnDependencyValue; re-registers on every apply, torn down with the driver. - Factory + probe registered in DriverFactoryBootstrap (keeps the T0-3 guard green); guard test csproj references the driver so the bin-scan discovers the factory. - DeploymentArtifact injects the resolved scriptSource (scriptId → SourceCode) into a calc tag's delivered TagConfig so the host-blind driver can compile it. - DraftValidator deploy gates: CalculationScriptMissing / CalculationScriptNotFound (scriptId existence) + CalculationDependencyCycle (DependencyGraph.DetectCycles over calc→calc edges). - Tests: driver units (dep-ref extraction, change-gate, dedupe, Bad+script-log+recovery, timer, read), tag-config parsing, DraftValidator gates (self/2-cycle/cross-driver-terminal), and a Runtime integration test proving values FLOW mux → adapter → driver → calc-of-calc end-to-end. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
using System.Collections.Concurrent;
|
||||
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.Driver.Calculation;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// WP7 register-AND-consume proof: dependency values actually FLOW end-to-end through the REAL
|
||||
/// <see cref="DependencyMuxActor"/> → <see cref="DependencyConsumerMuxAdapter"/> →
|
||||
/// <see cref="CalculationDriver"/> path (not merely that <see cref="IDependencyConsumer"/> exists).
|
||||
/// A source value published to the mux is fed into the calc driver, which computes and publishes a
|
||||
/// value; and a calc tag reading ANOTHER calc tag's output (calc-of-calc) recomputes when the first
|
||||
/// tag's output re-enters the mux — the exact re-entry the host's <c>ForwardToMux</c> performs, mirrored
|
||||
/// here by looping the driver's <see cref="ISubscribable.OnDataChange"/> back into the mux.
|
||||
/// </summary>
|
||||
public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
|
||||
{
|
||||
private static readonly DateTime Ts = new(2026, 7, 16, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static RawTagEntry CalcTag(string rawPath, string source)
|
||||
{
|
||||
var json = $"{{\"scriptId\":\"s-{rawPath}\",\"changeTriggered\":true," +
|
||||
$"\"scriptSource\":{System.Text.Json.JsonSerializer.Serialize(source)}}}";
|
||||
return new RawTagEntry(rawPath, json, WriteIdempotent: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Source_value_flows_through_mux_and_adapter_into_the_calc_driver_and_propagates_calc_of_calc()
|
||||
{
|
||||
// A: out = src/a * 2. B: b = A.out + 1 (calc-of-calc; B depends on A's RawPath).
|
||||
var driver = new CalculationDriver(
|
||||
new CalculationDriverOptions
|
||||
{
|
||||
RawTags =
|
||||
[
|
||||
CalcTag("calc/engine/out", "return (double)ctx.GetTag(\"src/a\").Value * 2.0;"),
|
||||
CalcTag("calc/engine/b", "return (double)ctx.GetTag(\"calc/engine/out\").Value + 1.0;"),
|
||||
],
|
||||
},
|
||||
"calc-drv");
|
||||
await driver.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
// The driver declares interest in both the external source AND the first calc tag's output.
|
||||
driver.DependencyRefs.OrderBy(r => r).ShouldBe(new[] { "calc/engine/out", "src/a" });
|
||||
|
||||
var published = new ConcurrentQueue<DataChangeEventArgs>();
|
||||
var mux = Sys.ActorOf(DependencyMuxActor.Props(), "dep-mux");
|
||||
|
||||
// Loop the driver's outputs back into the mux exactly as DriverHostActor.ForwardToMux does — this
|
||||
// is what makes calc-of-calc chains work.
|
||||
driver.OnDataChange += (_, e) =>
|
||||
{
|
||||
published.Enqueue(e);
|
||||
var quality = e.Snapshot.StatusCode == 0u ? OpcUaQuality.Good : OpcUaQuality.Bad;
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"calc-drv", e.FullReference, e.Snapshot.Value, quality, Ts));
|
||||
};
|
||||
|
||||
// The REAL adapter registers the driver's dependency refs on the mux + forwards changes into it.
|
||||
Sys.ActorOf(DependencyConsumerMuxAdapter.Props(driver, mux), "dep-adapter");
|
||||
|
||||
// Publish the external source into the mux. AwaitAssert re-Tells (idempotent after the first
|
||||
// compute dedups) until the computed value has flowed all the way through.
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"src-drv", "src/a", 21.0, OpcUaQuality.Good, Ts));
|
||||
|
||||
// Leg 1 — dep flow: src/a=21 → A computes 42 and publishes on calc/engine/out.
|
||||
published.ShouldContain(e => e.FullReference == "calc/engine/out" && Equals(e.Snapshot.Value, 42.0));
|
||||
// Leg 2 — calc-of-calc: A's 42 re-entered the mux → B computes 43 on calc/engine/b.
|
||||
published.ShouldContain(e => e.FullReference == "calc/engine/b" && Equals(e.Snapshot.Value, 43.0));
|
||||
}, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Configuration\ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Commons\ZB.MOM.WW.OtOpcUa.Commons.csproj"/>
|
||||
<!-- WP7: the real Calculation driver, so the dependency-flow integration test drives values through
|
||||
the real DependencyMuxActor → DependencyConsumerMuxAdapter → CalculationDriver path. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Calculation\ZB.MOM.WW.OtOpcUa.Driver.Calculation.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user