using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; using ZB.MOM.WW.ScadaBridge.TestSupport; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Regression test for SiteRuntime-015 — must /// reuse a single, injected for every Instance Actor it /// creates rather than newing (and leaking) a fresh per /// instance. /// public class DeploymentManagerLoggerFactoryTests : TestKit, IDisposable { private readonly SiteStorageService _storage; private readonly ScriptCompilationService _compilationService; private readonly SharedScriptLibrary _sharedScriptLibrary; private readonly TestLocalDb _localDb; public DeploymentManagerLoggerFactoryTests() { _localDb = TestLocalDb.CreateTemp("dm-loggerfactory-test"); _storage = new SiteStorageService( _localDb.Db, NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService( NullLogger.Instance); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); } void IDisposable.Dispose() { // TestKit teardown first, so no in-flight actor can reach a disposed ILocalDb; // then dispose the database before deleting — the master connection anchors the WAL. Shutdown(); var path = _localDb.Path; _localDb.Dispose(); TestLocalDb.DeleteFiles(path); } private static string MakeConfigJson(string instanceName) { var config = new FlattenedConfiguration { InstanceUniqueName = instanceName, Attributes = [ new ResolvedAttribute { CanonicalName = "TestAttr", Value = "1", DataType = "Int32" } ] }; return JsonSerializer.Serialize(config); } /// /// Counts calls and records whether /// the factory was disposed. A passing test proves the single injected factory /// is the one used for every Instance Actor. /// private sealed class CountingLoggerFactory : ILoggerFactory { public int CreateLoggerCalls; public bool Disposed; public ILogger CreateLogger(string categoryName) { Interlocked.Increment(ref CreateLoggerCalls); return NullLogger.Instance; } public void AddProvider(ILoggerProvider provider) { } public void Dispose() => Disposed = true; } [Fact] public async Task CreateInstanceActor_ReusesInjectedLoggerFactory_ForEveryInstance() { // Pre-populate several enabled instances so startup creates multiple // Instance Actors. const int instanceCount = 6; for (int i = 0; i < instanceCount; i++) { var name = $"Inst{i}"; await _storage.StoreDeployedConfigAsync(name, MakeConfigJson(name), $"d{i}", $"h{i}", true); } var loggerFactory = new CountingLoggerFactory(); var actor = ActorOf(Props.Create(() => new DeploymentManagerActor( _storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions { StartupBatchSize = 100, StartupBatchDelayMs = 5 }, NullLogger.Instance, // dclManager, healthCollector, serviceProvider — padded positionally because // Props.Create is an expression tree and rejects out-of-position named args. null, null, null, loggerFactory: loggerFactory))); // Allow async startup (load configs + staggered creation). await Task.Delay(2000); // Every Instance Actor logger must come from the single injected factory. // Before the fix, each CreateInstanceActor allocated its own LoggerFactory, // so the injected factory would never be touched (CreateLoggerCalls == 0). Assert.Equal(instanceCount, loggerFactory.CreateLoggerCalls); } }