-
-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
XUnit test logger improvements (#297)
* XUnit test logger improvements * Missed one * More updates and compat * More updates
- Loading branch information
Showing
22 changed files
with
577 additions
and
172 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using System; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.DependencyInjection.Extensions; | ||
using Microsoft.Extensions.Logging; | ||
using Xunit.Abstractions; | ||
|
||
namespace Foundatio.Xunit; | ||
|
||
public static class LoggingExtensions | ||
{ | ||
public static TestLogger GetTestLogger(this IServiceProvider serviceProvider) | ||
{ | ||
return serviceProvider.GetRequiredService<TestLogger>(); | ||
} | ||
|
||
public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, ITestOutputHelper outputHelper, | ||
Action<TestLoggerOptions> configure = null) | ||
{ | ||
|
||
var options = new TestLoggerOptions { | ||
WriteLogEntryFunc = logEntry => | ||
{ | ||
outputHelper.WriteLine(logEntry.ToString(false)); | ||
} | ||
}; | ||
|
||
configure?.Invoke(options); | ||
|
||
return builder.AddTestLogger(options); | ||
} | ||
|
||
public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, Action<TestLoggerOptions> configure) | ||
{ | ||
var options = new TestLoggerOptions(); | ||
configure?.Invoke(options); | ||
return builder.AddTestLogger(options); | ||
} | ||
|
||
public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, TestLoggerOptions options = null) | ||
{ | ||
if (builder == null) | ||
throw new ArgumentNullException(nameof(builder)); | ||
|
||
var loggerProvider = new TestLoggerProvider(options); | ||
builder.AddProvider(loggerProvider); | ||
builder.Services.TryAddSingleton(loggerProvider.Log); | ||
|
||
return builder; | ||
} | ||
|
||
public static ILoggerFactory AddTestLogger(this ILoggerFactory factory, Action<TestLoggerOptions> configure = null) | ||
{ | ||
if (factory == null) | ||
throw new ArgumentNullException(nameof(factory)); | ||
|
||
var options = new TestLoggerOptions(); | ||
configure?.Invoke(options); | ||
|
||
factory.AddProvider(new TestLoggerProvider(options)); | ||
|
||
return factory; | ||
} | ||
|
||
public static TestLogger ToTestLogger(this ITestOutputHelper outputHelper, Action<TestLoggerOptions> configure = null) | ||
{ | ||
if (outputHelper == null) | ||
throw new ArgumentNullException(nameof(outputHelper)); | ||
|
||
var options = new TestLoggerOptions(); | ||
options.WriteLogEntryFunc = logEntry => | ||
{ | ||
outputHelper.WriteLine(logEntry.ToString()); | ||
}; | ||
|
||
configure?.Invoke(options); | ||
|
||
var testLogger = new TestLogger(options); | ||
|
||
return testLogger; | ||
} | ||
|
||
public static ILogger<T> ToTestLogger<T>(this ITestOutputHelper outputHelper, Action<TestLoggerOptions> configure = null) | ||
=> outputHelper.ToTestLogger(configure).CreateLogger<T>(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,112 +1,128 @@ | ||
using System; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Threading; | ||
using Foundatio.Utility; | ||
using Microsoft.Extensions.Logging; | ||
using Xunit.Abstractions; | ||
|
||
namespace Foundatio.Xunit; | ||
|
||
internal class TestLogger : ILogger | ||
public class TestLogger : ILoggerFactory | ||
{ | ||
private readonly TestLoggerFactory _loggerFactory; | ||
private readonly string _categoryName; | ||
private readonly Dictionary<string, LogLevel> _logLevels = new(); | ||
private readonly Queue<LogEntry> _logEntries = new(); | ||
private int _logEntriesWritten; | ||
|
||
public TestLogger(string categoryName, TestLoggerFactory loggerFactory) | ||
public TestLogger(Action<TestLoggerOptions> configure = null) | ||
{ | ||
_loggerFactory = loggerFactory; | ||
_categoryName = categoryName; | ||
Options = new TestLoggerOptions(); | ||
configure?.Invoke(Options); | ||
} | ||
|
||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) | ||
public TestLogger(ITestOutputHelper output, Action<TestLoggerOptions> configure = null) | ||
{ | ||
if (!_loggerFactory.IsEnabled(_categoryName, logLevel)) | ||
return; | ||
|
||
object[] scopes = CurrentScopeStack.Reverse().ToArray(); | ||
var logEntry = new LogEntry | ||
{ | ||
Date = SystemClock.UtcNow, | ||
LogLevel = logLevel, | ||
EventId = eventId, | ||
State = state, | ||
Exception = exception, | ||
Formatter = (s, e) => formatter((TState)s, e), | ||
CategoryName = _categoryName, | ||
Scopes = scopes | ||
Options = new TestLoggerOptions { | ||
WriteLogEntryFunc = logEntry => | ||
{ | ||
output.WriteLine(logEntry.ToString(false)); | ||
} | ||
}; | ||
|
||
switch (state) | ||
{ | ||
//case LogData logData: | ||
// logEntry.Properties["CallerMemberName"] = logData.MemberName; | ||
// logEntry.Properties["CallerFilePath"] = logData.FilePath; | ||
// logEntry.Properties["CallerLineNumber"] = logData.LineNumber; | ||
|
||
// foreach (var property in logData.Properties) | ||
// logEntry.Properties[property.Key] = property.Value; | ||
// break; | ||
case IDictionary<string, object> logDictionary: | ||
foreach (var property in logDictionary) | ||
logEntry.Properties[property.Key] = property.Value; | ||
break; | ||
} | ||
configure?.Invoke(Options); | ||
} | ||
|
||
foreach (object scope in scopes) | ||
{ | ||
if (!(scope is IDictionary<string, object> scopeData)) | ||
continue; | ||
public TestLogger(TestLoggerOptions options) | ||
{ | ||
Options = options ?? new TestLoggerOptions(); | ||
} | ||
|
||
foreach (var property in scopeData) | ||
logEntry.Properties[property.Key] = property.Value; | ||
} | ||
public TestLoggerOptions Options { get; } | ||
|
||
_loggerFactory.AddLogEntry(logEntry); | ||
[Obsolete("Use DefaultMinimumLevel instead.")] | ||
public LogLevel MinimumLevel | ||
{ | ||
get => Options.DefaultMinimumLevel; | ||
set => Options.DefaultMinimumLevel = value; | ||
} | ||
|
||
public bool IsEnabled(LogLevel logLevel) | ||
public LogLevel DefaultMinimumLevel | ||
{ | ||
return _loggerFactory.IsEnabled(_categoryName, logLevel); | ||
get => Options.DefaultMinimumLevel; | ||
set => Options.DefaultMinimumLevel = value; | ||
} | ||
|
||
public IDisposable BeginScope<TState>(TState state) | ||
public int MaxLogEntriesToStore | ||
{ | ||
if (state == null) | ||
throw new ArgumentNullException(nameof(state)); | ||
|
||
return Push(state); | ||
get => Options.MaxLogEntriesToStore; | ||
set => Options.MaxLogEntriesToStore = value; | ||
} | ||
|
||
public IDisposable BeginScope<TState, TScope>(Func<TState, TScope> scopeFactory, TState state) | ||
public int MaxLogEntriesToWrite | ||
{ | ||
if (state == null) | ||
throw new ArgumentNullException(nameof(state)); | ||
get => Options.MaxLogEntriesToWrite; | ||
set => Options.MaxLogEntriesToWrite = value; | ||
} | ||
|
||
return Push(scopeFactory(state)); | ||
public IReadOnlyList<LogEntry> LogEntries => _logEntries.ToArray(); | ||
|
||
|
||
public void Clear() | ||
{ | ||
lock (_logEntries) | ||
{ | ||
_logEntries.Clear(); | ||
Interlocked.Exchange(ref _logEntriesWritten, 0); | ||
} | ||
} | ||
|
||
private static readonly AsyncLocal<Wrapper> _currentScopeStack = new(); | ||
internal void AddLogEntry(LogEntry logEntry) | ||
{ | ||
lock (_logEntries) | ||
{ | ||
_logEntries.Enqueue(logEntry); | ||
|
||
if (_logEntries.Count > Options.MaxLogEntriesToStore) | ||
_logEntries.Dequeue(); | ||
} | ||
|
||
if (Options.WriteLogEntryFunc == null || _logEntriesWritten >= Options.MaxLogEntriesToWrite) | ||
return; | ||
|
||
try | ||
{ | ||
Options.WriteLogEntry(logEntry); | ||
Interlocked.Increment(ref _logEntriesWritten); | ||
} | ||
catch (Exception) | ||
{ | ||
// ignored | ||
} | ||
} | ||
|
||
private sealed class Wrapper | ||
public ILogger CreateLogger(string categoryName) | ||
{ | ||
public ImmutableStack<object> Value { get; set; } | ||
return new TestLoggerLogger(categoryName, this); | ||
} | ||
|
||
private static ImmutableStack<object> CurrentScopeStack | ||
public void AddProvider(ILoggerProvider loggerProvider) { } | ||
|
||
public bool IsEnabled(string category, LogLevel logLevel) | ||
{ | ||
get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create<object>(); | ||
set => _currentScopeStack.Value = new Wrapper { Value = value }; | ||
if (_logLevels.TryGetValue(category, out var categoryLevel)) | ||
return logLevel >= categoryLevel; | ||
|
||
return logLevel >= Options.DefaultMinimumLevel; | ||
} | ||
|
||
private static IDisposable Push(object state) | ||
public void SetLogLevel(string category, LogLevel minLogLevel) | ||
{ | ||
CurrentScopeStack = CurrentScopeStack.Push(state); | ||
return new DisposableAction(Pop); | ||
_logLevels[category] = minLogLevel; | ||
} | ||
|
||
private static void Pop() | ||
public void SetLogLevel<T>(LogLevel minLogLevel) | ||
{ | ||
CurrentScopeStack = CurrentScopeStack.Pop(); | ||
SetLogLevel(TypeHelper.GetTypeDisplayName(typeof(T)), minLogLevel); | ||
} | ||
|
||
public void Dispose() { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace Foundatio.Xunit; | ||
|
||
public abstract class TestLoggerBase : IClassFixture<TestLoggerFixture>, IAsyncLifetime | ||
{ | ||
protected TestLoggerBase(ITestOutputHelper output, TestLoggerFixture fixture) | ||
{ | ||
Fixture = fixture; | ||
fixture.Output = output; | ||
fixture.AddServiceRegistrations(RegisterServices); | ||
} | ||
|
||
protected TestLoggerFixture Fixture { get; } | ||
protected IServiceProvider Services => Fixture.Services; | ||
protected TestLogger TestLogger => Fixture.TestLogger; | ||
protected ILogger Log => Fixture.Log; | ||
|
||
protected virtual void RegisterServices(IServiceCollection services) | ||
{ | ||
} | ||
|
||
public virtual Task InitializeAsync() | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
public virtual Task DisposeAsync() | ||
{ | ||
Fixture.TestLogger.Clear(); | ||
return Task.CompletedTask; | ||
} | ||
} |
Oops, something went wrong.