diff --git a/.github/template-sync.yml b/.github/template-sync.yml deleted file mode 100644 index 982577b..0000000 --- a/.github/template-sync.yml +++ /dev/null @@ -1,34 +0,0 @@ -additional: - - analyzer - - arguments - - article.benchmarks - - editorconfig - - equaduct - - extensions.hosting.winforms - - extensions.logging.measurement - - extensions.logging.xunit - - extensions.strings - - extensions.tasks - - extensions.test - - guard - - healthchecks - - http.correlation - - sequentialguid - -files: - - "!**/*" - - ".editorconfig" - - ".gitattributes" - - ".gitignore" - - ".github/CODEOWNERS" - - ".github/dependabot.yml" - - ".github/FUNDING.yml" - - ".github/release-drafter.yml" - - ".github/ISSUE_TEMPLATE/**/*" - - ".github/PULL_REQUEST_TEMPLATE/**/*" - - ".github/workflows/update-license.yml" - - # you probably want to exclude these files: - - "!.github/workflows/dependabot-merge.yml" - - "!.github/workflows/template-sync.yml" - - "!.github/template-sync.yml" diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 39026ab..db637fe 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -24,9 +24,9 @@ jobs: name: Build & Tests uses: dailydevops/pipelines/.github/workflows/cicd-dotnet.yml@0.9.3 with: - disablePublish: true + enableSonarQube: true dotnet-logging: ${{ inputs.dotnet-logging }} dotnet-version: | 8.x - solution: ###SOLUTION### + solution: ./Logging.XUnit.sln secrets: inherit diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml deleted file mode 100644 index 3d0acf1..0000000 --- a/.github/workflows/template-sync.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Update template files - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - template-sync: - if: github.actor != 'dependabot[bot]' && github.repository == 'dailydevops/dotnet-template' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: ahmadnassri/action-template-repository-sync@v2.6.0 - with: - github-token: ${{ secrets.TEMPLATE_SYNC }} - dry-run: false - skip-ci: true diff --git a/.gitmodules b/.gitmodules index 33c81aa..a898072 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "eng"] path = eng url = https://github.com/dailydevops/dotnet-engineering.git - update = merge + update = rebase diff --git a/Directory.Build.props b/Directory.Build.props index 2958b68..b3f0ce0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,4 +9,11 @@ + + net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 + + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 78f453b..7ad1b69 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,18 +1,31 @@ - true true - - + - - + + - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Logging.XUnit.sln b/Logging.XUnit.sln new file mode 100644 index 0000000..5214a1e --- /dev/null +++ b/Logging.XUnit.sln @@ -0,0 +1,81 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9954CB8-DB50-4331-A461-36A4AD4DA06E}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .filenesting.json = .filenesting.json + .gitattributes = .gitattributes + .gitignore = .gitignore + .gitmodules = .gitmodules + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + Directory.Solution.props = Directory.Solution.props + GitVersion.yml = GitVersion.yml + LICENSE = LICENSE + new-project.ps1 = new-project.ps1 + nuget.config = nuget.config + README.md = README.md + update-solution.ps1 = update-solution.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EFE8181B-6FAB-4E04-BD9B-557C1286B858}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BD750CCE-0318-424D-89B4-9C66EF329E96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Logging.XUnit", "src\NetEvolve.Logging.XUnit\NetEvolve.Logging.XUnit.csproj", "{3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Logging.XUnit.Tests.Unit", "tests\NetEvolve.Logging.XUnit.Tests.Unit\NetEvolve.Logging.XUnit.Tests.Unit.csproj", "{EAAD2C22-48F4-4E37-9A46-0350C6B84388}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Logging.XUnit.Tests.Integration", "tests\NetEvolve.Logging.XUnit.Tests.Integration\NetEvolve.Logging.XUnit.Tests.Integration.csproj", "{F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Release|Any CPU.Build.0 = Release|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Release|Any CPU.Build.0 = Release|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF} = {EFE8181B-6FAB-4E04-BD9B-557C1286B858} + {EAAD2C22-48F4-4E37-9A46-0350C6B84388} = {BD750CCE-0318-424D-89B4-9C66EF329E96} + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12} = {BD750CCE-0318-424D-89B4-9C66EF329E96} + EndGlobalSection +EndGlobal +CPU.Build.0 = Release|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}.Release|Any CPU.Build.0 = Release|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAAD2C22-48F4-4E37-9A46-0350C6B84388}.Release|Any CPU.Build.0 = Release|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF} = {EFE8181B-6FAB-4E04-BD9B-557C1286B858} + {EAAD2C22-48F4-4E37-9A46-0350C6B84388} = {BD750CCE-0318-424D-89B4-9C66EF329E96} + {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12} = {BD750CCE-0318-424D-89B4-9C66EF329E96} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 0e844c8..dbc0860 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# template-dotnet -.NET template for repositories +# NetEvolve.Logging.XUnit + +This library provides a logging implementation for [XUnit](https://xunit.net/). When using this library, you have the ability to access the logs generated while executing your tests. This can be useful for debugging purposes. \ No newline at end of file diff --git a/new-project.ps1 b/new-project.ps1 index fe28792..bd80458 100644 --- a/new-project.ps1 +++ b/new-project.ps1 @@ -49,7 +49,7 @@ New-Project ` -DisableTests $DisableTests ` -DisableUnitTests $DisableUnitTests ` -DisableIntegrationTests $DisableIntegrationTests ` - -SolutionFile "###SOLUTION###" ` + -SolutionFile "./Logging.XUnit.sln" ` -OutputDirectory (Get-Location) ` -EnableProjectGrouping $EnableProjectGrouping ` -DisableArchitectureTests $DisableArchitectureTests diff --git a/new-solution.ps1 b/new-solution.ps1 deleted file mode 100644 index d517277..0000000 --- a/new-solution.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -[CmdletBinding()] -param ( - # Name of the solution to be created. - [Parameter(Mandatory = $true)] - [string] - $SolutionName -) - -Write-Output "Updating submodules ..." -git submodule update --init --recursive --remote | Out-Null - -Write-Output "Creating $SolutionName.sln ..." -$location = Get-Location -. .\eng\scripts\new-solution.ps1 - -New-Solution -SolutionName $SolutionName -Output $location -Remove-Item -Path "$location\new-solution.ps1" -Force -Remove-Item -Path "$location\.github\template-sync.yml" -Force -Remove-Item -Path "$location\.github\workflows\template-sync.yml" -Force diff --git a/src/NetEvolve.Logging.XUnit/IXUnitLoggerOptions.cs b/src/NetEvolve.Logging.XUnit/IXUnitLoggerOptions.cs new file mode 100644 index 0000000..5fe98ed --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/IXUnitLoggerOptions.cs @@ -0,0 +1,32 @@ +namespace NetEvolve.Logging.XUnit; + +/// +/// Accessor for the options of the . +/// +public interface IXUnitLoggerOptions +{ + /// + /// Disables the output of the additional information in the log output. Default . + /// + bool DisableAdditionalInformation { get; } + + /// + /// Disable the log level in the log output. Default . + /// + bool DisableLogLevel { get; } + + /// + /// Disable the scopes in the log output. Default . + /// + bool DisableScopes { get; } + + /// + /// Disables the timestamps in the log output. Default . + /// + bool DisableTimestamp { get; } + + /// + /// The format of the timestamp in the log output. Default "o". + /// + string TimestampFormat { get; } +} diff --git a/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj new file mode 100644 index 0000000..2c4ae7a --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj @@ -0,0 +1,27 @@ + + + + $(ProjectTargetFrameworks) + enable + enable + + + + $(MSBuildProjectName) + Extensions for `ILogger` implementations to log messages to xUnit test output. + https://github.com/dailydevops/logging.xunit.git + https://github.com/dailydevops/logging.xunit.git + logging;provider;xunit + 2024 + + + + + + + + + + + + diff --git a/src/NetEvolve.Logging.XUnit/README.md b/src/NetEvolve.Logging.XUnit/README.md new file mode 100644 index 0000000..54f5861 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/README.md @@ -0,0 +1,67 @@ +# NetEvolve.Logging.XUnit + +This library provides a logging implementation for [XUnit](https://xunit.net/). When using this library, you have the ability to access the logs generated while executing your tests. This can be useful for debugging purposes. + +## Installation +```bash +dotnet add package NetEvolve.Logging.XUnit +``` + +## Usage + +You can choose to use the `XUnitLogger` class directly or use the `AddXUnit` extension method on the `ILoggingBuilder` instance. + +### Direct usage + +```csharp +using Microsoft.Extensions.Logging; +using NetEvolve.Logging.XUnit; +using XUnit; + +public class ExampleTests +{ + private readonly ITestOutputHelper _output; + + public ExampleTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Test() + { + var logger = XUnitLogger.CreateLogger(_output); + + // Arrange + ... + // Act + ... + // Assert + ... + + Assert.NotEmpty(logger.LoggedMessages); + } +} +``` + +### Usage with `ILoggingBuilder.AddXUnit` + +Or you can use the `AddXUnit` extension method on the `ILoggingBuilder` instance. + +```csharp +using Microsoft.Extensions.Logging; +using NetEvolve.Logging.XUnit; + +var services = new ServiceCollection(); +services.AddLogging(builder => +{ + // Add the XUnit logging implementation + builder.AddXUnit(); + + // Or alternatively with options + builder.AddXUnit(options => + { + options.TimestampFormat = "HH:mm:ss.fff"; + }); +}); +``` \ No newline at end of file diff --git a/src/NetEvolve.Logging.XUnit/XUnitLogger.cs b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs new file mode 100644 index 0000000..809cf08 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs @@ -0,0 +1,312 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using NetEvolve.Arguments; +using NetEvolve.Logging.Abstractions; +using Xunit.Abstractions; + +/// +/// Represents a logger that writes messages to xunit output. +/// +public class XUnitLogger : ILogger, ISupportExternalScope +{ + private readonly IXUnitLoggerOptions _options; + private readonly ITestOutputHelper _testOutputHelper; + private readonly TimeProvider _timeProvider; + + private readonly List _loggedMessages; + + private const int DefaultCapacity = 1024; + + [ThreadStatic] + private static StringBuilder? _builder; + + /// + /// Gets the external scope provider. + /// + public IExternalScopeProvider ScopeProvider { get; private set; } + + /// + public IReadOnlyList LoggedMessages => _loggedMessages.AsReadOnly(); + + /// + /// Creates a new instance of . + /// + /// The to write the log messages to. + /// The to use to get the current time. + /// The to use to get the current scope. + /// The options to control the behavior of the logger. + /// A cached or new instance of . + public static XUnitLogger CreateLogger( + ITestOutputHelper testOutputHelper, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider = null, + IXUnitLoggerOptions? options = null + ) + { + Argument.ThrowIfNull(testOutputHelper); + + return new XUnitLogger(testOutputHelper, timeProvider, scopeProvider, options); + } + + /// + /// Creates a new instance of . + /// + /// The type who's fullname is used as the category name for messages produced by the logger. + /// The to write the log messages to. + /// The to use to get the current time. + /// The to use to get the current scope. + /// The options to control the behavior of the logger. + /// A cached or new instance of . + public static XUnitLogger CreateLogger( + ITestOutputHelper testOutputHelper, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider = null, + IXUnitLoggerOptions? options = null + ) + where T : notnull => + new XUnitLogger(testOutputHelper, timeProvider, scopeProvider, options); + + private protected XUnitLogger( + ITestOutputHelper testOutputHelper, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider, + IXUnitLoggerOptions? options + ) + { + Argument.ThrowIfNull(testOutputHelper); + Argument.ThrowIfNull(timeProvider); + + ScopeProvider = scopeProvider ?? NullExternalScopeProvider.Instance; + _testOutputHelper = testOutputHelper; + _timeProvider = timeProvider; + _options = options ?? XUnitLoggerOptions.Default; + + _loggedMessages = []; + } + + /// + public IDisposable? BeginScope(TState state) + where TState : notnull => ScopeProvider.Push(state); + + /// + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + /// + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter + ) + { + Argument.ThrowIfNull(formatter); + + if (!IsEnabled(logLevel)) + { + return; + } + + var builder = _builder; + _builder = null; + builder ??= new StringBuilder(DefaultCapacity); + + try + { + var message = formatter(state, exception); + var now = _timeProvider.GetLocalNow(); + (builder, var scopes) = CreateMessage( + logLevel, + state, + exception, + builder, + message, + now + ); + + _loggedMessages.Add( + new LoggedMessage(now, logLevel, eventId, message, exception, scopes) + ); + _testOutputHelper.WriteLine(builder.ToString()); + } + catch + { + // Ignore exception. + // Unfortunately, this can happen if the process is terminated before the end of the test. + } + finally + { + _ = builder.Clear(); + if (builder.Capacity > DefaultCapacity) + { + builder.Capacity = DefaultCapacity; + } + _builder = builder; + } + } + + private (StringBuilder, List) CreateMessage( + LogLevel logLevel, + TState state, + Exception? exception, + StringBuilder builder, + string message, + DateTimeOffset now + ) + { + var scopes = new List(); + if (!_options.DisableTimestamp) + { + _ = builder + .Append(now.ToString(_options.TimestampFormat, CultureInfo.InvariantCulture)) + .Append(' '); + } + + if (!_options.DisableLogLevel) + { + _ = builder.Append('[').Append(LogLevelToString(logLevel)).Append("] "); + } + + _ = builder.Append(message); + + if (exception is not null) + { + _ = builder.Append('\n').Append(exception); + } + + if ( + !_options.DisableAdditionalInformation + && state is IReadOnlyList> additionalInformation + ) + { + _ = builder.Append('\n').Append('\t').Append("Additional Information"); + foreach (var info in additionalInformation) + { + AddAdditionalInformation(builder, info); + } + } + + ScopeProvider.ForEachScope(IterateScopes, builder); + + return (builder, scopes); + + void IterateScopes(object? scope, StringBuilder state) + { + if (scope is null) + { + return; + } + + scopes.Add(scope); + + if (!_options.DisableScopes) + { + PrintScope(scope, state); + } + } + } + + private static void PrintScope(object? scope, StringBuilder state) + { + if (scope is IEnumerable> scopeList) + { + foreach (var subScope in scopeList) + { + PrintScope(subScope, state); + } + + return; + } + + _ = state.Append('\n').Append(' ', 4).Append("=>").Append(' '); + + if (scope is KeyValuePair info) + { + _ = state.Append(info.Key).Append(": ").Append(info.Value); + } + else + { + _ = state.Append(scope); + } + } + + private static void AddAdditionalInformation( + StringBuilder builder, + KeyValuePair info + ) => _ = builder.Append('\n').Append(' ', 4).Append(info.Key).Append(": ").Append(info.Value); + + internal static string LogLevelToString(LogLevel logLevel) => + logLevel switch + { + LogLevel.Trace => "TRCE", + LogLevel.Debug => "DBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "FAIL", + LogLevel.Critical => "CRIT", + _ => "NONE" + }; + + /// + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + Argument.ThrowIfNull(scopeProvider); + + ScopeProvider = scopeProvider; + } + + /// + public override string ToString() + { + var builder = _builder; + _builder = null; + builder ??= new StringBuilder(DefaultCapacity); + + try + { + foreach (var lmsg in LoggedMessages) + { + if (!_options.DisableTimestamp) + { + _ = builder + .Append( + lmsg.Timestamp.ToString( + _options.TimestampFormat, + CultureInfo.InvariantCulture + ) + ) + .Append(' '); + } + + if (!_options.DisableLogLevel) + { + _ = builder.Append('[').Append(LogLevelToString(lmsg.LogLevel)).Append("] "); + } + + _ = builder.Append(lmsg.Message); + _ = builder.AppendLine(); + + if (lmsg.Exception is not null) + { + _ = builder.Append(lmsg.Exception).AppendLine(); + } + } + + return builder.ToString().Trim(); + } + finally + { + _ = builder.Clear(); + if (builder.Capacity > DefaultCapacity) + { + builder.Capacity = DefaultCapacity; + } + _builder = builder; + } + } +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs new file mode 100644 index 0000000..51356c9 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs @@ -0,0 +1,42 @@ +namespace NetEvolve.Logging.XUnit; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using NetEvolve.Arguments; +using Xunit.Abstractions; + +/// +/// Extensions for to add a xunit logger. +/// +public static class XUnitLoggerExtensions +{ + /// + /// Adds a xunit logger named `xunit` to the factory. + /// + public static ILoggingBuilder AddXUnit( + this ILoggingBuilder builder, + ITestOutputHelper testOutputHelper, + XUnitLoggerOptions? options = null + ) + { + Argument.ThrowIfNull(builder); + Argument.ThrowIfNull(testOutputHelper); + + var services = builder.Services.AddSingleton(testOutputHelper); + services.TryAddSingleton(_ => TimeProvider.System); + services.TryAddScoped(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton( + sp => new XUnitLoggerProvider( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + options ?? XUnitLoggerOptions.Default + ) + ) + ); + + return builder; + } +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs new file mode 100644 index 0000000..14accbe --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs @@ -0,0 +1,70 @@ +namespace NetEvolve.Logging.XUnit; + +/// +/// Options for the . +/// +public class XUnitLoggerOptions : IXUnitLoggerOptions +{ + /// + /// Default options, which disables the category name, additional information and scopes in the log output. + /// + public static XUnitLoggerOptions Default { get; } = + new XUnitLoggerOptions { DisableAdditionalInformation = true, DisableScopes = true }; + + /// + /// Disables all features in the log output. + /// + public static XUnitLoggerOptions DisableAllFeatures { get; } = + new XUnitLoggerOptions + { + DisableAdditionalInformation = true, + DisableLogLevel = true, + DisableScopes = true, + DisableTimestamp = true + }; + + /// + /// Enables all features in the log output. + /// + public static XUnitLoggerOptions EnableAllFeatures { get; } = + new XUnitLoggerOptions + { + DisableAdditionalInformation = false, + DisableLogLevel = false, + DisableScopes = false, + DisableTimestamp = false + }; + + /// + public bool DisableAdditionalInformation { get; set; } + + /// + public bool DisableLogLevel { get; set; } + + /// + public bool DisableScopes { get; set; } + + /// + public bool DisableTimestamp { get; set; } + + private string? _timestampFormat; + + /// +#if NET7_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.StringSyntax( + System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.DateTimeFormat + )] +#endif + public string TimestampFormat + { + get + { + if (string.IsNullOrWhiteSpace(_timestampFormat)) + { + return "o"; + } + return _timestampFormat; + } + set => _timestampFormat = value; + } +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs new file mode 100644 index 0000000..b920693 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs @@ -0,0 +1,96 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Microsoft.Extensions.Logging; +using NetEvolve.Arguments; +using NetEvolve.Logging.Abstractions; +using Xunit.Abstractions; + +[ProviderAlias("XUnit")] +internal sealed class XUnitLoggerProvider + : ILoggerProvider, + ISupportExternalScope, + IXUnitLoggerOptions +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly IExternalScopeProvider _scopeProvider = NullExternalScopeProvider.Instance; + private readonly XUnitLoggerOptions _options; + private readonly ConcurrentDictionary _loggers; + private readonly TimeProvider _timeProvider; + + internal ImmutableList Loggers => _loggers.Values.ToImmutableList(); + + /// + public bool DisableAdditionalInformation => _options.DisableAdditionalInformation; + + /// + public bool DisableLogLevel => _options.DisableLogLevel; + + /// + public bool DisableScopes => _options.DisableScopes; + + /// + public bool DisableTimestamp => _options.DisableTimestamp; + + /// + public string TimestampFormat => _options.TimestampFormat; + + public XUnitLoggerProvider( + ITestOutputHelper testOutputHelper, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider = null, + XUnitLoggerOptions? options = null + ) + { + Argument.ThrowIfNull(testOutputHelper); + Argument.ThrowIfNull(timeProvider); + + _scopeProvider = scopeProvider ?? new LoggerExternalScopeProvider(); + _options = options ?? XUnitLoggerOptions.Default; + + _timeProvider = timeProvider; + + _loggers = new ConcurrentDictionary(StringComparer.Ordinal); + _testOutputHelper = testOutputHelper; + } + + /// + public ILogger CreateLogger(string categoryName) + { + Argument.ThrowIfNullOrWhiteSpace(categoryName); + + return _loggers.GetOrAdd( + categoryName, + name => XUnitLogger.CreateLogger(_testOutputHelper, _timeProvider, _scopeProvider, this) + ); + } + + /// + public ILogger CreateLogger() + where T : notnull => + _loggers.GetOrAdd( + typeof(T).FullName!, + _ => XUnitLogger.CreateLogger(_testOutputHelper, _timeProvider, _scopeProvider, this) + ); + + /// + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + Argument.ThrowIfNull(scopeProvider); + + if (_scopeProvider == scopeProvider) + { + return; + } + + foreach (var logger in _loggers.Values) + { + logger.SetScopeProvider(scopeProvider); + } + } + + /// + public void Dispose() => _loggers.Clear(); +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs b/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs new file mode 100644 index 0000000..912608f --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLogger`T.cs @@ -0,0 +1,18 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +/// +public sealed class XUnitLogger : XUnitLogger, ILogger + where T : notnull +{ + internal XUnitLogger( + ITestOutputHelper testOutputHelper, + TimeProvider timeProvider, + IExternalScopeProvider? scopeProvider, + IXUnitLoggerOptions? options + ) + : base(testOutputHelper, timeProvider, scopeProvider, options) { } +} diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj b/tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj new file mode 100644 index 0000000..08b60ce --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj @@ -0,0 +1,30 @@ + + + + $(TestTargetFrameworks) + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerExtensionsTests.cs b/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerExtensionsTests.cs new file mode 100644 index 0000000..89f1bee --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerExtensionsTests.cs @@ -0,0 +1,142 @@ +namespace NetEvolve.Logging.XUnit.Tests.Integration; + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +public partial class XUnitLoggerExtensionsTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + public XUnitLoggerExtensionsTests(ITestOutputHelper testOutputHelper) => + _testOutputHelper = testOutputHelper; + + [Theory] + [MemberData(nameof(AddXUnitData))] + public void AddXUnit_TestCase1_Expected(XUnitLoggerOptions? options) + { + // Arrange + var services = new ServiceCollection() + .AddLogging(builder => _ = builder.AddXUnit(_testOutputHelper, options)) + .AddSingleton(); + using var serviceProvider = services.BuildServiceProvider(); + + // Act + var ex = Record.Exception(() => + { + var testCase = serviceProvider.GetRequiredService(); + + testCase.Run(); + }); + + // Assert + Assert.Null(ex); + } + + [Fact] + public void AddXUnit_TestCase2_Expected() + { + // Arrange + var services = new ServiceCollection() + .AddLogging(builder => _ = builder.AddXUnit(_testOutputHelper)) + .AddSingleton(); + using var serviceProvider = services.BuildServiceProvider(); + + // Act + var ex = Record.Exception(() => + { + var testCase = serviceProvider.GetRequiredService(); + + testCase.Run(); + }); + + // Assert + Assert.Null(ex); + } + + public static TheoryData AddXUnitData => + new TheoryData + { + null, + XUnitLoggerOptions.Default, + XUnitLoggerOptions.EnableAllFeatures, + XUnitLoggerOptions.DisableAllFeatures + }; + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + private sealed partial class TestCase1 + { + private readonly ILogger _logger; + +#pragma warning disable S1144 // Unused private types or members should be removed + public TestCase1(ILogger logger) => _logger = logger; +#pragma warning restore S1144 // Unused private types or members should be removed + + public void Run() + { + using var scope1 = _logger.BeginScope("Scope"); +#pragma warning disable CA1848 // Use the LoggerMessage delegates + using var scope2 = _logger.BeginScope("Execution {Now}", DateTimeOffset.Now); + using var scope3 = _logger.BeginScope( + new Dictionary { { "ExectionTime", DateTimeOffset.Now } } + ); +#pragma warning restore CA1848 // Use the LoggerMessage delegates + LogTrace(); + LogDebug(); + LogInformation(); + LogWarning(); + LogError(); + LogCritical(); + } + + [LoggerMessage(0, LogLevel.Trace, "Trace")] + private partial void LogTrace(); + + [LoggerMessage(1, LogLevel.Debug, "Debug")] + private partial void LogDebug(); + + [LoggerMessage(2, LogLevel.Information, "Information")] + private partial void LogInformation(); + + [LoggerMessage(3, LogLevel.Warning, "Warning")] + private partial void LogWarning(); + + [LoggerMessage(4, LogLevel.Error, "Error")] + private partial void LogError(); + + [LoggerMessage(5, LogLevel.Critical, "Critical")] + private partial void LogCritical(); + } + + private sealed partial class TestCase2 + { + private readonly ILogger _logger; + +#pragma warning disable S1144 // Unused private types or members should be removed + public TestCase2(ILogger logger) => _logger = logger; +#pragma warning restore S1144 // Unused private types or members should be removed + + public void Run() + { + LogBefore(1, null); + try + { + throw new InvalidOperationException(); + } + catch (Exception ex) + { + LogException(ex, "Unknown exception."); + } + } + + [LoggerMessage(0, LogLevel.Information, "Before {Number}: {Name}")] + private partial void LogBefore(int number, string? name); + + [LoggerMessage(1, LogLevel.Error, "Exception: {Message}")] + private partial void LogException(Exception ex, string message); + } +#pragma warning restore CA1812 // Avoid uninstantiated internal classes +} diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerTests.cs b/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerTests.cs new file mode 100644 index 0000000..ccf6f02 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/XUnitLoggerTests.cs @@ -0,0 +1,148 @@ +namespace NetEvolve.Logging.XUnit.Tests.Integration; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using VerifyXunit; +using Xunit; +using Xunit.Abstractions; + +public partial class XUnitLoggerTests +{ + private readonly TimeProvider _fakeTimeProvider = new FakeTimeProvider( + new DateTimeOffset(2000, 1, 1, 13, 37, 00, TimeSpan.FromHours(2)) + ); + private readonly ITestOutputHelper _testOutputHelper; + + public XUnitLoggerTests(ITestOutputHelper testOutputHelper) => + _testOutputHelper = testOutputHelper; + + [Theory] + [MemberData(nameof(LoggedMessageOrToStringData))] + public async Task LoggedMessages_Theory_Expected( + bool disableAdditionalInformation, + bool disableLogLevel, + bool disableScopes, + bool disableTimestamp, + string? formatTimestamp + ) + { + var options = new XUnitLoggerOptions + { + DisableAdditionalInformation = disableAdditionalInformation, + DisableLogLevel = disableLogLevel, + DisableScopes = disableScopes, + DisableTimestamp = disableTimestamp, + TimestampFormat = formatTimestamp! + }; + var logger = XUnitLogger.CreateLogger( + _testOutputHelper, + _fakeTimeProvider, + new LoggerExternalScopeProvider(), + options + ); + var @case = new TestCase(logger); + + // Act + @case.Run(); + + // Assert + _ = await Verifier + .Verify(logger.LoggedMessages) + .UseDirectory("_snapshots") + .UseHashedParameters( + disableAdditionalInformation, + disableLogLevel, + disableScopes, + disableTimestamp, + formatTimestamp + ); + } + + [Theory] + [MemberData(nameof(LoggedMessageOrToStringData))] + public async Task ToString_Theory_Expected( + bool disableAdditionalInformation, + bool disableLogLevel, + bool disableScopes, + bool disableTimestamp, + string? formatTimestamp + ) + { + var options = new XUnitLoggerOptions + { + DisableAdditionalInformation = disableAdditionalInformation, + DisableLogLevel = disableLogLevel, + DisableScopes = disableScopes, + DisableTimestamp = disableTimestamp, + TimestampFormat = formatTimestamp! + }; + var logger = XUnitLogger.CreateLogger( + _testOutputHelper, + _fakeTimeProvider, + new LoggerExternalScopeProvider(), + options + ); + var @case = new TestCase(logger); + + // Act + @case.Run(); + + // Assert + _ = await Verifier + .Verify(logger.ToString()) + .UseDirectory("_snapshots") + .UseHashedParameters( + disableAdditionalInformation, + disableLogLevel, + disableScopes, + disableTimestamp, + formatTimestamp + ); + } + + public static TheoryData LoggedMessageOrToStringData => + new TheoryData + { + { false, false, false, false, null }, + { true, false, false, false, null }, + { false, true, false, false, null }, + { false, false, true, false, null }, + { false, false, false, true, null }, + { false, false, false, false, "yyyy-MM-dd HH:mm:ss" } + }; + + private sealed partial class TestCase + { + private readonly ILogger _logger; + + public TestCase(ILogger logger) => _logger = logger; + + public void Run() + { + using var scopeNull = _logger.BeginScope((string)null!); + using var scopeOne = _logger.BeginScope( + new Dictionary { { "MethodName", nameof(Run) } } + ); +#pragma warning disable CA1848 // Use the LoggerMessage delegates + try + { + _logger.LogTrace("This is a Trace."); + _logger.LogDebug("This is a Debug."); + _logger.LogInformation("This is an Information."); + _logger.LogWarning("This is a Warning."); + _logger.LogError("This is an Error."); + _logger.LogCritical("This is a Critical."); + + throw new NotImplementedException(); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "This is a Critical with exception."); + } +#pragma warning restore CA1848 // Use the LoggerMessage delegates + } + } +} diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_239eb6e5afbe5154.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_239eb6e5afbe5154.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_239eb6e5afbe5154.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_56be48173312786d.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_56be48173312786d.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_56be48173312786d.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_70ab237ef6d4482a.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_70ab237ef6d4482a.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_70ab237ef6d4482a.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_7c4961359b3868c7.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_7c4961359b3868c7.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_7c4961359b3868c7.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_d7bac2eb9436e03b.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_d7bac2eb9436e03b.verified.txt new file mode 100644 index 0000000..eadbd50 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.LoggedMessages_Theory_Expected_d7bac2eb9436e03b.verified.txt @@ -0,0 +1,77 @@ +[ + { + Timestamp: DateTimeOffset_1, + Message: This is a Trace., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Debug, + Message: This is a Debug., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Information, + Message: This is an Information., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Warning, + Message: This is a Warning., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Error, + Message: This is an Error., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical., + Scopes: [ + { + MethodName: Run + } + ] + }, + { + Timestamp: DateTimeOffset_1, + LogLevel: Critical, + Message: This is a Critical with exception., + Exception: { + $type: NotImplementedException, + Type: NotImplementedException, + Message: The method or operation is not implemented., + StackTrace: at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() + }, + Scopes: [ + { + MethodName: Run + } + ] + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_239eb6e5afbe5154.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_239eb6e5afbe5154.verified.txt new file mode 100644 index 0000000..26e67d2 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_239eb6e5afbe5154.verified.txt @@ -0,0 +1,9 @@ +2000-01-01 13:37:00 [TRCE] This is a Trace. +2000-01-01 13:37:00 [DBUG] This is a Debug. +2000-01-01 13:37:00 [INFO] This is an Information. +2000-01-01 13:37:00 [WARN] This is a Warning. +2000-01-01 13:37:00 [FAIL] This is an Error. +2000-01-01 13:37:00 [CRIT] This is a Critical. +2000-01-01 13:37:00 [CRIT] This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt new file mode 100644 index 0000000..8501ba3 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_45a9a7d9ce8ebe1d.verified.txt @@ -0,0 +1,9 @@ +2000-01-01T13:37:00.0000000+02:00 [TRCE] This is a Trace. +2000-01-01T13:37:00.0000000+02:00 [DBUG] This is a Debug. +2000-01-01T13:37:00.0000000+02:00 [INFO] This is an Information. +2000-01-01T13:37:00.0000000+02:00 [WARN] This is a Warning. +2000-01-01T13:37:00.0000000+02:00 [FAIL] This is an Error. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_56be48173312786d.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_56be48173312786d.verified.txt new file mode 100644 index 0000000..8501ba3 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_56be48173312786d.verified.txt @@ -0,0 +1,9 @@ +2000-01-01T13:37:00.0000000+02:00 [TRCE] This is a Trace. +2000-01-01T13:37:00.0000000+02:00 [DBUG] This is a Debug. +2000-01-01T13:37:00.0000000+02:00 [INFO] This is an Information. +2000-01-01T13:37:00.0000000+02:00 [WARN] This is a Warning. +2000-01-01T13:37:00.0000000+02:00 [FAIL] This is an Error. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_70ab237ef6d4482a.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_70ab237ef6d4482a.verified.txt new file mode 100644 index 0000000..1b1bd4c --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_70ab237ef6d4482a.verified.txt @@ -0,0 +1,9 @@ +[TRCE] This is a Trace. +[DBUG] This is a Debug. +[INFO] This is an Information. +[WARN] This is a Warning. +[FAIL] This is an Error. +[CRIT] This is a Critical. +[CRIT] This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_7c4961359b3868c7.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_7c4961359b3868c7.verified.txt new file mode 100644 index 0000000..236ae83 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_7c4961359b3868c7.verified.txt @@ -0,0 +1,9 @@ +2000-01-01T13:37:00.0000000+02:00 This is a Trace. +2000-01-01T13:37:00.0000000+02:00 This is a Debug. +2000-01-01T13:37:00.0000000+02:00 This is an Information. +2000-01-01T13:37:00.0000000+02:00 This is a Warning. +2000-01-01T13:37:00.0000000+02:00 This is an Error. +2000-01-01T13:37:00.0000000+02:00 This is a Critical. +2000-01-01T13:37:00.0000000+02:00 This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_d7bac2eb9436e03b.verified.txt b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_d7bac2eb9436e03b.verified.txt new file mode 100644 index 0000000..8501ba3 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/_snapshots/XUnitLoggerTests.ToString_Theory_Expected_d7bac2eb9436e03b.verified.txt @@ -0,0 +1,9 @@ +2000-01-01T13:37:00.0000000+02:00 [TRCE] This is a Trace. +2000-01-01T13:37:00.0000000+02:00 [DBUG] This is a Debug. +2000-01-01T13:37:00.0000000+02:00 [INFO] This is an Information. +2000-01-01T13:37:00.0000000+02:00 [WARN] This is a Warning. +2000-01-01T13:37:00.0000000+02:00 [FAIL] This is an Error. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical. +2000-01-01T13:37:00.0000000+02:00 [CRIT] This is a Critical with exception. +System.NotImplementedException: The method or operation is not implemented. + at NetEvolve.Logging.XUnit.Tests.Integration.XUnitLoggerTests.TestCase.Run() in {ProjectDirectory}XUnitLoggerTests.cs:line 139 \ No newline at end of file diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj b/tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj new file mode 100644 index 0000000..aa354f3 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj @@ -0,0 +1,28 @@ + + + + $(TestTargetFrameworks) + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerExtensionsTests.cs b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerExtensionsTests.cs new file mode 100644 index 0000000..3834398 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerExtensionsTests.cs @@ -0,0 +1,47 @@ +namespace NetEvolve.Logging.XUnit.Tests.Unit; + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +public class XUnitLoggerExtensionsTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + public XUnitLoggerExtensionsTests(ITestOutputHelper testOutputHelper) => + _testOutputHelper = testOutputHelper; + + [Fact] + public void AddXUnit_WithNullBuilder_ThrowArgumentNullException() + { + // Arrange + ILoggingBuilder builder = null!; + + // Act + void Act() => builder.AddXUnit(_testOutputHelper); + + // Assert + _ = Assert.Throws("builder", Act); + } + + [Fact] + public void AddXUnit_WithNullTestOutputHelper_ThrowArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + + // Act & Assert + _ = Assert.Throws( + "testOutputHelper", + () => + { + _ = services.AddLogging(builder => + { + _ = builder.AddXUnit(null!); + }); + } + ); + } +} diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerProviderTests.cs b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerProviderTests.cs new file mode 100644 index 0000000..ceee635 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerProviderTests.cs @@ -0,0 +1,94 @@ +namespace NetEvolve.Logging.XUnit.Tests.Unit; + +using System; +using Microsoft.Extensions.Logging; +using NetEvolve.Logging.Abstractions; +using Xunit; +using Xunit.Abstractions; + +public class XUnitLoggerProviderTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + public XUnitLoggerProviderTests(ITestOutputHelper testOutputHelper) => + _testOutputHelper = testOutputHelper; + + [Fact] + public void CreateLogger_WithTestOutputHelper() + { + // Arrange + using var provider = new XUnitLoggerProvider(_testOutputHelper, TimeProvider.System); + + // Act + var logger = provider.CreateLogger(nameof(XUnitLoggerProviderTests)); + + // Assert + Assert.NotNull(logger); + _ = Assert.IsType(logger); + } + + [Fact] + public void CreateLoggerGeneric_WithTestOutputHelper() + { + // Arrange + using var provider = new XUnitLoggerProvider(_testOutputHelper, TimeProvider.System); + + // Act + var logger = provider.CreateLogger(); + + // Assert + Assert.NotNull(logger); + _ = Assert.IsType>(logger); + } + + [Fact] + public void SetScopeProvider_Null_ThrowArgumentNullException() + { + // Arrange + using var provider = new XUnitLoggerProvider(_testOutputHelper, TimeProvider.System); + + // Act + void Act() => provider.SetScopeProvider(null!); + + // Assert + _ = Assert.Throws("scopeProvider", Act); + } + + [Fact] + public void SetScopeProvider_WithNullScopeProvider_NoExceptionThrown() + { + // Arrange + using var provider = new XUnitLoggerProvider(_testOutputHelper, TimeProvider.System); + + _ = provider.CreateLogger(nameof(XUnitLoggerProviderTests)); + _ = provider.CreateLogger(); + + // Act + var ex = Record.Exception( + () => provider.SetScopeProvider(NullExternalScopeProvider.Instance) + ); + + // Assert + Assert.Null(ex); + } + + [Fact] + public void SetScopeProvider_WithScopeProvider_Expected() + { + // Arrange + using var provider = new XUnitLoggerProvider(_testOutputHelper, TimeProvider.System); + var scopeProvider = new LoggerExternalScopeProvider(); + + _ = provider.CreateLogger(nameof(XUnitLoggerProviderTests)); + _ = provider.CreateLogger(); + + // Act + provider.SetScopeProvider(scopeProvider); + + // Assert + foreach (var logger in provider.Loggers) + { + Assert.Same(scopeProvider, logger.ScopeProvider); + } + } +} diff --git a/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerTests.cs b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerTests.cs new file mode 100644 index 0000000..55b4671 --- /dev/null +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/XUnitLoggerTests.cs @@ -0,0 +1,31 @@ +namespace NetEvolve.Logging.XUnit.Tests.Unit; + +using Microsoft.Extensions.Logging; +using Xunit; + +public partial class XUnitLoggerTests +{ + [Theory] + [MemberData(nameof(LogLevelToStringData))] + public void LogLevelToString_Theory_Expected(string expected, LogLevel logLevel) + { + // Arrange + // Act + var result = XUnitLogger.LogLevelToString(logLevel); + + // Assert + Assert.Equal(expected, result); + } + + public static TheoryData LogLevelToStringData => + new TheoryData + { + { "TRCE", LogLevel.Trace }, + { "DBUG", LogLevel.Debug }, + { "INFO", LogLevel.Information }, + { "WARN", LogLevel.Warning }, + { "FAIL", LogLevel.Error }, + { "CRIT", LogLevel.Critical }, + { "NONE", LogLevel.None } + }; +}