119 lines
5.3 KiB
C#
119 lines
5.3 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// End-to-end smoke for the model-change watch (PR-10). Boots a real session against
|
|
/// opc-plc, asserts the driver wires up the model-change subscription without
|
|
/// destabilising the rest of the capability surface, and asserts a synthetic event
|
|
/// injection still runs the debounced re-import path under live conditions.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// opc-plc doesn't currently expose a stable HTTP control endpoint for forcing a
|
|
/// <c>GeneralModelChangeEventType</c> from outside the simulator. The native
|
|
/// <c>OpcPlc.AddSlowNode</c> method is invocable via OPC UA <c>Call</c> and does
|
|
/// trigger the event, but it requires elevated permissions on the simulator's
|
|
/// security model that the default <c>--aa</c> deployment doesn't grant. So this
|
|
/// smoke uses the driver's <c>InjectModelChangeForTest</c> seam — the same code
|
|
/// path a real upstream notification takes — and asserts the debounced re-import
|
|
/// ran end-to-end against the live session.
|
|
/// </remarks>
|
|
[Collection(OpcPlcCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "opc-plc")]
|
|
public sealed class OpcUaClientModelChangeSmokeTests(OpcPlcFixture sim)
|
|
{
|
|
[Fact]
|
|
public async Task Driver_initializes_with_model_change_watch_enabled_against_live_simulator()
|
|
{
|
|
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
|
|
|
// Default options have WatchModelChanges=true; a successful Initialize against
|
|
// opc-plc proves the EventFilter + WhereClause + monitored-item create path is
|
|
// accepted by an independent OPC UA stack.
|
|
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
|
|
await using var drv = new OpcUaClientDriver(options, "opcua-modelchange-init");
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
|
|
// Driver should be Healthy after init even though we created an extra
|
|
// subscription on top of any pre-existing ones.
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
|
|
// Reads still work — i.e. the model-change subscription didn't starve the
|
|
// session of publish slots or otherwise destabilise the data path.
|
|
var snaps = await drv.ReadAsync([OpcPlcProfile.StepUp], TestContext.Current.CancellationToken);
|
|
snaps.Count.ShouldBe(1);
|
|
snaps[0].StatusCode.ShouldBe(0u);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_reimports_on_model_change_event()
|
|
{
|
|
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
|
|
|
var debounce = TimeSpan.FromMilliseconds(500);
|
|
var baseOpts = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
|
|
var options = new OpcUaClientDriverOptions
|
|
{
|
|
EndpointUrl = baseOpts.EndpointUrl,
|
|
SecurityPolicy = baseOpts.SecurityPolicy,
|
|
SecurityMode = baseOpts.SecurityMode,
|
|
AuthType = baseOpts.AuthType,
|
|
AutoAcceptCertificates = baseOpts.AutoAcceptCertificates,
|
|
Timeout = baseOpts.Timeout,
|
|
SessionTimeout = baseOpts.SessionTimeout,
|
|
ModelChangeDebounce = debounce,
|
|
};
|
|
|
|
await using var drv = new OpcUaClientDriver(options, "opcua-modelchange-reimport");
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
|
|
// Use the test seam so we don't depend on opc-plc's HTTP control endpoint;
|
|
// the production wiring takes the same OnModelChangeNotification path.
|
|
var fires = 0;
|
|
drv.ModelChangeReimportHookForTest = _ =>
|
|
{
|
|
Interlocked.Increment(ref fires);
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
// Burst of 5 events within the debounce window → exactly one re-import.
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
drv.InjectModelChangeForTest();
|
|
await Task.Delay(50, TestContext.Current.CancellationToken);
|
|
}
|
|
await Task.Delay(debounce + TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
|
|
|
|
fires.ShouldBe(1, "burst within debounce window must coalesce to one re-import");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_initializes_with_model_change_watch_disabled()
|
|
{
|
|
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
|
|
|
// Operators who don't want the brief browse-gap on topology change can flip
|
|
// WatchModelChanges off — Initialize must still succeed end-to-end.
|
|
var baseOpts = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
|
|
var options = new OpcUaClientDriverOptions
|
|
{
|
|
EndpointUrl = baseOpts.EndpointUrl,
|
|
SecurityPolicy = baseOpts.SecurityPolicy,
|
|
SecurityMode = baseOpts.SecurityMode,
|
|
AuthType = baseOpts.AuthType,
|
|
AutoAcceptCertificates = baseOpts.AutoAcceptCertificates,
|
|
Timeout = baseOpts.Timeout,
|
|
SessionTimeout = baseOpts.SessionTimeout,
|
|
WatchModelChanges = false,
|
|
};
|
|
|
|
await using var drv = new OpcUaClientDriver(options, "opcua-modelchange-disabled");
|
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
|
|
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
|
}
|
|
}
|