Skip to content

Commit ea86954

Browse files
Merge pull request #246 from 0xced/IMessageSink
Add support for IMessageSink
2 parents bedd07b + 144c55a commit ea86954

22 files changed

+985
-87
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
</ItemGroup>
1212
<ItemGroup Condition=" '$(IsTestProject)' != 'true' ">
1313
<PackageVersion Include="Microsoft.Extensions.Logging" Version="2.0.0" />
14-
<PackageVersion Include="xunit.abstractions" Version="2.0.1" />
14+
<PackageVersion Include="xunit.abstractions" Version="2.0.2" />
15+
<PackageVersion Include="xunit.extensibility.execution" Version="2.4.0" />
1516
</ItemGroup>
1617
<ItemGroup Condition=" '$(IsTestProject)' == 'true' ">
1718
<PackageVersion Include="Microsoft.Extensions.Logging" Version="5.0.0" />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Martin Costello, 2018. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using Xunit.Abstractions;
5+
6+
namespace MartinCostello.Logging.XUnit
7+
{
8+
/// <summary>
9+
/// Defines a property for accessing an <see cref="IMessageSink"/>.
10+
/// </summary>
11+
public interface IMessageSinkAccessor
12+
{
13+
/// <summary>
14+
/// Gets or sets the <see cref="IMessageSink"/> to use.
15+
/// </summary>
16+
IMessageSink? MessageSink { get; set; }
17+
}
18+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Martin Costello, 2018. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.ComponentModel;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Xunit.Abstractions
9+
{
10+
/// <summary>
11+
/// A class containing extension methods for the <see cref="IMessageSink"/> interface. This class cannot be inherited.
12+
/// </summary>
13+
[EditorBrowsable(EditorBrowsableState.Never)]
14+
public static class IMessageSinkExtensions
15+
{
16+
/// <summary>
17+
/// Returns an <see cref="ILoggerFactory"/> that logs to the message sink.
18+
/// </summary>
19+
/// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger factory from.</param>
20+
/// <returns>
21+
/// An <see cref="ILoggerFactory"/> that writes messages to the message sink.
22+
/// </returns>
23+
/// <exception cref="ArgumentNullException">
24+
/// <paramref name="messageSink"/> is <see langword="null"/>.
25+
/// </exception>
26+
public static ILoggerFactory ToLoggerFactory(this IMessageSink messageSink)
27+
{
28+
if (messageSink == null)
29+
{
30+
throw new ArgumentNullException(nameof(messageSink));
31+
}
32+
33+
return new LoggerFactory().AddXUnit(messageSink);
34+
}
35+
36+
/// <summary>
37+
/// Returns an <see cref="ILogger{T}"/> that logs to the message sink.
38+
/// </summary>
39+
/// <typeparam name="T">The type of the logger to create.</typeparam>
40+
/// <param name="messageSink">The <see cref="IMessageSink"/> to create the logger from.</param>
41+
/// <returns>
42+
/// An <see cref="ILogger{T}"/> that writes messages to the message sink.
43+
/// </returns>
44+
/// <exception cref="ArgumentNullException">
45+
/// <paramref name="messageSink"/> is <see langword="null"/>.
46+
/// </exception>
47+
public static ILogger<T> ToLogger<T>(this IMessageSink messageSink)
48+
=> messageSink.ToLoggerFactory().CreateLogger<T>();
49+
}
50+
}

src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<ItemGroup>
1818
<PackageReference Include="Microsoft.Extensions.Logging" />
1919
<PackageReference Include="xunit.abstractions" />
20+
<PackageReference Include="xunit.extensibility.execution" />
2021
</ItemGroup>
2122
</Project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Martin Costello, 2018. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using Xunit.Abstractions;
6+
7+
namespace MartinCostello.Logging.XUnit
8+
{
9+
/// <summary>
10+
/// A class representing the default implementation of <see cref="IMessageSinkAccessor"/>. This class cannot be inherited.
11+
/// </summary>
12+
internal sealed class MessageSinkAccessor : IMessageSinkAccessor
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="MessageSinkAccessor"/> class.
16+
/// </summary>
17+
/// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
18+
/// <exception cref="ArgumentNullException">
19+
/// <paramref name="messageSink"/> is <see langword="null"/>.
20+
/// </exception>
21+
internal MessageSinkAccessor(IMessageSink messageSink)
22+
{
23+
MessageSink = messageSink ?? throw new ArgumentNullException(nameof(messageSink));
24+
}
25+
26+
/// <summary>
27+
/// Gets or sets the current <see cref="IMessageSink"/>.
28+
/// </summary>
29+
public IMessageSink? MessageSink { get; set; }
30+
}
31+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Martin Costello, 2018. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
using Xunit.Abstractions;
7+
8+
namespace MartinCostello.Logging.XUnit
9+
{
10+
/// <summary>
11+
/// A class representing an <see cref="ILogger"/> to use with xunit.
12+
/// </summary>
13+
public partial class XUnitLogger
14+
{
15+
/// <summary>
16+
/// The <see cref="IMessageSinkAccessor"/> to use. This field is read-only.
17+
/// </summary>
18+
private readonly IMessageSinkAccessor? _messageSinkAccessor;
19+
20+
/// <summary>
21+
/// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>.
22+
/// </summary>
23+
private Func<string, IMessageSinkMessage> _messageSinkMessageFactory;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
27+
/// </summary>
28+
/// <param name="name">The name for messages produced by the logger.</param>
29+
/// <param name="messageSink">The <see cref="IMessageSink"/> to use.</param>
30+
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
31+
/// <exception cref="ArgumentNullException">
32+
/// <paramref name="name"/> or <paramref name="messageSink"/> is <see langword="null"/>.
33+
/// </exception>
34+
public XUnitLogger(string name, IMessageSink messageSink, XUnitLoggerOptions? options)
35+
: this(name, new MessageSinkAccessor(messageSink), options)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
41+
/// </summary>
42+
/// <param name="name">The name for messages produced by the logger.</param>
43+
/// <param name="accessor">The <see cref="IMessageSinkAccessor"/> to use.</param>
44+
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
45+
/// <exception cref="ArgumentNullException">
46+
/// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
47+
/// </exception>
48+
public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOptions? options)
49+
: this(name, options)
50+
{
51+
_messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
52+
}
53+
54+
/// <summary>
55+
/// Gets or sets the message sink message factory to use when writing to an <see cref="IMessageSink"/>.
56+
/// </summary>
57+
/// <exception cref="ArgumentNullException">
58+
/// <paramref name="value"/> is <see langword="null"/>.
59+
/// </exception>
60+
public Func<string, IMessageSinkMessage> MessageSinkMessageFactory
61+
{
62+
get { return _messageSinkMessageFactory; }
63+
set { _messageSinkMessageFactory = value ?? throw new ArgumentNullException(nameof(value)); }
64+
}
65+
}
66+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Martin Costello, 2018. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
using Xunit.Abstractions;
7+
8+
namespace MartinCostello.Logging.XUnit
9+
{
10+
/// <summary>
11+
/// A class representing an <see cref="ILogger"/> to use with xunit.
12+
/// </summary>
13+
public partial class XUnitLogger
14+
{
15+
/// <summary>
16+
/// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only.
17+
/// </summary>
18+
private readonly ITestOutputHelperAccessor? _outputHelperAccessor;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
22+
/// </summary>
23+
/// <param name="name">The name for messages produced by the logger.</param>
24+
/// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
25+
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
26+
/// <exception cref="ArgumentNullException">
27+
/// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>.
28+
/// </exception>
29+
public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options)
30+
: this(name, new TestOutputHelperAccessor(outputHelper), options)
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
36+
/// </summary>
37+
/// <param name="name">The name for messages produced by the logger.</param>
38+
/// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
39+
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
40+
/// <exception cref="ArgumentNullException">
41+
/// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
42+
/// </exception>
43+
public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options)
44+
: this(name, options)
45+
{
46+
_outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
47+
}
48+
}
49+
}

