Replace local xunit logging project with new signed nuget package

This commit is contained in:
Caelan
2024-09-21 10:51:11 -06:00
parent 046bccb712
commit 89955cad94
27 changed files with 2 additions and 1479 deletions

View File

@@ -37,8 +37,8 @@
</ItemGroup>
<ItemGroup Condition=" $(MSBuildProjectName.EndsWith('Tests')) ">
<ProjectReference Include="..\Divergic.Logging.Xunit\Divergic.Logging.Xunit.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Neovolve.Logging.Xunit.Signed" Version="6.1.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />

View File

@@ -1,156 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="CacheLogger" />
/// class provides a cache of log entries written to the logger.
/// </summary>
public class CacheLogger : FilterLogger, ICacheLogger
{
private static readonly AsyncLocal<ConcurrentStack<CacheScope>> _scopes =
new AsyncLocal<ConcurrentStack<CacheScope>>();
private readonly ILoggerFactory? _factory;
private readonly IList<LogEntry> _logEntries = new List<LogEntry>();
private readonly ILogger? _logger;
/// <summary>
/// Creates a new instance of the <see cref="CacheLogger" /> class.
/// </summary>
public CacheLogger()
{
}
/// <summary>
/// Creates a new instance of the <see cref="CacheLogger" /> class.
/// </summary>
/// <param name="logger">The source logger.</param>
/// <exception cref="ArgumentNullException">The <paramref name="logger" /> is <c>null</c>.</exception>
public CacheLogger(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
internal CacheLogger(ILogger logger, ILoggerFactory factory)
{
_logger = logger;
_factory = factory;
}
/// <inheritdoc />
public override IDisposable? BeginScope<TState>(TState state)
{
var scope = _logger?.BeginScope(state) ?? NoopDisposable.Instance;
var cacheScope = new CacheScope(scope, state, () => Scopes.TryPop(out _));
Scopes.Push(cacheScope);
return cacheScope;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public override bool IsEnabled(LogLevel logLevel)
{
if (_logger == null)
{
return true;
}
return _logger.IsEnabled(logLevel);
}
/// <summary>
/// Disposes resources held by this instance.
/// </summary>
/// <param name="disposing"><c>true</c> if disposing unmanaged types; otherwise <c>false</c>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_factory?.Dispose();
}
}
/// <inheritdoc />
protected override void WriteLogEntry<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
string message,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var entry = new LogEntry(
logLevel,
eventId,
state,
exception,
message,
Scopes.Select(s => s.State).ToArray());
_logEntries.Add(entry);
_logger?.Log(logLevel, eventId, state, exception, formatter);
}
/// <summary>
/// Gets the count of cached log entries.
/// </summary>
public int Count => _logEntries.Count;
/// <summary>
/// Gets the cached log entries.
/// </summary>
public IReadOnlyCollection<LogEntry> Entries => new ReadOnlyCollection<LogEntry>(_logEntries);
/// <summary>
/// Gets the last entry logged.
/// </summary>
public LogEntry? Last
{
get
{
var count = _logEntries.Count;
if (count == 0)
{
return null;
}
return _logEntries[count - 1];
}
}
private static ConcurrentStack<CacheScope> Scopes
{
get
{
var scopes = _scopes.Value;
if (scopes == null)
{
scopes = new ConcurrentStack<CacheScope>();
_scopes.Value = scopes;
}
return scopes;
}
}
}
}

View File

@@ -1,35 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="CacheLogger{T}" />
/// class provides a cache logger for <see cref="ILogger{TCategoryName}" />.
/// </summary>
/// <typeparam name="T">The generic type of logger.</typeparam>
public class CacheLogger<T> : CacheLogger, ICacheLogger<T>
{
/// <summary>
/// Creates a new instance of the <see cref="CacheLogger{T}" /> class.
/// </summary>
public CacheLogger()
{
}
/// <summary>
/// Creates a new instance of the <see cref="CacheLogger{T}" /> class.
/// </summary>
/// <param name="logger">The source logger.</param>
/// <exception cref="ArgumentNullException">The <paramref name="logger" /> is <c>null</c>.</exception>
public CacheLogger(ILogger logger)
: base(logger)
{
}
internal CacheLogger(ILogger logger, ILoggerFactory factory)
: base(logger, factory)
{
}
}
}

