using Microsoft.Extensions.Logging; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; /// /// Driver.AbLegacy-005 — verifies the driver accepts and uses an optional /// . Probe transitions, init failures, and the first /// non-zero libplctag status per device must be logged (rather than only folded into the /// transient string) so a field operator can correlate a /// PCCC comms problem with a structured log entry. /// [Trait("Category", "Unit")] public sealed class AbLegacyLoggerInjectionTests { private sealed class CapturingLogger : ILogger { public readonly List<(LogLevel Level, string Message)> Entries = new(); public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => Entries.Add((logLevel, formatter(state, exception))); private sealed class NullScope : IDisposable { public static readonly NullScope Instance = new(); public void Dispose() { } } } [Fact] public void Driver_accepts_optional_logger_parameter() { // Constructor must accept an ILogger as an optional named arg, matching // the Modbus/S7/Galaxy driver pattern. The driver runs with NullLogger when omitted. var logger = new CapturingLogger(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions(), "drv-logged", tagFactory: null, logger: logger); drv.ShouldNotBeNull(); } [Fact] public async Task InitializeAsync_failure_emits_error_log() { var logger = new CapturingLogger(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("not-a-valid-address")], }, "drv-logged", tagFactory: null, logger: logger); await Should.ThrowAsync( () => drv.InitializeAsync("{}", CancellationToken.None)); var errors = logger.Entries.Where(e => e.Level >= LogLevel.Error).ToList(); errors.ShouldNotBeEmpty("init failure must surface as an Error-level log"); errors[0].Message.ShouldContain("drv-logged"); } [Fact] public async Task First_nonzero_libplctag_status_per_device_is_logged() { // The driver should log the first occurrence of a non-zero libplctag status per device, // so that a field operator can correlate a comms problem with a structured log entry // even though Detail is overwritten by the next read. var factory = new FakeAbLegacyTagFactory(); var logger = new CapturingLogger(); var drv = new AbLegacyDriver(new AbLegacyDriverOptions { Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")], Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)], Probe = new AbLegacyProbeOptions { Enabled = false }, }, "drv-logged", factory, logger); await drv.InitializeAsync("{}", CancellationToken.None); factory.Customise = p => new FakeAbLegacyTag(p) { Status = -32 }; // timeout await drv.ReadAsync(["X"], CancellationToken.None); var warnings = logger.Entries.Where(e => e.Level == LogLevel.Warning).ToList(); warnings.ShouldNotBeEmpty("first non-zero libplctag status must emit a structured warning"); warnings.Any(w => w.Message.Contains("ab://10.0.0.5/1,0") || w.Message.Contains("drv-logged")) .ShouldBeTrue("warning must identify the device or driver instance"); // Second read with the same status — the per-device de-dupe should suppress. var warningCountAfterFirst = warnings.Count; await drv.ReadAsync(["X"], CancellationToken.None); var newWarnings = logger.Entries.Where(e => e.Level == LogLevel.Warning).ToList(); newWarnings.Count.ShouldBe(warningCountAfterFirst, "subsequent same-device non-zero status stays quiet"); } }