using Microsoft.Extensions.Logging; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// 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. /// [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}}"}]}"""; /// The init line names the configured-vs-effective transport (final review I3: default = Libplctag). [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}")); } /// /// 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. /// [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); } /// /// 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. /// [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> 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 state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { lock (sink) sink.Add((logLevel, formatter(state, exception))); } } }