View File

@@ -1,28 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
internal class CacheScope : IDisposable
{
private readonly Action _onScopeEnd;
private readonly IDisposable _scope;
public CacheScope(IDisposable scope, object? state, Action onScopeEnd)
{
_scope = scope;
State = state;
_onScopeEnd = onScopeEnd;
}
public void Dispose()
{
// Pass on the end scope request
_scope.Dispose();
// Clean up the scope in the cache logger
_onScopeEnd.Invoke();
}
public object? State { get; }
}
}

View File

@@ -1,93 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="DefaultFormatter" />
/// class provides the default formatting of log messages for xUnit test output.
/// </summary>
public class DefaultFormatter : ILogFormatter
{
private readonly LoggingConfig _config;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultFormatter" /> class.
/// </summary>
/// <param name="config">The logging configuration.</param>
/// <exception cref="ArgumentNullException">The <paramref name="config" /> value is <c>null</c>.</exception>
public DefaultFormatter(LoggingConfig config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <inheritdoc />
public virtual string Format(
int scopeLevel,
string categoryName,
LogLevel logLevel,
EventId eventId,
string message,
Exception? exception)
{
var padding = new string(' ', scopeLevel * _config.ScopePaddingSpaces);
var parts = new List<string>(2);
if (string.IsNullOrWhiteSpace(message) == false)
{
var part = string.Format(CultureInfo.InvariantCulture, FormatMask, padding, logLevel, eventId.Id,
message);
part = MaskSensitiveValues(part);
parts.Add(part);
}
if (exception != null)
{
var part = string.Format(
CultureInfo.InvariantCulture,
FormatMask,
padding,
logLevel,
eventId.Id,
exception);
part = MaskSensitiveValues(part);
parts.Add(part);
}
return string.Join(Environment.NewLine, parts);
}
private string MaskSensitiveValues(string value)
{
const string mask = "****";
for (var index = 0; index < _config.SensitiveValues.Count; index++)
{
var sensitiveValue = _config.SensitiveValues[index];
value = value.Replace(sensitiveValue, mask);
}
return value;
}
/// <summary>
/// Returns the string format mask used to generate a log message.
/// </summary>
/// <remarks>The format values are:
/// <ul>
/// <li>0: Padding</li>
/// <li>1: Level</li>
/// <li>2: Event Id</li>
/// <li>3: Message</li>
/// </ul>
/// </remarks>
protected virtual string FormatMask { get; } = "{0}{1} [{2}]: {3}";
}
}

View File

@@ -1,19 +0,0 @@
namespace Divergic.Logging.Xunit;
/// <summary>
/// The <see cref="DefaultScopeFormatter" />
/// class is used to provide log message formatting logic for scope beginning and end messages.
/// </summary>
public class DefaultScopeFormatter : DefaultFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="DefaultScopeFormatter" /> class.
/// </summary>
/// <param name="config">The logging configuration.</param>
public DefaultScopeFormatter(LoggingConfig config) : base(config)
{
}
/// <inheritdoc />
protected override string FormatMask { get; } = "{0}{3}";
}

View File

@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\Velopack.snk</AssemblyOriginatorKeyFile>
<NoWarn>$(NoWarn);IDE0161</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<PackageReference Include="Xunit.Abstractions" Version="2.0.3" />
</ItemGroup>
</Project>

View File

