feat(abcip): final review I1 — node-level Drivers:AbCip:DefaultTransport
The old AdminUI cannot author the per-driver transport key (#576), so the 'pin before upgrading' recipe had no working path. A node-level appsetting (env Drivers__AbCip__DefaultTransport) now supplies the default for AbCip drivers with no key: explicit key > node default > compiled Native. Bound and validated unconditionally (bad value fails host start); the driver factory and the Test Connect probe both read it so they agree. AbCip.md upgrade recipe rewritten around the key; Configuration.md documents it. Also documents first-wins for AllowPacking/probe and host-alias throttles (final review M6) and moves the status-bit note out of AbCip.md (M5). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -191,4 +191,73 @@ public sealed class AbCipTransportSwitchTests
|
||||
r.Ok.ShouldBeFalse();
|
||||
r.Message!.ShouldContain("Config JSON is invalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final review I1 precedence: explicit key > node default > compiled Native. The pinned rows are
|
||||
/// the falsification rows — a resolver that ignores the node default returns Native for them.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(null, AbCipTransport.Libplctag, AbCipTransport.Libplctag)]
|
||||
[InlineData(null, AbCipTransport.Native, AbCipTransport.Native)]
|
||||
[InlineData(AbCipTransport.Native, AbCipTransport.Libplctag, AbCipTransport.Native)]
|
||||
[InlineData(AbCipTransport.Libplctag, AbCipTransport.Native, AbCipTransport.Libplctag)]
|
||||
public void Explicit_key_beats_node_default_beats_compiled_default(
|
||||
AbCipTransport? configured, AbCipTransport nodeDefault, AbCipTransport expected)
|
||||
{
|
||||
AbCipTransportResolver.Resolve(configured, nodeDefault).ShouldBe(expected);
|
||||
var json = configured is null ? Config(null) : Config($"\"{configured}\"");
|
||||
AbCipDriverFactoryExtensions.CreateInstance("ab1", json, loggerFactory: null, defaultTransport: nodeDefault)
|
||||
.EffectiveTransport.ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>The init-log rendering distinguishes the compiled default from a node pin.</summary>
|
||||
[Fact]
|
||||
public void Describe_distinguishes_compiled_default_node_default_and_configured()
|
||||
{
|
||||
AbCipTransportResolver.Describe(null).ShouldBe("Native (default)");
|
||||
AbCipTransportResolver.Describe(null, AbCipTransport.Libplctag).ShouldBe("Libplctag (node default)");
|
||||
AbCipTransportResolver.Describe(AbCipTransport.Native, AbCipTransport.Libplctag).ShouldBe("Native (configured)");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Native", true)]
|
||||
[InlineData("libplctag", true)]
|
||||
[InlineData(" Libplctag ", true)]
|
||||
[InlineData("1", false)]
|
||||
[InlineData("Native,Libplctag", false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData("Bogus", false)]
|
||||
public void TryParse_accepts_names_only(string raw, bool ok) =>
|
||||
AbCipTransportResolver.TryParse(raw, out _).ShouldBe(ok);
|
||||
|
||||
/// <summary>
|
||||
/// The probe honours its node default for an absent key: pinned to Libplctag it does NOT produce the
|
||||
/// native failure text against the same listener the unpinned probe does (falsification control).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Probe_with_no_transport_uses_the_node_default()
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try { (await listener.AcceptTcpClientAsync(TestContext.Current.CancellationToken)).Dispose(); }
|
||||
catch { return; }
|
||||
}
|
||||
}, TestContext.Current.CancellationToken);
|
||||
|
||||
var cfg = $$"""{"devices":[{"hostAddress":"ab://127.0.0.1:{{port}}/1,0"}]}""";
|
||||
var pinned = await new AbCipDriverProbe(defaultTransport: AbCipTransport.Libplctag)
|
||||
.ProbeAsync(cfg, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
||||
var unpinned = await new AbCipDriverProbe()
|
||||
.ProbeAsync(cfg, TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
|
||||
listener.Stop();
|
||||
|
||||
unpinned.Message!.ShouldContain("CIP handshake failed: Connection lost");
|
||||
pinned.Ok.ShouldBeFalse();
|
||||
pinned.Message!.ShouldNotContain("Connection lost");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user