feat(otopcua): DriverInstanceActor.TriggerRediscovery message (follow-up C)

This commit is contained in:
Joseph Doherty
2026-06-26 12:45:26 -04:00
parent a1a655e6c9
commit f7358bf4fd
2 changed files with 109 additions and 0 deletions
@@ -292,6 +292,93 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
}
/// <summary>
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> received while Connected re-kicks the
/// post-connect discovery loop: after the initial discovery has settled, sending the message drives a
/// FRESH discovery pass — the driver's <c>DiscoverCount</c> advances and a new
/// <see cref="DriverInstanceActor.DiscoveredNodesReady"/> is published. This is the message
/// <see cref="DriverHostActor"/> uses to re-run discovery after rebinding the driver to a new equipment.
/// </summary>
[Fact]
public void TriggerRediscovery_when_Connected_reruns_discovery()
{
var driver = new DiscoverableStubDriver();
var parent = CreateTestProbe();
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
driver, rediscoverInterval: TimeSpan.FromMilliseconds(20)));
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
// Let the initial post-connect loop settle (passes 0,0,3,3) and confirm it stopped.
for (var i = 0; i < 4; i++)
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
var passesBeforeTrigger = driver.DiscoverCount; // 4
// Re-kick discovery via the new message — the cache is warm, so the fresh pass sees the 3-node set.
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
var afterTrigger = parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
afterTrigger.Nodes.Count.ShouldBe(3);
afterTrigger.DriverInstanceId.ShouldBe(driver.DriverInstanceId);
// A fresh pass genuinely ran — DiscoverCount advanced past the settled count.
driver.DiscoverCount.ShouldBeGreaterThan(passesBeforeTrigger);
}
/// <summary>
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> on a driver whose
/// <see cref="ITagDiscovery.RediscoverPolicy"/> is <see cref="DiscoveryRediscoverPolicy.Never"/> does
/// NOT re-discover: the handler calls <c>StartDiscovery</c>, which returns early for <c>Never</c>, so
/// no pass runs and nothing is published — mirroring the Connected-entry Never opt-out.
/// </summary>
[Fact]
public void TriggerRediscovery_with_policy_Never_does_not_rediscover()
{
var driver = new DiscoverableStubDriver(DiscoveryRediscoverPolicy.Never);
var parent = CreateTestProbe();
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
driver, rediscoverInterval: TimeSpan.FromMilliseconds(20)));
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
// Connected, but policy=Never — the trigger is honoured by StartDiscovery's early return.
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
driver.DiscoverCount.ShouldBe(0);
}
/// <summary>
/// <see cref="DriverInstanceActor.TriggerRediscovery"/> received while NOT Connected (still Connecting,
/// before init completes) is a clean silent no-op: no discovery pass runs, nothing is published, and
/// the actor neither crashes nor dies (the driver's eventual reconnect re-discovers anyway). A
/// follow-up connect then discovers normally, proving the actor is unharmed.
/// </summary>
[Fact]
public void TriggerRediscovery_when_not_Connected_is_a_silent_noop()
{
var driver = new DiscoverableStubDriver();
var parent = CreateTestProbe();
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
driver, rediscoverInterval: TimeSpan.FromMilliseconds(20)));
Watch(actor);
// The actor boots into Connecting; send the trigger BEFORE InitializeRequested so it is handled
// in a non-Connected state.
actor.Tell(new DriverInstanceActor.TriggerRediscovery());
// No discovery resulted, and the actor is unharmed (no Terminated arrives at the watching test actor).
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
ExpectNoMsg(TimeSpan.FromMilliseconds(100));
driver.DiscoverCount.ShouldBe(0);
// Sanity: the actor still works — driving it to Connected discovers normally afterwards.
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
parent.ExpectMsg<DriverInstanceActor.DiscoveredNodesReady>(TimeSpan.FromSeconds(2));
}
/// <summary>
/// A <see cref="StubDriver"/> that also exposes <see cref="ITagDiscovery"/>. Each <c>DiscoverAsync</c>
/// pass is counted; passes 12 yield nothing (cache warming), passes 3+ yield a stable 3-node set —