103 lines
4.9 KiB
C#
103 lines
4.9 KiB
C#
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);
|
|
}
|
|
}
|