@@ -0,0 +1,102 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// PR ablegacy-12 / #255 — wire-level smoke for auto-demote on comm failure.
|
||||
/// Runs only when ab_server is reachable. Two devices: one healthy (the live
|
||||
/// ab_server slc500 simulator), one pointed at <c>127.0.0.1:1</c> which
|
||||
/// refuses every connection. After three consecutive failures the faulty
|
||||
/// device's reads must short-circuit with <c>BadCommunicationError</c>
|
||||
/// while the healthy device keeps returning <c>Good</c> — the whole point
|
||||
/// of the feature: one slow / unreachable PLC sharing the driver thread
|
||||
/// can't starve faster peers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Build-only by default — the assertion that demotion latency is
|
||||
/// bounded depends on the ab_server simulator timing out on the faulty
|
||||
/// port within the per-device timeout. We pin the faulty endpoint at
|
||||
/// <c>127.0.0.1:1</c> (the bogus-port standard) which RST's the
|
||||
/// connection immediately on most stacks; environments that whitelist
|
||||
/// outbound to localhost:1 will see different timing but still trip
|
||||
/// the threshold within the test budget.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The Docker fixture extension (<c>slc500-faulty</c>) noted in the PR
|
||||
/// plan is a documentation-only placeholder for now — implementing a
|
||||
/// refusing-proxy container is non-trivial and the localhost:1 trick
|
||||
/// covers the same surface deterministically.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Collection(AbLegacyServerCollection.Name)]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Simulator", "ab_server-PCCC")]
|
||||
public sealed class AbLegacyAutoDemoteTests(AbLegacyServerFixture sim)
|
||||
{
|
||||
[AbLegacyFact]
|
||||
public async Task Two_devices_one_unreachable_does_not_starve_healthy_reads()
|
||||
{
|
||||
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
|
||||
|
||||
var healthy = $"ab://{sim.Host}:{sim.Port}/{sim.CipPath}";
|
||||
// 127.0.0.1:1 is the bogus-port standard — typical Linux/Windows TCP
|
||||
// stacks RST immediately. The driver still reports it as a comm
|
||||
// failure (libplctag wraps the failure as a transient throw).
|
||||
var faulty = "ab://127.0.0.1:1/1,0";
|
||||
|
||||
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices =
|
||||
[
|
||||
new AbLegacyDeviceOptions(healthy, AbLegacyPlcFamily.Slc500,
|
||||
Timeout: TimeSpan.FromSeconds(5)),
|
||||
new AbLegacyDeviceOptions(faulty, AbLegacyPlcFamily.Slc500,
|
||||
// Snappy timeout so the test budget stays short.
|
||||
Timeout: TimeSpan.FromMilliseconds(500),
|
||||
Demote: new AbLegacyDemoteOptions(
|
||||
FailureThreshold: 3,
|
||||
DemoteFor: TimeSpan.FromSeconds(30))),
|
||||
],
|
||||
Tags =
|
||||
[
|
||||
new AbLegacyTagDefinition("Healthy", healthy, "N7:0", AbLegacyDataType.Int),
|
||||
new AbLegacyTagDefinition("Faulty", faulty, "N7:0", AbLegacyDataType.Int),
|
||||
],
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, driverInstanceId: "ablegacy-auto-demote-it");
|
||||
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
// Trip the demote on the faulty device.
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
await drv.ReadAsync(["Faulty"], TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
// Healthy host MUST keep returning Good even though the sibling is demoted.
|
||||
var healthyResult = await drv.ReadAsync(["Healthy"], TestContext.Current.CancellationToken);
|
||||
healthyResult[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
|
||||
// Faulty host now short-circuits without waiting on libplctag's timeout.
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var faultyResult = await drv.ReadAsync(["Faulty"], TestContext.Current.CancellationToken);
|
||||
sw.Stop();
|
||||
faultyResult[0].StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
||||
// Short-circuit should be ~1 ms; pad generously for CI noise. The pre-PR-12
|
||||
// path would have waited the full 500 ms timeout.
|
||||
sw.ElapsedMilliseconds.ShouldBeLessThan(200);
|
||||
|
||||
// Counter access via the public diagnostic short-circuit path — the
|
||||
// internal Snapshot() seam isn't visible from this assembly.
|
||||
var demoteCountRef = $"_Diagnostics/{faulty}/DemoteCount";
|
||||
var lastDemotedRef = $"_Diagnostics/{faulty}/LastDemotedUtc";
|
||||
var diag = await drv.ReadAsync(
|
||||
[demoteCountRef, lastDemotedRef], TestContext.Current.CancellationToken);
|
||||
((long)diag[0].Value!).ShouldBeGreaterThan(0);
|
||||
((string)diag[1].Value!).Length.ShouldBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -72,3 +72,30 @@ services:
|
||||
"--tag=F8[120]",
|
||||
"--tag=B3[10]"
|
||||
]
|
||||
|
||||
# PR ablegacy-12 / #255 — faulty-PLC fixture for the auto-demote contract.
|
||||
# FIXTURE-TIER FOLLOW-UP: implementing a refusing-proxy container that
|
||||
# round-trips libplctag's CIP framing far enough to trigger comm failures
|
||||
# (vs. just RST'ing the TCP handshake) is non-trivial — the integration
|
||||
# test currently uses 127.0.0.1:1 (the bogus-port standard) which RST's
|
||||
# immediately on most TCP stacks. That gets us deterministic comm-failure
|
||||
# coverage without standing up a second container; if the localhost:1
|
||||
# trick stops working on a future test runner (e.g. a sandbox that
|
||||
# blocks port 1) re-enable this stub:
|
||||
#
|
||||
# slc500-faulty:
|
||||
# profiles: ["slc500-faulty"]
|
||||
# image: otopcua-ab-server:libplctag-release
|
||||
# build:
|
||||
# context: ../../ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/Docker
|
||||
# dockerfile: Dockerfile
|
||||
# container_name: otopcua-ab-server-slc500-faulty
|
||||
# restart: "no"
|
||||
# ports:
|
||||
# - "44819:44819"
|
||||
# # Hostile entrypoint: bind the port but exit immediately so subsequent
|
||||
# # connection attempts get RST'd. Future iteration: a libplctag-aware
|
||||
# # proxy that accepts the CIP open and then drops the wire halfway
|
||||
# # through, exercising the read-timeout path rather than the
|
||||
# # connection-refused path.
|
||||
# entrypoint: ["sh", "-c", "exit 1"]
|
||||
|
||||
Reference in New Issue
Block a user