fix(ab): Task 22 review r1 — release-note timing and AbLegacy regressions, probe E2E on both transports
- I1: the upgrade restart itself moves every no-key AbCip/AbLegacy driver to Native (every process start constructs drivers, incl. boot-from-cache). Docs + CLAUDE.md now say so and tell operators to set "transport": "Libplctag" BEFORE upgrading (older builds ignore the unknown key). - I2: AbLegacy release note leads with the status-bit write refusal (T4:0.DN, C5:0.CU, R6:0.EN, ...) on defaulted SLC/MicroLogix drivers and the fallback. The real client status is BadCommunicationError, not BadNotSupported (the driver's write catch-all maps the runtime's AbTransportException like any transport failure, as #571 does for PLC-5), with handle eviction and Degraded health; pinned by a driver-level test (no 0xAA write on the wire, with a positive control). ST string byte order stated as unverified. Also corrects the docs' "T4:0/DN" examples: the driver's address grammar is "T4:0.DN" ("/DN" is a malformed address). - I3: DriverProbeHandshakeE2eTests' two AbCip probe tests are a [Theory] over Native and Libplctag. Run against ab_server controllogix: all 4 rows pass with identical exact messages. - M1: documented (docs + resolver XML doc): the AdminUI merges only the probed device, so a mixed-family defaulted AbLegacy driver can probe Native while running Libplctag. - M2: factory ParseEnum rejects undefined integer strings ("7") via Enum.IsDefined (both drivers). - M3: AbCip driver-level bad value reads "AB CIP config in '<id>' has unknown Transport ..."; the named constant is gone (tagName: null now means driver-level). - M4: the explicit-Native limited-family Warning is once per instance, not per (re)init. - Nit: EffectiveTransport docs note that injected factories bypass it. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -66,6 +66,28 @@ public sealed class AbCipTransportSwitchTests
|
||||
existing.Message.ShouldContain("unknown PlcFamily 'Bogus'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An undefined integer string is rejected, not silently run on a switch's default arm (Task 22 review
|
||||
/// r1 M2): <c>Enum.TryParse</c> alone accepts "7". A defined numeric ("1") still parses, as before.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void An_undefined_numeric_transport_is_rejected()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => AbCipDriverFactoryExtensions.ParseOptions("ab1", Config("\"7\"")))
|
||||
.Message.ShouldContain("unknown Transport '7'");
|
||||
AbCipDriverFactoryExtensions.ParseOptions("ab1", Config("\"Native\"")).Transport.ShouldBe(AbCipTransport.Native);
|
||||
}
|
||||
|
||||
/// <summary>A bad driver-level value names the driver config, not a "tag" (Task 22 review r1 M3).</summary>
|
||||
[Fact]
|
||||
public void A_bad_transport_names_the_driver_config()
|
||||
{
|
||||
var ex = Should.Throw<InvalidOperationException>(
|
||||
() => AbCipDriverFactoryExtensions.ParseOptions("ab1", Config("\"Bogus\"")));
|
||||
ex.Message.ShouldStartWith("AB CIP config in 'ab1' has unknown Transport 'Bogus'");
|
||||
}
|
||||
|
||||
/// <summary>An explicitly injected factory wins over the option.</summary>
|
||||
[Fact]
|
||||
public void Injected_factory_wins_over_the_option()
|
||||
|
||||
+28
@@ -48,6 +48,34 @@ public sealed class AbLegacyTransportInitLogTests
|
||||
if (warns) warnings[0].Message.ShouldContain(family);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Warning is once per INSTANCE, not per init: every comms-loss reconnect retry re-runs init
|
||||
/// (Task 22 review r1 M4). Three inits, one warning.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task The_limited_family_warning_is_logged_once_per_instance_across_reinits()
|
||||
{
|
||||
var config = Config("Native", "Plc5");
|
||||
var captured = new List<(LogLevel, string)>();
|
||||
using var factory = new CapturingLoggerFactory(captured);
|
||||
var driver = AbLegacyDriverFactoryExtensions.CreateInstance("drv-t19", config, factory);
|
||||
try
|
||||
{
|
||||
await driver.InitializeAsync(config, TestContext.Current.CancellationToken);
|
||||
await driver.ReinitializeAsync(config, TestContext.Current.CancellationToken);
|
||||
await driver.ReinitializeAsync(config, TestContext.Current.CancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await driver.ShutdownAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
List<(LogLevel Level, string Message)> snapshot;
|
||||
lock (captured) snapshot = [.. captured];
|
||||
snapshot.Count(m => m.Message.Contains("initializing")).ShouldBe(3); // positive control: three inits ran
|
||||
snapshot.Count(m => m.Level == LogLevel.Warning && m.Message.Contains("native support is limited")).ShouldBe(1);
|
||||
}
|
||||
|
||||
private static async Task<List<(LogLevel Level, string Message)>> InitAndCaptureAsync(string config)
|
||||
{
|
||||
var captured = new List<(LogLevel, string)>();
|
||||
|
||||
@@ -96,6 +96,19 @@ public sealed class AbLegacyTransportSwitchTests
|
||||
existing.Message.ShouldContain("unknown PlcFamily 'Bogus'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An undefined integer string is rejected, not silently run on a switch's default arm (Task 22 review
|
||||
/// r1 M2): <c>Enum.TryParse</c> alone accepts "7". A defined numeric ("1") still parses, as before.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void An_undefined_numeric_transport_is_rejected()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => AbLegacyDriverFactoryExtensions.ParseOptions("ab1", Config("\"7\"")))
|
||||
.Message.ShouldContain("unknown Transport '7'");
|
||||
AbLegacyDriverFactoryExtensions.ParseOptions("ab1", Config("\"Native\"")).Transport.ShouldBe(AbLegacyTransport.Native);
|
||||
}
|
||||
|
||||
/// <summary>An explicitly injected factory wins over the option.</summary>
|
||||
[Fact]
|
||||
public void Injected_factory_wins_over_the_option()
|
||||
|
||||
+67
@@ -1,6 +1,8 @@
|
||||
using System.Buffers.Binary;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
|
||||
@@ -380,6 +382,71 @@ public sealed class NativeAbLegacyTagRuntimeTests
|
||||
h.Channel.SentCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
// These two tests call the driver's WriteAsync directly to pin its OWN status mapping; resilience wrapping
|
||||
// (CapabilityInvoker) is out of scope and would retry/mask the very status under test.
|
||||
#pragma warning disable OTOPCUA0001
|
||||
private sealed class HarnessFactory(Harness h) : IAbLegacyTagFactory
|
||||
{
|
||||
public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams p) =>
|
||||
new NativeAbLegacyTagRuntime(p, h.Scheduler, Vendor, Serial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pins the CLIENT-VISIBLE status of the native status-bit write refusal, through the driver (Task 22
|
||||
/// review r1 I2 — the release note states it). The runtime throws AbTransportException(Unsupported);
|
||||
/// the driver's write path does not special-case it, so the client sees the generic transport
|
||||
/// status, and nothing reaches the wire.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("T4:0.DN")]
|
||||
[InlineData("C5:0.CU")]
|
||||
[InlineData("R6:0.EN")]
|
||||
public async Task A_status_bit_write_through_the_driver_reports_the_documented_status(string address)
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies(Words(0));
|
||||
const string host = "ab://10.0.0.1:44818/1,0";
|
||||
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.Slc500)],
|
||||
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit", host, address, AbLegacyDataType.Bit)]),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, "drv-sb", new HarnessFactory(h));
|
||||
await driver.InitializeAsync("{}", CancellationToken.None);
|
||||
var results = await driver.WriteAsync([new WriteRequest("Bit", true)], CancellationToken.None);
|
||||
|
||||
// BadCommunicationError, NOT BadNotSupported: the driver's catch-all maps any runtime exception to
|
||||
// it (same as the PLC-5 write refusal, #571), evicts the handle and marks health Degraded.
|
||||
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
||||
driver.GetHealth().LastError.ShouldNotBeNull().ShouldContain("status-bit sub-element");
|
||||
// Refused before the wire: the lazy runtime's init read may go out, but never a write (fnc 0xAA).
|
||||
lock (h.Channel.Sent) h.Channel.Sent.ShouldNotContain(r => FncOf(r) == 0xAA);
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Positive control for the "no 0xAA on the wire" assertion above: the same driver path for a WORD
|
||||
/// sub-element does send a typed write, so the detector can see one.</summary>
|
||||
[Fact]
|
||||
public async Task A_word_sub_element_write_through_the_driver_sends_a_typed_write()
|
||||
{
|
||||
await using var h = new Harness("slc500", "N7:0").Replies(Words(0));
|
||||
const string host = "ab://10.0.0.1:44818/1,0";
|
||||
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.Slc500)],
|
||||
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Pre", host, "T4:0.PRE", AbLegacyDataType.Int)]),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
}, "drv-sb", new HarnessFactory(h));
|
||||
await driver.InitializeAsync("{}", CancellationToken.None);
|
||||
|
||||
var results = await driver.WriteAsync([new WriteRequest("Pre", (short)5)], CancellationToken.None);
|
||||
|
||||
results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
||||
lock (h.Channel.Sent) h.Channel.Sent.ShouldContain(r => FncOf(r) == 0xAA);
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
#pragma warning restore OTOPCUA0001
|
||||
|
||||
[Theory]
|
||||
[InlineData("T4:0.PRE")]
|
||||
[InlineData("T4:0.ACC")]
|
||||
|
||||
Reference in New Issue
Block a user