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); } }