@@ -1,113 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="FilterLogger" />
/// class provides common filtering logic to ensure log records are only written for enabled loggers where there is a
/// formatted message and/or exception to log.
/// </summary>
public abstract class FilterLogger : ILogger
{
private readonly string _nullFormatted = "[null]";
/// <inheritdoc />
public abstract IDisposable? BeginScope<TState>(TState state) where TState : notnull;
/// <inheritdoc />
public abstract bool IsEnabled(LogLevel logLevel);
/// <inheritdoc />
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
if (IsEnabled(logLevel) == false)
{
return;
}
var formattedMessage = FormatMessage(state, exception, formatter);
if (ShouldFilter(formattedMessage, exception))
{
return;
}
WriteLogEntry(logLevel, eventId, state, formattedMessage, exception, formatter);
}
/// <summary>
/// Returns the formatted log message.
/// </summary>
/// <typeparam name="TState">The type of state data to log.</typeparam>
/// <param name="state">The state data to log.</param>
/// <param name="exception">The exception to log.</param>
/// <param name="formatter">The formatter that creates the log message.</param>
/// <returns>The log message.</returns>
protected string FormatMessage<TState>(
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
#pragma warning disable CA1062 // Validate arguments of public methods
var formattedMessage = formatter(state, exception);
#pragma warning restore CA1062 // Validate arguments of public methods
// Clear the message if it looks like a null formatted message
if (formattedMessage == _nullFormatted)
{
return string.Empty;
}
return formattedMessage;
}
/// <summary>
/// Determines whether the log message should be filtered and not written.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="exception">The exception to log.</param>
/// <returns><c>true</c> if the log should not be written; otherwise <c>false</c>.</returns>
protected virtual bool ShouldFilter(string message, Exception? exception)
{
if (exception != null)
{
return false;
}
if (string.IsNullOrWhiteSpace(message))
{
return true;
}
return false;
}
/// <summary>
/// Writes the log entry with the specified values.
/// </summary>
/// <typeparam name="TState">The type of state data to log.</typeparam>
/// <param name="logLevel">The log level.</param>
/// <param name="eventId">The event id.</param>
/// <param name="state">The state data to log.</param>
/// <param name="message">The formatted message.</param>
/// <param name="exception">The exception to log.</param>
/// <param name="formatter">The formatter that creates the log message.</param>
protected abstract void WriteLogEntry<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
string message,
Exception? exception,
Func<TState, Exception?, string> formatter);
}
}

View File

@@ -1,28 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="ICacheLogger" />
/// interface defines the members for recording and accessing log entries.
/// </summary>
public interface ICacheLogger : ILogger, IDisposable
{
/// <summary>
/// Gets the number of cache entries recorded.
/// </summary>
int Count { get; }
/// <summary>
/// Gets the recorded cache entries.
/// </summary>
IReadOnlyCollection<LogEntry> Entries { get; }
/// <summary>
/// Gets the latest cache entry.
/// </summary>
LogEntry? Last { get; }
}
}

View File

@@ -1,13 +0,0 @@
namespace Divergic.Logging.Xunit
{
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="ICacheLogger" />
/// interface defines the members for recording and accessing log entries.
/// </summary>
/// <typeparam name="T">The type of class using the cache.</typeparam>
public interface ICacheLogger<out T> : ICacheLogger, ILogger<T>
{
}
}

View File

@@ -1,30 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="ILogFormatter" />
/// interface defines the members for formatting log messages.
/// </summary>
public interface ILogFormatter
{
/// <summary>
/// Formats the log message with the specified values.
/// </summary>
/// <param name="scopeLevel">The number of active logging scopes.</param>
/// <param name="categoryName">The logger name.</param>
/// <param name="logLevel">The log level.</param>
/// <param name="eventId">The event id.</param>
/// <param name="message">The log message.</param>
/// <param name="exception">The exception to be logged.</param>
/// <returns>The formatted log message.</returns>
string Format(
int scopeLevel,
string categoryName,
LogLevel logLevel,
EventId eventId,
string message,
Exception? exception);
}
}

View File

@@ -1,68 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LogEntry" />
/// class is used to identify the data related to a log entry.
/// </summary>
public class LogEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="LogEntry" /> class.
/// </summary>
/// <param name="logLevel">The log level.</param>
/// <param name="eventId">The event id.</param>
/// <param name="state">The state.</param>
/// <param name="exception">The exception.</param>
/// <param name="message">The message.</param>
/// <param name="scopes">The currently active scopes.</param>
public LogEntry(
LogLevel logLevel,
EventId eventId,
object? state,
Exception? exception,
string message,
IReadOnlyCollection<object?> scopes)
{
LogLevel = logLevel;
EventId = eventId;
State = state;
Exception = exception;
Message = message;
Scopes = scopes;
}
/// <summary>
/// Gets the event id of the entry.
/// </summary>
public EventId EventId { get; }
/// <summary>
/// Gets the exception of the entry.
/// </summary>
public Exception? Exception { get; }
/// <summary>
/// Gets the log level of the entry.
/// </summary>
public LogLevel LogLevel { get; }
/// <summary>
/// Gets the message of the entry.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets the scopes active at the time of the call to <see cref="ILogger.Log{TState}" />
/// </summary>
public IReadOnlyCollection<object?> Scopes { get; }
/// <summary>
/// Gets the state of the entry.
/// </summary>
public object? State { get; }
}
}

