test(opcuaclient): reconnect + subscription-resume wire test via restartable rig (#622)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect + resume leg of the OpcUaClient driver's wire coverage (Gitea #622): the in-process server is
|
||||||
|
/// stopped and a fresh one is booted on the SAME port / application URI / PKI store — a process restart as the
|
||||||
|
/// driver sees it, with an empty address space the test then re-creates. The driver must detect the loss,
|
||||||
|
/// rebuild a session, and its existing subscription must deliver again with no caller action.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Integration")]
|
||||||
|
public sealed class OpcUaClientDriverReconnectWireTests
|
||||||
|
{
|
||||||
|
private static async Task WaitAsync(Func<bool> condition, string because, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(60));
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
if (condition()) return;
|
||||||
|
await Task.Delay(50);
|
||||||
|
}
|
||||||
|
condition().ShouldBeTrue(because);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task After_a_server_restart_the_driver_reconnects_and_the_subscription_resumes_delivering()
|
||||||
|
{
|
||||||
|
var ct = TestContext.Current.CancellationToken;
|
||||||
|
var rig = await OpcUaClientDriverWireTests.StartRigAsync("drv-reconnect", ct);
|
||||||
|
await using var _rig = rig;
|
||||||
|
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 10.0f);
|
||||||
|
|
||||||
|
await using var drv = OpcUaClientDriverWireTests.NewDriver(
|
||||||
|
rig, "wire-reconnect", reconnectPeriod: TimeSpan.FromSeconds(1));
|
||||||
|
await drv.InitializeAsync("{}", ct);
|
||||||
|
|
||||||
|
var seen = new List<float>();
|
||||||
|
drv.OnDataChange += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.FullReference == OpcUaClientDriverWireTests.SpeedRef && e.Snapshot.StatusCode == 0u)
|
||||||
|
lock (seen) seen.Add(Convert.ToSingle(e.Snapshot.Value));
|
||||||
|
};
|
||||||
|
var states = new List<HostState>();
|
||||||
|
drv.OnHostStatusChanged += (_, e) => { lock (states) states.Add(e.NewState); };
|
||||||
|
|
||||||
|
await drv.SubscribeAsync([OpcUaClientDriverWireTests.SpeedRef], TimeSpan.FromMilliseconds(100), ct);
|
||||||
|
await WaitAsync(() => { lock (seen) return seen.Contains(10.0f); }, "initial value must arrive before the outage");
|
||||||
|
|
||||||
|
// Outage: the loss must be DETECTED (host leaves Running) while the server is genuinely down.
|
||||||
|
await rig.RestartAsync(ct, whileDown: async () =>
|
||||||
|
{
|
||||||
|
await WaitAsync(
|
||||||
|
() => { lock (states) return states.Any(s => s != HostState.Running); },
|
||||||
|
"the driver must notice the server is gone", TimeSpan.FromSeconds(30));
|
||||||
|
});
|
||||||
|
|
||||||
|
// The new server is empty: redeploy the nodes and publish a value no earlier delivery could have carried.
|
||||||
|
rig.EnsureRawVariable("Speed", "Float", writable: true);
|
||||||
|
rig.EnsureRawVariable("Counter", "Int32", writable: false);
|
||||||
|
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 77.0f);
|
||||||
|
|
||||||
|
await WaitAsync(() => drv.GetHostStatuses().Single().State == HostState.Running,
|
||||||
|
"the driver must reconnect after the server is back");
|
||||||
|
await WaitAsync(() => { lock (seen) return seen.Contains(77.0f); },
|
||||||
|
"the pre-outage subscription must resume and deliver the post-restart value");
|
||||||
|
|
||||||
|
// Ongoing delivery, not just a one-off initial value.
|
||||||
|
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 88.0f);
|
||||||
|
await WaitAsync(() => { lock (seen) return seen.Contains(88.0f); },
|
||||||
|
"a later change must also reach the resumed subscription");
|
||||||
|
|
||||||
|
// And the data plane works for plain reads on the new session.
|
||||||
|
var back = await drv.ReadAsync([OpcUaClientDriverWireTests.SpeedRef], ct);
|
||||||
|
back[0].StatusCode.ShouldBe(0u);
|
||||||
|
Convert.ToSingle(back[0].Value).ShouldBe(88.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user