f51055b509
Against opc-plc (opc.tcp://10.100.0.35:50000): a shutdown with three live subscriptions is bounded and the same instance re-initialises and reads (O-9 + lifecycle generation through the real SDK); a server-refused write (BadTypeMismatch) passes through and keeps the driver Healthy with its stamp advanced (health contract); the browse side-panel reports a live variable's type, shape and access (O-5). Against the MTConnect fixture Agent: the headers-only ping reaches it and fails on a dead port (T-7); a delivering /sample stream serves reads and reports its lag and effective count on the diagnostics bag (T-4/T-5). Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
100 lines
4.4 KiB
C#
100 lines
4.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Live-server tests against the opc-plc Docker fixture. Bring up with
|
|
/// `lmxopcua-fix up opcuaclient` from PowerShell before running. These tests
|
|
/// drive the internal <c>OpcUaClientBrowseSession</c> through its public factory
|
|
/// <see cref="OpcUaClientDriverBrowser"/> so no InternalsVisibleTo is needed.
|
|
/// Each test skips (<see cref="OpcPlcProbe"/>) when the fixture is unreachable. Moved here from the
|
|
/// unit project, which must hold no live-server tests (#594).
|
|
/// </summary>
|
|
[Trait("Category", "Integration"), Trait("Fixture", "opc-plc")]
|
|
public sealed class OpcUaClientBrowseSessionTests
|
|
{
|
|
private static string Endpoint => OpcPlcProbe.Endpoint;
|
|
|
|
// SecurityPolicy.None, SecurityMode.None, OpcUaAuthType.Anonymous are all
|
|
// index 0 and can also be written as string names now that the browser carries
|
|
// JsonStringEnumConverter (Driver.OpcUaClient.Browser-003). Numeric ordinals are
|
|
// kept here as they were before the fix — both forms are accepted.
|
|
private static string ConfigJson => $$"""
|
|
{
|
|
"EndpointUrl":"{{Endpoint}}",
|
|
"SecurityPolicy":0,
|
|
"SecurityMode":0,
|
|
"AuthType":0,
|
|
"SessionTimeout":"00:01:00",
|
|
"Timeout":"00:00:10",
|
|
"PerEndpointConnectTimeout":"00:00:10"
|
|
}
|
|
""";
|
|
|
|
/// <summary>RootAsync should surface at least one node under ObjectsFolder
|
|
/// (opc-plc exposes a non-empty top level).</summary>
|
|
[Fact]
|
|
public async Task RootAsync_returns_at_least_one_node()
|
|
{
|
|
OpcPlcProbe.SkipUnlessReachable();
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var browser = new OpcUaClientDriverBrowser();
|
|
await using var session = await browser.OpenAsync(ConfigJson, ct);
|
|
var roots = await session.RootAsync(ct);
|
|
roots.Count.ShouldBeGreaterThan(0);
|
|
}
|
|
|
|
/// <summary>ExpandAsync should accept a stable NodeId returned by RootAsync
|
|
/// and round-trip through the live namespace table.</summary>
|
|
[Fact]
|
|
public async Task ExpandAsync_round_trips_stable_NodeId()
|
|
{
|
|
OpcPlcProbe.SkipUnlessReachable();
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var browser = new OpcUaClientDriverBrowser();
|
|
await using var session = await browser.OpenAsync(ConfigJson, ct);
|
|
var roots = await session.RootAsync(ct);
|
|
var folder = roots.FirstOrDefault(n => n.Kind == BrowseNodeKind.Folder);
|
|
folder.ShouldNotBeNull("expected at least one Folder under ObjectsFolder");
|
|
var children = await session.ExpandAsync(folder!.NodeId, ct);
|
|
children.ShouldNotBeNull();
|
|
}
|
|
|
|
/// <summary>Review 2026-10-21 O-5: an Object (folder) has no value attributes, so AttributesAsync is empty for
|
|
/// it. (It used to be empty for EVERYTHING — the side-panel was a stub; see the variable test below.)</summary>
|
|
[Fact]
|
|
public async Task AttributesAsync_is_empty_for_an_object_node()
|
|
{
|
|
OpcPlcProbe.SkipUnlessReachable();
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var browser = new OpcUaClientDriverBrowser();
|
|
await using var session = await browser.OpenAsync(ConfigJson, ct);
|
|
var attrs = await session.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=85", ct);
|
|
attrs.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Review 2026-10-21 O-5 against the live server: a standard variable (Server/ServerStatus/CurrentTime — a
|
|
/// read-only scalar UtcTime every server exposes) reports its mapped type, shape and access.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AttributesAsync_reports_a_live_variables_type_shape_and_access()
|
|
{
|
|
OpcPlcProbe.SkipUnlessReachable();
|
|
var ct = TestContext.Current.CancellationToken;
|
|
var browser = new OpcUaClientDriverBrowser();
|
|
await using var session = await browser.OpenAsync(ConfigJson, ct);
|
|
|
|
var attrs = await session.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=2258", ct);
|
|
|
|
var a = attrs.ShouldHaveSingleItem();
|
|
a.Name.ShouldBe("CurrentTime");
|
|
a.DriverDataType.ShouldBe("DateTime");
|
|
a.IsArray.ShouldBeFalse();
|
|
a.SecurityClass.ShouldBe("ViewOnly");
|
|
}
|
|
}
|