From 77e3c5acd1fe7e1331aa1328290006fb22d86657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 17 May 2024 09:23:12 +0200 Subject: [PATCH] chore: Extended implementation --- Directory.Build.props | 7 + Directory.Packages.props | 3 + ...ons.Logging.XUnit.sln => Logging.XUnit.sln | 9 +- .../XUnitLogger.cs | 89 ------- .../XUnitLoggerOfT.cs | 15 -- .../XUnitLoggerOptions.cs | 3 - src/NetEvolve.Logging.XUnit/LoggedMessage.cs | 22 ++ .../NetEvolve.Logging.XUnit.csproj} | 5 +- .../README.md | 0 src/NetEvolve.Logging.XUnit/XUnitLogger.cs | 233 ++++++++++++++++++ .../XUnitLoggerExtensions.cs | 29 +++ src/NetEvolve.Logging.XUnit/XUnitLoggerOfT.cs | 21 ++ .../XUnitLoggerOptions.cs | 75 ++++++ .../XUnitLoggerProvider.cs | 96 ++++++++ .../GlobalUsings.cs | 0 ...ve.Logging.XUnit.Tests.Integration.csproj} | 2 +- .../UnitTest1.cs | 2 +- .../GlobalUsings.cs | 0 ...NetEvolve.Logging.XUnit.Tests.Unit.csproj} | 2 +- .../UnitTest1.cs | 2 +- 20 files changed, 498 insertions(+), 117 deletions(-) rename Extensions.Logging.XUnit.sln => Logging.XUnit.sln (85%) delete mode 100644 src/NetEvolve.Extensions.Logging.XUnit/XUnitLogger.cs delete mode 100644 src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOfT.cs delete mode 100644 src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOptions.cs create mode 100644 src/NetEvolve.Logging.XUnit/LoggedMessage.cs rename src/{NetEvolve.Extensions.Logging.XUnit/NetEvolve.Extensions.Logging.XUnit.csproj => NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj} (55%) rename src/{NetEvolve.Extensions.Logging.XUnit => NetEvolve.Logging.XUnit}/README.md (100%) create mode 100644 src/NetEvolve.Logging.XUnit/XUnitLogger.cs create mode 100644 src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs create mode 100644 src/NetEvolve.Logging.XUnit/XUnitLoggerOfT.cs create mode 100644 src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs create mode 100644 src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Integration => NetEvolve.Logging.XUnit.Tests.Integration}/GlobalUsings.cs (100%) rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Integration/NetEvolve.Extensions.Logging.XUnit.Tests.Integration.csproj => NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj} (88%) rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Unit => NetEvolve.Logging.XUnit.Tests.Integration}/UnitTest1.cs (54%) rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Unit => NetEvolve.Logging.XUnit.Tests.Unit}/GlobalUsings.cs (100%) rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Unit/NetEvolve.Extensions.Logging.XUnit.Tests.Unit.csproj => NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj} (88%) rename tests/{NetEvolve.Extensions.Logging.XUnit.Tests.Integration => NetEvolve.Logging.XUnit.Tests.Unit}/UnitTest1.cs (51%) 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 998a072..11cd075 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,9 +16,12 @@ + + + \ No newline at end of file diff --git a/Extensions.Logging.XUnit.sln b/Logging.XUnit.sln similarity index 85% rename from Extensions.Logging.XUnit.sln rename to Logging.XUnit.sln index dbec575..70a8721 100644 --- a/Extensions.Logging.XUnit.sln +++ b/Logging.XUnit.sln @@ -25,11 +25,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EFE8181B-6FA EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BD750CCE-0318-424D-89B4-9C66EF329E96}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.Extensions.Logging.XUnit", "src\NetEvolve.Extensions.Logging.XUnit\NetEvolve.Extensions.Logging.XUnit.csproj", "{3F3CD6EC-4636-4B58-8400-09AC2B9BAFDF}" +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.Extensions.Logging.XUnit.Tests.Unit", "tests\NetEvolve.Extensions.Logging.XUnit.Tests.Unit\NetEvolve.Extensions.Logging.XUnit.Tests.Unit.csproj", "{EAAD2C22-48F4-4E37-9A46-0350C6B84388}" +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.Extensions.Logging.XUnit.Tests.Integration", "tests\NetEvolve.Extensions.Logging.XUnit.Tests.Integration\NetEvolve.Extensions.Logging.XUnit.Tests.Integration.csproj", "{F2B38EDD-63F9-4027-9EA6-D1E0C6387C12}" +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 @@ -59,8 +59,7 @@ Global {F2B38EDD-63F9-4027-9EA6-D1E0C6387C12} = {BD750CCE-0318-424D-89B4-9C66EF329E96} EndGlobalSection EndGlobal -Any CPU - {6211ACA9-9A91-4218-8B4B-E94F91796B58}.Release|Any CPU.Build.0 = Release|Any CPU +CPU.Build.0 = Release|Any CPU {FFF70EC4-D0FD-4D73-8599-7FF442133436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FFF70EC4-D0FD-4D73-8599-7FF442133436}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFF70EC4-D0FD-4D73-8599-7FF442133436}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLogger.cs b/src/NetEvolve.Extensions.Logging.XUnit/XUnitLogger.cs deleted file mode 100644 index c584490..0000000 --- a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLogger.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace NetEvolve.Extensions.Logging.XUnit; - -using System; -using System.Text; -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -public class XUnitLogger : ILogger -{ - private readonly ITestOutputHelper _testOutputHelper; - private readonly IExternalScopeProvider _scopeProvider; - private readonly string? _categoryName; - private readonly XUnitLoggerOptions? _options; - - public static ILogger CreateLogger( - ITestOutputHelper testOutputHelper, - IExternalScopeProvider? scopeProvider = null, - string? categoryName = null, - XUnitLoggerOptions? options = null - ) => new XUnitLogger(testOutputHelper, scopeProvider, categoryName, options); - - public static ILogger CreateLogger( - ITestOutputHelper testOutputHelper, - IExternalScopeProvider? scopeProvider = null, - string? categoryName = null, - XUnitLoggerOptions? options = null - ) => new XUnitLogger(testOutputHelper, scopeProvider, categoryName, options); - - internal XUnitLogger( - ITestOutputHelper testOutputHelper, - IExternalScopeProvider? scopeProvider, - string? categoryName, - XUnitLoggerOptions? options - ) - { - ArgumentNullException.ThrowIfNull(testOutputHelper); - - _testOutputHelper = testOutputHelper; - _scopeProvider = scopeProvider ?? new LoggerExternalScopeProvider(); - _categoryName = categoryName; - _options = options ?? new XUnitLoggerOptions(); - } - - /// - 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 - ) - { - ArgumentNullException.ThrowIfNull(formatter); - - var sb = new StringBuilder(500); - - _ = sb.Append('[').Append(LogLevelToString(logLevel)).Append("] "); - - _ = sb.Append(formatter(state, exception)); - - try - { - _testOutputHelper.WriteLine(sb.ToString()); - } - catch - { - // Unfortunately, this can happen if the process is terminated before the end of the test. - } - } - - private 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", - LogLevel.None => "none", - _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) - }; -} diff --git a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOfT.cs b/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOfT.cs deleted file mode 100644 index 0159822..0000000 --- a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOfT.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace NetEvolve.Extensions.Logging.XUnit; - -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -public sealed class XUnitLogger : XUnitLogger, ILogger -{ - internal XUnitLogger( - ITestOutputHelper testOutputHelper, - IExternalScopeProvider? scopeProvider, - string? categoryName, - XUnitLoggerOptions? options - ) - : base(testOutputHelper, scopeProvider, categoryName, options) { } -} diff --git a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOptions.cs b/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOptions.cs deleted file mode 100644 index 9ce14c5..0000000 --- a/src/NetEvolve.Extensions.Logging.XUnit/XUnitLoggerOptions.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace NetEvolve.Extensions.Logging.XUnit; - -public class XUnitLoggerOptions { } diff --git a/src/NetEvolve.Logging.XUnit/LoggedMessage.cs b/src/NetEvolve.Logging.XUnit/LoggedMessage.cs new file mode 100644 index 0000000..a55b0ea --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/LoggedMessage.cs @@ -0,0 +1,22 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using Microsoft.Extensions.Logging; + +/// +/// Represents a logged message, including the timestamp, log level, event ID, message, exception, and scopes. +/// +/// Timestamp of the logged message. +/// LogLevel of the logged message. +/// EventId of the logged message. +/// Logged message. +/// Logged exception. (optional) +/// Logged scopes. +public record struct LoggedMessage( + DateTimeOffset Timestamp, + LogLevel LogLevel, + EventId EventId, + string Message, + Exception? Exception, + IReadOnlyCollection Scopes +); diff --git a/src/NetEvolve.Extensions.Logging.XUnit/NetEvolve.Extensions.Logging.XUnit.csproj b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj similarity index 55% rename from src/NetEvolve.Extensions.Logging.XUnit/NetEvolve.Extensions.Logging.XUnit.csproj rename to src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj index 4bcb180..878e607 100644 --- a/src/NetEvolve.Extensions.Logging.XUnit/NetEvolve.Extensions.Logging.XUnit.csproj +++ b/src/NetEvolve.Logging.XUnit/NetEvolve.Logging.XUnit.csproj @@ -1,14 +1,17 @@ - net7.0 + $(ProjectTargetFrameworks) enable enable + + + diff --git a/src/NetEvolve.Extensions.Logging.XUnit/README.md b/src/NetEvolve.Logging.XUnit/README.md similarity index 100% rename from src/NetEvolve.Extensions.Logging.XUnit/README.md rename to src/NetEvolve.Logging.XUnit/README.md diff --git a/src/NetEvolve.Logging.XUnit/XUnitLogger.cs b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs new file mode 100644 index 0000000..7dd0063 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLogger.cs @@ -0,0 +1,233 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; +using Xunit.Sdk; + +public class XUnitLogger : ILogger +{ + private readonly string? _categoryName; + private readonly XUnitLoggerOptions _options; + private readonly Action? _writeToLogger; + + private readonly List _loggedMessages; + + private const int DefaultCapacity = 1024; + + [ThreadStatic] + private static StringBuilder? _builder; + + public IReadOnlyList LoggedMessages => _loggedMessages.AsReadOnly(); + + internal IExternalScopeProvider? ScopeProvider { get; set; } + + public static XUnitLogger CreateLogger( + ITestOutputHelper testOutputHelper, + IExternalScopeProvider? scopeProvider = null, + string? categoryName = null, + XUnitLoggerOptions? options = null + ) => new XUnitLogger(testOutputHelper, scopeProvider, categoryName, options); + + public static XUnitLogger CreateLogger( + ITestOutputHelper testOutputHelper, + IExternalScopeProvider? scopeProvider = null, + XUnitLoggerOptions? options = null + ) => new XUnitLogger(testOutputHelper, scopeProvider, options); + + internal XUnitLogger( + ITestOutputHelper testOutputHelper, + IExternalScopeProvider? scopeProvider, + string? categoryName, + XUnitLoggerOptions? options + ) + { + ArgumentNullException.ThrowIfNull(testOutputHelper); + + _writeToLogger = testOutputHelper.WriteLine; + ScopeProvider = scopeProvider; + _categoryName = categoryName; + _options = options ?? XUnitLoggerOptions.Default; + + _loggedMessages = []; + } + + internal XUnitLogger( + IMessageSink messageSink, + IExternalScopeProvider? scopeProvider, + string? categoryName, + XUnitLoggerOptions? options + ) + { + ArgumentNullException.ThrowIfNull(messageSink); + + _writeToLogger = message => messageSink.OnMessage(new DiagnosticMessage(message)); + ScopeProvider = scopeProvider; + _categoryName = categoryName; + _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 + ) + { + ArgumentNullException.ThrowIfNull(formatter); + + if (!IsEnabled(logLevel)) + { + return; + } + + var builder = _builder; + _builder = null; + builder ??= new StringBuilder(DefaultCapacity); + + var message = formatter(state, exception); + var now = DateTimeOffset.Now; + (builder, var scopes) = CreateMessage(logLevel, state, exception, builder, message, now); + + try + { + _loggedMessages.Add( + new LoggedMessage(now, logLevel, eventId, message, exception, scopes) + ); + _writeToLogger?.Invoke(builder.ToString()); + } + catch + { + // Ignore exception. + // Unfortunately, this can happen if the process is terminated before the end of the test. + } + + _ = 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("] "); + } + + if (!_options.DisableCategory) + { + _ = builder.Append('[').Append(_categoryName).Append("] "); + } + + _ = builder.Append(message); + + if (exception is not null) + { + _ = builder.Append('\n').Append(exception); + } + + if ( + !_options.DisableAdditionalInformation + && state is IReadOnlyList> additionalInformation + ) + { + var level = 1; + _ = builder.Append('\n').Append('\t').Append("Additional Information"); + foreach (var info in additionalInformation) + { + AddAdditionalInformation(builder, info, level); + } + } + + ScopeProvider?.ForEachScope( + (scope, state) => + { + scopes.Add(scope); + + if (!_options.DisableScopes) + { + _ = state.Append("\n=>\t").Append(scope); + } + }, + builder + ); + + return (builder, scopes); + } + + private static void AddAdditionalInformation( + StringBuilder builder, + KeyValuePair info, + int level + ) + { + _ = builder + .Append('\n') + .Append('\t', level) + .Append(CultureInfo.InvariantCulture, $"`{info.Key}`:"); + + if (info.Value is null) + { + _ = builder.Append(" `null`"); + } + else if (info.Value is IConvertible convertible) + { + _ = builder.Append(CultureInfo.InvariantCulture, $" `{convertible.ToString()}`"); + } + else if (info.Value is KeyValuePair kvp) + { + AddAdditionalInformation(builder, kvp, level + 1); + } + else if (info.Value is IEnumerable> enumerable) + { + foreach (var item in enumerable) + { + AddAdditionalInformation(builder, item, level + 1); + } + } + } + + private 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", + LogLevel.None => "NONE", + _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) + }; +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs new file mode 100644 index 0000000..24e9c6e --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerExtensions.cs @@ -0,0 +1,29 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using Xunit.Abstractions; + +public static class XUnitLoggerExtensions +{ + public static ILoggingBuilder AddXUnit( + this ILoggingBuilder builder, + ITestOutputHelper testOutputHelper + ) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(testOutputHelper); + + builder.AddConfiguration(); + + _ = builder.Services.AddSingleton(testOutputHelper); + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton() + ); + + return builder; + } +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerOfT.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerOfT.cs new file mode 100644 index 0000000..3c1f9be --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerOfT.cs @@ -0,0 +1,21 @@ +namespace NetEvolve.Logging.XUnit; + +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +public sealed class XUnitLogger : XUnitLogger, ILogger +{ + internal XUnitLogger( + ITestOutputHelper testOutputHelper, + IExternalScopeProvider? scopeProvider, + XUnitLoggerOptions? options + ) + : base(testOutputHelper, scopeProvider, typeof(T).FullName, options) { } + + internal XUnitLogger( + IMessageSink messageSink, + IExternalScopeProvider? scopeProvider, + XUnitLoggerOptions? options + ) + : base(messageSink, scopeProvider, typeof(T).FullName, options) { } +} diff --git a/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs b/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs new file mode 100644 index 0000000..f6f20a3 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerOptions.cs @@ -0,0 +1,75 @@ +namespace NetEvolve.Logging.XUnit; + +/// +/// Options for the . +/// +public class XUnitLoggerOptions +{ + public static XUnitLoggerOptions Default { get; } = + new XUnitLoggerOptions { DisableAdditionalInformation = true, DisableScopes = true }; + + public static XUnitLoggerOptions DisableAllFeatures { get; } = + new XUnitLoggerOptions + { + DisableAdditionalInformation = true, + DisableCategory = true, + DisableLogLevel = true, + DisableScopes = true, + DisableTimestamp = true + }; + + public static XUnitLoggerOptions EnableAllFeatures { get; } = + new XUnitLoggerOptions + { + DisableAdditionalInformation = false, + DisableCategory = false, + DisableLogLevel = false, + DisableScopes = false, + DisableTimestamp = false + }; + + /// + /// Disables the output of the additional information in the log output. Default . + /// + public bool DisableAdditionalInformation { get; set; } + + /// + /// Disable the category name in the log output. Default . + /// + public bool DisableCategory { get; set; } + + /// + /// Disable the log level in the log output. Default . + /// + public bool DisableLogLevel { get; set; } + + /// + /// Disable the scopes in the log output. Default . + /// + public bool DisableScopes { get; set; } + + /// + /// Disables the timestamps in the log output. Default . + /// + 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..7690af2 --- /dev/null +++ b/src/NetEvolve.Logging.XUnit/XUnitLoggerProvider.cs @@ -0,0 +1,96 @@ +namespace NetEvolve.Logging.XUnit; + +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using NetEvolve.Arguments; +using Xunit.Abstractions; + +internal sealed class XUnitLoggerProvider : ILoggerProvider, ISupportExternalScope +{ + private readonly ITestOutputHelper? _testOutputHelper; + private readonly IMessageSink? _messageSink; + private readonly IExternalScopeProvider _scopeProvider; // = NullExternalScopeProvider.Instance; + private readonly XUnitLoggerOptions _options; + private readonly ConcurrentDictionary _loggers; + + public XUnitLoggerProvider( + ITestOutputHelper testOutputHelper, + IExternalScopeProvider? scopeProvider = null, + XUnitLoggerOptions? options = null + ) + : this(scopeProvider, options) + { + ArgumentNullException.ThrowIfNull(testOutputHelper); + + _testOutputHelper = testOutputHelper; + } + + public XUnitLoggerProvider( + IMessageSink messageSink, + IExternalScopeProvider? scopeProvider = null, + XUnitLoggerOptions? options = null + ) + : this(scopeProvider, options) + { + ArgumentNullException.ThrowIfNull(messageSink); + + _messageSink = messageSink; + } + + private XUnitLoggerProvider( + IExternalScopeProvider? scopeProvider = null, + XUnitLoggerOptions? options = null + ) + { + _scopeProvider = scopeProvider ?? new LoggerExternalScopeProvider(); + _options = options ?? new XUnitLoggerOptions(); + + _loggers = new ConcurrentDictionary(StringComparer.Ordinal); + } + + /// + public ILogger CreateLogger(string categoryName) + { + Argument.ThrowIfNullOrWhiteSpace(categoryName); + + return _loggers.GetOrAdd(categoryName, CreateLoggerInternal); + } + + /// + public ILogger CreateLogger() + where T : notnull => _loggers.GetOrAdd(typeof(T).FullName!, CreateLoggerInternal); + + private XUnitLogger CreateLoggerInternal(string name) + { + if (_testOutputHelper is not null) + { + return new XUnitLogger(_testOutputHelper, _scopeProvider, name, _options); + } + else if (_messageSink is not null) + { + return new XUnitLogger(_messageSink, _scopeProvider, name, _options); + } + + throw new InvalidOperationException("No output destination was provided."); + } + + /// + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + { + ArgumentNullException.ThrowIfNull(scopeProvider); + + if (_scopeProvider == scopeProvider) + { + return; + } + + foreach (var logger in _loggers.Values) + { + logger.ScopeProvider = scopeProvider; + } + } + + /// + public void Dispose() { } +} diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/GlobalUsings.cs b/tests/NetEvolve.Logging.XUnit.Tests.Integration/GlobalUsings.cs similarity index 100% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/GlobalUsings.cs rename to tests/NetEvolve.Logging.XUnit.Tests.Integration/GlobalUsings.cs diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/NetEvolve.Extensions.Logging.XUnit.Tests.Integration.csproj b/tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj similarity index 88% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/NetEvolve.Extensions.Logging.XUnit.Tests.Integration.csproj rename to tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj index 62b4eec..655d983 100644 --- a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/NetEvolve.Extensions.Logging.XUnit.Tests.Integration.csproj +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/NetEvolve.Logging.XUnit.Tests.Integration.csproj @@ -22,7 +22,7 @@ - + diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/UnitTest1.cs b/tests/NetEvolve.Logging.XUnit.Tests.Integration/UnitTest1.cs similarity index 54% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/UnitTest1.cs rename to tests/NetEvolve.Logging.XUnit.Tests.Integration/UnitTest1.cs index ed6a7fe..4ddc983 100644 --- a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/UnitTest1.cs +++ b/tests/NetEvolve.Logging.XUnit.Tests.Integration/UnitTest1.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Extensions.Logging.XUnit.Tests.Unit; +namespace NetEvolve.Logging.XUnit.Tests.Integration; public class UnitTest1 { diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/GlobalUsings.cs b/tests/NetEvolve.Logging.XUnit.Tests.Unit/GlobalUsings.cs similarity index 100% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/GlobalUsings.cs rename to tests/NetEvolve.Logging.XUnit.Tests.Unit/GlobalUsings.cs diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/NetEvolve.Extensions.Logging.XUnit.Tests.Unit.csproj b/tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj similarity index 88% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/NetEvolve.Extensions.Logging.XUnit.Tests.Unit.csproj rename to tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj index 62b4eec..655d983 100644 --- a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Unit/NetEvolve.Extensions.Logging.XUnit.Tests.Unit.csproj +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/NetEvolve.Logging.XUnit.Tests.Unit.csproj @@ -22,7 +22,7 @@ - + diff --git a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/UnitTest1.cs b/tests/NetEvolve.Logging.XUnit.Tests.Unit/UnitTest1.cs similarity index 51% rename from tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/UnitTest1.cs rename to tests/NetEvolve.Logging.XUnit.Tests.Unit/UnitTest1.cs index e5cd8f0..6ec4be9 100644 --- a/tests/NetEvolve.Extensions.Logging.XUnit.Tests.Integration/UnitTest1.cs +++ b/tests/NetEvolve.Logging.XUnit.Tests.Unit/UnitTest1.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Extensions.Logging.XUnit.Tests.Integration; +namespace NetEvolve.Logging.XUnit.Tests.Unit; public class UnitTest1 {