diff --git a/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj b/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj index 45456a13..8272d996 100644 --- a/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj +++ b/samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj @@ -1,6 +1,7 @@  + diff --git a/src/Foundatio.TestHarness/Locks/LockTestBase.cs b/src/Foundatio.TestHarness/Locks/LockTestBase.cs index 01d7513a..1b987aab 100644 --- a/src/Foundatio.TestHarness/Locks/LockTestBase.cs +++ b/src/Foundatio.TestHarness/Locks/LockTestBase.cs @@ -271,7 +271,7 @@ private Task DoLockedWorkAsync(ILockProvider locker) public virtual async Task WillThrottleCallsAsync() { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; Log.SetLogLevel(LogLevel.Information); Log.SetLogLevel(LogLevel.Trace); diff --git a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs index 1bf7a243..be043688 100644 --- a/src/Foundatio.TestHarness/Queue/QueueTestBase.cs +++ b/src/Foundatio.TestHarness/Queue/QueueTestBase.cs @@ -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; @@ -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) @@ -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(metrics, loggerFactory: Log)); diff --git a/src/Foundatio.Xunit/Foundatio.Xunit.csproj b/src/Foundatio.Xunit/Foundatio.Xunit.csproj index f881eaee..5b974843 100644 --- a/src/Foundatio.Xunit/Foundatio.Xunit.csproj +++ b/src/Foundatio.Xunit/Foundatio.Xunit.csproj @@ -8,6 +8,7 @@ - + + diff --git a/src/Foundatio.Xunit/Logging/LogEntry.cs b/src/Foundatio.Xunit/Logging/LogEntry.cs index 6f68b732..85f51762 100644 --- a/src/Foundatio.Xunit/Logging/LogEntry.cs +++ b/src/Foundatio.Xunit/Logging/LogEntry.cs @@ -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; } diff --git a/src/Foundatio.Xunit/Logging/LoggingExtensions.cs b/src/Foundatio.Xunit/Logging/LoggingExtensions.cs new file mode 100644 index 00000000..526434a8 --- /dev/null +++ b/src/Foundatio.Xunit/Logging/LoggingExtensions.cs @@ -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(); + } + + public static ILoggingBuilder AddTestLogger(this ILoggingBuilder builder, ITestOutputHelper outputHelper, + Action 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 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 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 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 ToTestLogger(this ITestOutputHelper outputHelper, Action configure = null) + => outputHelper.ToTestLogger(configure).CreateLogger(); +} diff --git a/src/Foundatio.Xunit/Logging/TestLogger.cs b/src/Foundatio.Xunit/Logging/TestLogger.cs index 2d525cad..11554a43 100644 --- a/src/Foundatio.Xunit/Logging/TestLogger.cs +++ b/src/Foundatio.Xunit/Logging/TestLogger.cs @@ -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 _logLevels = new(); + private readonly Queue _logEntries = new(); + private int _logEntriesWritten; - public TestLogger(string categoryName, TestLoggerFactory loggerFactory) + public TestLogger(Action configure = null) { - _loggerFactory = loggerFactory; - _categoryName = categoryName; + Options = new TestLoggerOptions(); + configure?.Invoke(Options); } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + public TestLogger(ITestOutputHelper output, Action 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 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 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 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(Func 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 LogEntries => _logEntries.ToArray(); + + + public void Clear() + { + lock (_logEntries) + { + _logEntries.Clear(); + Interlocked.Exchange(ref _logEntriesWritten, 0); + } } - private static readonly AsyncLocal _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 Value { get; set; } + return new TestLoggerLogger(categoryName, this); } - private static ImmutableStack CurrentScopeStack + public void AddProvider(ILoggerProvider loggerProvider) { } + + public bool IsEnabled(string category, LogLevel logLevel) { - get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create(); - 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(LogLevel minLogLevel) { - CurrentScopeStack = CurrentScopeStack.Pop(); + SetLogLevel(TypeHelper.GetTypeDisplayName(typeof(T)), minLogLevel); } + + public void Dispose() { } } diff --git a/src/Foundatio.Xunit/Logging/TestLoggerBase.cs b/src/Foundatio.Xunit/Logging/TestLoggerBase.cs new file mode 100644 index 00000000..6aa2c907 --- /dev/null +++ b/src/Foundatio.Xunit/Logging/TestLoggerBase.cs @@ -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, 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; + } +} diff --git a/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs b/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs deleted file mode 100644 index 7379a3f6..00000000 --- a/src/Foundatio.Xunit/Logging/TestLoggerFactory.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Foundatio.Utility; -using Microsoft.Extensions.Logging; -using Xunit.Abstractions; - -namespace Foundatio.Xunit; - -public class TestLoggerFactory : ILoggerFactory -{ - private readonly Dictionary _logLevels = new(); - private readonly Queue _logEntries = new(); - private readonly Action _writeLogEntryFunc; - - public TestLoggerFactory() - { - _writeLogEntryFunc = e => { }; - } - - public TestLoggerFactory(Action writeLogEntryFunc) - { - _writeLogEntryFunc = writeLogEntryFunc; - } - - public TestLoggerFactory(ITestOutputHelper output) : this(e => output.WriteLine(e.ToString(false))) { } - - public LogLevel MinimumLevel { get; set; } = LogLevel.Information; - public IReadOnlyList LogEntries => _logEntries.ToArray(); - public int MaxLogEntriesToStore = 100; - public int MaxLogEntriesToWrite = 1000; - - internal void AddLogEntry(LogEntry logEntry) - { - lock (_logEntries) - { - _logEntries.Enqueue(logEntry); - - if (_logEntries.Count > MaxLogEntriesToStore) - _logEntries.Dequeue(); - } - - if (_writeLogEntryFunc == null || _logEntriesWritten >= MaxLogEntriesToWrite) - return; - - try - { - _writeLogEntryFunc(logEntry); - Interlocked.Increment(ref _logEntriesWritten); - } - catch (Exception) { } - } - - private int _logEntriesWritten = 0; - - public ILogger CreateLogger(string categoryName) - { - return new TestLogger(categoryName, this); - } - - public void AddProvider(ILoggerProvider loggerProvider) { } - - public bool IsEnabled(string category, LogLevel logLevel) - { - if (_logLevels.TryGetValue(category, out var categoryLevel)) - return logLevel >= categoryLevel; - - return logLevel >= MinimumLevel; - } - - public void SetLogLevel(string category, LogLevel minLogLevel) - { - _logLevels[category] = minLogLevel; - } - - public void SetLogLevel(LogLevel minLogLevel) - { - SetLogLevel(TypeHelper.GetTypeDisplayName(typeof(T)), minLogLevel); - } - - public void Dispose() { } -} diff --git a/src/Foundatio.Xunit/Logging/TestLoggerFixture.cs b/src/Foundatio.Xunit/Logging/TestLoggerFixture.cs new file mode 100644 index 00000000..22388fdc --- /dev/null +++ b/src/Foundatio.Xunit/Logging/TestLoggerFixture.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Foundatio.Xunit; + +public class TestLoggerFixture : IAsyncLifetime +{ + private readonly List _disposables = []; + private readonly List> _serviceRegistrations = []; + private readonly Lazy _serviceProvider; + private readonly Lazy _testLogger; + private readonly Lazy _log; + + public TestLoggerFixture() + { + _serviceProvider = new Lazy(BuildServiceProvider); + _testLogger = new Lazy(() => Services.GetTestLogger()); + _log = new Lazy(() => Services.GetRequiredService().CreateLogger(GetType())); + } + + public ITestOutputHelper Output { get; set; } + + public void AddServiceRegistrations(Action registerServices) + { + _serviceRegistrations.Add(registerServices); + } + + public IServiceProvider Services => _serviceProvider.Value; + public TestLogger TestLogger => _testLogger.Value; + public ILogger Log => _log.Value; + + protected virtual void RegisterServices(IServiceCollection services) + { + if (Output == null) + throw new InvalidOperationException("Output should be set before registering services."); + + services.AddLogging(c => c.AddTestLogger(Output)); + foreach (var registration in _serviceRegistrations) + registration(services); + } + + protected virtual IServiceProvider BuildServiceProvider() + { + var services = new ServiceCollection(); + RegisterServices(services); + var sp = services.BuildServiceProvider(); + _disposables.Add(sp); + return sp; + } + + public virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + public virtual Task DisposeAsync() + { + foreach (var disposable in _disposables) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + Log?.LogError(ex, "Error disposing resource."); + } + } + + return Task.CompletedTask; + } +} diff --git a/src/Foundatio.Xunit/Logging/TestLoggerLogger.cs b/src/Foundatio.Xunit/Logging/TestLoggerLogger.cs new file mode 100644 index 00000000..d7cff822 --- /dev/null +++ b/src/Foundatio.Xunit/Logging/TestLoggerLogger.cs @@ -0,0 +1,112 @@ +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 TestLoggerLogger : ILogger +{ + private readonly TestLogger _logger; + private readonly string _categoryName; + + public TestLoggerLogger(string categoryName, TestLogger logger) + { + _logger = logger; + _categoryName = categoryName; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!_logger.IsEnabled(_categoryName, logLevel)) + return; + + object[] scopes = _logger.Options.IncludeScopes ? CurrentScopeStack.Reverse().ToArray() : Array.Empty(); + var logEntry = new LogEntry + { + Date = _logger.Options.GetNow(), + 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 logDictionary: + foreach (var property in logDictionary) + logEntry.Properties[property.Key] = property.Value; + break; + } + + foreach (object scope in scopes) + { + if (!(scope is IDictionary scopeData)) + continue; + + foreach (var property in scopeData) + logEntry.Properties[property.Key] = property.Value; + } + + _logger.AddLogEntry(logEntry); + } + + public bool IsEnabled(LogLevel logLevel) + { + return _logger.IsEnabled(_categoryName, logLevel); + } + + public IDisposable BeginScope(TState state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); + + return Push(state); + } + + public IDisposable BeginScope(Func scopeFactory, TState state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); + + return Push(scopeFactory(state)); + } + + private static readonly AsyncLocal _currentScopeStack = new(); + + private sealed class Wrapper + { + public ImmutableStack Value { get; set; } + } + + private static ImmutableStack CurrentScopeStack + { + get => _currentScopeStack.Value?.Value ?? ImmutableStack.Create(); + set => _currentScopeStack.Value = new Wrapper { Value = value }; + } + + private static IDisposable Push(object state) + { + CurrentScopeStack = CurrentScopeStack.Push(state); + return new DisposableAction(Pop); + } + + private static void Pop() + { + CurrentScopeStack = CurrentScopeStack.Pop(); + } +} diff --git a/src/Foundatio.Xunit/Logging/TestLoggerOptions.cs b/src/Foundatio.Xunit/Logging/TestLoggerOptions.cs new file mode 100644 index 00000000..f8489c66 --- /dev/null +++ b/src/Foundatio.Xunit/Logging/TestLoggerOptions.cs @@ -0,0 +1,19 @@ +using System; +using Foundatio.Utility; +using Microsoft.Extensions.Logging; + +namespace Foundatio.Xunit; + +public class TestLoggerOptions +{ + public LogLevel DefaultMinimumLevel { get; set; } = LogLevel.Information; + public int MaxLogEntriesToStore { get; set; } = 100; + public int MaxLogEntriesToWrite { get; set; } = 1000; + public bool IncludeScopes { get; set; } = true; + + public Action WriteLogEntryFunc { get; set; } + internal void WriteLogEntry(LogEntry logEntry) => WriteLogEntryFunc?.Invoke(logEntry); + + public Func NowFunc { get; set; } + internal DateTimeOffset GetNow() => NowFunc?.Invoke() ?? SystemClock.OffsetNow; +} diff --git a/src/Foundatio.Xunit/Logging/TestLoggerProvider.cs b/src/Foundatio.Xunit/Logging/TestLoggerProvider.cs new file mode 100644 index 00000000..41335a5f --- /dev/null +++ b/src/Foundatio.Xunit/Logging/TestLoggerProvider.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Foundatio.Xunit; + +[ProviderAlias("Test")] +public class TestLoggerProvider : ILoggerProvider +{ + public TestLoggerProvider(TestLoggerOptions options) + { + Log = new TestLogger(options); + } + + public TestLogger Log { get; } + + public virtual ILogger CreateLogger(string categoryName) + { + return Log.CreateLogger(categoryName); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + + ~TestLoggerProvider() + { + Dispose(false); + } +} diff --git a/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs b/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs index a3e91ba7..e3adbc67 100644 --- a/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs +++ b/src/Foundatio.Xunit/Logging/TestWithLoggingBase.cs @@ -9,9 +9,9 @@ public abstract class TestWithLoggingBase protected TestWithLoggingBase(ITestOutputHelper output) { - Log = new TestLoggerFactory(output); + Log = new TestLogger(output); _logger = Log.CreateLogger(GetType()); } - protected TestLoggerFactory Log { get; } + protected TestLogger Log { get; } } diff --git a/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs b/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs index 7dc699eb..dabaff30 100644 --- a/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs +++ b/tests/Foundatio.Tests/Caching/InMemoryCacheClientTests.cs @@ -155,7 +155,7 @@ public override Task CanManageListsAsync() [Fact] public async Task CanSetMaxItems() { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; // run in tight loop so that the code is warmed up and we can catch timing issues for (int x = 0; x < 5; x++) diff --git a/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs b/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs index d2e9b197..3003f60c 100644 --- a/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs +++ b/tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs @@ -81,7 +81,7 @@ public override Task WillExpireRemoteItems() [Fact(Skip = "Skip because cache invalidation loops on this with 2 in memory cache client instances")] public override Task WillWorkWithSets() { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; return base.WillWorkWithSets(); } diff --git a/tests/Foundatio.Tests/Hosting/HostingTests.cs b/tests/Foundatio.Tests/Hosting/HostingTests.cs index 2b81859a..b5eb71e4 100644 --- a/tests/Foundatio.Tests/Hosting/HostingTests.cs +++ b/tests/Foundatio.Tests/Hosting/HostingTests.cs @@ -16,18 +16,23 @@ namespace Foundatio.Tests.Hosting; -public class HostingTests : TestWithLoggingBase +public class HostingTests { - public HostingTests(ITestOutputHelper output) : base(output) { } + private readonly ITestOutputHelper _output; + + public HostingTests(ITestOutputHelper output) + { + _output = output; + } [Fact] public async Task WillRunSyncStartupAction() { var resetEvent = new AsyncManualResetEvent(false); var builder = new WebHostBuilder() + .ConfigureLogging(l => l.AddTestLogger(_output)) .ConfigureServices(s => { - s.AddSingleton(Log); s.AddStartupAction("Hey", () => resetEvent.Set()); s.AddHealthChecks().AddCheckForStartupActions("Critical"); }) @@ -52,9 +57,9 @@ public async Task WillRunAsyncStartupAction() { var resetEvent = new AsyncManualResetEvent(false); var builder = new WebHostBuilder() + .ConfigureLogging(l => l.AddTestLogger(_output)) .ConfigureServices(s => { - s.AddSingleton(Log); s.AddStartupAction("Hey", () => { resetEvent.Set(); @@ -82,9 +87,9 @@ public async Task WillRunAsyncStartupAction() public async Task WillRunClassStartupAction() { var builder = new WebHostBuilder() + .ConfigureLogging(l => l.AddTestLogger(_output)) .ConfigureServices(s => { - s.AddSingleton(Log); s.AddStartupAction("Hey"); s.AddHealthChecks().AddCheckForStartupActions("Critical"); }) @@ -109,10 +114,10 @@ public async Task WillRunClassStartupAction() public async Task WillStopWaitingWhenStartupActionFails() { var builder = new WebHostBuilder() + .ConfigureLogging(l => l.AddTestLogger(_output)) .CaptureStartupErrors(true) .ConfigureServices(s => { - s.AddSingleton(Log); s.AddStartupAction("Boom", () => throw new ApplicationException("Boom")); s.AddHealthChecks().AddCheckForStartupActions("Critical"); }) @@ -134,11 +139,11 @@ public async Task WillStopWaitingWhenStartupActionFails() public async Task WillHandleNoRegisteredStartupActions() { var builder = new WebHostBuilder() + .ConfigureLogging(l => l.AddTestLogger(_output)) .UseEnvironment(Environments.Development) .CaptureStartupErrors(true) .ConfigureServices(s => { - s.AddSingleton(Log); s.AddHealthChecks().AddCheckForStartupActions("Critical"); }) .Configure(app => diff --git a/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs b/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs index af162c2a..3f010eaa 100644 --- a/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs +++ b/tests/Foundatio.Tests/Locks/InMemoryLockTests.cs @@ -59,7 +59,7 @@ public override Task CanAcquireMultipleResources() return base.CanAcquireMultipleResources(); } - [Fact] + [RetryFact] public override Task CanAcquireLocksInParallel() { return base.CanAcquireLocksInParallel(); diff --git a/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs b/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs index b0f9012d..92c1c4f6 100644 --- a/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs +++ b/tests/Foundatio.Tests/Metrics/DiagnosticsMetricsTests.cs @@ -16,7 +16,7 @@ public class DiagnosticsMetricsTests : TestWithLoggingBase, IDisposable public DiagnosticsMetricsTests(ITestOutputHelper output) : base(output) { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; _client = new DiagnosticsMetricsClient(o => o.MeterName("Test")); } diff --git a/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs b/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs index 969ece92..29893357 100644 --- a/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs +++ b/tests/Foundatio.Tests/Queue/InMemoryQueueTests.cs @@ -290,7 +290,7 @@ public override Task VerifyDelayedRetryAttemptsAsync() [Fact] public override Task CanHandleAutoAbandonInWorker() { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; return base.CanHandleAutoAbandonInWorker(); } diff --git a/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs b/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs index 09d7b0ec..fae29893 100644 --- a/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs +++ b/tests/Foundatio.Tests/Utility/ScheduledTimerTests.cs @@ -47,7 +47,7 @@ public Task CanRunWithMinimumInterval() private async Task CanRunConcurrentlyAsync(TimeSpan? minimumIntervalTime = null) { - Log.MinimumLevel = LogLevel.Trace; + Log.DefaultMinimumLevel = LogLevel.Trace; const int iterations = 2; var countdown = new AsyncCountdownEvent(iterations); diff --git a/tests/Foundatio.Tests/Utility/TestLoggerTests.cs b/tests/Foundatio.Tests/Utility/TestLoggerTests.cs new file mode 100644 index 00000000..3e3f23f3 --- /dev/null +++ b/tests/Foundatio.Tests/Utility/TestLoggerTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using Foundatio.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Foundatio.Tests.Utility; + +public class TestLoggerTests : TestLoggerBase +{ + private readonly ITestOutputHelper _output; + + public TestLoggerTests(ITestOutputHelper output, TestLoggerFixture fixture) : base(output, fixture) + { + _output = output; + fixture.AddServiceRegistrations(s => s.AddSingleton()); + } + + [Fact] + public void CanUseTestLogger() + { + var services = new ServiceCollection() + .AddLogging(b => b.AddDebug().AddTestLogger(_output)) + .AddSingleton(); + + IServiceProvider provider = services.BuildServiceProvider(); + + var someClass = provider.GetRequiredService(); + + someClass.DoSomething(1); + + var testLogger = provider.GetTestLogger(); + Assert.Single(testLogger.LogEntries); + Assert.Contains("Doing something", testLogger.LogEntries[0].Message); + + testLogger.Clear(); + testLogger.SetLogLevel(LogLevel.Error); + + someClass.DoSomething(2); + Assert.Empty(testLogger.LogEntries); + } + + [Fact] + public void CanUseTestLoggerFixture() + { + var someClass = Services.GetRequiredService(); + + for (int i = 1; i <= 9999; i++) + someClass.DoSomething(i); + + Log.LogInformation("Hello 1"); + Log.LogInformation("Hello 2"); + + Assert.Equal(100, TestLogger.LogEntries.Count); + Assert.Contains("Hello 2", TestLogger.LogEntries.Last().Message); + + Fixture.TestLogger.Clear(); + TestLogger.SetLogLevel(LogLevel.Error); + + someClass.DoSomething(1002); + + Assert.Empty(TestLogger.LogEntries); + TestLogger.SetLogLevel(LogLevel.Information); + } + + [Fact] + public void CanUseTestLoggerFixture2() + { + var someClass = Services.GetRequiredService(); + + someClass.DoSomething(1); + + Assert.Single(TestLogger.LogEntries); + Assert.Contains("Doing something", TestLogger.LogEntries[0].Message); + + TestLogger.Clear(); + TestLogger.SetLogLevel(LogLevel.Error); + + someClass.DoSomething(2); + Assert.Empty(TestLogger.LogEntries); + } +} + +public class SomeClass +{ + private readonly ILogger _logger; + + public SomeClass(ILogger logger) + { + _logger = logger; + } + + public void DoSomething(int number) + { + _logger.LogInformation("Doing something {Number}", number); + } +}