Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/docs/library/test-capture-logger-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogEntry>`: 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()

Expand Down
29 changes: 28 additions & 1 deletion docs/docs/library/test-capture-logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<LogEntry>`: 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()
Expand Down
1 change: 1 addition & 0 deletions release-notes/wip-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ namespace Stravaig.Extensions.Logging.Diagnostics.Tests;
[TestFixture]
public class TestCaptureLoggerOfTTests
{
[Test]
public void ConstructorWithNullLoggerThrows()
{
Should.Throw<ArgumentNullException>(() => new TestCaptureLogger<object>(null!))
.ParamName.ShouldBe("logger");
}

[Test]
public void ConstructorCategoryMismatchThrows()
{
var underlyingLogger = new TestCaptureLogger("NotTheRightCategory");
Should.Throw<InvalidOperationException>(() => new TestCaptureLogger<object>(underlyingLogger))
.Message.ShouldBe("The category name does not match the type of this logger. Expected \"object\", got \"NotTheRightCategory\".");
}

[Test]
public void CategoryNameIsBasedOnType()
{
Expand Down Expand Up @@ -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<TestCaptureLoggerOfTTests>();
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Stravaig.Extensions.Logging.Diagnostics.Render;
using Xunit.Abstractions;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;

Expand All @@ -20,6 +21,13 @@ public interface ITestCaptureLogger : ILogger
/// called won't be available in the list, and it will have to be called again.</remarks>
IReadOnlyList<LogEntry> GetLogs();

/// <summary>
/// Gets a read-only list of logs that is a snapshot of this logger filtered by the predicate.
/// </summary>
/// <remarks>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.</remarks>
IReadOnlyList<LogEntry> GetLogs(Func<LogEntry, bool> predicate);

/// <summary>
/// Gets a read-only list of logs that have an exception attached in sequential order.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public TestCaptureLogger()
/// </summary>
public TestCaptureLogger(TestCaptureLogger logger)
{
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
var expectedCategoryName = TypeNameHelper.GetTypeDisplayName(typeof(TCategoryType));
if (logger.CategoryName != expectedCategoryName)
throw new InvalidOperationException(
Expand Down Expand Up @@ -66,6 +67,10 @@ public void Reset()
public IReadOnlyList<LogEntry> GetLogs()
=> _logger.GetLogs();

/// <inheritdoc />
public IReadOnlyList<LogEntry> GetLogs(Func<LogEntry, bool> predicate)
=> _logger.GetLogs(predicate);

/// <summary>
/// Gets a read-only list of logs that have an exception attached in sequential order.
/// </summary>
Expand Down
17 changes: 9 additions & 8 deletions src/Stravaig.Extensions.Logging.Diagnostics/TestCaptureLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,22 @@ public IReadOnlyList<LogEntry> GetLogs()
}
}


/// <inheritdoc />
public IReadOnlyList<LogEntry> GetLogEntriesWithExceptions()
public IReadOnlyList<LogEntry> GetLogs(Func<LogEntry, bool> predicate)
{
List<LogEntry> result;
lock (_syncRoot)
{
result = _logs
.Where(l => l.Exception != null)
.ToList();
return _logs
.Where(predicate)
.OrderBy(static log => log)
.ToArray();
}
result.Sort();
return result;
}

/// <inheritdoc />
public IReadOnlyList<LogEntry> GetLogEntriesWithExceptions()
=> GetLogs(log => log.Exception != null);

/// <summary>
/// Writes a log entry
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ ILogger ILoggerProvider.CreateLogger(string categoryName)
/// <param name="categoryName">The category name for messages produced by the logger.</param>
/// <returns>The instance of the <see cref="TestCaptureLogger"/> that was created.</returns>
public TestCaptureLogger CreateLogger(string categoryName)
=> _captures.GetOrAdd(categoryName, static (cn) => new TestCaptureLogger(cn));
=> _captures.GetOrAdd(categoryName, static cn => new TestCaptureLogger(cn));

/// <summary>
/// Creates a new <see cref="T:TestCaptureLogger"/> instance.
Expand All @@ -76,7 +76,7 @@ public TestCaptureLogger CreateLogger(string categoryName)
public TestCaptureLogger<T> CreateLogger<T>()
=> (TestCaptureLogger<T>)_typedCaptures.GetOrAdd(typeof(T), type =>
{
var categoryName = GetTypeDisplayName(typeof(T));
var categoryName = GetTypeDisplayName(type);
var underlyingLogger = CreateLogger(categoryName);
return new TestCaptureLogger<T>(underlyingLogger);
});
Expand All @@ -97,24 +97,33 @@ public IReadOnlyList<string> GetCategories()
public IReadOnlyList<LogEntry> 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;
}

/// <summary>
/// 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.
/// </summary>
/// <returns>A read only list of <see cref="LogEntry"/></returns>
public IReadOnlyList<LogEntry> GetAllLogEntriesWithExceptions()
/// <returns>A read only list of <see cref="LogEntry"/> objects.</returns>
public IReadOnlyList<LogEntry> GetLogs(Func<LogEntry, bool> 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;
}


/// <summary>
/// Gets all log entries with exceptions attached regardless of the
/// Category they were logged as.
/// </summary>
/// <returns>A read only list of <see cref="LogEntry"/> objects.</returns>
public IReadOnlyList<LogEntry> GetAllLogEntriesWithExceptions()
=> GetLogs(static log => log.Exception != null);

/// <summary>
/// Resets the captures to an empty state.
/// </summary>
Expand All @@ -131,6 +140,8 @@ public void Reset()
/// </summary>
public void Dispose()
{
Reset();
_captures.Clear();
_typedCaptures.Clear();
GC.SuppressFinalize(this);
}
}