View File

@@ -1,32 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LogFactory" />
/// class is used to create <see cref="ILogger" /> instances.
/// </summary>
public static class LogFactory
{
/// <summary>
/// Creates an <see cref="ILoggerFactory" /> instance that is configured for xUnit output.
/// </summary>
/// <param name="output">The test output.</param>
/// <param name="config">Optional logging configuration.</param>
/// <returns>The logger factory.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ILoggerFactory Create(
ITestOutputHelper output, LoggingConfig? config = null)
{
output = output ?? throw new ArgumentNullException(nameof(output));
var factory = new LoggerFactory();
factory.AddXunit(output, config);
return factory;
}
}
}

View File

@@ -1,57 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LoggerExtensions" />
/// class provides extension methods for wrapping <see cref="ILogger" /> instances in <see cref="ICacheLogger" />.
/// </summary>
public static class LoggerExtensions
{
/// <summary>
/// Returns a <see cref="ICacheLogger" /> for the specified logger.
/// </summary>
/// <param name="logger">The source logger.</param>
/// <returns>The cache logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="logger" /> is <c>null</c>.</exception>
public static ICacheLogger WithCache(this ILogger logger)
{
logger = logger ?? throw new ArgumentNullException(nameof(logger));
var cacheLogger = new CacheLogger(logger);
return cacheLogger;
}
/// <summary>
/// Returns a <see cref="ICacheLogger{T}" /> for the specified logger.
/// </summary>
/// <typeparam name="T">The type of generic logger.</typeparam>
/// <param name="logger">The source logger.</param>
/// <returns>The cache logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="logger" /> is <c>null</c>.</exception>
public static ICacheLogger<T> WithCache<T>(this ILogger<T> logger)
{
logger = logger ?? throw new ArgumentNullException(nameof(logger));
var cacheLogger = new CacheLogger<T>(logger);
return cacheLogger;
}
internal static ICacheLogger WithCache(this ILogger logger, ILoggerFactory factory)
{
var cacheLogger = new CacheLogger(logger, factory);
return cacheLogger;
}
internal static ICacheLogger<T> WithCache<T>(this ILogger<T> logger, ILoggerFactory factory)
{
var cacheLogger = new CacheLogger<T>(logger, factory);
return cacheLogger;
}
}
}

View File

@@ -1,40 +0,0 @@
namespace Microsoft.Extensions.Logging
{
using System;
using Divergic.Logging.Xunit;
using Xunit.Abstractions;
/// <summary>
/// The <see cref="LoggerFactoryExtensions" />
/// class provides extension methods for configuring <see cref="ILoggerFactory" /> with providers.
/// </summary>
public static class LoggerFactoryExtensions
{
/// <summary>
/// Registers the <see cref="TestOutputLoggerProvider" /> in the factory using the specified
/// <see cref="ITestOutputHelper" />.
/// </summary>
/// <param name="factory">The factory to add the provider to.</param>
/// <param name="output">The test output reference.</param>
/// <param name="config">Optional logging configuration.</param>
/// <returns>The logger factory.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="factory" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ILoggerFactory AddXunit(this ILoggerFactory factory, ITestOutputHelper output,
LoggingConfig? config = null)
{
factory = factory ?? throw new ArgumentNullException(nameof(factory));
output = output ?? throw new ArgumentNullException(nameof(output));
#pragma warning disable CA2000 // Dispose objects before losing scope
var provider = new TestOutputLoggerProvider(output, config);
#pragma warning restore CA2000 // Dispose objects before losing scope
#pragma warning disable CA1062 // Validate arguments of public methods
factory.AddProvider(provider);
#pragma warning restore CA1062 // Validate arguments of public methods
return factory;
}
}
}

View File

