b66972b6f5
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
114 lines
5.2 KiB
C#
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)));
|
|
}
|
|
}
|
|
}
|