Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for IMessageSink #246

Merged
merged 14 commits into from
Oct 2, 2021
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
0xced marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
<ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" />
Expand Down
18 changes: 18 additions & 0 deletions src/Logging.XUnit/IMessageSinkAccessor.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
50 changes: 50 additions & 0 deletions src/Logging.XUnit/IMessageSinkExtensions.cs
Original file line number Diff line number Diff line change
@@ -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>();
}
}
1 change: 1 addition & 0 deletions src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="xunit.abstractions" />
<PackageReference Include="xunit.extensibility.execution" />
</ItemGroup>
</Project>
31 changes: 31 additions & 0 deletions src/Logging.XUnit/MessageSinkAccessor.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
66 changes: 66 additions & 0 deletions src/Logging.XUnit/XUnitLogger.IMessageSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Martin Costello, 2018. All rights reserved.
0xced marked this conversation as resolved.
Show resolved Hide resolved
// 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"/>.
0xced marked this conversation as resolved.
Show resolved Hide resolved
/// </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"/>.
0xced marked this conversation as resolved.
Show resolved Hide resolved
/// </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)); }
}
}
}
49 changes: 49 additions & 0 deletions src/Logging.XUnit/XUnitLogger.ITestOutputHelper.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
58 changes: 20 additions & 38 deletions src/Logging.XUnit/XUnitLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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>
Expand All @@ -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);
0xced marked this conversation as resolved.
Show resolved Hide resolved
_messageSinkMessageFactory = options?.MessageSinkMessageFactory ?? (message => new DiagnosticMessage(message));
IncludeScopes = options?.IncludeScopes ?? false;
}

Expand Down Expand Up @@ -152,21 +128,14 @@ 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>
/// <param name="message">The message to write.</param>
/// <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;
0xced marked this conversation as resolved.
Show resolved Hide resolved

if (outputHelper == null)
{
return;
}

StringBuilder? logBuilder = _logBuilder;
_logBuilder = null;

Expand Down Expand Up @@ -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);
0xced marked this conversation as resolved.
Show resolved Hide resolved
messageSink.OnMessage(sinkMessage);
}
}
#pragma warning disable CA1031
catch (InvalidOperationException)
Expand Down
Loading