test(opcuaclient-browser): skip live browse tests when opc-plc is unreachable; move out of unit project (#594)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+2
-2
@@ -16,8 +16,8 @@ public class BrowseRoundTripTests
|
||||
[Fact]
|
||||
public async Task Three_level_expand_round_trips_resolve_back()
|
||||
{
|
||||
var endpoint = Environment.GetEnvironmentVariable("OPCUA_SIM_ENDPOINT")
|
||||
?? "opc.tcp://10.100.0.35:50000";
|
||||
OpcPlcProbe.SkipUnlessReachable();
|
||||
var endpoint = OpcPlcProbe.Endpoint;
|
||||
// Numeric ordinals because the production browser uses default System.Text.Json
|
||||
// without a JsonStringEnumConverter (SecurityPolicy.None=0, SecurityMode.None=0,
|
||||
// AuthType.Anonymous=0).
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Reachability gate for the shared opc-plc fixture (<c>OPCUA_SIM_ENDPOINT</c>, default
|
||||
/// <c>opc.tcp://10.100.0.35:50000</c>). A small copy of the probe in
|
||||
/// <c>Driver.OpcUaClient.IntegrationTests/OpcPlcFixture</c> rather than a cross-project reference —
|
||||
/// that project drags the whole driver in. Tests call <see cref="SkipUnlessReachable"/> first, so a
|
||||
/// machine without the simulator reports Skipped instead of a connect failure.
|
||||
/// </summary>
|
||||
internal static class OpcPlcProbe
|
||||
{
|
||||
private const string EndpointEnvVar = "OPCUA_SIM_ENDPOINT";
|
||||
private const string DefaultEndpoint = "opc.tcp://10.100.0.35:50000";
|
||||
|
||||
/// <summary>The simulator endpoint the tests should connect to.</summary>
|
||||
public static string Endpoint =>
|
||||
Environment.GetEnvironmentVariable(EndpointEnvVar) is { Length: > 0 } e ? e : DefaultEndpoint;
|
||||
|
||||
private static readonly Lazy<string?> Reason = new(Probe);
|
||||
|
||||
/// <summary>Skips the calling test when opc-plc does not accept a TCP connection (probed once per run).</summary>
|
||||
public static void SkipUnlessReachable()
|
||||
{
|
||||
if (Reason.Value is { } reason) Xunit.Assert.Skip(reason);
|
||||
}
|
||||
|
||||
private static string? Probe()
|
||||
{
|
||||
var body = Endpoint.StartsWith("opc.tcp://", StringComparison.OrdinalIgnoreCase)
|
||||
? Endpoint["opc.tcp://".Length..]
|
||||
: Endpoint;
|
||||
var slash = body.IndexOf('/');
|
||||
if (slash >= 0) body = body[..slash];
|
||||
var colon = body.LastIndexOf(':');
|
||||
var host = colon >= 0 ? body[..colon] : body;
|
||||
var port = colon >= 0 && int.TryParse(body[(colon + 1)..], out var p) ? p : 4840;
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient(AddressFamily.InterNetwork);
|
||||
var addr = Dns.GetHostAddresses(host).FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork)
|
||||
?? IPAddress.Loopback;
|
||||
if (client.ConnectAsync(addr, port).Wait(TimeSpan.FromSeconds(2)) && client.Connected) return null;
|
||||
return $"opc-plc simulator at {host}:{port} did not accept a TCP connection within 2s. Start it or override {EndpointEnvVar}.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"opc-plc simulator at {host}:{port} unreachable: {ex.GetType().Name}: {ex.Message}. Start it or override {EndpointEnvVar}.";
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
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>The OPC UA picker treats variables as terminal leaves, so
|
||||
/// AttributesAsync must always be empty for this driver.</summary>
|
||||
[Fact]
|
||||
public async Task AttributesAsync_is_empty_for_opcuaclient()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user