From 8745138078fb1edae0e8c7edcce88cdc7f40e96a Mon Sep 17 00:00:00 2001 From: Kevin Ferrare Date: Wed, 17 Jun 2026 23:27:11 +0200 Subject: [PATCH] fix: Resource leak in the tests --- .../Emulator/Devices/Video/Renderer.cs | 4 +- src/Spice86.Logging/LoggerService.cs | 38 ++++++------------- .../Interfaces/ILoggerService.cs | 5 --- src/Spice86/Spice86DependencyInjection.cs | 34 +++++++++-------- .../CfgCpu/CfgGraphReloadTest.cs | 15 ++++---- .../CfgCpu/InstructionsFeederTest.cs | 2 +- .../CollectibleAssemblyLoadContext.cs | 17 +++++++++ .../CompiledGeneratedOverride.cs | 19 +++++++++- .../Spice86.Tests/GeneratedCodeMachineTest.cs | 2 +- .../GeneratedCodeMachineTestRunner.cs | 2 +- .../GeneratedOverrideCompiler.cs | 22 +++++++---- tests/Spice86.Tests/Spice86.Tests.csproj | 11 +++++- tests/Spice86.Tests/Spice86Creator.cs | 1 + .../Spice86.Tests/Video/RendererTestsBase.cs | 4 +- .../Video/VgaRenderer256ColorTestsBase.cs | 3 +- 15 files changed, 106 insertions(+), 73 deletions(-) create mode 100644 tests/Spice86.Tests/CollectibleAssemblyLoadContext.cs diff --git a/src/Spice86.Core/Emulator/Devices/Video/Renderer.cs b/src/Spice86.Core/Emulator/Devices/Video/Renderer.cs index f6c1550ea9..d11e12ee05 100644 --- a/src/Spice86.Core/Emulator/Devices/Video/Renderer.cs +++ b/src/Spice86.Core/Emulator/Devices/Video/Renderer.cs @@ -7,7 +7,7 @@ namespace Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.Devices.Video.Registers; using Spice86.Core.Emulator.Devices.Video.Registers.Graphics; using Spice86.Core.Emulator.Memory; -using Spice86.Logging; +using Spice86.Shared.Interfaces; using ClockSelect = Registers.General.MiscellaneousOutput.ClockSelectValue; @@ -74,7 +74,7 @@ public class Renderer : IVgaRenderer { /// Shared blink state for text-mode attribute blinking. /// The logger service implementation. /// The 256-color scanline renderer selected for the current CPU. - public Renderer(IMemory memory, IVideoState state, VgaBlinkState blinkState, LoggerService loggerService, IVgaRenderer256Color renderer256Color) { + public Renderer(IMemory memory, IVideoState state, VgaBlinkState blinkState, ILoggerService loggerService, IVgaRenderer256Color renderer256Color) { _state = state; _blinkState = blinkState; _renderer256Color = renderer256Color; diff --git a/src/Spice86.Logging/LoggerService.cs b/src/Spice86.Logging/LoggerService.cs index d921ef38b9..0b75af8695 100644 --- a/src/Spice86.Logging/LoggerService.cs +++ b/src/Spice86.Logging/LoggerService.cs @@ -14,7 +14,6 @@ public class LoggerService : ILoggerService, IDisposable { private static readonly object?[] EmptyProperties = []; - private LoggerConfiguration _loggerConfiguration; private Logger? _logger; private LoggingLevelSwitch _logLevelSwitch; private bool _disposed; @@ -25,8 +24,6 @@ public class LoggerService : ILoggerService, IDisposable { public LoggerService() { _logLevelSwitch = new LoggingLevelSwitch(); LoggerPropertyBag = new LoggerPropertyBag(); - _loggerConfiguration = CreateLoggerConfiguration(); - _loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch); } /// @@ -34,7 +31,6 @@ public LoggingLevelSwitch LogLevelSwitch { get => _logLevelSwitch; set { _logLevelSwitch = value ?? throw new ArgumentNullException(nameof(value)); - _loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch); ResetLogger(); } } @@ -49,28 +45,14 @@ public LoggingLevelSwitch LogLevelSwitch { public LoggerConfiguration CreateLoggerConfiguration() { LoggerConfiguration configuration = new LoggerConfiguration() .Enrich.FromLogContext() - .Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag)) - .WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat)) - .WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat)) - .WriteTo.Async(conf3 => - conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day)); + .Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag)); + configuration.WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat)); + configuration.WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat)); + configuration.WriteTo.Async(conf3 => + conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day)); return configuration; } - /// - public void UseStderrForConsoleOutput() { - _logger?.Dispose(); - _logger = null; - _loggerConfiguration = new LoggerConfiguration() - .Enrich.FromLogContext() - .Enrich.With(new LoggerPropertyBagEnricher(LoggerPropertyBag)) - .WriteTo.Async(conf => conf.Console(outputTemplate: LogFormat, standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose)) - .WriteTo.Async(conf2 => conf2.Debug(outputTemplate: LogFormat)) - .WriteTo.Async(conf3 => - conf3.File("logs/log-.txt", outputTemplate: LogFormat, rollingInterval: RollingInterval.Day)); - _loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch); - } - public void Write(LogEventLevel level, string messageTemplate) { GetLoggerForLevel(level)?.Write(level, messageTemplate); } @@ -139,8 +121,6 @@ public void Dispose() { private void ResetLogger() { _logger?.Dispose(); _logger = null; - _loggerConfiguration = CreateLoggerConfiguration(); - _loggerConfiguration.MinimumLevel.ControlledBy(_logLevelSwitch); } private Logger? GetLoggerForLevel(LogEventLevel level) { @@ -148,10 +128,16 @@ private void ResetLogger() { return null; } - _logger ??= _loggerConfiguration.CreateLogger(); + _logger ??= BuildLogger(); return _logger; } + private Logger BuildLogger() { + LoggerConfiguration configuration = CreateLoggerConfiguration(); + configuration.MinimumLevel.ControlledBy(_logLevelSwitch); + return configuration.CreateLogger(); + } + private static object?[] Normalize(object?[]? properties) { return properties is { Length: > 0 } ? properties : EmptyProperties; } diff --git a/src/Spice86.Shared/Interfaces/ILoggerService.cs b/src/Spice86.Shared/Interfaces/ILoggerService.cs index 1b2bc61094..aee7ff40a2 100644 --- a/src/Spice86.Shared/Interfaces/ILoggerService.cs +++ b/src/Spice86.Shared/Interfaces/ILoggerService.cs @@ -33,9 +33,4 @@ public interface ILoggerService : ILogger { /// /// The new LoggerConfiguration CreateLoggerConfiguration(); - - /// - /// Redirects console log output to stderr, freeing stdout for protocol transports (e.g., MCP stdio). - /// - void UseStderrForConsoleOutput(); } diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index 2ed6f944ce..594f27849d 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -726,24 +726,26 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai McpHttpHost? mcpHttpTransport = null; - // Collect additional MCP tool assemblies and services from override supplier - IEnumerable? additionalToolAssemblies = null; - IEnumerable? additionalMcpServices = null; - if (configuration.OverrideSupplier is IMcpToolSupplier mcpToolSupplier) { - additionalToolAssemblies = mcpToolSupplier.GetMcpToolAssemblies(); - additionalMcpServices = mcpToolSupplier.GetMcpServices(); - } + if (configuration.McpHttpPort != 0) { + // Collect additional MCP tool assemblies and services from override supplier + IEnumerable? additionalToolAssemblies = null; + IEnumerable? additionalMcpServices = null; + if (configuration.OverrideSupplier is IMcpToolSupplier mcpToolSupplier) { + additionalToolAssemblies = mcpToolSupplier.GetMcpToolAssemblies(); + additionalMcpServices = mcpToolSupplier.GetMcpServices(); + } - mcpHttpTransport = new McpHttpHost(loggerService); - try { - mcpHttpTransport.Start(emulatorMcpServices, configuration.McpHttpPort, additionalToolAssemblies, additionalMcpServices); - if (loggerService.IsEnabled(LogEventLevel.Information)) { - loggerService.Information("MCP HTTP transport started on port {Port}", configuration.McpHttpPort); + mcpHttpTransport = new McpHttpHost(loggerService); + try { + mcpHttpTransport.Start(emulatorMcpServices, configuration.McpHttpPort, additionalToolAssemblies, additionalMcpServices); + if (loggerService.IsEnabled(LogEventLevel.Information)) { + loggerService.Information("MCP HTTP transport started on port {Port}", configuration.McpHttpPort); + } + } catch (InvalidOperationException ex) { + loggerService.Warning(ex, "Failed to configure MCP HTTP transport on port {Port}; MCP HTTP will be unavailable", configuration.McpHttpPort); + mcpHttpTransport.Dispose(); + mcpHttpTransport = null; } - } catch (InvalidOperationException ex) { - loggerService.Warning(ex, "Failed to configure MCP HTTP transport on port {Port}; MCP HTTP will be unavailable", configuration.McpHttpPort); - mcpHttpTransport.Dispose(); - mcpHttpTransport = null; } if (loggerService.IsEnabled(LogEventLevel.Information)) { diff --git a/tests/Spice86.Tests/CfgCpu/CfgGraphReloadTest.cs b/tests/Spice86.Tests/CfgCpu/CfgGraphReloadTest.cs index 6a4bca7b45..60c5b2d43d 100644 --- a/tests/Spice86.Tests/CfgCpu/CfgGraphReloadTest.cs +++ b/tests/Spice86.Tests/CfgCpu/CfgGraphReloadTest.cs @@ -1,5 +1,7 @@ namespace Spice86.Tests.CfgCpu; +using NSubstitute; + using Spice86.Core.CLI; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.CPU.CfgCpu; @@ -12,7 +14,6 @@ namespace Spice86.Tests.CfgCpu; using Spice86.Core.Emulator.StateSerialization; using Spice86.Core.Emulator.StateSerialization.CfgReload; using Spice86.Core.Emulator.VM; -using Spice86.Logging; using Spice86.Shared.Emulator.Memory; using Spice86.Shared.Interfaces; using Spice86.Tests.Utility; @@ -95,7 +96,7 @@ public void ReloadedGraphMatchesOriginal(string binName) { // Reload into a fresh machine (no execution) and export. string reloadedJson; - using (LoggerService loggerService = new()) + ILoggerService loggerService = Substitute.For(); using (Spice86Creator creator = CreateCreator(binName)) using (Spice86DependencyInjection di = creator.Create()) { using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService); @@ -120,7 +121,7 @@ public void ReloadedGraphReExportsToSameDump(string binName) { // restore: typed instruction edges, per-node MaxSucc, selector dispatch edges and ids. CfgReloadDump original = CaptureDump(binName); - using LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); using Spice86Creator creator = CreateCreator(binName); using Spice86DependencyInjection di = creator.Create(); using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService); @@ -144,7 +145,7 @@ public void ReloadPreservesIdsAndSeedsAllocator(string binName) { HashSet expectedIds = new(scaledDump.Nodes.Select(n => n.Id).Concat(scaledDump.Blocks.Select(b => b.Id))); int maxId = expectedIds.Count == 0 ? -1 : expectedIds.Max(); - using LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); using Spice86Creator creator = CreateCreator(binName); using Spice86DependencyInjection di = creator.Create(); using CfgNodeExecutionCompiler compiler = NewCompiler(loggerService); @@ -173,7 +174,7 @@ public void NewDiscoveryAfterReloadGetsNonCollidingId(string binName) { HashSet reloadedIds = new(scaledDump.Nodes.Select(n => n.Id).Concat(scaledDump.Blocks.Select(b => b.Id))); int maxReloadedId = reloadedIds.Max(); - using LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); using Spice86Creator creator = CreateCreator(binName); using Spice86DependencyInjection di = creator.Create(); Machine machine = di.Machine; @@ -227,7 +228,7 @@ public void ResumeAfterReloadReconnectsAndPromotesToLive(string binName) { .Select(n => n.Id) .Single(); - using LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); using Spice86Creator creator = CreateCreator(binName); using Spice86DependencyInjection di = creator.Create(); Machine machine = di.Machine; @@ -262,7 +263,7 @@ public void PromotedReloadedNodeHasMemoryWriteBreakpoint(string binName) { (CfgReloadDump dump, byte[] memoryImage) = CaptureDumpAndMemory(binName); SegmentedAddress entryAddress = ParseAddress(dump.EntryPoints[0]); - using LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); using Spice86Creator creator = CreateCreator(binName); using Spice86DependencyInjection di = creator.Create(); Machine machine = di.Machine; diff --git a/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs b/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs index c9e1763ca1..77977ba32a 100644 --- a/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs +++ b/tests/Spice86.Tests/CfgCpu/InstructionsFeederTest.cs @@ -41,7 +41,7 @@ private InstructionsFeeder CreateInstructionsFeeder() { } private InstructionsFeeder CreateInstructionsFeeder(IMmu mmu) { - ILoggerService loggerService = Substitute.For(); + ILoggerService loggerService = Substitute.For(); State state = new(CpuModel.INTEL_80286); AddressReadWriteBreakpoints memoryBreakpoints = new(); AddressReadWriteBreakpoints ioBreakpoints = new(); diff --git a/tests/Spice86.Tests/CollectibleAssemblyLoadContext.cs b/tests/Spice86.Tests/CollectibleAssemblyLoadContext.cs new file mode 100644 index 0000000000..a2d6ddda44 --- /dev/null +++ b/tests/Spice86.Tests/CollectibleAssemblyLoadContext.cs @@ -0,0 +1,17 @@ +namespace Spice86.Tests; + +using System.Reflection; +using System.Runtime.Loader; + +/// +/// Collectible load context used to host a single compiled generated-override assembly so it can be +/// unloaded once the test that produced it completes, instead of permanently growing the default context. +/// +internal sealed class CollectibleAssemblyLoadContext : AssemblyLoadContext { + public CollectibleAssemblyLoadContext() : base(isCollectible: true) { + } + + protected override Assembly? Load(AssemblyName assemblyName) { + return null; + } +} diff --git a/tests/Spice86.Tests/CompiledGeneratedOverride.cs b/tests/Spice86.Tests/CompiledGeneratedOverride.cs index 8b00b2fe6d..5be56c530e 100644 --- a/tests/Spice86.Tests/CompiledGeneratedOverride.cs +++ b/tests/Spice86.Tests/CompiledGeneratedOverride.cs @@ -2,6 +2,21 @@ namespace Spice86.Tests; using Spice86.Core.Emulator.Function; -internal sealed class CompiledGeneratedOverride(IOverrideSupplier supplier) { - public IOverrideSupplier Supplier { get; } = supplier; +using System.Runtime.Loader; + +internal sealed class CompiledGeneratedOverride : IDisposable { + private readonly AssemblyLoadContext _loadContext; + + public CompiledGeneratedOverride(AssemblyLoadContext loadContext, IOverrideSupplier supplier) { + _loadContext = loadContext; + Supplier = supplier; + } + + public IOverrideSupplier Supplier { get; } + + public void Dispose() { + if (_loadContext.IsCollectible) { + _loadContext.Unload(); + } + } } diff --git a/tests/Spice86.Tests/GeneratedCodeMachineTest.cs b/tests/Spice86.Tests/GeneratedCodeMachineTest.cs index 1eb5e87b35..05f97dd15c 100644 --- a/tests/Spice86.Tests/GeneratedCodeMachineTest.cs +++ b/tests/Spice86.Tests/GeneratedCodeMachineTest.cs @@ -250,7 +250,7 @@ public void WideSegmentSpanProgramInitializesSegmentFieldsFromObservedConstants( source.Should().NotContain("relocationBaseSegment"); source.Should().NotContain("relocated from the runtime entry segment"); // The generated source still compiles. - new GeneratedOverrideCompiler().CompileSupplier(source); + using CompiledGeneratedOverride compiledOverride = new GeneratedOverrideCompiler().CompileSupplier(source); } [Fact] diff --git a/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs b/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs index 397564d3a0..5b4f806d1f 100644 --- a/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs +++ b/tests/Spice86.Tests/GeneratedCodeMachineTestRunner.cs @@ -27,7 +27,7 @@ public void TestGeneratedCode(string binName, byte[] expected, long maxCycles = } public void TestGeneratedCode(string binName, byte[] expected, GeneratedCodeRunOptions options, Action? assertions = null) { - CompiledGeneratedOverride compiledOverride = GenerateAndCompileSupplier(binName, options); + using CompiledGeneratedOverride compiledOverride = GenerateAndCompileSupplier(binName, options); using Spice86Creator creator = new(binName: binName, maxCycles: options.MaxCycles, enablePit: options.EnablePit, installInterruptVectors: options.InstallInterruptVectors, failOnUnhandledPort: options.FailOnUnhandledPort, diff --git a/tests/Spice86.Tests/GeneratedOverrideCompiler.cs b/tests/Spice86.Tests/GeneratedOverrideCompiler.cs index f4404730ce..5ac49eefc0 100644 --- a/tests/Spice86.Tests/GeneratedOverrideCompiler.cs +++ b/tests/Spice86.Tests/GeneratedOverrideCompiler.cs @@ -12,12 +12,14 @@ namespace Spice86.Tests; using System.Runtime.Loader; internal sealed class GeneratedOverrideCompiler { + private static readonly MetadataReference[] PlatformMetadataReferences = CreateMetadataReferences(); + public CompiledGeneratedOverride CompileSupplier(string source) { SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview)); CSharpCompilation compilation = CSharpCompilation.Create( assemblyName: "Spice86.GeneratedCode.Tests." + Guid.NewGuid().ToString("N"), syntaxTrees: [syntaxTree], - references: GetMetadataReferences(), + references: PlatformMetadataReferences, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); using MemoryStream peStream = new(); @@ -30,14 +32,20 @@ public CompiledGeneratedOverride CompileSupplier(string source) { } peStream.Position = 0; - Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(peStream); - Type supplierType = assembly.GetTypes().Single(type => typeof(IOverrideSupplier).IsAssignableFrom(type) && !type.IsAbstract); - object supplierInstance = Activator.CreateInstance(supplierType) - ?? throw new InvalidOperationException($"Could not instantiate generated supplier type {supplierType.FullName}."); - return new CompiledGeneratedOverride((IOverrideSupplier)supplierInstance); + CollectibleAssemblyLoadContext loadContext = new(); + try { + Assembly assembly = loadContext.LoadFromStream(peStream); + Type supplierType = assembly.GetTypes().Single(type => typeof(IOverrideSupplier).IsAssignableFrom(type) && !type.IsAbstract); + object supplierInstance = Activator.CreateInstance(supplierType) + ?? throw new InvalidOperationException($"Could not instantiate generated supplier type {supplierType.FullName}."); + return new CompiledGeneratedOverride(loadContext, (IOverrideSupplier)supplierInstance); + } catch { + loadContext.Unload(); + throw; + } } - private static MetadataReference[] GetMetadataReferences() { + private static MetadataReference[] CreateMetadataReferences() { string trustedAssemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty; IEnumerable trustedPaths = trustedAssemblies.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); IEnumerable loadedPaths = AppDomain.CurrentDomain.GetAssemblies() diff --git a/tests/Spice86.Tests/Spice86.Tests.csproj b/tests/Spice86.Tests/Spice86.Tests.csproj index f70da68dad..4f6831015c 100644 --- a/tests/Spice86.Tests/Spice86.Tests.csproj +++ b/tests/Spice86.Tests/Spice86.Tests.csproj @@ -5,6 +5,15 @@ enable nullable false + + false + false @@ -84,4 +93,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/tests/Spice86.Tests/Spice86Creator.cs b/tests/Spice86.Tests/Spice86Creator.cs index 1963562000..cd8b9c33f1 100644 --- a/tests/Spice86.Tests/Spice86Creator.cs +++ b/tests/Spice86.Tests/Spice86Creator.cs @@ -74,6 +74,7 @@ public Spice86Creator(string binName, bool enablePit = false, RecordedDataDirectory = exportFolder, SilencedLogs = true, HttpApiPort = 0, + McpHttpPort = 0, // Deterministic cycle-based clock (CyclesClock) to avoid // wall-clock non-determinism in tests. // 333333 is the value that allowed most wall clock based tests to pass without changing all the expected values. diff --git a/tests/Spice86.Tests/Video/RendererTestsBase.cs b/tests/Spice86.Tests/Video/RendererTestsBase.cs index dbeee3e4e7..80e7d548d7 100644 --- a/tests/Spice86.Tests/Video/RendererTestsBase.cs +++ b/tests/Spice86.Tests/Video/RendererTestsBase.cs @@ -8,7 +8,7 @@ using Spice86.Core.Emulator.Devices.Video.Registers.CrtController; using Spice86.Core.Emulator.Devices.Video.Registers.Graphics; using Spice86.Core.Emulator.Memory; -using Spice86.Logging; +using Spice86.Shared.Interfaces; using Xunit; @@ -1408,7 +1408,7 @@ public void HorizontalPixelPanning_TextMode_9DotWithLineGraphics_AddsOnePan() { mockMemory.When(m => m.RegisterMapping(Arg.Any(), Arg.Any(), Arg.Any())) .Do(callInfo => captured = (VideoMemory)callInfo.ArgAt(2)); - LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); IVgaRenderer256Color renderer256Color = Create256ColorRenderer(); Renderer renderer = new(mockMemory, state, blinkState, loggerService, renderer256Color); diff --git a/tests/Spice86.Tests/Video/VgaRenderer256ColorTestsBase.cs b/tests/Spice86.Tests/Video/VgaRenderer256ColorTestsBase.cs index 8b34593ee0..83f6b19c13 100644 --- a/tests/Spice86.Tests/Video/VgaRenderer256ColorTestsBase.cs +++ b/tests/Spice86.Tests/Video/VgaRenderer256ColorTestsBase.cs @@ -7,7 +7,6 @@ namespace Spice86.Tests.Video; using Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.Devices.Video.Registers.CrtController; using Spice86.Core.Emulator.Memory; -using Spice86.Logging; using Spice86.Shared.Interfaces; using Xunit; @@ -42,7 +41,7 @@ protected void EnsureSupported() => mockMemory.When(m => m.RegisterMapping(Arg.Any(), Arg.Any(), Arg.Any())) .Do(callInfo => captured = (VideoMemory)callInfo.ArgAt(2)); - LoggerService loggerService = new(); + ILoggerService loggerService = Substitute.For(); VgaBlinkState blinkState = new(); IVgaRenderer256Color renderer256Color = Create256ColorRenderer(); Renderer renderer = new(mockMemory, state, blinkState, loggerService, renderer256Color);