From ff6f1debbdc43aee19a5817d6f963f8ca11f6fbc Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:26:18 +0200 Subject: [PATCH 1/5] feat(logging): add logging support for TUnit test framework --- docs/preview/03-Features/03-logging.mdx | 40 +++++++- .../Arcus.Testing.Logging.TUnit.csproj | 35 +++++++ .../Extensions/ILoggerBuilderExtensions.cs | 59 ++++++++++++ .../TUnitTestLogger.cs | 93 +++++++++++++++++++ .../Arcus.Testing.Tests.Unit.csproj | 1 + .../Fixture/InMemoryTUnitTestLogger.cs | 36 +++++++ .../Logging/Fixture/MockTestContext.cs | 2 +- .../Logging/ILoggerBuilderExtensionsTests.cs | 34 ++++++- .../Logging/TUnitTestLoggerTests.cs | 29 ++++++ src/Arcus.Testing.sln | 7 ++ 10 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj create mode 100644 src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs create mode 100644 src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs create mode 100644 src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs create mode 100644 src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs diff --git a/docs/preview/03-Features/03-logging.mdx b/docs/preview/03-Features/03-logging.mdx index 7bfbc005..40f399de 100644 --- a/docs/preview/03-Features/03-logging.mdx +++ b/docs/preview/03-Features/03-logging.mdx @@ -10,7 +10,7 @@ This page describes functionality related to logging in tests. - The `Arcus.Testing.Logging.Xunit` library provides a `XunitTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) + The `Arcus.Testing.Logging.Xunit` library provides a `XunitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) inside the [xUnit](https://xunit.net/) test framework. **Installation** @@ -45,7 +45,7 @@ This page describes functionality related to logging in tests. * [`AddXunitTestLogging`] extension to add a `ILoggerProvider` to a [Microsoft Logging](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1) setup. - The `Arcus.Testing.Logging.NUnit` library provides a `NUnitTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) + The `Arcus.Testing.Logging.NUnit` library provides a `NUnitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) inside the [NUnit](https://nunit.org/) test framework. **Installation** @@ -80,7 +80,7 @@ This page describes functionality related to logging in tests. * [`AddNUnitTestLogging`] extension to add a `ILoggerProvider` to a [Microsoft Logging](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1) setup. - The `Arcus.Testing.Logging.MSTest` library provides a `MSTestLogger` type that's an implementation of the abstracted Microsoft [`Ilogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) + The `Arcus.Testing.Logging.MSTest` library provides a `MSTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) inside the [MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) test framework. **Installation** @@ -110,4 +110,38 @@ This page describes functionality related to logging in tests. In the same fashion there is a: * [`AddMSTestLogging`] extension to add a `ILoggerProvider` to a [Microsoft Logging](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1) setup. + + The `Arcus.Testing.Logging.TUnit` library provides a `TUnitTestLogger` type that's an implementation of the abstracted Microsoft [`ILogger`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) inside the [TUnit](https://tunit.dev/) test framework. + + **Installation** + + The following functionality is available when installing this package: + + ```powershell + PM> Install-Package -Name Arcus.Testing.Logging.TUnit + ``` + + **Example** + + Log messages written to the `ILogger` instance will be written to the `TUnit`'s test logger. + + ```csharp + using Arcus.Testing; + using Microsoft.Extensions.Logging; + using TUnit.Core.Logging; + + public class TestClass + { + [Test] + public async Task TestMethod() + { + var tunitTestLogger = TestContext.Current.GetDefaultLogger(); + ILogger logger = new TUnitTestLogger(tunitTestLogger); + } + } + ``` + + In the same fashion there is a: + * [`AddTUnitTestLogging`] extension to add a `ILoggerProvider` to a [Microsoft Logging](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1) setup. + \ No newline at end of file diff --git a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj new file mode 100644 index 00000000..11a3a8df --- /dev/null +++ b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + Arcus.Testing + Arcus + Arcus + Provides logging capabilities during testing within the TUnit framework + Copyright (c) Arcus + https://github.com/arcus-azure/arcus.testing + https://github.com/arcus-azure/arcus.testing + LICENSE + icon.png + README.md + Git + Azure;Testing + Arcus.Testing.Logging.TUnit + true + true + true + $(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904 + + + + + + + + + + + + + + diff --git a/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs new file mode 100644 index 00000000..1be83da7 --- /dev/null +++ b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs @@ -0,0 +1,59 @@ +using System; +using Arcus.Testing; +using ITUnitLogger = TUnit.Core.Logging.ILogger; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.Logging +{ + /// + /// Extensions on the related to logging. + /// + // ReSharper disable once InconsistentNaming + public static class ILoggerBuilderExtensions + { + /// + /// Adds the logging messages from the given TUnit as a provider to the . + /// + /// The logging builder to add the NUnit logging test messages to. + /// The TUnit test writer to write custom test output. + /// Thrown when the or the is null. + public static ILoggingBuilder AddTUnitTestLogging(this ILoggingBuilder builder, ITUnitLogger outputWriter) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(outputWriter); + + return builder.AddProvider(new TUnitLoggerProvider(outputWriter)); + } + + [ProviderAlias("TUnit")] + private sealed class TUnitLoggerProvider : ILoggerProvider + { + private readonly ITUnitLogger _testLogger; + + /// + /// Initializes a new instance of the class. + /// + internal TUnitLoggerProvider(ITUnitLogger testLogger) + { + _testLogger = testLogger; + } + + /// + /// Creates a new instance. + /// + /// The category name for messages produced by the logger. + /// The instance of that was created. + public ILogger CreateLogger(string categoryName) + { + return new TUnitTestLogger(_testLogger); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + } + } + } +} diff --git a/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs new file mode 100644 index 00000000..c532b3a9 --- /dev/null +++ b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.Extensions.Logging; +using ITUnitLogger = TUnit.Core.Logging.ILogger; +using TUnitLogLevel = TUnit.Core.Logging.LogLevel; + +namespace Arcus.Testing +{ + /// + /// representation of a TUnit logger. + /// + public class TUnitTestLogger : ILogger + { + private readonly ITUnitLogger _outputWriter; + + /// + /// Initializes a new instance of the class. + /// + /// The TUnit test writer to write custom test output. + /// Thrown when the is null. + public TUnitTestLogger(ITUnitLogger outputWriter) + { + ArgumentNullException.ThrowIfNull(outputWriter); + _outputWriter = outputWriter; + } + + /// + /// Writes a log entry. + /// + /// Entry will be written on this level. + /// Id of the event. + /// The entry to be written. Can be also an object. + /// The exception related to this entry. + /// Function to create a message of the and . + /// The type of the object to be written. + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + TUnitLogLevel level = ConvertToTUnitLogLevel(logLevel); + + _outputWriter.Log(level, state, exception, formatter); + } + + /// + /// Checks if the given is enabled. + /// + /// Level to be checked. + /// true if enabled. + public bool IsEnabled(LogLevel logLevel) + { + TUnitLogLevel level = ConvertToTUnitLogLevel(logLevel); + return _outputWriter.IsEnabled(level); + } + + private static TUnitLogLevel ConvertToTUnitLogLevel(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Critical => TUnitLogLevel.Critical, + LogLevel.Error => TUnitLogLevel.Error, + LogLevel.Warning => TUnitLogLevel.Warning, + LogLevel.Information => TUnitLogLevel.Information, + LogLevel.Debug => TUnitLogLevel.Debug, + LogLevel.Trace => TUnitLogLevel.Trace, + LogLevel.None => TUnitLogLevel.None, + _ => TUnitLogLevel.None + }; + } + + /// + /// Begins a logical operation scope. + /// + /// The identifier for the scope. + /// The type of the state to begin scope for. + /// An that ends the logical operation scope on dispose. + public IDisposable BeginScope(TState state) where TState : notnull + { + return null; + } + } + + /// + /// representation of a TUnit logger. + /// + /// The type whose name is used for the logger category name. + public class TUnitTestLogger : TUnitTestLogger, ILogger + { + /// + /// Initializes a new instance of the class. + /// + public TUnitTestLogger(ITUnitLogger outputWriter) : base(outputWriter) + { + } + } +} diff --git a/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj b/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj index 7bdce1dd..d8c921cb 100644 --- a/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj +++ b/src/Arcus.Testing.Tests.Unit/Arcus.Testing.Tests.Unit.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs new file mode 100644 index 00000000..a8bd9fb1 --- /dev/null +++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using TUnit.Core.Logging; +using Xunit; + +namespace Arcus.Testing.Tests.Unit.Logging.Fixture +{ + public class MockTUnitTestLogger : ILogger + { + internal Collection<(LogLevel level, string message)> Logs { get; } = []; + + public ValueTask LogAsync(LogLevel logLevel, TState state, Exception exception, Func formatter) + { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, TState state, Exception exception, Func formatter) + { + Logs.Add((logLevel, formatter(state, exception))); + } + + public bool IsEnabled(LogLevel logLevel) + { + throw new NotImplementedException(); + } + + /// + /// Verifies that there was a written for the given to this logger. + /// + public void VerifyWritten(Microsoft.Extensions.Logging.LogLevel level, string message) + { + Assert.Contains(Logs, log => (int) level == (int) log.level && message == log.message); + } + } +} \ No newline at end of file diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs index 4acca961..14be0bb2 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs @@ -1,8 +1,8 @@ using System; using System.Collections; using System.Collections.ObjectModel; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Assert = Xunit.Assert; +using TestContext = Microsoft.VisualStudio.TestTools.UnitTesting.TestContext; namespace Arcus.Testing.Tests.Unit.Logging.Fixture { diff --git a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs index 7a74910e..eddb60a6 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs @@ -149,12 +149,44 @@ public void AddXunitTestLogging_WithoutXunitTestLogger_Throws() { // Arrange var builder = new HostBuilder(); - + // Act builder.ConfigureLogging(logging => logging.AddXunitTestLogging(outputWriter: null)); // Assert Assert.ThrowsAny(() => builder.Build()); } + + [Fact] + public void AddTUnitTestLogging_WithTestLogger_LogsMessage() + { + // Arrange + var mockLogger = new MockTUnitTestLogger(); + var builder = new HostBuilder(); + + // Act + builder.ConfigureLogging(logging => logging.AddTUnitTestLogging(mockLogger)); + + // Assert + using IHost host = builder.Build(); + var logger = host.Services.GetRequiredService>(); + + string expected = Bogus.Lorem.Sentence(); + logger.LogInformation(expected); + mockLogger.VerifyWritten(LogLevel.Information, expected); + } + + [Fact] + public void AddTUnitTestLogging_WithoutTestLogger_Throws() + { + // Arrange + var builder = new HostBuilder(); + + // Act + builder.ConfigureLogging(logging => logging.AddTUnitTestLogging(outputWriter: null)); + + // Assert + Assert.ThrowsAny(() => builder.Build()); + } } } diff --git a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs new file mode 100644 index 00000000..5723a432 --- /dev/null +++ b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs @@ -0,0 +1,29 @@ +using Arcus.Testing.Tests.Unit.Logging.Fixture; +using Bogus; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Arcus.Testing.Tests.Unit.Logging +{ + public class TUnitTestLoggerTests + { + private static readonly Faker Bogus = new(); + + [Fact] + public void Log_WithLevel_SucceedsWithMicrosoftLevel() + { + // Arrange + string expectedMessage = Bogus.Lorem.Sentence(); + var expectedLevel = Bogus.PickRandom(); + + var mockLogger = new MockTUnitTestLogger(); + var logger = new TUnitTestLogger(mockLogger); + + // Act + logger.Log(expectedLevel, expectedMessage); + + // Assert + mockLogger.VerifyWritten(expectedLevel, expectedMessage); + } + } +} diff --git a/src/Arcus.Testing.sln b/src/Arcus.Testing.sln index 5ddbe02d..bdc47a85 100644 --- a/src/Arcus.Testing.sln +++ b/src/Arcus.Testing.sln @@ -48,6 +48,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Messaging", "Messaging", "{4992B48D-3C17-45A9-979B-B79CE1E987CB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arcus.Testing.Logging.TUnit", "Arcus.Testing.Logging.TUnit\Arcus.Testing.Logging.TUnit.csproj", "{2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,6 +110,10 @@ Global {74AB9F6E-791F-4609-96BD-15420C25AA72}.Debug|Any CPU.Build.0 = Debug|Any CPU {74AB9F6E-791F-4609-96BD-15420C25AA72}.Release|Any CPU.ActiveCfg = Release|Any CPU {74AB9F6E-791F-4609-96BD-15420C25AA72}.Release|Any CPU.Build.0 = Release|Any CPU + {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -125,6 +131,7 @@ Global {4F199527-761E-4F8A-AB44-8DE8D030518F} = {4992B48D-3C17-45A9-979B-B79CE1E987CB} {54A67E0F-270B-4979-9E72-C68EA8222F89} = {FA2E21E0-953E-4B84-9C47-C4F0A3833E4E} {74AB9F6E-791F-4609-96BD-15420C25AA72} = {FA2E21E0-953E-4B84-9C47-C4F0A3833E4E} + {2A0C03CC-3D26-4AC4-A4B8-3B7F4DB418EF} = {45B1870D-C19A-4680-BDD8-2F670C793BC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E5382820-51FF-4B00-92BE-C78E80EA0841} From 18be50c48123aa4a1203572cbb4dc47cd5a5b1e9 Mon Sep 17 00:00:00 2001 From: stijnmoreels <9039753+stijnmoreels@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:49:33 +0200 Subject: [PATCH 2/5] chore: remove unnecessary namespace --- src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs index 5723a432..7e5fd6fa 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs @@ -14,7 +14,7 @@ public void Log_WithLevel_SucceedsWithMicrosoftLevel() { // Arrange string expectedMessage = Bogus.Lorem.Sentence(); - var expectedLevel = Bogus.PickRandom(); + var expectedLevel = Bogus.PickRandom(); var mockLogger = new MockTUnitTestLogger(); var logger = new TUnitTestLogger(mockLogger); From 2eaa54da210bf4d7ccf88e3e0680fcd6a0ff99d6 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Thu, 19 Jun 2025 09:21:00 +0200 Subject: [PATCH 3/5] fix(merge): finalize merge with 'main' --- src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs index 28f5bb6a..f43af84b 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/MockTestContext.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.ObjectModel; -using Assert = Xunit.Assert; +using Microsoft.VisualStudio.TestTools.UnitTesting; using TestContext = Microsoft.VisualStudio.TestTools.UnitTesting.TestContext; namespace Arcus.Testing.Tests.Unit.Logging.Fixture From 6a14363fd3625e844ccb5fcd0a825575e38b85a8 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:55:22 +0100 Subject: [PATCH 4/5] Update Arcus.Testing.Logging.TUnit.csproj --- .../Arcus.Testing.Logging.TUnit.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj index 11a3a8df..8787fb0b 100644 --- a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj +++ b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 Arcus.Testing Arcus Arcus @@ -28,8 +28,8 @@ - - + + From 7084b4fbc58e20c0560d61c172d875e827c04dcf Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:31:29 +0100 Subject: [PATCH 5/5] chore(tunit): include scope + category names in tunit logging --- .../Arcus.Testing.Logging.TUnit.csproj | 6 ++++ .../Extensions/ILoggerBuilderExtensions.cs | 19 ++++++++-- .../TUnitTestLogger.cs | 35 ++++++++++++++++--- .../Fixture/InMemoryTUnitTestLogger.cs | 15 ++++++-- .../Logging/ILoggerBuilderExtensionsTests.cs | 5 ++- .../Logging/TUnitTestLoggerTests.cs | 5 +-- 6 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj index 11a3a8df..15e724a6 100644 --- a/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj +++ b/src/Arcus.Testing.Logging.TUnit/Arcus.Testing.Logging.TUnit.csproj @@ -19,8 +19,14 @@ true true $(WarningsNotAsErrors);NU1901;NU1902;NU1903;NU1904 + S1133 + All + + + + diff --git a/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs index 1be83da7..70c2ea8e 100644 --- a/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs +++ b/src/Arcus.Testing.Logging.TUnit/Extensions/ILoggerBuilderExtensions.cs @@ -22,13 +22,17 @@ public static ILoggingBuilder AddTUnitTestLogging(this ILoggingBuilder builder, ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(outputWriter); - return builder.AddProvider(new TUnitLoggerProvider(outputWriter)); +#pragma warning disable CA2000 // Responsibility of disposing the created object is transferred to the caller + var provider = new TUnitLoggerProvider(outputWriter); +#pragma warning restore CA2000 + return builder.AddProvider(provider); } [ProviderAlias("TUnit")] - private sealed class TUnitLoggerProvider : ILoggerProvider + private sealed class TUnitLoggerProvider : ILoggerProvider, ISupportExternalScope { private readonly ITUnitLogger _testLogger; + private IExternalScopeProvider _scopeProvider; /// /// Initializes a new instance of the class. @@ -45,7 +49,16 @@ internal TUnitLoggerProvider(ITUnitLogger testLogger) /// The instance of that was created. public ILogger CreateLogger(string categoryName) { - return new TUnitTestLogger(_testLogger); + return new TUnitTestLogger(_testLogger, _scopeProvider, categoryName); + } + + /// + /// Sets external scope information source for logger provider. + /// + /// The provider of scope data. + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; } /// diff --git a/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs index c532b3a9..6341ddfb 100644 --- a/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs +++ b/src/Arcus.Testing.Logging.TUnit/TUnitTestLogger.cs @@ -11,6 +11,8 @@ namespace Arcus.Testing public class TUnitTestLogger : ILogger { private readonly ITUnitLogger _outputWriter; + private readonly IExternalScopeProvider _scopeProvider; + private readonly string _categoryName; /// /// Initializes a new instance of the class. @@ -18,9 +20,23 @@ public class TUnitTestLogger : ILogger /// The TUnit test writer to write custom test output. /// Thrown when the is null. public TUnitTestLogger(ITUnitLogger outputWriter) + : this(outputWriter, scopeProvider: null, categoryName: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The TUnit test writer to write custom test output. + /// The instance to provide logging scopes. + /// The category name for messages produced by the logger. + /// Thrown when the is null. + internal TUnitTestLogger(ITUnitLogger outputWriter, IExternalScopeProvider scopeProvider, string categoryName) { ArgumentNullException.ThrowIfNull(outputWriter); _outputWriter = outputWriter; + _scopeProvider = scopeProvider; + _categoryName = categoryName; } /// @@ -36,7 +52,18 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { TUnitLogLevel level = ConvertToTUnitLogLevel(logLevel); - _outputWriter.Log(level, state, exception, formatter); + ArgumentNullException.ThrowIfNull(formatter); + string message = formatter(state, exception); + + var builder = new LogMessageBuilder(logLevel); + builder.AddCategory(_categoryName) + .AddUserMessage(message) + .AddException(exception); + + _scopeProvider?.ForEachScope((st, lb) => lb.AddScope(st), builder); + + string result = builder.ToString(); + _outputWriter.Log(level, result, exception, formatter: (st, _) => st); } /// @@ -60,7 +87,6 @@ private static TUnitLogLevel ConvertToTUnitLogLevel(LogLevel logLevel) LogLevel.Information => TUnitLogLevel.Information, LogLevel.Debug => TUnitLogLevel.Debug, LogLevel.Trace => TUnitLogLevel.Trace, - LogLevel.None => TUnitLogLevel.None, _ => TUnitLogLevel.None }; } @@ -73,7 +99,7 @@ private static TUnitLogLevel ConvertToTUnitLogLevel(LogLevel logLevel) /// An that ends the logical operation scope on dispose. public IDisposable BeginScope(TState state) where TState : notnull { - return null; + return _scopeProvider?.Push(state); } } @@ -86,7 +112,8 @@ public class TUnitTestLogger : TUnitTestLogger, ILogger /// Initializes a new instance of the class. /// - public TUnitTestLogger(ITUnitLogger outputWriter) : base(outputWriter) + public TUnitTestLogger(ITUnitLogger outputWriter) + : base(outputWriter, scopeProvider: null, categoryName: typeof(TCategoryName).FullName) { } } diff --git a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs index a8bd9fb1..95337d78 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/Fixture/InMemoryTUnitTestLogger.cs @@ -28,9 +28,18 @@ public bool IsEnabled(LogLevel logLevel) /// /// Verifies that there was a written for the given to this logger. /// - public void VerifyWritten(Microsoft.Extensions.Logging.LogLevel level, string message) + public void VerifyWritten(Microsoft.Extensions.Logging.LogLevel level, string message, Exception exception = null, string state = null) { - Assert.Contains(Logs, log => (int) level == (int) log.level && message == log.message); + Assert.Contains(Logs, log => + { + bool hasException = exception is null || log.message.Contains(exception.Message); + bool hasState = state is null || log.message.Contains(state); + + return (int) level == (int) log.level + && log.message.Contains(message) + && hasState + && hasException; + }); } } -} \ No newline at end of file +} diff --git a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs index 3d41986d..9cd853c1 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/ILoggerBuilderExtensionsTests.cs @@ -185,9 +185,12 @@ public void AddTUnitTestLogging_WithTestLogger_LogsMessage() using IHost host = builder.Build(); var logger = host.Services.GetRequiredService>(); + string state = Bogus.Lorem.Word(); + using var _ = logger.BeginScope(state); + string expected = Bogus.Lorem.Sentence(); logger.LogInformation(expected); - mockLogger.VerifyWritten(LogLevel.Information, expected); + mockLogger.VerifyWritten(LogLevel.Information, expected, state: state); } [Fact] diff --git a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs index 7e5fd6fa..d8f6e88a 100644 --- a/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs +++ b/src/Arcus.Testing.Tests.Unit/Logging/TUnitTestLoggerTests.cs @@ -15,15 +15,16 @@ public void Log_WithLevel_SucceedsWithMicrosoftLevel() // Arrange string expectedMessage = Bogus.Lorem.Sentence(); var expectedLevel = Bogus.PickRandom(); + var exception = Bogus.System.Exception().OrNull(Bogus); var mockLogger = new MockTUnitTestLogger(); var logger = new TUnitTestLogger(mockLogger); // Act - logger.Log(expectedLevel, expectedMessage); + logger.Log(expectedLevel, exception, expectedMessage); // Assert - mockLogger.VerifyWritten(expectedLevel, expectedMessage); + mockLogger.VerifyWritten(expectedLevel, expectedMessage, exception); } } }