using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
/// The shared pool is process-wide, so the tests that touch it must not run in parallel.
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class AbTransportSharedPoolCollection
{
public const string Name = "AbTransportSharedPool";
}
///
/// Final review I2: DeviceSessionPool.Shared must log through the HOST logger factory, on driver
/// nodes (via the driver registry) and on admin-only nodes (via the AbCip / AbLegacy Test Connect probes).
/// Each test first resets the pool to a null logger and asserts that baseline, so deleting the wiring
/// call turns it red (the logger would stay the reset one).
///
[Trait("Category", "Unit")]
[Collection(AbTransportSharedPoolCollection.Name)]
public sealed class AbTransportSharedLoggerWiringTests
{
private static RecordingLoggerFactory Reset()
{
DeviceSessionPool.ConfigureSharedLogger(NullLoggerFactory.Instance);
var factory = new RecordingLoggerFactory();
AssertPoolLogsTo(factory, expected: false); // baseline: the wiring under test has not run yet
return factory;
}
// Logger wraps the factory's logger, so identity cannot be compared: emit through the pool's logger
// and check whether it lands in the recording factory under the pool's category.
private static void AssertPoolLogsTo(RecordingLoggerFactory factory, bool expected)
{
var marker = Guid.NewGuid().ToString("N");
DeviceSessionPool.Shared.Logger.LogWarning("wiring-probe {Marker}", marker);
factory.Messages(typeof(DeviceSessionPool).FullName!).Any(m => m.Contains(marker)).ShouldBe(expected);
}
[Fact]
public void Resolving_the_driver_registry_wires_the_pool_to_the_host_logger_factory()
{
var factory = Reset();
var services = new ServiceCollection();
services.AddSingleton(factory);
services.AddSingleton(new StubSecretResolver());
services.AddOtOpcUaDriverFactories();
using var sp = services.BuildServiceProvider();
sp.GetRequiredService();
AssertPoolLogsTo(factory, expected: true);
}
[Theory]
[InlineData(DriverTypeNames.AbCip)]
[InlineData(DriverTypeNames.AbLegacy)]
public void An_admin_only_node_wires_the_pool_through_the_AB_probes(string driverType)
{
var factory = Reset();
var services = new ServiceCollection();
services.AddSingleton(factory);
services.AddOtOpcUaDriverProbes(); // the admin-only path: no driver registry
using var sp = services.BuildServiceProvider();
sp.GetServices().Single(p => p.DriverType == driverType);
AssertPoolLogsTo(factory, expected: true);
}
private sealed class RecordingLoggerFactory : ILoggerFactory
{
private readonly List<(string Category, string Message)> _messages = [];
public IReadOnlyList Messages(string category)
{
lock (_messages) return [.. _messages.Where(m => m.Category == category).Select(m => m.Message)];
}
public ILogger CreateLogger(string categoryName) => new Recording(this, categoryName);
public void AddProvider(ILoggerProvider provider) { }
public void Dispose() { }
private sealed class Recording(RecordingLoggerFactory owner, string category) : 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 (owner._messages) owner._messages.Add((category, formatter(state, exception)));
}
}
}
private sealed class StubSecretResolver : ISecretResolver
{
public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(null);
}
}