From c069f1e814ce7604ca086a729341afcfb9aed395 Mon Sep 17 00:00:00 2001 From: Colin Mackay Date: Wed, 30 Oct 2024 23:14:27 +0000 Subject: [PATCH 1/5] (#171) Get logs with predicate --- .../TestCaptureLoggerOfTTests.cs | 31 +++++++++++++++++++ .../ITestCaptureLogger.cs | 8 +++++ .../TestCaptureLogger(OfTCategoryType).cs | 5 +++ .../TestCaptureLogger.cs | 18 ++++++----- .../TestCaptureLoggerProvider.cs | 19 +++++++++--- 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/Stravaig.Extensions.Logging.Diagnostics.Tests/TestCaptureLoggerOfTTests.cs b/src/Stravaig.Extensions.Logging.Diagnostics.Tests/TestCaptureLoggerOfTTests.cs index 1e9ab27..99c8858 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics.Tests/TestCaptureLoggerOfTTests.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics.Tests/TestCaptureLoggerOfTTests.cs @@ -8,6 +8,21 @@ namespace Stravaig.Extensions.Logging.Diagnostics.Tests; [TestFixture] public class TestCaptureLoggerOfTTests { + [Test] + public void ConstructorWithNullLoggerThrows() + { + Should.Throw(() => new TestCaptureLogger(null!)) + .ParamName.ShouldBe("logger"); + } + + [Test] + public void ConstructorCategoryMismatchThrows() + { + var underlyingLogger = new TestCaptureLogger("NotTheRightCategory"); + Should.Throw(() => new TestCaptureLogger(underlyingLogger)) + .Message.ShouldBe("The category name does not match the type of this logger. Expected \"object\", got \"NotTheRightCategory\"."); + } + [Test] public void CategoryNameIsBasedOnType() { @@ -52,4 +67,20 @@ public void GetLogsWithExceptionsWillFilterOutNonExceptionLogs() logger.GetLogEntriesWithExceptions().Count.ShouldBe(2); logger.GetLogs().Count.ShouldBe(4); } + + [Test] + public void GetLogsMatchingPredicateWillFilterOutUnwantedLogs() + { + var logger = new TestCaptureLogger(); + logger.LogInformation("Hello"); + logger.LogWarning("Hello, {Location}!", "World"); + logger.LogInformation("This is a log."); + logger.LogError(new Exception(), "This has an exception."); + + logger.GetLogs(static l => l.LogLevel == LogLevel.Information).Count.ShouldBe(2); + + logger.GetLogs(static l => l.PropertyDictionary.ContainsKey("Location") && (string)l.PropertyDictionary["Location"] == "World") + .Count.ShouldBe(1); + logger.GetLogs().Count.ShouldBe(4); + } } diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/ITestCaptureLogger.cs b/src/Stravaig.Extensions.Logging.Diagnostics/ITestCaptureLogger.cs index 1614f30..41f170e 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/ITestCaptureLogger.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/ITestCaptureLogger.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; @@ -20,6 +21,13 @@ public interface ITestCaptureLogger : ILogger /// called won't be available in the list, and it will have to be called again. IReadOnlyList GetLogs(); + /// + /// Gets a read-only list of logs that is a snapshot of this logger filtered by the predicate. + /// + /// Any additional logs added to the logger after this is + /// called won't be available in the list, and it will have to be called again. + IReadOnlyList GetLogs(Func predicate); + /// /// Gets a read-only list of logs that have an exception attached in sequential order. /// diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger(OfTCategoryType).cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger(OfTCategoryType).cs index 5e03d48..e1cb8c4 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger(OfTCategoryType).cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger(OfTCategoryType).cs @@ -28,6 +28,7 @@ public TestCaptureLogger() /// public TestCaptureLogger(TestCaptureLogger logger) { + ArgumentNullException.ThrowIfNull(logger, nameof(logger)); var expectedCategoryName = TypeNameHelper.GetTypeDisplayName(typeof(TCategoryType)); if (logger.CategoryName != expectedCategoryName) throw new InvalidOperationException( @@ -66,6 +67,10 @@ public void Reset() public IReadOnlyList GetLogs() => _logger.GetLogs(); + /// + public IReadOnlyList GetLogs(Func predicate) + => _logger.GetLogs(predicate); + /// /// Gets a read-only list of logs that have an exception attached in sequential order. /// diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs index dbae76f..9eae9fa 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs @@ -49,21 +49,23 @@ public IReadOnlyList GetLogs() } } - /// - public IReadOnlyList GetLogEntriesWithExceptions() + public IReadOnlyList GetLogs(Func predicate) { - List result; lock (_syncRoot) { - result = _logs - .Where(l => l.Exception != null) - .ToList(); + return _logs + .Where(predicate) + .OrderBy(static log => log) + .ToArray(); } - result.Sort(); - return result; } + + /// + public IReadOnlyList GetLogEntriesWithExceptions() + => GetLogs(log => log.Exception != null); + /// /// Writes a log entry /// diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs index eeed8e9..f86af44 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs @@ -103,18 +103,27 @@ public IReadOnlyList GetAllLogEntries() } /// - /// Gets all log entries with exceptions attached regardless of the - /// Category they were logged as. + /// Gets all log entries matching the predicate regardless of the Category + /// they were logged as. /// - /// A read only list of - public IReadOnlyList GetAllLogEntriesWithExceptions() + /// A read only list of objects. + public IReadOnlyList GetLogEntriesMatchingPredicate(Func predicate) { var loggers = _captures.Values; - var allLogs = loggers.SelectMany(l => l.GetLogEntriesWithExceptions()).ToList(); + var allLogs = loggers.SelectMany(l => l.GetLogs(predicate)).ToList(); allLogs.Sort(); return allLogs; } + + /// + /// Gets all log entries with exceptions attached regardless of the + /// Category they were logged as. + /// + /// A read only list of objects. + public IReadOnlyList GetAllLogEntriesWithExceptions() + => GetLogEntriesMatchingPredicate(static log => log.Exception != null); + /// /// Resets the captures to an empty state. /// From 597aecde8aaaebeb5cb814082a2237ac76a0616e Mon Sep 17 00:00:00 2001 From: Colin Mackay Date: Thu, 31 Oct 2024 19:12:10 +0000 Subject: [PATCH 2/5] (#171) Rename to GetLogs --- .../TestCaptureLogger.cs | 1 - .../TestCaptureLoggerProvider.cs | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs index 9eae9fa..97634a6 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs @@ -61,7 +61,6 @@ public IReadOnlyList GetLogs(Func predicate) } } - /// public IReadOnlyList GetLogEntriesWithExceptions() => GetLogs(log => log.Exception != null); diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs index f86af44..780818a 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs @@ -107,7 +107,7 @@ public IReadOnlyList GetAllLogEntries() /// they were logged as. /// /// A read only list of objects. - public IReadOnlyList GetLogEntriesMatchingPredicate(Func predicate) + public IReadOnlyList GetLogs(Func predicate) { var loggers = _captures.Values; var allLogs = loggers.SelectMany(l => l.GetLogs(predicate)).ToList(); @@ -122,7 +122,7 @@ public IReadOnlyList GetLogEntriesMatchingPredicate(Func /// A read only list of objects. public IReadOnlyList GetAllLogEntriesWithExceptions() - => GetLogEntriesMatchingPredicate(static log => log.Exception != null); + => GetLogs(static log => log.Exception != null); /// /// Resets the captures to an empty state. @@ -140,6 +140,8 @@ public void Reset() /// public void Dispose() { - Reset(); + _captures.Clear(); + _typedCaptures.Clear(); + GC.SuppressFinalize(this); } } From bdb2df79c4b8b343237d7cee230ce796fe8f92c7 Mon Sep 17 00:00:00 2001 From: Colin Mackay Date: Thu, 31 Oct 2024 19:16:06 +0000 Subject: [PATCH 3/5] (#171) Update release notes --- release-notes/wip-release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes/wip-release-notes.md b/release-notes/wip-release-notes.md index ddccade..c6fb491 100644 --- a/release-notes/wip-release-notes.md +++ b/release-notes/wip-release-notes.md @@ -19,6 +19,7 @@ Date: ??? - #170: Additional xunit extension methods to write out all log messages to the `ITestOutputHelper`. - `ITestOutputHelper.WriteLogs(ITestCaptureLogger...)` - `ITestOutputHelper.WriteLogs(TestCaptureLoggerProvider...)` +- #171: Add `GetLogs(predicate)` to `TestCaptureLogger` and `TestCaptureLoggerProvider`. ### Miscellaneous From d526172fcb743c066c0c3895087a53396b0776d5 Mon Sep 17 00:00:00 2001 From: Colin Mackay Date: Thu, 31 Oct 2024 19:21:13 +0000 Subject: [PATCH 4/5] (#171) Fix warnings --- .../OutputTestHelperExtensions.cs | 1 - .../TestCaptureLoggerProvider.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Stravaig.Extensions.Logging.Diagnostics.XUnit/OutputTestHelperExtensions.cs b/src/Stravaig.Extensions.Logging.Diagnostics.XUnit/OutputTestHelperExtensions.cs index 03bd882..54e6f37 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics.XUnit/OutputTestHelperExtensions.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics.XUnit/OutputTestHelperExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Logging; using Stravaig.Extensions.Logging.Diagnostics.Render; using Xunit.Abstractions; diff --git a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs index 780818a..5150b59 100644 --- a/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs +++ b/src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLoggerProvider.cs @@ -66,7 +66,7 @@ ILogger ILoggerProvider.CreateLogger(string categoryName) /// The category name for messages produced by the logger. /// The instance of the that was created. public TestCaptureLogger CreateLogger(string categoryName) - => _captures.GetOrAdd(categoryName, static (cn) => new TestCaptureLogger(cn)); + => _captures.GetOrAdd(categoryName, static cn => new TestCaptureLogger(cn)); /// /// Creates a new instance. @@ -76,7 +76,7 @@ public TestCaptureLogger CreateLogger(string categoryName) public TestCaptureLogger CreateLogger() => (TestCaptureLogger)_typedCaptures.GetOrAdd(typeof(T), type => { - var categoryName = GetTypeDisplayName(typeof(T)); + var categoryName = GetTypeDisplayName(type); var underlyingLogger = CreateLogger(categoryName); return new TestCaptureLogger(underlyingLogger); }); @@ -97,7 +97,7 @@ public IReadOnlyList GetCategories() public IReadOnlyList GetAllLogEntries() { var loggers = _captures.Values; - var allLogs = loggers.SelectMany(l => l.GetLogs()).ToList(); + var allLogs = loggers.SelectMany(static l => l.GetLogs()).ToList(); allLogs.Sort(); return allLogs; } From 160a7c3eb6105e27c02ea308fd7c8415345b1822 Mon Sep 17 00:00:00 2001 From: Colin Mackay Date: Thu, 31 Oct 2024 19:28:53 +0000 Subject: [PATCH 5/5] (#171) Update docs --- .../library/test-capture-logger-provider.md | 27 +++++++++++++++++ docs/docs/library/test-capture-logger.md | 29 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/docs/library/test-capture-logger-provider.md b/docs/docs/library/test-capture-logger-provider.md index 0902e24..7e2769a 100644 --- a/docs/docs/library/test-capture-logger-provider.md +++ b/docs/docs/library/test-capture-logger-provider.md @@ -78,6 +78,33 @@ var allLogs = logProvider.GetAllLogEntries(); The result of the method can be passed into `RenderLogs()` extension method. See [Renderer](log-entry-renderer-extensions.md) +--- + +## GetLogs(predicate) + +The `TestCaptureLoggerProvider.GetLogs(predicate)` allows you to retrieve specific logs within your test method that match the predicate. The log entries will be in sequence, timestamps will be incremental, however adjacent log entries created sufficiently close to one another may contain the same timestamp due to the resolution of the clock. + +#### Returns + +`IReadOnlyList`: A read only list of log entries. See [LogEntry](log-entry.md) + +##### Example + +This example checks that a specific log entry was generated. + +```csharp +// Arrange +var logProvider = new TestCaptureLoggerProvider(); + +// Act: Do something using the log provider. + +// Assert +var logs = logProvider.GetLogs( + static le => le.LogLevel == LogLevel.Warning && + le.OriginalMessage == "A thing happened."); +logs.Count.ShouldBe(1); +``` + --- ## GetAllLogEntriesWithExceptions() diff --git a/docs/docs/library/test-capture-logger.md b/docs/docs/library/test-capture-logger.md index d116e2a..c2281e2 100644 --- a/docs/docs/library/test-capture-logger.md +++ b/docs/docs/library/test-capture-logger.md @@ -13,7 +13,7 @@ This class implements ## GetLogs() -The `TestCaptureLogger.GetLogs()` allows you to retrieve the logs within your test method. The property will be in sequence, timestamps will be incremental, however adjacent log entries created sufficiently close to one another may contain the same timestamp due to the resolution of the clock. +The `TestCaptureLogger.GetLogs()` allows you to retrieve the logs within your test method. The log entries will be in sequence, timestamps will be incremental, however adjacent log entries created sufficiently close to one another may contain the same timestamp due to the resolution of the clock. #### Returns @@ -23,6 +23,33 @@ The `TestCaptureLogger.GetLogs()` allows you to retrieve the logs within your te The result of the method can be passed into `RenderLogs()` extension method. See [Renderer](log-entry-renderer-extensions.md) +--- +## GetLogs(predicate) + +The `TestCaptureLogger.GetLogs(predicate)` allows you to retrieve specific logs within your test method that match the predicate. The log entries will be in sequence, timestamps will be incremental, however adjacent log entries created sufficiently close to one another may contain the same timestamp due to the resolution of the clock. + +#### Returns + +`IReadOnlyList`: A read only list of log entries. See [LogEntry](log-entry.md) + +##### Example + +This example checks that a specific log entry was generated. + +```csharp +// Arrange +var logger = new TestCaptureLogger(); + +// Act: Do something using the logger + +// Assert +var logs = logger.GetLogs( + static le => le.LogLevel == LogLevel.Warning && + le.OriginalMessage == "A thing happened."); +logs.Count.ShouldBe(1); +``` + + --- ## GetLogEntriesWithExceptions()