test(opcuaclient,mtconnect): live-fixture cases for the lane G1 fixes
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
This commit is contained in:
+53
@@ -165,6 +165,59 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
|
||||
result.Latency.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 T-7 — the connectivity probe's headers-only ping (no device-model download/parse) answers
|
||||
/// against the live Agent, and fails against a closed port (the falsifiability control).
|
||||
/// </summary>
|
||||
[Fact(Timeout = TestTimeoutMs)]
|
||||
public async Task Ping_reaches_the_live_agent_and_not_a_dead_port()
|
||||
{
|
||||
SkipIfDown();
|
||||
|
||||
using (var client = new MTConnectAgentClient(LiveOptions()))
|
||||
{
|
||||
await client.PingAsync(Ct);
|
||||
}
|
||||
|
||||
using var dead = new MTConnectAgentClient(
|
||||
new MTConnectDriverOptions { AgentUri = $"http://127.0.0.1:{ClosedLoopbackPort()}", RequestTimeoutMs = 5_000 });
|
||||
await Should.ThrowAsync<Exception>(async () => await dead.PingAsync(Ct));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 T-4 / T-5 against the live Agent: once the <c>/sample</c> stream is delivering, a read is
|
||||
/// answered (from the pump's live index) with Good values, and the stream's lag measurement — the real Agent
|
||||
/// sends <c>lastSequence</c> — is on the diagnostics bag beside the effective count.
|
||||
/// </summary>
|
||||
[Fact(Timeout = TestTimeoutMs)]
|
||||
public async Task A_delivering_stream_serves_reads_and_reports_its_lag()
|
||||
{
|
||||
SkipIfDown();
|
||||
|
||||
var ids = await NumericSampleIdsAsync();
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
Assert.Skip($"The Agent at {agent.AgentUri} is currently reporting no numeric SAMPLE value.");
|
||||
}
|
||||
|
||||
await using var live = await LiveDriverAsync(ids.Select(id => Tag(id, DriverDataType.Float64)));
|
||||
await live.Driver.SubscribeAsync([.. ids], TimeSpan.FromMilliseconds(200), Ct);
|
||||
|
||||
var deadline = DateTime.UtcNow + StreamWait;
|
||||
while (!live.Driver.GetDiagnostics().ContainsKey("sampleLag") && DateTime.UtcNow < deadline)
|
||||
{
|
||||
await Task.Delay(100, Ct);
|
||||
}
|
||||
|
||||
var diag = live.Driver.GetDiagnostics();
|
||||
diag.ShouldContainKey("sampleLag", "the live stream must have delivered a measured chunk");
|
||||
diag["sampleCount"].ShouldBe("500");
|
||||
long.Parse(diag["sampleLag"], System.Globalization.CultureInfo.InvariantCulture).ShouldBeGreaterThanOrEqualTo(0);
|
||||
|
||||
var results = await live.Driver.ReadAsync([.. ids], Ct);
|
||||
results.Where(r => r.StatusCode == Good).ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
// ---- discovery: the /probe device model ----
|
||||
|
||||
/// <summary>
|
||||
|
||||
+24
-3
@@ -63,10 +63,10 @@ public sealed class OpcUaClientBrowseSessionTests
|
||||
children.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>The OPC UA picker treats variables as terminal leaves, so
|
||||
/// AttributesAsync must always be empty for this driver.</summary>
|
||||
/// <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_opcuaclient()
|
||||
public async Task AttributesAsync_is_empty_for_an_object_node()
|
||||
{
|
||||
OpcPlcProbe.SkipUnlessReachable();
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
@@ -75,4 +75,25 @@ public sealed class OpcUaClientBrowseSessionTests
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
+57
@@ -92,4 +92,61 @@ public sealed class OpcUaClientSmokeTests(OpcPlcFixture sim)
|
||||
|
||||
await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 O-9 + lifecycle generation, through the real SDK: a shutdown with several live
|
||||
/// subscriptions completes inside the one teardown budget, lands Unknown, and a re-initialise of the same
|
||||
/// instance reconnects and reads.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Shutdown_with_live_subscriptions_is_bounded_and_the_instance_reinitialises()
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
|
||||
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-lifecycle");
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
for (var i = 0; i < 3; i++)
|
||||
await drv.SubscribeAsync([OpcPlcProfile.FastUInt1Ref], TimeSpan.FromMilliseconds(250), ct);
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
await drv.ShutdownAsync(ct);
|
||||
sw.Stop();
|
||||
|
||||
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10), "one 4 s budget bounds the whole teardown");
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Unknown);
|
||||
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
var read = await drv.ReadAsync([OpcPlcProfile.StepUpRef], ct);
|
||||
read.Single().StatusCode.ShouldBe(0u, "the re-initialised instance must be fully usable");
|
||||
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The health contract against a real server: a write the server REFUSES (a string into the UInt32 StepUp —
|
||||
/// BadTypeMismatch) is a reply — the refusal status passes through and the driver stays Healthy with its stamp
|
||||
/// advanced.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_server_refused_write_is_a_reply_and_keeps_the_driver_healthy()
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
|
||||
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-refused-write");
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
var before = drv.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
|
||||
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
||||
while (DateTime.UtcNow <= before && DateTime.UtcNow < deadline) await Task.Yield();
|
||||
|
||||
var result = await drv.WriteAsync([new WriteRequest(OpcPlcProfile.StepUpRef, "not-a-number")], ct);
|
||||
|
||||
(result.Single().StatusCode & 0x80000000u).ShouldNotBe(0u, "opc-plc refuses a string written into a UInt32");
|
||||
result.Single().StatusCode.ShouldNotBe(0x80050000u, "a server refusal is not a local comms error");
|
||||
var h = drv.GetHealth();
|
||||
h.State.ShouldBe(DriverState.Healthy);
|
||||
h.LastSuccessfulRead.ShouldNotBeNull().ShouldBeGreaterThan(before);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user