using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
///
/// Regression coverage for Driver.FOCAS-007 — the driver previously swallowed every
/// exception in its poll / probe / recycle / fixed-tree loops with no logging at all,
/// leaving operators blind when a CNC was silently failing every tick.
///
[Trait("Category", "Unit")]
public sealed class FocasLoggingTests
{
[Fact]
public void Constructor_accepts_an_ILogger()
{
// Constructor signature must allow an ILogger so the host can wire one
// through Microsoft.Extensions.DependencyInjection. The driver code project already
// references Microsoft.Extensions.Logging.Abstractions.
var logger = new CapturingLogger();
var drv = new FocasDriver(
new FocasDriverOptions { Probe = new FocasProbeOptions { Enabled = false } },
"drv-1",
new FakeFocasClientFactory(),
logger);
drv.ShouldNotBeNull();
}
[Fact]
public async Task ProbeLoop_logs_when_an_exception_is_swallowed()
{
var logger = new CapturingLogger();
var factory = new FakeFocasClientFactory
{
Customise = () => new FakeFocasClient
{
// Make ProbeAsync throw — the probe loop swallows it but must log.
ThrowOnConnect = false,
ProbeResult = true, // not used because the underlying probe path throws
},
};
// Force the probe to throw by making the client throw on connect.
factory.Customise = () => new FakeFocasClient
{
ThrowOnConnect = true,
Exception = new InvalidOperationException("simulated probe failure"),
};
var drv = new FocasDriver(
new FocasDriverOptions
{
Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
Probe = new FocasProbeOptions
{
Enabled = true,
Interval = TimeSpan.FromMilliseconds(50),
Timeout = TimeSpan.FromMilliseconds(100),
},
},
"drv-log",
factory,
logger);
await drv.InitializeAsync("{}", CancellationToken.None);
// Give the probe loop one tick or two to log.
await Task.Delay(250);
await drv.ShutdownAsync(CancellationToken.None);
// We expect at least one log entry at Debug / Warning that mentions the simulated
// failure or the probe loop. Without logging there's literally no record on a wedged
// CNC — exactly the gap the finding called out.
logger.Entries.ShouldNotBeEmpty();
logger.Entries.Any(e => e.Message.Contains("probe", StringComparison.OrdinalIgnoreCase)
|| (e.Exception?.Message.Contains("simulated probe failure") ?? false))
.ShouldBeTrue("at least one log entry should reference the probe loop or surface the swallowed exception");
}
private sealed class CapturingLogger : ILogger
{
public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = 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), exception));
}
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
public void Dispose() { }
}
}
}