fix(abcip): an unknown tag no longer throttles its whole device or reads as a comms error
Live read-only check against a production CompactLogix press: with one nonexistent tag
first in the read list, libplctag's create threw LibPlcTagException("ErrorNotFound"); the
driver counted it as a failed Forward-Open, opened the device's 05/STAB-8 connect-backoff
window, and every other cold tag on the device - all real (WW_InCycle, WW_ActualPress,
WW_WorkOrderNum) - read BadCommunicationError. In steady polling the bad tag re-opens
the window each attempt, so siblings ordered after it may never connect.
- A create that fails with a "controller answered" status (NotFound/NoMatch/BadDevice,
the probe's existing reachable classification) records backoff SUCCESS, not failure.
Genuine connection failures still open the window (falsification control test).
- Read exceptions map through the libplctag Status they carry: unknown tag ->
BadNodeIdUnknown, decode past the buffer -> BadOutOfRange, timeout -> BadTimeout,
instead of a blanket BadCommunicationError. One status-name parser
(AbCipStatusMapper.TryGetStatus) now serves both the read path and the probe.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using libplctag;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A tag the controller does not have is a configuration error, not a lost connection. Seen live
|
||||
/// against a production CompactLogix: with one misspelled tag first in the read list, libplctag's
|
||||
/// create threw <c>LibPlcTagException("ErrorNotFound")</c>, the driver counted that as a failed
|
||||
/// Forward-Open, opened the device's connect-backoff window, and every other not-yet-connected tag
|
||||
/// on the device — all of them real — came back <c>BadCommunicationError</c> (throttled). The bad
|
||||
/// tag itself also read <c>BadCommunicationError</c> instead of <c>BadNodeIdUnknown</c>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbCipUnknownTagReadTests
|
||||
{
|
||||
private const string Device = "ab://10.0.0.5/1,0";
|
||||
|
||||
private static (AbCipDriver Driver, FakeAbCipTagFactory Factory) Build(Func<string, Exception?> initFault)
|
||||
{
|
||||
var factory = new FakeAbCipTagFactory
|
||||
{
|
||||
Customise = p => initFault(p.TagName) is { } ex
|
||||
? new FakeAbCipTag(p) { ThrowOnInitialize = true, Exception = ex }
|
||||
: new FakeAbCipTag(p) { Value = 42 },
|
||||
};
|
||||
var drv = new AbCipDriver(new AbCipDriverOptions
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(Device)],
|
||||
RawTags = AbCipRawTags.From(
|
||||
new AbCipTagDefinition("Typo", Device, "NoSuchTag_xyz", AbCipDataType.DInt),
|
||||
new AbCipTagDefinition("Real1", Device, "WW_InCycle", AbCipDataType.DInt),
|
||||
new AbCipTagDefinition("Real2", Device, "WW_ActualPress", AbCipDataType.DInt)),
|
||||
Probe = new AbCipProbeOptions { Enabled = false },
|
||||
MaxConcurrentReads = 1,
|
||||
}, "abcip-1", factory);
|
||||
return (drv, factory);
|
||||
}
|
||||
|
||||
/// <summary>The unknown tag reads BadNodeIdUnknown and its siblings still connect and read Good.</summary>
|
||||
[Fact]
|
||||
public async Task Unknown_tag_does_not_throttle_sibling_tags_and_reads_BadNodeIdUnknown()
|
||||
{
|
||||
var (drv, _) = Build(name => name == "NoSuchTag_xyz" ? new LibPlcTagException("ErrorNotFound") : null);
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
var r = await drv.ReadAsync(["Typo", "Real1", "Real2"], TestContext.Current.CancellationToken);
|
||||
|
||||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
|
||||
r[1].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
r[1].Value.ShouldBe(42);
|
||||
r[2].StatusCode.ShouldBe(AbCipStatusMapper.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Falsification control: a genuine connection failure still opens the backoff window, so a
|
||||
/// sibling cold tag is throttled exactly as before (05/STAB-8 is unchanged).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_connection_failure_still_opens_the_backoff_window()
|
||||
{
|
||||
var (drv, factory) = Build(name => name == "NoSuchTag_xyz" ? new LibPlcTagException("ErrorTimeout") : null);
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
var r = await drv.ReadAsync(["Typo", "Real1"], TestContext.Current.CancellationToken);
|
||||
|
||||
r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadTimeout);
|
||||
r[1].StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
||||
factory.Tags.ContainsKey("WW_InCycle").ShouldBeFalse(); // throttled before any create
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user