src/Logging.XUnit/XUnitLogger.cs

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
33

44
using System;
5-
using System.Collections;
65
using System.Collections.Generic;
7-
using System.Linq;
86
using System.Text;
97
using Microsoft.Extensions.Logging;
108
using Xunit.Abstractions;
9+
using Xunit.Sdk;
1110

1211
namespace MartinCostello.Logging.XUnit
1312
{
1413
/// <summary>
1514
/// A class representing an <see cref="ILogger"/> to use with xunit.
1615
/// </summary>
17-
public class XUnitLogger : ILogger
16+
public partial class XUnitLogger : ILogger
1817
{
1918
//// Based on https://github.com/aspnet/Logging/blob/master/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs
2019

@@ -39,11 +38,6 @@ public class XUnitLogger : ILogger
3938
[ThreadStatic]
4039
private static StringBuilder? _logBuilder;
4140

42-
/// <summary>
43-
/// The <see cref="ITestOutputHelperAccessor"/> to use. This field is read-only.
44-
/// </summary>
45-
private readonly ITestOutputHelperAccessor _accessor;
46-
4741
/// <summary>
4842
/// Gets or sets the filter to use.
4943
/// </summary>
@@ -53,31 +47,13 @@ public class XUnitLogger : ILogger
5347
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
5448
/// </summary>
5549
/// <param name="name">The name for messages produced by the logger.</param>
56-
/// <param name="outputHelper">The <see cref="ITestOutputHelper"/> to use.</param>
57-
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
58-
/// <exception cref="ArgumentNullException">
59-
/// <paramref name="name"/> or <paramref name="outputHelper"/> is <see langword="null"/>.
60-
/// </exception>
61-
public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options)
62-
: this(name, new TestOutputHelperAccessor(outputHelper), options)
63-
{
64-
}
65-
66-
/// <summary>
67-
/// Initializes a new instance of the <see cref="XUnitLogger"/> class.
68-
/// </summary>
69-
/// <param name="name">The name for messages produced by the logger.</param>
70-
/// <param name="accessor">The <see cref="ITestOutputHelperAccessor"/> to use.</param>
7150
/// <param name="options">The <see cref="XUnitLoggerOptions"/> to use.</param>
72-
/// <exception cref="ArgumentNullException">
73-
/// <paramref name="name"/> or <paramref name="accessor"/> is <see langword="null"/>.
74-
/// </exception>
75-
public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options)
51+
private XUnitLogger(string name, XUnitLoggerOptions? options)
7652
{
7753
Name = name ?? throw new ArgumentNullException(nameof(name));
78-
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
7954

80-
_filter = options?.Filter ?? ((category, logLevel) => true);
55+
_filter = options?.Filter ?? ((_, _) => true);
56+
_messageSinkMessageFactory = options?.MessageSinkMessageFactory ?? (message => new DiagnosticMessage(message));
8157
IncludeScopes = options?.IncludeScopes ?? false;
8258
}
8359

