From 60fa8cade8d077fd644947f5794d90da2d4cef66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Fri, 1 Oct 2021 21:53:04 +0200 Subject: [PATCH 01/14] Add support for IMessageSink This enables logging in extensibility classes such as fixtures and discoverers. See https://xunit.net/docs/capturing-output#output-in-extensions for more information. All classes were modeled after the similar classes for `ITestOutputHelper`. The only method impossible to replicate was `public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder)`. --- Directory.Packages.props | 3 +- src/Logging.XUnit/IMessageSinkAccessor.cs | 18 + src/Logging.XUnit/IMessageSinkExtensions.cs | 50 +++ .../MartinCostello.Logging.XUnit.csproj | 1 + src/Logging.XUnit/MessageSinkAccessor.cs | 31 ++ src/Logging.XUnit/XUnitLogger.IMessageSink.cs | 66 ++++ .../XUnitLogger.ITestOutputHelper.cs | 49 +++ src/Logging.XUnit/XUnitLogger.cs | 58 +-- .../XUnitLoggerExtensions.IMessageSink.cs | 342 ++++++++++++++++++ ...UnitLoggerExtensions.ITestOutputHelper.cs} | 2 +- src/Logging.XUnit/XUnitLoggerOptions.cs | 8 + .../XUnitLoggerProvider.IMessageSink.cs | 47 +++ .../XUnitLoggerProvider.ITestOutputHelper.cs | 47 +++ src/Logging.XUnit/XUnitLoggerProvider.cs | 49 +-- tests/Logging.XUnit.Tests/Constructor.cs | 11 + .../Integration/DatabaseFixture.cs | 40 ++ .../Integration/DatabaseTests.cs | 24 ++ .../Integration/PrintableDiagnosticMessage.cs | 20 + .../XUnitLoggerExtensionsTests.cs | 65 +++- .../XUnitLoggerProviderTests.cs | 32 +- tests/Logging.XUnit.Tests/XUnitLoggerTests.cs | 33 +- tests/Logging.XUnit.Tests/xunit.runner.json | 2 + 22 files changed, 907 insertions(+), 91 deletions(-) create mode 100644 src/Logging.XUnit/IMessageSinkAccessor.cs create mode 100644 src/Logging.XUnit/IMessageSinkExtensions.cs create mode 100644 src/Logging.XUnit/MessageSinkAccessor.cs create mode 100644 src/Logging.XUnit/XUnitLogger.IMessageSink.cs create mode 100644 src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs create mode 100644 src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs rename src/Logging.XUnit/{XUnitLoggerExtensions.cs => XUnitLoggerExtensions.ITestOutputHelper.cs} (99%) create mode 100644 src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs create mode 100644 src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs create mode 100644 tests/Logging.XUnit.Tests/Constructor.cs create mode 100644 tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs create mode 100644 tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs create mode 100644 tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 132ae7ed..4c3d48f5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,8 @@ </ItemGroup> <ItemGroup Condition=" '$(IsTestProject)' != 'true' "> <PackageVersion Include="Microsoft.Extensions.Logging" Version="2.0.0" /> - <PackageVersion Include="xunit.abstractions" Version="2.0.1" /> + <PackageVersion Include="xunit.abstractions" Version="2.0.3" /> + <PackageVersion Include="xunit.extensibility.execution" Version="2.4.1" /> </ItemGroup> <ItemGroup Condition=" '$(IsTestProject)' == 'true' "> <PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" /> diff --git a/src/Logging.XUnit/IMessageSinkAccessor.cs b/src/Logging.XUnit/IMessageSinkAccessor.cs new file mode 100644 index 00000000..724961ed --- /dev/null +++ b/src/Logging.XUnit/IMessageSinkAccessor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// Defines a property for accessing an <see cref="IMessageSink"/>. + /// </summary> + public interface IMessageSinkAccessor + { + /// <summary> + /// Gets or sets the <see cref="IMessageSink"/> to use. + /// </summary> + IMessageSink? MessageSink { get; set; } + } +} diff --git a/src/Logging.XUnit/IMessageSinkExtensions.cs b/src/Logging.XUnit/IMessageSinkExtensions.cs new file mode 100644 index 00000000..34ff47ec --- /dev/null +++ b/src/Logging.XUnit/IMessageSinkExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.ComponentModel; +using Microsoft.Extensions.Logging; + +namespace Xunit.Abstractions +{ + /// <summary> + /// A class containing extension methods for the <see cref="IMessageSink"/> interface. This class cannot be inherited. + /// </summary> + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IMessageSinkExtensions + { + /// <summary> + /// Returns an <see cref="ILoggerFactory"/> that logs to the message sink. + /// </summary> + /// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger factory from.</param> + /// <returns> + /// An <see cref="ILoggerFactory"/> that writes messages to the message sink. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory ToLoggerFactory(this IMessageSink messageSink) + { + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + return new LoggerFactory().AddXUnit(messageSink); + } + + /// <summary> + /// Returns an <see cref="ILogger{T}"/> that logs to the message sink. + /// </summary> + /// <typeparam name="T">The type of the logger to create.</typeparam> + /// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger from.</param> + /// <returns> + /// An <see cref="ILogger{T}"/> that writes messages to the message sink. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public static ILogger<T> ToLogger<T>(this IMessageSink messageSink) + => messageSink.ToLoggerFactory().CreateLogger<T>(); + } +} diff --git a/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj b/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj index c715777d..fc133a4c 100644 --- a/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj +++ b/src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj @@ -17,5 +17,6 @@ <ItemGroup> <PackageReference Include="Microsoft.Extensions.Logging" /> <PackageReference Include="xunit.abstractions" /> + <PackageReference Include="xunit.extensibility.execution" /> </ItemGroup> </Project> diff --git a/src/Logging.XUnit/MessageSinkAccessor.cs b/src/Logging.XUnit/MessageSinkAccessor.cs new file mode 100644 index 00000000..a932867d --- /dev/null +++ b/src/Logging.XUnit/MessageSinkAccessor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// A class representing the default implementation of <see cref="IMessageSinkAccessor"/>. This class cannot be inherited. + /// </summary> + internal sealed class MessageSinkAccessor : IMessageSinkAccessor + { + /// <summary> + /// Initializes a new instance of the <see cref="MessageSinkAccessor"/> class. + /// </summary> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + internal MessageSinkAccessor(IMessageSink messageSink) + { + MessageSink = messageSink ?? throw new ArgumentNullException(nameof(messageSink)); + } + + /// <summary> + /// Gets or sets the current <see cref="IMessageSink"/>. + /// </summary> + public IMessageSink? MessageSink { get; set; } + } +} diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs new file mode 100644 index 00000000..9cb27d67 --- /dev/null +++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs @@ -0,0 +1,66 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// A class representing an <see cref="ILogger"/> to use with xunit. + /// </summary> + public partial class XUnitLogger + { + /// <summary> + /// The <see cref="IMessageSinkAccessor"/> to use. This field is read-only. + /// </summary> + private readonly IMessageSinkAccessor? _messageSinkAccessor; + + /// <summary> + /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. + /// </summary> + private Func<string, IMessageSinkMessage> _messageSinkMessageFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLogger"/> class. + /// </summary> + /// <param name="name">The name for messages produced by the logger.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="name"/> or <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public XUnitLogger(string name, IMessageSink messageSink, XUnitLoggerOptions? options) + : this(name, new MessageSinkAccessor(messageSink), options) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLogger"/> class. + /// </summary> + /// <param name="name">The name for messages produced by the logger.</param> + /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param> + /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>. + /// </exception> + public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOptions? options) + : this(name, options) + { + _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + } + + /// <summary> + /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. + /// </summary> + /// <exception cref="ArgumentNullException"> + /// <paramref name="value"/> is <see langword="null"/>. + /// </exception> + public Func<string, IMessageSinkMessage> MessageSinkMessageFactory + { + get { return _messageSinkMessageFactory; } + set { _messageSinkMessageFactory = value ?? throw new ArgumentNullException(nameof(value)); } + } + } +} diff --git a/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs b/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs new file mode 100644 index 00000000..84630e6b --- /dev/null +++ b/src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// A class representing an <see cref="ILogger"/> to use with xunit. + /// </summary> + public partial class XUnitLogger + { + /// <summary> + /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only. + /// </summary> + private readonly ITestOutputHelperAccessor? _outputHelperAccessor; + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLogger"/> class. + /// </summary> + /// <param name="name">The name for messages produced by the logger.</param> + /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param> + /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>. + /// </exception> + public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options) + : this(name, new TestOutputHelperAccessor(outputHelper), options) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLogger"/> class. + /// </summary> + /// <param name="name">The name for messages produced by the logger.</param> + /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param> + /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>. + /// </exception> + public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options) + : this(name, options) + { + _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + } + } +} diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs index c108e621..94f035fc 100644 --- a/src/Logging.XUnit/XUnitLogger.cs +++ b/src/Logging.XUnit/XUnitLogger.cs @@ -2,19 +2,18 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System; -using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Xunit.Abstractions; +using Xunit.Sdk; namespace MartinCostello.Logging.XUnit { /// <summary> /// A class representing an <see cref="ILogger"/> to use with xunit. /// </summary> - public class XUnitLogger : ILogger + public partial class XUnitLogger : ILogger { //// Based on https://github.com/aspnet/Logging/blob/master/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs @@ -39,11 +38,6 @@ public class XUnitLogger : ILogger [ThreadStatic] private static StringBuilder? _logBuilder; - /// <summary> - /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only. - /// </summary> - private readonly ITestOutputHelperAccessor _accessor; - /// <summary> /// Gets or sets the filter to use. /// </summary> @@ -53,31 +47,13 @@ public class XUnitLogger : ILogger /// Initializes a new instance of the <see cref="XUnitLogger"/> class. /// </summary> /// <param name="name">The name for messages produced by the logger.</param> - /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param> - /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> - /// <exception cref="ArgumentNullException"> - /// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>. - /// </exception> - public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options) - : this(name, new TestOutputHelperAccessor(outputHelper), options) - { - } - - /// <summary> - /// Initializes a new instance of the <see cref="XUnitLogger"/> class. - /// </summary> - /// <param name="name">The name for messages produced by the logger.</param> - /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param> /// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param> - /// <exception cref="ArgumentNullException"> - /// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>. - /// </exception> - public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options) + private XUnitLogger(string name, XUnitLoggerOptions? options) { Name = name ?? throw new ArgumentNullException(nameof(name)); - _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); - _filter = options?.Filter ?? ((category, logLevel) => true); + _filter = options?.Filter ?? ((_, _) => true); + _messageSinkMessageFactory = options?.MessageSinkMessageFactory ?? (message => new DiagnosticMessage(message)); IncludeScopes = options?.IncludeScopes ?? false; } @@ -152,7 +128,7 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep } /// <summary> - /// Writes a message to the <see cref="ITestOutputHelper"/> associated with the instance. + /// Writes a message to the <see cref="ITestOutputHelper"/> or <see cref="IMessageSink"/> associated with the instance. /// </summary> /// <param name="logLevel">The message to write will be written on this level.</param> /// <param name="eventId">The Id of the event.</param> @@ -160,13 +136,6 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep /// <param name="exception">The exception related to this message.</param> public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception) { - ITestOutputHelper? outputHelper = _accessor.OutputHelper; - - if (outputHelper == null) - { - return; - } - StringBuilder? logBuilder = _logBuilder; _logBuilder = null; @@ -213,7 +182,20 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message try { - outputHelper.WriteLine($"[{Clock():u}] {logLevelString}{formatted}"); + ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper; + IMessageSink? messageSink = _messageSinkAccessor?.MessageSink; + + var line = $"[{Clock():u}] {logLevelString}{formatted}"; + if (outputHelper != null) + { + outputHelper.WriteLine(line); + } + + if (messageSink != null) + { + var sinkMessage = MessageSinkMessageFactory(line); + messageSink.OnMessage(sinkMessage); + } } #pragma warning disable CA1031 catch (InvalidOperationException) diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs new file mode 100644 index 00000000..cdfcaa56 --- /dev/null +++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs @@ -0,0 +1,342 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using MartinCostello.Logging.XUnit; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.Logging +{ + /// <summary> + /// A class containing extension methods for configuring logging to xunit. This class cannot be inherited. + /// </summary> + public static partial class XUnitLoggerExtensions + { + /// <summary> + /// Adds an xunit logger to the logging builder. + /// </summary> + /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> + /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param> + /// <returns> + /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="builder"/> or <paramref name="accessor"/> is <see langword="null"/>. + /// </exception> + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (accessor == null) + { + throw new ArgumentNullException(nameof(accessor)); + } + + return builder.AddXUnit(accessor, (_) => { }); + } + + /// <summary> + /// Adds an xunit logger to the logging builder. + /// </summary> + /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> + /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param> + /// <param name="configure">A delegate to a method to use to configure the logging options.</param> + /// <returns> + /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="builder"/>, <paramref name="accessor"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// </exception> + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor, Action<XUnitLoggerOptions> configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (accessor == null) + { + throw new ArgumentNullException(nameof(accessor)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var options = new XUnitLoggerOptions(); + + configure(options); + +#pragma warning disable CA2000 + builder.AddProvider(new XUnitLoggerProvider(accessor, options)); +#pragma warning restore CA2000 + + builder.Services.TryAddSingleton(accessor); + + return builder; + } + + /// <summary> + /// Adds an xunit logger to the logging builder. + /// </summary> + /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <returns> + /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="builder"/> or <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + return builder.AddXUnit(messageSink, (_) => { }); + } + + /// <summary> + /// Adds an xunit logger to the logging builder. + /// </summary> + /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="configure">A delegate to a method to use to configure the logging options.</param> + /// <returns> + /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="builder"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// </exception> + public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink, Action<XUnitLoggerOptions> configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var options = new XUnitLoggerOptions(); + + configure(options); + +#pragma warning disable CA2000 + return builder.AddProvider(new XUnitLoggerProvider(messageSink, options)); +#pragma warning restore CA2000 + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="minLevel">The minimum <see cref="LogLevel"/> to be logged.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/> or <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, LogLevel minLevel) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + return factory.AddXUnit(messageSink, (_, level) => level >= minLevel); + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="filter">The category filter to apply to logs.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="filter"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func<string?, LogLevel, bool> filter) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + return factory.AddXUnit(messageSink, (options) => options.Filter = filter); + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/> or <paramref name="messageSink"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + return factory.AddXUnit(messageSink, (_) => { }); + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="options">The options to use for logging to xunit.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="options"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, XUnitLoggerOptions options) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return factory.AddXUnit(messageSink, () => options); + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="configure">A delegate to a method to use to configure the logging options.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Action<XUnitLoggerOptions> configure) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + return factory.AddXUnit( + messageSink, + () => + { + var options = new XUnitLoggerOptions(); + configure(options); + return options; + }); + } + + /// <summary> + /// Adds an xunit logger to the factory. + /// </summary> + /// <param name="factory">The <see cref="ILoggerFactory"/> to use.</param> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="configure">A delegate to a method that returns a configured <see cref="XUnitLoggerOptions"/> to use.</param> + /// <returns> + /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. + /// </returns> + /// <exception cref="ArgumentNullException"> + /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>. + /// </exception> + public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func<XUnitLoggerOptions> configure) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (messageSink == null) + { + throw new ArgumentNullException(nameof(messageSink)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var options = configure(); + +#pragma warning disable CA2000 + factory.AddProvider(new XUnitLoggerProvider(messageSink, options)); +#pragma warning restore CA2000 + + return factory; + } + } +} diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.cs b/src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs similarity index 99% rename from src/Logging.XUnit/XUnitLoggerExtensions.cs rename to src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs index ea9027dc..5bd73b22 100644 --- a/src/Logging.XUnit/XUnitLoggerExtensions.cs +++ b/src/Logging.XUnit/XUnitLoggerExtensions.ITestOutputHelper.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.Logging /// A class containing extension methods for configuring logging to xunit. This class cannot be inherited. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] - public static class XUnitLoggerExtensions + public static partial class XUnitLoggerExtensions { /// <summary> /// Adds an xunit logger to the logging builder. diff --git a/src/Logging.XUnit/XUnitLoggerOptions.cs b/src/Logging.XUnit/XUnitLoggerOptions.cs index ecc71fb1..f306b577 100644 --- a/src/Logging.XUnit/XUnitLoggerOptions.cs +++ b/src/Logging.XUnit/XUnitLoggerOptions.cs @@ -3,6 +3,8 @@ using System; using Microsoft.Extensions.Logging; +using Xunit.Abstractions; +using Xunit.Sdk; namespace MartinCostello.Logging.XUnit { @@ -23,6 +25,12 @@ public XUnitLoggerOptions() /// </summary> public Func<string?, LogLevel, bool> Filter { get; set; } = (c, l) => true; // By default log everything + /// <summary> + /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. + /// By default, creates a <see cref="DiagnosticMessage"/>. + /// </summary> + public Func<string, IMessageSinkMessage> MessageSinkMessageFactory { get; set; } = m => new DiagnosticMessage(m); + /// <summary> /// Gets or sets a value indicating whether to include scopes. /// </summary> diff --git a/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs new file mode 100644 index 00000000..41c1c656 --- /dev/null +++ b/src/Logging.XUnit/XUnitLoggerProvider.IMessageSink.cs @@ -0,0 +1,47 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// A class representing an <see cref="ILoggerProvider"/> to use with xunit. + /// </summary> + public partial class XUnitLoggerProvider + { + /// <summary> + /// The <see cref="IMessageSinkAccessor"/> to use. This field is readonly. + /// </summary> + private readonly IMessageSinkAccessor? _messageSinkAccessor; + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. + /// </summary> + /// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param> + /// <param name="options">The options to use for logging to xunit.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="messageSink"/> or <paramref name="options"/> is <see langword="null"/>. + /// </exception> + public XUnitLoggerProvider(IMessageSink messageSink, XUnitLoggerOptions options) + : this(new MessageSinkAccessor(messageSink), options) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. + /// </summary> + /// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param> + /// <param name="options">The options to use for logging to xunit.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>. + /// </exception> + public XUnitLoggerProvider(IMessageSinkAccessor accessor, XUnitLoggerOptions options) + { + _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + } +} diff --git a/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs b/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs new file mode 100644 index 00000000..d2e34ea9 --- /dev/null +++ b/src/Logging.XUnit/XUnitLoggerProvider.ITestOutputHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit +{ + /// <summary> + /// A class representing an <see cref="ILoggerProvider"/> to use with xunit. + /// </summary> + public partial class XUnitLoggerProvider + { + /// <summary> + /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is readonly. + /// </summary> + private readonly ITestOutputHelperAccessor? _outputHelperAccessor; + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. + /// </summary> + /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param> + /// <param name="options">The options to use for logging to xunit.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="outputHelper"/> or <paramref name="options"/> is <see langword="null"/>. + /// </exception> + public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options) + : this(new TestOutputHelperAccessor(outputHelper), options) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. + /// </summary> + /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param> + /// <param name="options">The options to use for logging to xunit.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>. + /// </exception> + public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options) + { + _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + } +} diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs index c6ccaab5..d72cb165 100644 --- a/src/Logging.XUnit/XUnitLoggerProvider.cs +++ b/src/Logging.XUnit/XUnitLoggerProvider.cs @@ -10,45 +10,13 @@ namespace MartinCostello.Logging.XUnit /// <summary> /// A class representing an <see cref="ILoggerProvider"/> to use with xunit. /// </summary> - public class XUnitLoggerProvider : ILoggerProvider + public partial class XUnitLoggerProvider : ILoggerProvider { - /// <summary> - /// The <see cref="ITestOutputHelperAccessor"/> to use. This field is readonly. - /// </summary> - private readonly ITestOutputHelperAccessor _accessor; - /// <summary> /// The <see cref="XUnitLoggerOptions"/> to use. This field is readonly. /// </summary> private readonly XUnitLoggerOptions _options; - /// <summary> - /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. - /// </summary> - /// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param> - /// <param name="options">The options to use for logging to xunit.</param> - /// <exception cref="ArgumentNullException"> - /// <paramref name="outputHelper"/> or <paramref name="options"/> is <see langword="null"/>. - /// </exception> - public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options) - : this(new TestOutputHelperAccessor(outputHelper), options) - { - } - - /// <summary> - /// Initializes a new instance of the <see cref="XUnitLoggerProvider"/> class. - /// </summary> - /// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param> - /// <param name="options">The options to use for logging to xunit.</param> - /// <exception cref="ArgumentNullException"> - /// <paramref name="accessor"/> or <paramref name="options"/> is <see langword="null"/>. - /// </exception> - public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options) - { - _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - /// <summary> /// Finalizes an instance of the <see cref="XUnitLoggerProvider"/> class. /// </summary> @@ -58,7 +26,20 @@ public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOption } /// <inheritdoc /> - public virtual ILogger CreateLogger(string categoryName) => new XUnitLogger(categoryName, _accessor, _options); + public virtual ILogger CreateLogger(string categoryName) + { + if (_outputHelperAccessor != null) + { + return new XUnitLogger(categoryName, _outputHelperAccessor, _options); + } + + if (_messageSinkAccessor != null) + { + return new XUnitLogger(categoryName, _messageSinkAccessor, _options); + } + + throw new InvalidOperationException($"Either {nameof(_outputHelperAccessor)} or {nameof(_messageSinkAccessor)} must not be null."); + } /// <inheritdoc /> public void Dispose() diff --git a/tests/Logging.XUnit.Tests/Constructor.cs b/tests/Logging.XUnit.Tests/Constructor.cs new file mode 100644 index 00000000..21aae94c --- /dev/null +++ b/tests/Logging.XUnit.Tests/Constructor.cs @@ -0,0 +1,11 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +namespace MartinCostello.Logging.XUnit +{ + public enum Constructor + { + ITestOutputHelper, + IMessageSink, + } +} diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs new file mode 100644 index 00000000..730b8e5a --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs @@ -0,0 +1,40 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace MartinCostello.Logging.XUnit.Integration +{ + public sealed class DatabaseFixture : IAsyncLifetime + { + private readonly ILogger _loggerInitialize; + private readonly ILogger _loggerDispose; + private string? _connectionString; + + public DatabaseFixture(IMessageSink messageSink) + { + using var loggerFactory = new LoggerFactory(); + _loggerInitialize = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>(); + _loggerDispose = messageSink.ToLogger<DatabaseFixture>(); + } + + public string ConnectionString => _connectionString ?? throw new InvalidOperationException("The connection string is only available after InitializeAsync has completed."); + + Task IAsyncLifetime.InitializeAsync() + { + _loggerInitialize.LogInformation("Initializing database"); + _connectionString = "Server=localhost"; + return Task.CompletedTask; + } + + Task IAsyncLifetime.DisposeAsync() + { + _loggerDispose.LogInformation("Disposing database"); + return Task.CompletedTask; + } + } +} diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs new file mode 100644 index 00000000..cad38552 --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/DatabaseTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Shouldly; +using Xunit; + +namespace MartinCostello.Logging.XUnit.Integration +{ + public class DatabaseTests : IClassFixture<DatabaseFixture> + { + public DatabaseTests(DatabaseFixture databaseFixture) + { + DatabaseFixture = databaseFixture; + } + + public DatabaseFixture DatabaseFixture { get; } + + [Fact] + public void Run_Database_Test() + { + DatabaseFixture.ConnectionString.ShouldNotBeEmpty(); + } + } +} diff --git a/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs b/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs new file mode 100644 index 00000000..de817298 --- /dev/null +++ b/tests/Logging.XUnit.Tests/Integration/PrintableDiagnosticMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Martin Costello, 2018. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using Xunit.Sdk; + +namespace MartinCostello.Logging.XUnit.Integration +{ + /// <summary> + /// See https://github.com/xunit/xunit/pull/2148#issuecomment-839838421 + /// </summary> + internal class PrintableDiagnosticMessage : DiagnosticMessage + { + public PrintableDiagnosticMessage(string message) + : base(message) + { + } + + public override string ToString() => Message; + } +} diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs index f81c20f1..74ec7936 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs @@ -13,7 +13,7 @@ namespace MartinCostello.Logging.XUnit public static class XUnitLoggerExtensionsTests { [Fact] - public static void AddXUnit_For_ILoggerBuilder_Validates_Parameters() + public static void AddXUnit_TestOutputHelper_For_ILoggerBuilder_Validates_Parameters() { // Arrange var builder = Mock.Of<ILoggingBuilder>(); @@ -35,7 +35,28 @@ public static void AddXUnit_For_ILoggerBuilder_Validates_Parameters() } [Fact] - public static void AddXUnit_For_ILoggerFactory_Validates_Parameters() + public static void AddXUnit_MessageSink_For_ILoggerBuilder_Validates_Parameters() + { + // Arrange + var builder = Mock.Of<ILoggingBuilder>(); + var messageSink = Mock.Of<IMessageSink>(); + var accessor = Mock.Of<IMessageSinkAccessor>(); + + // Act and Assert + Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(messageSink)); + Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(messageSink, ConfigureAction)); + Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(accessor)); + Assert.Throws<ArgumentNullException>("builder", () => (null as ILoggingBuilder) !.AddXUnit(accessor, ConfigureAction)); + Assert.Throws<ArgumentNullException>("accessor", () => builder.AddXUnit((null as IMessageSinkAccessor) !)); + Assert.Throws<ArgumentNullException>("accessor", () => builder.AddXUnit((null as IMessageSinkAccessor) !, ConfigureAction)); + Assert.Throws<ArgumentNullException>("messageSink", () => builder.AddXUnit((null as IMessageSink) !)); + Assert.Throws<ArgumentNullException>("messageSink", () => builder.AddXUnit((null as IMessageSink) !, ConfigureAction)); + Assert.Throws<ArgumentNullException>("configure", () => builder.AddXUnit(messageSink, (null as Action<XUnitLoggerOptions>) !)); + Assert.Throws<ArgumentNullException>("configure", () => builder.AddXUnit(accessor, (null as Action<XUnitLoggerOptions>) !)); + } + + [Fact] + public static void AddXUnit_TestOutputHelper_For_ILoggerFactory_Validates_Parameters() { // Arrange ILoggerFactory factory = NullLoggerFactory.Instance; @@ -51,25 +72,55 @@ public static void AddXUnit_For_ILoggerFactory_Validates_Parameters() Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(outputHelper, Filter)); Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(outputHelper, logLevel)); Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !)); - Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, ConfigureAction)); - Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, ConfigureFunction)); - Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, Filter)); - Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, logLevel)); - Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit(null!, options)); + Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, ConfigureAction)); + Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, ConfigureFunction)); + Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, Filter)); + Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, logLevel)); + Assert.Throws<ArgumentNullException>("outputHelper", () => factory.AddXUnit((null as ITestOutputHelper) !, options)); Assert.Throws<ArgumentNullException>("options", () => factory.AddXUnit(outputHelper, (null as XUnitLoggerOptions) !)); Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(outputHelper, (null as Action<XUnitLoggerOptions>) !)); Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(outputHelper, (null as Func<XUnitLoggerOptions>) !)); Assert.Throws<ArgumentNullException>("filter", () => factory.AddXUnit(outputHelper, (null as Func<string, LogLevel, bool>) !)); } + [Fact] + public static void AddXUnit_MessageSink_For_ILoggerFactory_Validates_Parameters() + { + // Arrange + ILoggerFactory factory = NullLoggerFactory.Instance; + var logLevel = LogLevel.Information; + var messageSink = Mock.Of<IMessageSink>(); + var options = new XUnitLoggerOptions(); + + // Act and Assert + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink)); + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, options)); + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, ConfigureAction)); + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, ConfigureFunction)); + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, Filter)); + Assert.Throws<ArgumentNullException>("factory", () => (null as ILoggerFactory) !.AddXUnit(messageSink, logLevel)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, ConfigureAction)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, ConfigureFunction)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, Filter)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, logLevel)); + Assert.Throws<ArgumentNullException>("messageSink", () => factory.AddXUnit((null as IMessageSink) !, options)); + Assert.Throws<ArgumentNullException>("options", () => factory.AddXUnit(messageSink, (null as XUnitLoggerOptions) !)); + Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(messageSink, (null as Action<XUnitLoggerOptions>) !)); + Assert.Throws<ArgumentNullException>("configure", () => factory.AddXUnit(messageSink, (null as Func<XUnitLoggerOptions>) !)); + Assert.Throws<ArgumentNullException>("filter", () => factory.AddXUnit(messageSink, (null as Func<string, LogLevel, bool>) !)); + } + [Fact] public static void ToLoggerFactory_Validates_Parameters() { // Arrange ITestOutputHelper? outputHelper = null; + IMessageSink? messageSink = null; // Act and Assert Assert.Throws<ArgumentNullException>("outputHelper", () => outputHelper!.ToLoggerFactory()); + Assert.Throws<ArgumentNullException>("messageSink", () => messageSink!.ToLoggerFactory()); } private static void ConfigureAction(XUnitLoggerOptions options) diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs index 15b0e3fe..15620325 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerProviderTests.cs @@ -13,7 +13,7 @@ namespace MartinCostello.Logging.XUnit public static class XUnitLoggerProviderTests { [Fact] - public static void XUnitLoggerProvider_Constructor_Validates_Parameters() + public static void XUnitLoggerProvider_TestOutputHelper_Constructor_Validates_Parameters() { // Arrange var outputHelper = Mock.Of<ITestOutputHelper>(); @@ -28,15 +28,38 @@ public static void XUnitLoggerProvider_Constructor_Validates_Parameters() } [Fact] - public static void XUnitLoggerProvider_Creates_Logger() + public static void XUnitLoggerProvider_MessageSink_Constructor_Validates_Parameters() { // Arrange - var outputHelper = Mock.Of<ITestOutputHelper>(); + var messageSink = Mock.Of<IMessageSink>(); + var accessor = Mock.Of<IMessageSinkAccessor>(); + var options = new XUnitLoggerOptions(); + + // Act and Assert + Assert.Throws<ArgumentNullException>("messageSink", () => new XUnitLoggerProvider((null as IMessageSink) !, options)); + Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLoggerProvider((null as IMessageSinkAccessor) !, options)); + Assert.Throws<ArgumentNullException>("options", () => new XUnitLoggerProvider(messageSink, null!)); + Assert.Throws<ArgumentNullException>("options", () => new XUnitLoggerProvider(accessor, null!)); + } + + [Theory] + [InlineData(Constructor.ITestOutputHelper)] + [InlineData(Constructor.IMessageSink)] + public static void XUnitLoggerProvider_Creates_Logger(Constructor constructor) + { + // Arrange + var testOutputHelper = Mock.Of<ITestOutputHelper>(); + var messageSink = Mock.Of<IMessageSink>(); var options = new XUnitLoggerOptions(); string categoryName = "MyLogger"; - using var target = new XUnitLoggerProvider(outputHelper, options); + using var target = constructor switch + { + Constructor.ITestOutputHelper => new XUnitLoggerProvider(testOutputHelper, options), + Constructor.IMessageSink => new XUnitLoggerProvider(messageSink, options), + _ => throw new ArgumentOutOfRangeException(nameof(constructor), constructor, null) + }; // Act ILogger actual = target.CreateLogger(categoryName); @@ -47,6 +70,7 @@ public static void XUnitLoggerProvider_Creates_Logger() var xunit = actual.ShouldBeOfType<XUnitLogger>(); xunit.Name.ShouldBe(categoryName); xunit.Filter.ShouldBeSameAs(options.Filter); + xunit.MessageSinkMessageFactory.ShouldBeSameAs(options.MessageSinkMessageFactory); xunit.IncludeScopes.ShouldBeFalse(); } } diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs index 751b39d6..9a4df491 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerTests.cs @@ -8,6 +8,7 @@ using Shouldly; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace MartinCostello.Logging.XUnit { @@ -28,7 +29,9 @@ public static void XUnitLogger_Validates_Parameters() // Act and Assert Assert.Throws<ArgumentNullException>("name", () => new XUnitLogger(null!, outputHelper, options)); Assert.Throws<ArgumentNullException>("outputHelper", () => new XUnitLogger(name, (null as ITestOutputHelper) !, options)); + Assert.Throws<ArgumentNullException>("messageSink", () => new XUnitLogger(name, (null as IMessageSink) !, options)); Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLogger(name, (null as ITestOutputHelperAccessor) !, options)); + Assert.Throws<ArgumentNullException>("accessor", () => new XUnitLogger(name, (null as IMessageSinkAccessor) !, options)); // Arrange var logger = new XUnitLogger(name, outputHelper, options); @@ -37,33 +40,49 @@ public static void XUnitLogger_Validates_Parameters() Assert.Throws<ArgumentNullException>("value", () => logger.Filter = null!); } - [Fact] - public static void XUnitLogger_Constructor_Initializes_Instance() + [Theory] + [InlineData(Constructor.ITestOutputHelper)] + [InlineData(Constructor.IMessageSink)] + public static void XUnitLogger_Constructor_Initializes_Instance(Constructor constructor) { // Arrange string name = "MyName"; - var outputHelper = Mock.Of<ITestOutputHelper>(); - + var testOutputHelper = Mock.Of<ITestOutputHelper>(); + var messageSink = Mock.Of<IMessageSink>(); var options = new XUnitLoggerOptions() { Filter = FilterTrue, + MessageSinkMessageFactory = DiagnosticMessageFactory, IncludeScopes = true, }; + XUnitLogger CreateLogger(XUnitLoggerOptions? opts) + { + return constructor switch + { + Constructor.ITestOutputHelper => new XUnitLogger(name, testOutputHelper, opts), + Constructor.IMessageSink => new XUnitLogger(name, messageSink, opts), + _ => throw new ArgumentOutOfRangeException(nameof(constructor), constructor, null) + }; + } + // Act - var actual = new XUnitLogger(name, outputHelper, options); + var actual = CreateLogger(options); // Assert actual.Filter.ShouldBeSameAs(options.Filter); + actual.MessageSinkMessageFactory.ShouldBeSameAs(options.MessageSinkMessageFactory); actual.IncludeScopes.ShouldBeTrue(); actual.Name.ShouldBe(name); // Act - actual = new XUnitLogger(name, outputHelper, null); + actual = CreateLogger(null); // Assert actual.Filter.ShouldNotBeNull(); actual.Filter(null, LogLevel.None).ShouldBeTrue(); + actual.MessageSinkMessageFactory.ShouldNotBeNull(); + actual.MessageSinkMessageFactory("message").ShouldBeOfType<DiagnosticMessage>(); actual.IncludeScopes.ShouldBeFalse(); actual.Name.ShouldBe(name); } @@ -655,6 +674,8 @@ public static void XUnitLogger_Log_Logs_Message_If_Scopes_Included_And_There_Is_ private static DateTimeOffset StaticClock() => new DateTimeOffset(2018, 08, 19, 17, 12, 16, TimeSpan.FromHours(1)); + private static IMessageSinkMessage DiagnosticMessageFactory(string message) => new DiagnosticMessage(message); + private static bool FilterTrue(string? categoryName, LogLevel level) => true; private static bool FilterFalse(string? categoryName, LogLevel level) => false; diff --git a/tests/Logging.XUnit.Tests/xunit.runner.json b/tests/Logging.XUnit.Tests/xunit.runner.json index 89e7724d..ea67ff35 100644 --- a/tests/Logging.XUnit.Tests/xunit.runner.json +++ b/tests/Logging.XUnit.Tests/xunit.runner.json @@ -1,4 +1,6 @@ { + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true, "methodDisplay": "method", "methodDisplayOptions": "replaceUnderscoreWithSpace" } From 0b75f6aad21fe72bece9e7a24c79fafd1caacc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 14:58:10 +0200 Subject: [PATCH 02/14] Use the lowest versions possible for xunit dependencies xunit.abstractions 2.0.2 and xunit.extensibility.execution 2.4.0 are the first versions to support .NET Standard 2.0 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c3d48f5..7b90576b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,8 +11,8 @@ </ItemGroup> <ItemGroup Condition=" '$(IsTestProject)' != 'true' "> <PackageVersion Include="Microsoft.Extensions.Logging" Version="2.0.0" /> - <PackageVersion Include="xunit.abstractions" Version="2.0.3" /> - <PackageVersion Include="xunit.extensibility.execution" Version="2.4.1" /> + <PackageVersion Include="xunit.abstractions" Version="2.0.2" /> + <PackageVersion Include="xunit.extensibility.execution" Version="2.4.0" /> </ItemGroup> <ItemGroup Condition=" '$(IsTestProject)' == 'true' "> <PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" /> From 68ca3f3cbb909750b00a771457cf961ffbb16b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:00:04 +0200 Subject: [PATCH 03/14] Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLogger.IMessageSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs index 9cb27d67..d17f91a4 100644 --- a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs +++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs @@ -52,7 +52,7 @@ public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOption } /// <summary> - /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. + /// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>. /// </summary> /// <exception cref="ArgumentNullException"> /// <paramref name="value"/> is <see langword="null"/>. From 300a51cfbe30f85ee50482a67beb92507f0dcf97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:00:20 +0200 Subject: [PATCH 04/14] Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLogger.IMessageSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs index d17f91a4..cb363850 100644 --- a/src/Logging.XUnit/XUnitLogger.IMessageSink.cs +++ b/src/Logging.XUnit/XUnitLogger.IMessageSink.cs @@ -18,7 +18,7 @@ public partial class XUnitLogger private readonly IMessageSinkAccessor? _messageSinkAccessor; /// <summary> - /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. + /// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>. /// </summary> private Func<string, IMessageSinkMessage> _messageSinkMessageFactory; From 0ea106f403fe20aa5d2bc5c0e1cb9afb82453714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:04:12 +0200 Subject: [PATCH 05/14] Bail out earlier if the output helper and the sink are both null --- src/Logging.XUnit/XUnitLogger.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs index 94f035fc..d32d1985 100644 --- a/src/Logging.XUnit/XUnitLogger.cs +++ b/src/Logging.XUnit/XUnitLogger.cs @@ -136,6 +136,14 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep /// <param name="exception">The exception related to this message.</param> public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception) { + ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper; + IMessageSink? messageSink = _messageSinkAccessor?.MessageSink; + + if (outputHelper is null && messageSink is null) + { + return; + } + StringBuilder? logBuilder = _logBuilder; _logBuilder = null; @@ -182,9 +190,6 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message try { - ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper; - IMessageSink? messageSink = _messageSinkAccessor?.MessageSink; - var line = $"[{Clock():u}] {logLevelString}{formatted}"; if (outputHelper != null) { From 4361d4549b5e0224b281d690aac9ee754656f390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:05:48 +0200 Subject: [PATCH 06/14] Use the _messageSinkMessageFactory field directly --- src/Logging.XUnit/XUnitLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLogger.cs b/src/Logging.XUnit/XUnitLogger.cs index d32d1985..05273625 100644 --- a/src/Logging.XUnit/XUnitLogger.cs +++ b/src/Logging.XUnit/XUnitLogger.cs @@ -198,7 +198,7 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message if (messageSink != null) { - var sinkMessage = MessageSinkMessageFactory(line); + var sinkMessage = _messageSinkMessageFactory(line); messageSink.OnMessage(sinkMessage); } } From e41b0fa4fdc67b27120343e3a44b30c044c204e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:06:29 +0200 Subject: [PATCH 07/14] Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs index cdfcaa56..4d90a237 100644 --- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs +++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs @@ -49,7 +49,7 @@ public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSin /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. /// </returns> /// <exception cref="ArgumentNullException"> - /// <paramref name="builder"/>, <paramref name="accessor"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// <paramref name="builder"/>, <paramref name="accessor"/> or <paramref name="configure"/> is <see langword="null"/>. /// </exception> public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor, Action<XUnitLoggerOptions> configure) { From 1229d8f6d700affdaeefd8bcf0bde30706af38d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:11:50 +0200 Subject: [PATCH 08/14] Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs index 4d90a237..8f8ecc8a 100644 --- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs +++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs @@ -117,7 +117,7 @@ public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSin /// The instance of <see cref="ILoggingBuilder"/> specified by <paramref name="builder"/>. /// </returns> /// <exception cref="ArgumentNullException"> - /// <paramref name="builder"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// <paramref name="builder"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>. /// </exception> public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink, Action<XUnitLoggerOptions> configure) { From a8f6216cfc8acd79d41f4986f3d715aef5b07b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:12:42 +0200 Subject: [PATCH 09/14] Fix typo Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs index 8f8ecc8a..5d75fd2b 100644 --- a/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs +++ b/src/Logging.XUnit/XUnitLoggerExtensions.IMessageSink.cs @@ -272,7 +272,7 @@ public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink /// The instance of <see cref="ILoggerFactory"/> specified by <paramref name="factory"/>. /// </returns> /// <exception cref="ArgumentNullException"> - /// <paramref name="factory"/>, <paramref name="messageSink"/> OR <paramref name="configure"/> is <see langword="null"/>. + /// <paramref name="factory"/>, <paramref name="messageSink"/> or <paramref name="configure"/> is <see langword="null"/>. /// </exception> public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Action<XUnitLoggerOptions> configure) { From eb5a96b0dbe85988a4b1caf5d8e09146bf1989c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:13:35 +0200 Subject: [PATCH 10/14] Fix misleading documentation Co-authored-by: Martin Costello <martin@martincostello.com> --- src/Logging.XUnit/XUnitLoggerOptions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerOptions.cs b/src/Logging.XUnit/XUnitLoggerOptions.cs index f306b577..af1dade4 100644 --- a/src/Logging.XUnit/XUnitLoggerOptions.cs +++ b/src/Logging.XUnit/XUnitLoggerOptions.cs @@ -27,7 +27,6 @@ public XUnitLoggerOptions() /// <summary> /// Gets or sets the message sink message factory to use when writing to a <see cref="IMessageSink"/>. - /// By default, creates a <see cref="DiagnosticMessage"/>. /// </summary> public Func<string, IMessageSinkMessage> MessageSinkMessageFactory { get; set; } = m => new DiagnosticMessage(m); From 095a649903f023288e925ddc53e62076efaf4c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 15:17:30 +0200 Subject: [PATCH 11/14] Tweak logger names --- .../Integration/DatabaseFixture.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs index 730b8e5a..9820ce70 100644 --- a/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs +++ b/tests/Logging.XUnit.Tests/Integration/DatabaseFixture.cs @@ -11,29 +11,29 @@ namespace MartinCostello.Logging.XUnit.Integration { public sealed class DatabaseFixture : IAsyncLifetime { - private readonly ILogger _loggerInitialize; - private readonly ILogger _loggerDispose; + private readonly ILogger _initializeLogger; + private readonly ILogger _disposeLogger; private string? _connectionString; public DatabaseFixture(IMessageSink messageSink) { using var loggerFactory = new LoggerFactory(); - _loggerInitialize = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>(); - _loggerDispose = messageSink.ToLogger<DatabaseFixture>(); + _initializeLogger = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = m => new PrintableDiagnosticMessage(m)).CreateLogger<DatabaseFixture>(); + _disposeLogger = messageSink.ToLogger<DatabaseFixture>(); } public string ConnectionString => _connectionString ?? throw new InvalidOperationException("The connection string is only available after InitializeAsync has completed."); Task IAsyncLifetime.InitializeAsync() { - _loggerInitialize.LogInformation("Initializing database"); + _initializeLogger.LogInformation("Initializing database"); _connectionString = "Server=localhost"; return Task.CompletedTask; } Task IAsyncLifetime.DisposeAsync() { - _loggerDispose.LogInformation("Disposing database"); + _disposeLogger.LogInformation("Disposing database"); return Task.CompletedTask; } } From a752efb30e8988ef3c6cc1b12f89e8c12b29b4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 16:17:20 +0200 Subject: [PATCH 12/14] Improve the InvalidOperationException message for unreachable code path --- src/Logging.XUnit/XUnitLoggerProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs index d72cb165..1f36f0b6 100644 --- a/src/Logging.XUnit/XUnitLoggerProvider.cs +++ b/src/Logging.XUnit/XUnitLoggerProvider.cs @@ -38,7 +38,7 @@ public virtual ILogger CreateLogger(string categoryName) return new XUnitLogger(categoryName, _messageSinkAccessor, _options); } - throw new InvalidOperationException($"Either {nameof(_outputHelperAccessor)} or {nameof(_messageSinkAccessor)} must not be null."); + throw new InvalidOperationException("INTERNAL ERROR. This code path is not reachable since XUnitLoggerProvider is initialized with either a non null _outputHelperAccessor or a non null _messageSinkAccessor."); } /// <inheritdoc /> From 4d1b153e015ef213fd0cb955880d646c8f893632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 16:20:31 +0200 Subject: [PATCH 13/14] Delete unused import --- src/Logging.XUnit/XUnitLoggerProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Logging.XUnit/XUnitLoggerProvider.cs b/src/Logging.XUnit/XUnitLoggerProvider.cs index 1f36f0b6..be337a77 100644 --- a/src/Logging.XUnit/XUnitLoggerProvider.cs +++ b/src/Logging.XUnit/XUnitLoggerProvider.cs @@ -3,7 +3,6 @@ using System; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace MartinCostello.Logging.XUnit { From 144c55a5e4be472750cf803744f3279ababa7712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= <cedric.luthi@gmail.com> Date: Sat, 2 Oct 2021 16:41:24 +0200 Subject: [PATCH 14/14] Add some XUnitLoggerExtensions tests --- .../XUnitLoggerExtensionsTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs index 74ec7936..1827fb24 100644 --- a/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs +++ b/tests/Logging.XUnit.Tests/XUnitLoggerExtensionsTests.cs @@ -2,9 +2,11 @@ // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. using System; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; +using Shouldly; using Xunit; using Xunit.Abstractions; @@ -123,6 +125,83 @@ public static void ToLoggerFactory_Validates_Parameters() Assert.Throws<ArgumentNullException>("messageSink", () => messageSink!.ToLoggerFactory()); } + [Fact] + public static void AddXUnit_Registers_Services() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(c => c.AddXUnit()); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>(); + serviceProvider.GetService<ITestOutputHelperAccessor>().ShouldBeOfType<AmbientTestOutputHelperAccessor>(); + } + + [Fact] + public static void AddXUnit_ITestOutputHelperAccessor_Registers_Services() + { + // Arrange + var services = new ServiceCollection(); + var accessor = Mock.Of<ITestOutputHelperAccessor>(); + + // Act + services.AddLogging(c => c.AddXUnit(accessor)); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>(); + serviceProvider.GetService<ITestOutputHelperAccessor>().ShouldBe(accessor); + } + + [Fact] + public static void AddXUnit_IMessageSinkAccessor_Registers_Services() + { + // Arrange + var services = new ServiceCollection(); + var accessor = Mock.Of<IMessageSinkAccessor>(); + + // Act + services.AddLogging(c => c.AddXUnit(accessor)); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>(); + serviceProvider.GetService<IMessageSinkAccessor>().ShouldBe(accessor); + } + + [Fact] + public static void AddXUnit_ITestOutputHelper_Registers_Services() + { + // Arrange + var services = new ServiceCollection(); + var testOutputHelper = Mock.Of<ITestOutputHelper>(); + + // Act + services.AddLogging(c => c.AddXUnit(testOutputHelper)); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>(); + } + + [Fact] + public static void AddXUnit_IMessageSink_Registers_Services() + { + // Arrange + var services = new ServiceCollection(); + var messageSink = Mock.Of<IMessageSink>(); + + // Act + services.AddLogging(c => c.AddXUnit(messageSink)); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetService<ILoggerProvider>().ShouldBeOfType<XUnitLoggerProvider>(); + } + private static void ConfigureAction(XUnitLoggerOptions options) { }