Skip to content

Commit

Permalink
XUnit test logger improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
ejsmith committed Feb 13, 2024
1 parent 89ce086 commit 95343e2
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 171 deletions.
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.Options.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.Options.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.Options.DefaultMinimumLevel = LogLevel.Trace;
using var metrics = new InMemoryMetricsClient(new InMemoryMetricsClientOptions { Buffered = false, LoggerFactory = Log });

queue.AttachBehavior(new MetricsQueueBehavior<SimpleWorkItem>(metrics, loggerFactory: Log));
Expand Down
5 changes: 3 additions & 2 deletions src/Foundatio.Xunit/Foundatio.Xunit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<ProjectReference Include="..\Foundatio\Foundatio.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="6.0" />
<PackageReference Include="xunit.core" Version="2.6.5" />
<PackageReference Include="System.Collections.Immutable" Version="8.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.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 AddXUnit(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.AddXUnit(options);
}

public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, Action<TestLoggerOptions> configure)
{
var options = new TestLoggerOptions();
configure?.Invoke(options);
return builder.AddXUnit(options);
}

public static ILoggingBuilder AddXUnit(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 AddXUnit(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> ToLogger<T>(this ITestOutputHelper outputHelper, Action<TestLoggerOptions> configure = null)
=> outputHelper.ToTestLogger(configure).CreateLogger<T>();
}
119 changes: 44 additions & 75 deletions src/Foundatio.Xunit/Logging/TestLogger.cs
Original file line number Diff line number Diff line change
@@ -1,112 +1,81 @@
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;

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();

public TestLogger(string categoryName, TestLoggerFactory loggerFactory)
public TestLogger()
{
_loggerFactory = loggerFactory;
_categoryName = categoryName;
Options = new TestLoggerOptions();
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
public TestLogger(TestLoggerOptions options)
{
if (!_loggerFactory.IsEnabled(_categoryName, logLevel))
return;
Options = options ?? new TestLoggerOptions();
}

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
};

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;
}
public TestLoggerOptions Options { get; }
public IReadOnlyList<LogEntry> LogEntries => _logEntries.ToArray();

public void Clear() => _logEntries.Clear();

foreach (object scope in scopes)
internal void AddLogEntry(LogEntry logEntry)
{
lock (_logEntries)
{
if (!(scope is IDictionary<string, object> scopeData))
continue;
_logEntries.Enqueue(logEntry);

foreach (var property in scopeData)
logEntry.Properties[property.Key] = property.Value;
if (_logEntries.Count > Options.MaxLogEntriesToStore)
_logEntries.Dequeue();
}

_loggerFactory.AddLogEntry(logEntry);
}
if (Options.WriteLogEntryFunc == null || _logEntriesWritten >= Options.MaxLogEntriesToWrite)
return;

public bool IsEnabled(LogLevel logLevel)
{
return _loggerFactory.IsEnabled(_categoryName, logLevel);
try
{
Options.WriteLogEntry(logEntry);
Interlocked.Increment(ref _logEntriesWritten);
}
catch (Exception)
{
// ignored
}
}

public IDisposable BeginScope<TState>(TState state)
{
if (state == null)
throw new ArgumentNullException(nameof(state));

return Push(state);
}
private int _logEntriesWritten = 0;

public IDisposable BeginScope<TState, TScope>(Func<TState, TScope> scopeFactory, TState state)
public ILogger CreateLogger(string categoryName)
{
if (state == null)
throw new ArgumentNullException(nameof(state));

return Push(scopeFactory(state));
return new TestLoggerLogger(categoryName, this);
}

private static readonly AsyncLocal<Wrapper> _currentScopeStack = new();
public void AddProvider(ILoggerProvider loggerProvider) { }

private sealed class Wrapper
public bool IsEnabled(string category, LogLevel logLevel)
{
public ImmutableStack<object> Value { get; set; }
}
if (_logLevels.TryGetValue(category, out var categoryLevel))
return logLevel >= categoryLevel;

private static ImmutableStack<object> CurrentScopeStack
{
get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create<object>();
set => _currentScopeStack.Value = new Wrapper { Value = value };
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() { }
}
82 changes: 0 additions & 82 deletions src/Foundatio.Xunit/Logging/TestLoggerFactory.cs

This file was deleted.

Loading

0 comments on commit 95343e2

Please sign in to comment.