1806af54f0
- 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
113 lines
5.2 KiB
C#
113 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 (Task 22).</summary>
|
|
[Theory]
|
|
[InlineData("Native", "Slc500", "Native (configured)")]
|
|
[InlineData("Libplctag", "Slc500", "Libplctag (configured)")]
|
|
[InlineData(null, "Slc500", "Native (default)")]
|
|
[InlineData(null, "Plc5", "Libplctag (default; PLC-5/LogixPccc device present)")]
|
|
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("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)));
|
|
}
|
|
}
|
|
}
|