using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.Commons.Messages.Deployment; using ScadaLink.Commons.Types.Enums; using ScadaLink.Commons.Types.Flattening; using ScadaLink.SiteRuntime.Actors; using ScadaLink.SiteRuntime.Persistence; using ScadaLink.SiteRuntime.Scripts; using System.Text.Json; namespace ScadaLink.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 string _dbFile; public DeploymentManagerLoggerFactoryTests() { _dbFile = Path.Combine(Path.GetTempPath(), $"dm-loggerfactory-test-{Guid.NewGuid():N}.db"); _storage = new SiteStorageService( $"Data Source={_dbFile}", NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); _compilationService = new ScriptCompilationService( NullLogger.Instance); _sharedScriptLibrary = new SharedScriptLibrary( _compilationService, NullLogger.Instance); } void IDisposable.Dispose() { Shutdown(); try { File.Delete(_dbFile); } catch { /* cleanup */ } } 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, null, null, null, null, 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); } }