diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/BrowseRoundTripTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/BrowseRoundTripTests.cs
index f1e1b8ab..100b9945 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/BrowseRoundTripTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/BrowseRoundTripTests.cs
@@ -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).
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcPlcProbe.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcPlcProbe.cs
new file mode 100644
index 00000000..183bcd37
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcPlcProbe.cs
@@ -0,0 +1,53 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests;
+
+///
+/// Reachability gate for the shared opc-plc fixture (OPCUA_SIM_ENDPOINT, default
+/// opc.tcp://10.100.0.35:50000). A small copy of the probe in
+/// Driver.OpcUaClient.IntegrationTests/OpcPlcFixture rather than a cross-project reference —
+/// that project drags the whole driver in. Tests call first, so a
+/// machine without the simulator reports Skipped instead of a connect failure.
+///
+internal static class OpcPlcProbe
+{
+ private const string EndpointEnvVar = "OPCUA_SIM_ENDPOINT";
+ private const string DefaultEndpoint = "opc.tcp://10.100.0.35:50000";
+
+ /// The simulator endpoint the tests should connect to.
+ public static string Endpoint =>
+ Environment.GetEnvironmentVariable(EndpointEnvVar) is { Length: > 0 } e ? e : DefaultEndpoint;
+
+ private static readonly Lazy Reason = new(Probe);
+
+ /// Skips the calling test when opc-plc does not accept a TCP connection (probed once per run).
+ 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}.";
+ }
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
similarity index 85%
rename from tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs
rename to tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
index 7ea07a5e..d84794b6 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientBrowseSessionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
@@ -3,21 +3,20 @@ 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.Tests;
+namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests;
///
/// 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 OpcUaClientBrowseSession through its public factory
/// so no InternalsVisibleTo is needed.
-/// Filter out with --filter "Category!=RequiresOpcPlc" when the fixture
-/// is unreachable.
+/// Each test skips () when the fixture is unreachable. Moved here from the
+/// unit project, which must hold no live-server tests (#594).
///
-[Trait("Category", "RequiresOpcPlc")]
+[Trait("Category", "Integration"), Trait("Fixture", "opc-plc")]
public sealed class OpcUaClientBrowseSessionTests
{
- private static string Endpoint =>
- Environment.GetEnvironmentVariable("OPCUA_SIM_ENDPOINT") ?? "opc.tcp://10.100.0.35:50000";
+ 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
@@ -40,6 +39,7 @@ public sealed class OpcUaClientBrowseSessionTests
[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);
@@ -52,6 +52,7 @@ public sealed class OpcUaClientBrowseSessionTests
[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);
@@ -67,6 +68,7 @@ public sealed class OpcUaClientBrowseSessionTests
[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);