@@ -152,17 +128,18 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState? state, Excep
152128
}
153129

154130
/// <summary>
155-
/// Writes a message to the <see cref="ITestOutputHelper"/> associated with the instance.
131+
/// Writes a message to the <see cref="ITestOutputHelper"/> or <see cref="IMessageSink"/> associated with the instance.
156132
/// </summary>
157133
/// <param name="logLevel">The message to write will be written on this level.</param>
158134
/// <param name="eventId">The Id of the event.</param>
159135
/// <param name="message">The message to write.</param>
160136
/// <param name="exception">The exception related to this message.</param>
161137
public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception)
162138
{
163-
ITestOutputHelper? outputHelper = _accessor.OutputHelper;
139+
ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper;
140+
IMessageSink? messageSink = _messageSinkAccessor?.MessageSink;
164141

165-
if (outputHelper == null)
142+
if (outputHelper is null && messageSink is null)
166143
{
167144
return;
168145
}
@@ -213,7 +190,17 @@ public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message
213190

214191
try
215192
{
216-
outputHelper.WriteLine($"[{Clock():u}] {logLevelString}{formatted}");
193+
var line = $"[{Clock():u}] {logLevelString}{formatted}";
194+
if (outputHelper != null)
195+
{
196+
outputHelper.WriteLine(line);
197+
}
198+
199+
if (messageSink != null)
200+
{
201+
var sinkMessage = _messageSinkMessageFactory(line);
202+
messageSink.OnMessage(sinkMessage);
203+
}
217204
}
218205
#pragma warning disable CA1031
219206
catch (InvalidOperationException)

0 commit comments

Comments
 (0)