@@ -1,34 +0,0 @@
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Logging
{
using System;
using Divergic.Logging.Xunit;
using Xunit.Abstractions;
/// <summary>
/// The <see cref="LoggingBuilderExtensions" />
/// class provides extension methods for the <see cref="ILoggingBuilder" /> interface.
/// </summary>
public static class LoggingBuilderExtensions
{
/// <summary>
/// Adds a logger to writes to the xUnit test output to the specified logging builder.
/// </summary>
/// <param name="builder">The logging builder.</param>
/// <param name="output">The xUnit test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
public static void AddXunit(this ILoggingBuilder builder, ITestOutputHelper output,
LoggingConfig? config = null)
{
builder = builder ?? throw new ArgumentNullException(nameof(builder));
output = output ?? throw new ArgumentNullException(nameof(output));
// Object is added as a provider to the builder and cannot be disposed of here
#pragma warning disable CA2000 // Dispose objects before losing scope
var provider = new TestOutputLoggerProvider(output, config);
#pragma warning restore CA2000 // Dispose objects before losing scope
builder.AddProvider(provider);
}
}
}

View File

@@ -1,65 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LoggingConfig" />
/// class is used to configure how logging operates.
/// </summary>
public class LoggingConfig
{
private ILogFormatter _formatter;
private ILogFormatter _scopeFormatter;
/// <summary>
/// Initializes a new instance of the <see cref="LoggingConfig" /> class.
/// </summary>
public LoggingConfig()
{
_formatter = new DefaultFormatter(this);
_scopeFormatter = new DefaultScopeFormatter(this);
}
/// <summary>
/// Gets or sets a custom formatting for rendering log messages to xUnit test output.
/// </summary>
public ILogFormatter Formatter
{
get => _formatter;
set =>
_formatter = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Gets or sets whether exceptions thrown while logging outside of the test execution will be ignored.
/// </summary>
public bool IgnoreTestBoundaryException { get; set; }
/// <summary>
/// Gets or sets the minimum logging level.
/// </summary>
public LogLevel LogLevel { get; set; } = LogLevel.Trace;
/// <summary>
/// Gets or sets a custom formatting for rendering scope beginning and end messages to xUnit test output.
/// </summary>
public ILogFormatter ScopeFormatter
{
get => _scopeFormatter;
set =>
_scopeFormatter = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Identifies the number of spaces to use for indenting scopes.
/// </summary>
public int ScopePaddingSpaces { get; set; } = 3;
/// <summary>
/// Gets the set of sensitive values that should be filtered out when writing log messages.
/// </summary>
public Collection<string> SensitiveValues { get; } = new();
}
}

View File

@@ -1,64 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LoggingTestsBase" />
/// class is used to provide a simple logging bootstrap class for xUnit test classes.
/// </summary>
public abstract class LoggingTestsBase : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="LoggingTestsBase" /> class.
/// </summary>
/// <param name="output">The xUnit test output.</param>
/// <param name="logLevel">The minimum log level to output.</param>
protected LoggingTestsBase(ITestOutputHelper output, LogLevel logLevel)
{
Output = output ?? throw new ArgumentNullException(nameof(output));
Logger = output.BuildLogger(logLevel);
}
/// <summary>
/// Initializes a new instance of the <see cref="LoggingTestsBase" /> class.
/// </summary>
/// <param name="output">The xUnit test output.</param>
/// <param name="config">Optional logging configuration.</param>
protected LoggingTestsBase(ITestOutputHelper output, LoggingConfig? config = null)
{
Output = output ?? throw new ArgumentNullException(nameof(output));
Logger = output.BuildLogger(config);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes resources held by this instance.
/// </summary>
/// <param name="disposing"><c>true</c> if disposing unmanaged types; otherwise <c>false</c>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Logger.Dispose();
}
}
/// <summary>
/// Gets the logger instance.
/// </summary>
protected ICacheLogger Logger { get; }
/// <summary>
/// Gets the xUnit test output.
/// </summary>
protected ITestOutputHelper Output { get; }
}
}

View File

