bd6c0b4d3d
Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up misused inheritdoc across 481 files so the documented API surface is complete. Documentation-only (zero code lines changed). The 131 remaining findings are inheritdoc-style warnings deliberately left to preserve hand-written implementation rationale (plan-decision notes, race-condition explanations).
115 lines
5.9 KiB
C#
115 lines
5.9 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
/// <summary>
|
|
/// Driver.AbLegacy-005 — verifies the driver accepts and uses an optional
|
|
/// <see cref="ILogger{AbLegacyDriver}"/>. Probe transitions, init failures, and the first
|
|
/// non-zero libplctag status per device must be logged (rather than only folded into the
|
|
/// transient <see cref="DriverHealth.Detail"/> string) so a field operator can correlate a
|
|
/// PCCC comms problem with a structured log entry.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyLoggerInjectionTests
|
|
{
|
|
private sealed class CapturingLogger : ILogger<AbLegacyDriver>
|
|
{
|
|
public readonly List<(LogLevel Level, string Message)> Entries = new();
|
|
|
|
/// <summary>Begins a logical operation scope (returns a no-op scope).</summary>
|
|
/// <typeparam name="TState">The type of the state to associate with the scope.</typeparam>
|
|
/// <param name="state">The state identifier for the scope.</param>
|
|
/// <returns>A no-op disposable scope.</returns>
|
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
|
|
|
/// <summary>Checks whether logging is enabled for the given level (always true).</summary>
|
|
/// <param name="logLevel">The log level to check.</param>
|
|
/// <returns><see langword="true"/> always.</returns>
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
/// <summary>Records a log entry into the captured entries list.</summary>
|
|
/// <typeparam name="TState">The type of the log state object.</typeparam>
|
|
/// <param name="logLevel">The severity level of the log entry.</param>
|
|
/// <param name="eventId">The event identifier for the log entry.</param>
|
|
/// <param name="state">The state object associated with the log entry.</param>
|
|
/// <param name="exception">An optional exception to log.</param>
|
|
/// <param name="formatter">A function that formats the state and exception into a message string.</param>
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
=> Entries.Add((logLevel, formatter(state, exception)));
|
|
private sealed class NullScope : IDisposable
|
|
{
|
|
public static readonly NullScope Instance = new();
|
|
|
|
/// <summary>Disposes the no-op scope (no-op).</summary>
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that the driver accepts an optional logger parameter.</summary>
|
|
[Fact]
|
|
public void Driver_accepts_optional_logger_parameter()
|
|
{
|
|
// Constructor must accept an ILogger<AbLegacyDriver> 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();
|
|
}
|
|
|
|
/// <summary>Verifies that driver initialization failure emits an error log.</summary>
|
|
/// <returns>A task that represents the asynchronous test operation.</returns>
|
|
[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<InvalidOperationException>(
|
|
() => 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");
|
|
}
|
|
|
|
/// <summary>Verifies that the first non-zero libplctag status per device is logged.</summary>
|
|
/// <returns>A task that represents the asynchronous test operation.</returns>
|
|
[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");
|
|
}
|
|
}
|