Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyTransportInitLogTests.cs
T
Joseph Doherty b66972b6f5 fix(ablegacy): final review I3 — default reverts to libplctag; Native is opt-in
On a wire the native PCCC path is proven only for N-file reads/writes and
one F8 read (ab_server). An absent transport key now resolves to Libplctag
for every family; explicit Native still wins and still warns once per
instance on PLC-5/LogixPccc. AdminUI dropdown, CLI help, evidence claims
(PcccFileType, AbLegacy.md wire-evidence table, release note) corrected:
S:0 (default probe) and B/T/C/R/S/L/ST/A/O/I are unverified natively.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-07 21:34:04 -04:00

114 lines
5.2 KiB
C#

using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
/// <summary>
/// The transport the instance runs on is logged at every init through the HOST-supplied logger factory,
/// so an operator can tell from the node log which stack serves a driver (T19 live gate). The factory
/// path is the one under test: a driver built without a logger writes to the null logger.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyTransportInitLogTests
{
private static string Config(string? transport, string family = "Slc500") =>
$$"""{{{(transport is null ? "" : $"\"transport\":\"{transport}\",")}}"probe":{"enabled":false},"devices":[{"hostAddress":"ab://10.0.0.1:44818/1,0","plcFamily":"{{family}}"}]}""";
/// <summary>The init line names the configured-vs-effective transport (final review I3: default = Libplctag).</summary>
[Theory]
[InlineData("Native", "Slc500", "Native (configured)")]
[InlineData("Libplctag", "Slc500", "Libplctag (configured)")]
[InlineData(null, "Slc500", "Libplctag (default)")]
[InlineData(null, "Plc5", "Libplctag (default)")]
public async Task Init_logs_the_transport_through_the_registered_logger_factory(
string? json, string family, string expected)
{
var captured = await InitAndCaptureAsync(Config(json, family));
captured.ShouldContain(m => m.Message.Contains("drv-t19") && m.Message.Contains($"transport={expected}"));
}
/// <summary>
/// An explicit Native against a PLC-5 / LogixPccc device is an allowed operator choice, but logs ONE
/// Warning naming the limitation. The defaulted and explicit-libplctag rows are the falsification
/// controls: same device families, no warning.
/// </summary>
[Theory]
[InlineData("Native", "Plc5", true)]
[InlineData("Native", "LogixPccc", true)]
[InlineData(null, "Plc5", false)]
[InlineData(null, "Slc500", false)]
[InlineData("Libplctag", "LogixPccc", false)]
[InlineData("Native", "Slc500", false)]
public async Task Explicit_Native_on_a_limited_family_warns_once(string? json, string family, bool warns)
{
var captured = await InitAndCaptureAsync(Config(json, family));
var warnings = captured.Where(m => m.Level == LogLevel.Warning && m.Message.Contains("native support is limited"))
.ToList();
warnings.Count.ShouldBe(warns ? 1 : 0);
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)>();
using var factory = new CapturingLoggerFactory(captured);
var driver = AbLegacyDriverFactoryExtensions.CreateInstance("drv-t19", config, factory);
try
{
await driver.InitializeAsync(config, TestContext.Current.CancellationToken);
}
finally
{
await driver.ShutdownAsync(TestContext.Current.CancellationToken);
}
lock (captured) return [.. captured];
}
private sealed class CapturingLoggerFactory(List<(LogLevel, string)> sink) : ILoggerFactory
{
public ILogger CreateLogger(string categoryName) => new CapturingLogger(sink);
public void AddProvider(ILoggerProvider provider) { }
public void Dispose() { }
}
private sealed class CapturingLogger(List<(LogLevel, string)> sink) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
lock (sink) sink.Add((logLevel, formatter(state, exception)));
}
}
}