@@ -1,64 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="LoggingTestsBase{T}" />
/// class is used to provide a simple logging bootstrap class for xUnit test classes.
/// </summary>
public abstract class LoggingTestsBase<T> : IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="LoggingTestsBase{T}" /> class.
/// </summary>
/// <param name="output">The xUnit test output.</param>
/// <param name="logLevel">The minimum log level to output.</param>
protected LoggingTestsBase(ITestOutputHelper output, LogLevel logLevel)
{
Output = output ?? throw new ArgumentNullException(nameof(output));
Logger = output.BuildLoggerFor<T>(logLevel);
}
/// <summary>
/// Initializes a new instance of the <see cref="LoggingTestsBase{T}" /> class.
/// </summary>
/// <param name="output">The xUnit test output.</param>
/// <param name="config">Optional logging configuration.</param>
protected LoggingTestsBase(ITestOutputHelper output, LoggingConfig? config = null)
{
Output = output ?? throw new ArgumentNullException(nameof(output));
Logger = output.BuildLoggerFor<T>(config);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes resources held by this instance.
/// </summary>
/// <param name="disposing"><c>true</c> if disposing unmanaged types; otherwise <c>false</c>.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Logger.Dispose();
}
}
/// <summary>
/// Gets the logger instance.
/// </summary>
protected ICacheLogger<T> Logger { get; }
/// <summary>
/// Gets the xUnit test output.
/// </summary>
protected ITestOutputHelper Output { get; }
}
}

View File

@@ -1,14 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
internal class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new NoopDisposable();
public void Dispose()
{
// No-op
}
}
}

View File

@@ -1,129 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
internal class ScopeWriter : IDisposable
{
private readonly string _category;
private readonly LoggingConfig _config;
private readonly int _depth;
private readonly Action _onScopeEnd;
private readonly ITestOutputHelper _output;
private readonly object? _state;
private string _scopeMessage = string.Empty;
private string _structuredStateData = string.Empty;
public ScopeWriter(
ITestOutputHelper output,
object? state,
int depth,
string category,
Action onScopeEnd,
LoggingConfig config)
{
_output = output;
_state = state;
_depth = depth;
_category = category;
_onScopeEnd = onScopeEnd;
_config = config;
DetermineScopeStateMessage();
var scopeStartMessage = BuildScopeStateMessage(false);
WriteLog(_depth, scopeStartMessage);
if (string.IsNullOrWhiteSpace(_structuredStateData) == false)
{
// Add the padding to the structured data
var structuredLines =
_structuredStateData.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries);
WriteLog(_depth + 1, "Scope data: ");
foreach (var structuredLine in structuredLines)
{
WriteLog(_depth + 1, structuredLine);
}
}
}
private void WriteLog(int depth, string message)
{
var formattedMessage = _config.ScopeFormatter.Format(depth, _category, LogLevel.Information, 0, message, null);
_output.WriteLine(formattedMessage);
// Write the message to the output window
Trace.WriteLine(formattedMessage);
}
public void Dispose()
{
var scopeStartMessage = BuildScopeStateMessage(true);
_output.WriteLine(scopeStartMessage);
_onScopeEnd.Invoke();
}
private string BuildScopeStateMessage(bool isScopeEnd)
{
var endScopeMarker = isScopeEnd ? "/" : string.Empty;
const string format = "<{0}{1}>";
var message = string.Format(CultureInfo.InvariantCulture, format, endScopeMarker, _scopeMessage);
var formattedMessage =
_config.ScopeFormatter.Format(_depth, _category, LogLevel.Information, 0, message, null);
return formattedMessage;
}
private void DetermineScopeStateMessage()
{
const string scopeMarker = "Scope: ";
var defaultScopeMessage = "Scope " + (_depth + 1);
if (_state == null)
{
_scopeMessage = defaultScopeMessage;
}
else if (_state is string state)
{
if (string.IsNullOrWhiteSpace(state))
{
_scopeMessage = defaultScopeMessage;
}
else
{
_scopeMessage = scopeMarker + state;
}
}
else if (_state.GetType().IsValueType)
{
_scopeMessage = scopeMarker + _state;
}
else
{
try
{
// The data is probably a complex object or a structured log entry
_structuredStateData = JsonSerializer.Serialize(_state, SerializerSettings.Default);
}
catch (JsonException ex)
{
_structuredStateData = ex.ToString();
}
_scopeMessage = defaultScopeMessage;
}
}
}
}

View File

@@ -1,28 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// The <see cref="SerializerSettings" />
/// class provides access to settings for JSON serialization.
/// </summary>
public static class SerializerSettings
{
private static JsonSerializerOptions BuildSerializerSettings()
{
var settings = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
return settings;
}
/// <summary>
/// Gets the default serializer settings.
/// </summary>
public static JsonSerializerOptions Default { get; } = BuildSerializerSettings();
}
}

