Skip to content

Commit

Permalink
XUnit test logger improvements (#297)
Browse files Browse the repository at this point in the history
* XUnit test logger improvements

* Missed one

* More updates and compat

* More updates
  • Loading branch information
ejsmith authored Feb 13, 2024
1 parent 89ce086 commit c55ecc9
Show file tree
Hide file tree
Showing 22 changed files with 577 additions and 172 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\..\src\Foundatio.Extensions.Hosting\Foundatio.Extensions.Hosting.csproj" />
<ProjectReference Include="..\..\src\Foundatio.Xunit\Foundatio.Xunit.csproj" />
<ProjectReference Include="..\..\src\Foundatio\Foundatio.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Foundatio.TestHarness/Locks/LockTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ private Task<bool> DoLockedWorkAsync(ILockProvider locker)

public virtual async Task WillThrottleCallsAsync()
{
Log.MinimumLevel = LogLevel.Trace;
Log.DefaultMinimumLevel = LogLevel.Trace;
Log.SetLogLevel<ScheduledTimer>(LogLevel.Information);
Log.SetLogLevel<ThrottlingLockProvider>(LogLevel.Trace);

Expand Down
6 changes: 3 additions & 3 deletions src/Foundatio.TestHarness/Queue/QueueTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ public virtual async Task WillNotWaitForItemAsync()

public virtual async Task WillWaitForItemAsync()
{
Log.MinimumLevel = LogLevel.Trace;
Log.DefaultMinimumLevel = LogLevel.Trace;
var queue = GetQueue();
if (queue == null)
return;
Expand Down Expand Up @@ -792,7 +792,7 @@ await queue.StartWorkingAsync(w =>

public virtual async Task WorkItemsWillTimeoutAsync()
{
Log.MinimumLevel = LogLevel.Trace;
Log.DefaultMinimumLevel = LogLevel.Trace;
Log.SetLogLevel("Foundatio.Queues.RedisQueue", LogLevel.Trace);
var queue = GetQueue(retryDelay: TimeSpan.Zero, workItemTimeout: TimeSpan.FromMilliseconds(50));
if (queue == null)
Expand Down Expand Up @@ -1313,7 +1313,7 @@ protected async Task CanDequeueWithLockingImpAsync(CacheLockProvider distributed
await queue.DeleteQueueAsync();
await AssertEmptyQueueAsync(queue);

Log.MinimumLevel = LogLevel.Trace;
Log.DefaultMinimumLevel = LogLevel.Trace;
using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log });

queue.AttachBehavior(new MetricsQueueBehavior<SimpleWorkItem>(metrics, loggerFactory: Log));
Expand Down
3 changes: 2 additions & 1 deletion src/Foundatio.Xunit/Foundatio.Xunit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="6.0" />
<PackageReference Include="xunit.core" Version="2.6.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0" />
<PackageReference Include="xunit" Version="2.6" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/Foundatio.Xunit/Logging/LogEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Foundatio.Xunit;

public class LogEntry
{
public DateTime Date { get; set; }
public DateTimeOffset Date { get; set; }
public string CategoryName { get; set; }
public LogLevel LogLevel { get; set; }
public object[] Scopes { get; set; }
Expand Down
84 changes: 84 additions & 0 deletions src/Foundatio.Xunit/Logging/LoggingExtensions.cs
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>();
}
154 changes: 85 additions & 69 deletions src/Foundatio.Xunit/Logging/TestLogger.cs
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() { }
}
38 changes: 38 additions & 0 deletions src/Foundatio.Xunit/Logging/TestLoggerBase.cs
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;
}
}
Loading

0 comments on commit c55ecc9

Please sign in to comment.