2d20aeb779
#519 — a driver's OPC UA status code now reaches a client with its sub-code intact. DriverInstanceActor projected the driver's uint onto a 3-state Good/Uncertain/Bad enum and OtOpcUaNodeManager re-expanded Bad to a bare 0x80000000, so BadNoCommunication, BadTypeMismatch and BadNotSupported were indistinguishable on the wire and every driver's status constants (incl. the 16 #497 had just corrected) were diagnostic in logs only. Resolved by option (b): the raw uint rides AttributeValuePublished -> AttributeValueUpdate -> IOpcUaAddressSpaceSink.WriteValue, OpcUaQuality is deleted, and severity classification is a helper (OpcUaStatus.IsGood/IsUncertain/IsBad). The surface was wider than the issue costed (41 files, not "the sink and 3 implementations"): OpcUaQuality had non-driver producers with no uint to carry. Each got its natural code — BadInternalError for a failed virtual-tag script, BadNoCommunication for driver comms loss — and ScriptedAlarmHostActor's pair of mutually-inverse uint<->enum mappers is deleted, having round-tripped a code the engine already held. AddressSpaceApplier's removal writes deliberately keep the generic Bad: not losing a code the driver had is this issue; inventing one for the deploy path is a separate call. Behaviour change: WriteAlarmQuality fires its Part 9 event on a status-code change rather than a severity-bucket change. While the annotation was 3-state those were the same predicate; now a move between two Bad reasons changes what a subscriber reads. New SDK-free named constants live on Commons.OpcUa.OpcUaStatus, and StatusCodeParityTests is widened to cover them — that guard is reflection-driven, so it can only see a named constant, never an inline literal. #517 — the symptom is real but the issue named the wrong path and its fix was unsafe. ShutdownAsync cannot race: DriverInstanceActor calls it from PostStop, after the mailbox drains. (Core/Hosting/DriverHost, the one class with a concurrent GetHealth, has no production call site.) InitializeAsync can: it runs on a fire-and-forget Task.Run, and MTConnectDriver writes Healthy then starts loops that can throw into a catch which disposes the client before writing Faulted. Moving the write earlier, as suggested, inverts the terminal state — teardown drains in-flight loops that write health as they unwind, so a late loop could leave Healthy at rest with every connection closed. The fix is a bracket (conservative before, terminal after), applied to MTConnect's init-failure path only; shutdown keeps its single trailing write, and the rule plus the reachability table are documented on IDriver.ShutdownAsync. CalculationDriver.GetHealth returned a hard-coded Healthy that consulted nothing — Healthy before initialize, after shutdown and after dispose. Now derived from _running. Its test, named Health_is_always_Connected, asserted the defect and is replaced. Every new guard was falsified by breaking the production code and watching it go red. The two sub-code tests each assert two different Bad codes, since a single-code assertion could pass against a hard-coded constant under the old projection. Pre-existing failures, verified unchanged on a stashed tree and not chased: 4 in Driver.AbLegacy.IntegrationTests (slc500/micrologix/plc5 fixtures not up) and 3 in Host.IntegrationTests. Closes #517 Closes #519 Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
372 lines
20 KiB
C#
372 lines
20 KiB
C#
using Akka.Actor;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
|
|
|
public sealed class DriverInstanceActorTests : RuntimeActorTestBase
|
|
{
|
|
/// <summary>Verifies that ApplyDelta calls ReinitializeAsync when connected and replies success.</summary>
|
|
[Fact]
|
|
public async Task ApplyDelta_when_Connected_calls_ReinitializeAsync_and_replies_success()
|
|
{
|
|
var driver = new StubDriver();
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
// Drive: Initialize → Connected.
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
// Issue ApplyDelta and capture the reply via Ask.
|
|
var correlation = CorrelationId.NewId();
|
|
var reply = await actor.Ask<DriverInstanceActor.ApplyResult>(
|
|
new DriverInstanceActor.ApplyDelta("{\"changed\":true}", correlation),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.Success.ShouldBeTrue();
|
|
reply.Correlation.ShouldBe(correlation);
|
|
driver.ReinitializeCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Verifies that initialize failure keeps the actor in Reconnecting state.</summary>
|
|
[Fact]
|
|
public void Initialize_failure_keeps_actor_in_Reconnecting_state()
|
|
{
|
|
var driver = new StubDriver { InitializeShouldThrow = true };
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
|
|
// The actor should keep trying — we expect multiple Initialize calls because the
|
|
// reconnect timer fires every 50ms.
|
|
AwaitCondition(() => driver.InitializeCount >= 3, TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
/// <summary>Verifies that writing to a non-IWritable driver returns failure.</summary>
|
|
[Fact]
|
|
public async Task Write_against_non_IWritable_driver_returns_failure()
|
|
{
|
|
var driver = new StubDriver(); // IDriver only, no IWritable.
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
|
|
new DriverInstanceActor.WriteAttribute("tag-1", 42),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.Success.ShouldBeFalse();
|
|
reply.Reason!.ShouldContain("IWritable");
|
|
}
|
|
|
|
/// <summary>Verifies that writing to an IWritable driver returns success when status is Good.</summary>
|
|
[Fact]
|
|
public async Task Write_against_IWritable_returns_success_when_status_is_Good()
|
|
{
|
|
var driver = new WritableStubDriver();
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
|
|
new DriverInstanceActor.WriteAttribute("tag-1", 42),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.Success.ShouldBeTrue();
|
|
driver.Writes.Single().FullReference.ShouldBe("tag-1");
|
|
driver.Writes.Single().Value.ShouldBe(42);
|
|
}
|
|
|
|
/// <summary>Verifies that write propagates status code on Bad result.</summary>
|
|
[Fact]
|
|
public async Task Write_propagates_status_code_on_Bad_result()
|
|
{
|
|
const uint badStatus = 0x80340000; // BadNodeIdUnknown — top severity bits = 10b
|
|
var driver = new WritableStubDriver { NextStatusCode = badStatus };
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
|
|
new DriverInstanceActor.WriteAttribute("tag-1", 42),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.Success.ShouldBeFalse();
|
|
reply.Reason!.ShouldContain("80340000");
|
|
}
|
|
|
|
/// <summary>Verifies that subscribing to an ISubscribable driver forwards OnDataChange to parent.</summary>
|
|
[Fact]
|
|
public async Task Subscribe_against_ISubscribable_forwards_OnDataChange_to_parent()
|
|
{
|
|
var driver = new SubscribableStubDriver();
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(250)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
// Driver fires an OnDataChange — actor should forward it to its parent as
|
|
// AttributeValuePublished carrying the driver's status code.
|
|
driver.FireDataChange("tag-a", value: 3.14, statusCode: 0u);
|
|
|
|
var published = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2));
|
|
published.FullReference.ShouldBe("tag-a");
|
|
published.Value.ShouldBe(3.14);
|
|
published.StatusCode.ShouldBe(OpcUaStatus.Good);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gitea #519 — a driver's status code reaches the parent <b>with its sub-code intact</b>.
|
|
/// <para>This is the first of the two hops that used to destroy it: the actor projected the
|
|
/// driver's <c>uint</c> onto a 3-state Good/Uncertain/Bad enum, and the node manager then
|
|
/// re-expanded Bad to a bare <c>0x80000000</c>. The net effect was that no driver's sub-code ever
|
|
/// reached an OPC UA client — <c>BadNoCommunication</c>, <c>BadTypeMismatch</c> and
|
|
/// <c>BadNotSupported</c> were indistinguishable — while every driver maintained its own named
|
|
/// constants (16 of which #497 had just corrected) as if they mattered end to end.</para>
|
|
/// <para>The severity buckets are asserted too, since the bucket is still what
|
|
/// <see cref="OpcUaStatus.IsGood"/> and friends classify on; what changed is that the bucket is no
|
|
/// longer <em>all</em> that survives.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Subscribe_forwards_the_drivers_status_sub_code_without_collapsing_it()
|
|
{
|
|
var driver = new SubscribableStubDriver();
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-1" }, TimeSpan.FromMilliseconds(100)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
// Uncertain — severity bits 01 (top 2 bits = 01).
|
|
driver.FireDataChange("tag-1", value: 1, statusCode: 0x40000000u);
|
|
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Uncertain);
|
|
|
|
// Bad — severity bits 10.
|
|
driver.FireDataChange("tag-1", value: 2, statusCode: 0x80000000u);
|
|
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(OpcUaStatus.Bad);
|
|
|
|
// The regression itself: a Bad SUB-code must arrive intact, not flattened to OpcUaStatus.Bad.
|
|
// This is the exact code MTConnect maps the Agent's UNAVAILABLE sentinel to, and the live gate
|
|
// that opened #519 observed it reaching a client as 0x80000000.
|
|
driver.FireDataChange("tag-1", value: null, statusCode: OpcUaStatus.BadNoCommunication);
|
|
var noComms = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>();
|
|
noComms.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
|
|
noComms.StatusCode.ShouldNotBe(OpcUaStatus.Bad, "the sub-code was collapsed to a generic Bad");
|
|
OpcUaStatus.IsBad(noComms.StatusCode).ShouldBeTrue("it must still classify as Bad");
|
|
|
|
// A second, DIFFERENT Bad sub-code — the two were indistinguishable before the fix, so asserting
|
|
// one alone would not prove the sub-code is carried rather than hard-coded.
|
|
driver.FireDataChange("tag-1", value: null, statusCode: 0x80740000u /* BadTypeMismatch */);
|
|
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>().StatusCode.ShouldBe(0x80740000u);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the SubscribeBulk pass: SetDesiredSubscriptions retains the ref set and the actor
|
|
/// auto-subscribes when it (re)enters Connected — including a re-subscribe after a reconnect,
|
|
/// closing the F8b/#113 gap that previously left galaxy variables at BadWaitingForInitialData.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SetDesiredSubscriptions_auto_subscribes_on_connect_and_resubscribes_after_reconnect()
|
|
{
|
|
var driver = new SubscribableStubDriver();
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
|
|
|
// Desired set arrives BEFORE connect — retained, not yet applied.
|
|
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
|
|
new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(100)));
|
|
|
|
// Connecting → Connected triggers the auto-subscribe.
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.SubscribeCount >= 1, TimeSpan.FromSeconds(2));
|
|
driver.LastSubscribedRefs.ShouldBe(new[] { "tag-a", "tag-b" });
|
|
|
|
// The auto-subscription is live — a data change reaches the parent.
|
|
driver.FireDataChange("tag-a", value: 7, statusCode: 0u);
|
|
parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2)).Value.ShouldBe(7);
|
|
|
|
// Reconnect → the desired set is re-established without any new host message.
|
|
actor.Tell(new DriverInstanceActor.DisconnectObserved("backend blip"));
|
|
AwaitCondition(() => driver.SubscribeCount >= 2, TimeSpan.FromSeconds(3));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the re-subscribe path (the second Subscribe finds a live handle and first awaits
|
|
/// UnsubscribeAsync) still replies SubscriptionEstablished. Regression for the no-ActorContext
|
|
/// race: reading Sender after `await UnsubscribeAsync().ConfigureAwait(false)` resumed off the
|
|
/// actor context and threw, so the reply never arrived. This drives the exact deploy-re-apply /
|
|
/// bootstrap-restore path where `_subscriptionHandle is not null`.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Subscribe_twice_replies_SubscriptionEstablished_on_resubscribe()
|
|
{
|
|
// UnsubscribeYields makes the inner UnsubscribeAsync genuinely suspend, so the second
|
|
// Subscribe's `await UnsubscribeAsync()` resumes off the actor context if ConfigureAwait(false)
|
|
// is used — the exact condition that throws NotSupportedException on the subsequent Sender read.
|
|
var driver = new SubscribableStubDriver { UnsubscribeYields = true };
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
// First subscribe — establishes the handle.
|
|
await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(250)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
// Second subscribe — `_subscriptionHandle is not null`, so the handler awaits
|
|
// UnsubscribeAsync first, then reads Sender. Must still reply (today it threw → no reply).
|
|
var reply = await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(250)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.ReferenceCount.ShouldBe(2);
|
|
driver.SubscribeCount.ShouldBe(2);
|
|
// Old handler must have been detached before the new one was attached — no leak.
|
|
driver.OnDataChangeSubscriberCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Verifies that subscribing to a non-ISubscribable driver replies with failure.</summary>
|
|
[Fact]
|
|
public async Task Subscribe_against_non_ISubscribable_replies_with_failure()
|
|
{
|
|
var driver = new StubDriver(); // IDriver only
|
|
var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
|
|
var reply = await actor.Ask<DriverInstanceActor.SubscriptionFailed>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-1" }, TimeSpan.FromMilliseconds(100)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
reply.Reason.ShouldContain("ISubscribable");
|
|
}
|
|
|
|
/// <summary>Verifies that DisconnectObserved detaches subscription handler so late events are dropped.</summary>
|
|
[Fact]
|
|
public async Task DisconnectObserved_detaches_subscription_handler_so_late_events_are_dropped()
|
|
{
|
|
var driver = new SubscribableStubDriver();
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
|
|
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
|
|
await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
|
|
new DriverInstanceActor.Subscribe(new[] { "tag-1" }, TimeSpan.FromMilliseconds(100)),
|
|
TimeSpan.FromSeconds(3));
|
|
|
|
actor.Tell(new DriverInstanceActor.DisconnectObserved("backend went away"));
|
|
|
|
// Race window — once disconnect is processed, subsequent FireDataChange calls hit a
|
|
// detached handler and don't push anything to the parent.
|
|
AwaitCondition(() => driver.OnDataChangeSubscriberCount == 0, TimeSpan.FromSeconds(2));
|
|
driver.FireDataChange("tag-1", value: 99, statusCode: 0u);
|
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
|
}
|
|
|
|
/// <summary>A driver stuck Reconnecting (init failing on a bad config) adopts a corrected config
|
|
/// delivered via ApplyDelta and connects on it — no node restart. Closes pending.md #7.</summary>
|
|
[Fact]
|
|
public async Task ApplyDelta_while_Reconnecting_adopts_new_config_and_connects()
|
|
{
|
|
const string bad = "{\"v\":\"bad\"}";
|
|
const string good = "{\"v\":\"good\"}";
|
|
var driver = new SubscribableStubDriver
|
|
{
|
|
InitBehavior = cfg => cfg == good ? Task.CompletedTask : throw new InvalidOperationException("bad-cfg"),
|
|
};
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
|
|
|
|
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(new[] { "tag-a" }, TimeSpan.FromMilliseconds(100)));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested(bad));
|
|
AwaitCondition(() => driver.InitializeCount >= 2, TimeSpan.FromSeconds(2));
|
|
driver.SubscribeCount.ShouldBe(0);
|
|
|
|
var correlation = CorrelationId.NewId();
|
|
var reply = await actor.Ask<DriverInstanceActor.ApplyResult>(
|
|
new DriverInstanceActor.ApplyDelta(good, correlation), TimeSpan.FromSeconds(3));
|
|
reply.Success.ShouldBeTrue();
|
|
reply.Correlation.ShouldBe(correlation);
|
|
|
|
AwaitCondition(() => driver.SubscribeCount >= 1, TimeSpan.FromSeconds(3));
|
|
driver.InitConfigs.ShouldContain(good);
|
|
}
|
|
|
|
/// <summary>A stale InitializeSucceeded from an old (superseded) config cannot hijack the state:
|
|
/// while a gated old-config init is pending in Connecting, an ApplyDelta adopts a new config; the
|
|
/// old init completing afterwards is ignored, and only the new config drives Connected.</summary>
|
|
[Fact]
|
|
public async Task ApplyDelta_supersedes_in_flight_init_so_stale_result_is_ignored()
|
|
{
|
|
const string v1 = "{\"v\":1}";
|
|
const string v2 = "{\"v\":2}";
|
|
var gate1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var gate2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var driver = new SubscribableStubDriver
|
|
{
|
|
InitBehavior = cfg => cfg == v1 ? gate1.Task : gate2.Task,
|
|
};
|
|
var parent = CreateTestProbe();
|
|
// #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
|
|
// disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
|
|
parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
|
|
var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));
|
|
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(new[] { "tag-a" }, TimeSpan.FromMilliseconds(100)));
|
|
|
|
actor.Tell(new DriverInstanceActor.InitializeRequested(v1));
|
|
AwaitCondition(() => driver.InitConfigs.Contains(v1), TimeSpan.FromSeconds(2));
|
|
|
|
await actor.Ask<DriverInstanceActor.ApplyResult>(
|
|
new DriverInstanceActor.ApplyDelta(v2, CorrelationId.NewId()), TimeSpan.FromSeconds(3));
|
|
AwaitCondition(() => driver.InitConfigs.Contains(v2), TimeSpan.FromSeconds(2));
|
|
|
|
gate1.SetResult();
|
|
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
|
|
driver.SubscribeCount.ShouldBe(0);
|
|
|
|
gate2.SetResult();
|
|
AwaitCondition(() => driver.SubscribeCount >= 1, TimeSpan.FromSeconds(3));
|
|
}
|
|
|
|
}
|