View File

@@ -1,183 +0,0 @@
// ReSharper disable once CheckNamespace
namespace Xunit.Abstractions
{
using System;
using System.Runtime.CompilerServices;
using Divergic.Logging.Xunit;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="TestOutputHelperExtensions" /> class provides extension methods for the
/// <see cref="ITestOutputHelper" />.
/// </summary>
public static class TestOutputHelperExtensions
{
/// <summary>
/// Builds a logger from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="memberName">
/// The member to create the logger for. This is automatically populated using <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger BuildLogger(
this ITestOutputHelper output,
[CallerMemberName] string memberName = "")
{
return BuildLogger(output, null, memberName);
}
/// <summary>
/// Builds a logger from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="logLevel">The minimum log level to output.</param>
/// <param name="memberName">
/// The member to create the logger for. This is automatically populated using <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger BuildLogger(
this ITestOutputHelper output,
LogLevel logLevel,
[CallerMemberName] string memberName = "")
{
var config = new LoggingConfig
{
LogLevel = logLevel
};
return BuildLogger(output, config, memberName);
}
/// <summary>
/// Builds a logger from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
/// <param name="memberName">
/// The member to create the logger for. This is automatically populated using <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger BuildLogger(
this ITestOutputHelper output,
LoggingConfig? config,
[CallerMemberName] string memberName = "")
{
output = output ?? throw new ArgumentNullException(nameof(output));
// Do not use the using keyword here because the factory will be disposed before the cache logger is finished with it
var factory = LogFactory.Create(output, config);
var logger = factory.CreateLogger(memberName);
return logger.WithCache(factory);
}
/// <summary>
/// Builds a logger from the specified test output helper for the specified type.
/// </summary>
/// <typeparam name="T">The type to create the logger for.</typeparam>
/// <param name="output">The test output helper.</param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger<T> BuildLoggerFor<T>(this ITestOutputHelper output)
{
return BuildLoggerFor<T>(output, null);
}
/// <summary>
/// Builds a logger from the specified test output helper for the specified type.
/// </summary>
/// <typeparam name="T">The type to create the logger for.</typeparam>
/// <param name="output">The test output helper.</param>
/// <param name="logLevel">The minimum log level to output.</param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger<T> BuildLoggerFor<T>(this ITestOutputHelper output, LogLevel logLevel)
{
var config = new LoggingConfig
{
LogLevel = logLevel
};
return BuildLoggerFor<T>(output, config);
}
/// <summary>
/// Builds a logger from the specified test output helper for the specified type.
/// </summary>
/// <typeparam name="T">The type to create the logger for.</typeparam>
/// <param name="output">The test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
/// <returns>The logger.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ICacheLogger<T> BuildLoggerFor<T>(this ITestOutputHelper output, LoggingConfig? config)
{
output = output ?? throw new ArgumentNullException(nameof(output));
// Do not use the using keyword here because the factory will be disposed before the cache logger is finished with it
var factory = LogFactory.Create(output, config);
var logger = factory.CreateLogger<T>();
return logger.WithCache(factory);
}
/// <summary>
/// Builds a logger factory from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <returns>The logger factory.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ILoggerFactory BuildLoggerFactory(
this ITestOutputHelper output)
{
return BuildLoggerFactory(output, null);
}
/// <summary>
/// Builds a logger factory from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="logLevel">The minimum log level to output.</param>
/// <returns>The logger factory.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ILoggerFactory BuildLoggerFactory(
this ITestOutputHelper output,
LogLevel logLevel)
{
var config = new LoggingConfig
{
LogLevel = logLevel
};
return BuildLoggerFactory(output, config);
}
/// <summary>
/// Builds a logger factory from the specified test output helper.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
/// <returns>The logger factory.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public static ILoggerFactory BuildLoggerFactory(
this ITestOutputHelper output,
LoggingConfig? config)
{
output = output ?? throw new ArgumentNullException(nameof(output));
// Do not use the using keyword here because the factory will be disposed before the cache logger is finished with it
var factory = LogFactory.Create(output, config);
return factory;
}
}
}

