feat(siteruntime): event-driven Attributes.WaitAsync attribute-change helper
Adds InstanceActor one-shot waiter registry (fast-path + change-match + scheduled timeout self-eviction), threads per-script timeout token through ScriptRuntimeContext, and exposes Attributes.WaitAsync(value|predicate, timeout). Replaces handshake busy-poll. Implements spec docs/plans/2026-06-17-waitfor-attribute-change-helper-spec.md §3-§5; §6 routed variant + WaitForAsync + quality-only mode deferred.
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
@@ -137,3 +142,81 @@ public class ScopeAccessorTests
|
||||
Assert.Equal("[1,2,3]", encoded);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WaitAsync (spec §3-§5, acceptance §7.6) scope-resolution tests. Unlike the
|
||||
/// path-arithmetic tests above, these route a real <see cref="ScriptRuntimeContext"/>
|
||||
/// against a TestProbe standing in for the Instance Actor, so they need a live
|
||||
/// ActorSystem — hence a TestKit-derived class. They assert that
|
||||
/// <c>Attributes.WaitAsync</c> applies <see cref="AttributeAccessor.Resolve"/>
|
||||
/// (the composition prefix) to the key BEFORE the request is sent to the actor —
|
||||
/// the same contract Get/Set obey.
|
||||
/// </summary>
|
||||
public class AttributeAccessorWaitAsyncTests : TestKit, IDisposable
|
||||
{
|
||||
private ScriptRuntimeContext MakeContext(IActorRef instanceActor) =>
|
||||
new(
|
||||
instanceActor,
|
||||
instanceActor,
|
||||
sharedScriptLibrary: null!,
|
||||
currentCallDepth: 0,
|
||||
maxCallDepth: 10,
|
||||
askTimeout: TimeSpan.FromSeconds(2),
|
||||
instanceName: "Pump1",
|
||||
logger: NullLogger<ScriptRuntimeContext>.Instance);
|
||||
|
||||
void IDisposable.Dispose() => Shutdown();
|
||||
|
||||
[Fact]
|
||||
public void WaitAsync_Value_AppliesScopeResolution_BeforeSendingRequest()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var ctx = MakeContext(probe.Ref);
|
||||
|
||||
// Composed scope "TempSensor" — Resolve("Flag") => "TempSensor.Flag".
|
||||
var acc = new AttributeAccessor(ctx, "TempSensor");
|
||||
|
||||
// Fire-and-forget; the assertion is on the message the actor receives.
|
||||
_ = acc.WaitAsync("Flag", true, TimeSpan.FromSeconds(30));
|
||||
|
||||
var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("TempSensor.Flag", req.AttributeName);
|
||||
// The value overload encodes the target via AttributeValueCodec.Encode and
|
||||
// sends a null predicate. bool true encodes to "True" (capital T).
|
||||
Assert.Equal(AttributeValueCodec.Encode(true), req.TargetValueEncoded);
|
||||
Assert.Equal("True", req.TargetValueEncoded);
|
||||
Assert.Null(req.Predicate);
|
||||
Assert.Equal("Pump1", req.InstanceName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitAsync_Predicate_AppliesScopeResolution_AndSendsPredicate()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var ctx = MakeContext(probe.Ref);
|
||||
|
||||
var acc = new AttributeAccessor(ctx, "Motor.TempSensor");
|
||||
|
||||
Func<object?, bool> predicate = _ => true;
|
||||
_ = acc.WaitAsync("Level", predicate, TimeSpan.FromSeconds(30));
|
||||
|
||||
var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("Motor.TempSensor.Level", req.AttributeName);
|
||||
// The predicate overload sends the delegate and a null encoded target.
|
||||
Assert.Null(req.TargetValueEncoded);
|
||||
Assert.NotNull(req.Predicate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaitAsync_RootScope_LeavesKeyBare()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var ctx = MakeContext(probe.Ref);
|
||||
|
||||
var acc = new AttributeAccessor(ctx, "");
|
||||
_ = acc.WaitAsync("Flag", true, TimeSpan.FromSeconds(30));
|
||||
|
||||
var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("Flag", req.AttributeName);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user