View File

@@ -1,113 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="TestOutputLogger" />
/// class is used to provide logging implementation for Xunit.
/// </summary>
public class TestOutputLogger : FilterLogger
{
private static readonly AsyncLocal<ConcurrentStack<ScopeWriter>> _scopes =
new AsyncLocal<ConcurrentStack<ScopeWriter>>();
private readonly LoggingConfig _config;
private readonly string _categoryName;
private readonly ITestOutputHelper _output;
/// <summary>
/// Creates a new instance of the <see cref="TestOutputLogger" /> class.
/// </summary>
/// <param name="categoryName">The category name of the logger.</param>
/// <param name="output">The test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
/// <exception cref="ArgumentException">The <paramref name="categoryName" /> is <c>null</c>, empty or whitespace.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public TestOutputLogger(string categoryName, ITestOutputHelper output, LoggingConfig? config = null)
{
if (string.IsNullOrWhiteSpace(categoryName))
{
throw new ArgumentException("No name value has been supplied", nameof(categoryName));
}
_categoryName = categoryName;
_output = output ?? throw new ArgumentNullException(nameof(output));
_config = config ?? new LoggingConfig();
}
/// <inheritdoc />
public override IDisposable BeginScope<TState>(TState state)
{
var scopeWriter = new ScopeWriter(_output, state, Scopes.Count, _categoryName, () => Scopes.TryPop(out _), _config);
Scopes.Push(scopeWriter);
return scopeWriter;
}
/// <inheritdoc />
public override bool IsEnabled(LogLevel logLevel)
{
if (logLevel == LogLevel.None)
{
return false;
}
return logLevel >= _config.LogLevel;
}
/// <inheritdoc />
protected override void WriteLogEntry<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
string message,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
try
{
WriteLog(logLevel, eventId, message, exception);
}
catch (InvalidOperationException)
{
if (_config.IgnoreTestBoundaryException == false)
{
throw;
}
}
}
private void WriteLog(LogLevel logLevel, EventId eventId, string message, Exception? exception)
{
var formattedMessage = _config.Formatter.Format(Scopes.Count, _categoryName, logLevel, eventId, message, exception);
_output.WriteLine(formattedMessage);
// Write the message to the output window
Trace.WriteLine(formattedMessage);
}
private static ConcurrentStack<ScopeWriter> Scopes
{
get
{
var scopes = _scopes.Value;
if (scopes == null)
{
scopes = new ConcurrentStack<ScopeWriter>();
_scopes.Value = scopes;
}
return scopes;
}
}
}
}

View File

@@ -1,46 +0,0 @@
namespace Divergic.Logging.Xunit
{
using System;
using global::Xunit.Abstractions;
using Microsoft.Extensions.Logging;
/// <summary>
/// The <see cref="TestOutputLoggerProvider" /> class is used to provide Xunit logging to <see cref="ILoggerFactory" />
/// .
/// </summary>
public sealed class TestOutputLoggerProvider : ILoggerProvider
{
private readonly LoggingConfig? _config;
private readonly ITestOutputHelper _output;
/// <summary>
/// Initializes a new instance of the <see cref="TestOutputLoggerProvider" /> class.
/// </summary>
/// <param name="output">The test output helper.</param>
/// <param name="config">Optional logging configuration.</param>
/// <exception cref="ArgumentNullException">The <paramref name="output" /> is <c>null</c>.</exception>
public TestOutputLoggerProvider(ITestOutputHelper output, LoggingConfig? config = null)
{
_output = output ?? throw new ArgumentNullException(nameof(output));
_config = config;
}
/// <inheritdoc />
/// <exception cref="ArgumentException">The <paramref name="categoryName" /> is <c>null</c>, empty or whitespace.</exception>
public ILogger CreateLogger(string categoryName)
{
if (string.IsNullOrWhiteSpace(categoryName))
{
throw new ArgumentException("No categoryName value has been supplied", nameof(categoryName));
}
return new TestOutputLogger(categoryName, _output, _config);
}
/// <inheritdoc />
public void Dispose()
{
// no-op
}
}
}

View File

@@ -1,4 +1,4 @@
using Divergic.Logging.Xunit;
using Neovolve.Logging.Xunit;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Windows;
using Velopack.Vpk;