Skip to content

Commit

Permalink
chore(deps): bump test dependencies (#109)
Browse files Browse the repository at this point in the history
* chore(deps): replaced Serilog with own log capturing. Although we have to maintain some extra code, Serilog is not easy to maintain with the different target frameworks/libraries and its dependencies.

* chore(deps): bump test dependencies xunit, Microsoft.NET.Test.Sdk and Newtonsoft.Json
  • Loading branch information
skwasjer authored Sep 7, 2024
1 parent 3817b88 commit 85b2c58
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 40 deletions.
4 changes: 2 additions & 2 deletions test/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<NoWarn>$(NoWarn);NU1902;NU1903</NoWarn>
<NetTestSdkVersion>17.8.0</NetTestSdkVersion>
<NetTestSdkVersion>17.11.1</NetTestSdkVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp2.1'">
Expand All @@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit" Version="2.9.0" />
</ItemGroup>

<ItemGroup Condition="'$(IsTestProject)'=='true'">
Expand Down
230 changes: 230 additions & 0 deletions test/MockHttp.Server.Tests/Fixtures/CapturingLoggerFactoryFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;

namespace MockHttp.Fixtures;

public delegate void CaptureDelegate(string message);

public class CapturingLoggerFactoryFixture : LoggerFactoryFixture
{
private static readonly AsyncLocal<LogContext?> LogContextLocal = new();

public CapturingLoggerFactoryFixture()
: base(configure => configure
.AddConsole(opts => opts.FormatterName = ConsoleCapture.NameKey)
.AddConsoleFormatter<ConsoleCapture, ConsoleCaptureOptions>(opts => opts.IncludeScopes = true)
.Services.AddSingleton((CaptureDelegate)(message => LogContextLocal.Value?.Events.Add(message)))
)
{
}

public static LogContext CreateContext()
{
return LogContextLocal.Value = new LogContext(() => LogContextLocal.Value = null);
}

public sealed class LogContext(Action dispose) : IDisposable
{
public List<string> Events { get; } = new();

public void Dispose()
{
dispose();
}
}

private class ConsoleCapture : ConsoleFormatter
{
internal const string NameKey = "console-capture";
private const string LogLevelPadding = ": ";
private static readonly string MessagePadding = new(' ', GetLogLevelString(LogLevel.Information).Length + LogLevelPadding.Length);
private static readonly string NewLineWithMessagePadding = Environment.NewLine + MessagePadding;

private readonly CaptureDelegate _onWrite;

private readonly ConsoleCaptureOptions _options;

public ConsoleCapture
(
CaptureDelegate onWrite,
IOptions<ConsoleCaptureOptions> options
) : base(NameKey)
{
_onWrite = onWrite ?? throw new ArgumentNullException(nameof(onWrite));
_options = options.Value;
}

public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
{
var sb = new StringBuilder();
textWriter = new StringWriter(sb);

string? message = logEntry.Formatter(logEntry.State, logEntry.Exception);
if (logEntry.Exception is null && message is null)
{
return;
}

LogLevel logLevel = logEntry.LogLevel;
string logLevelString = GetLogLevelString(logLevel);

string? timestamp = null;
string? timestampFormat = _options.TimestampFormat;
if (timestampFormat is not null)
{
DateTimeOffset dateTimeOffset = GetCurrentDateTime();
timestamp = dateTimeOffset.ToString(timestampFormat);
}

if (!string.IsNullOrEmpty(timestamp))
{
textWriter.Write(timestamp);
}

if (!string.IsNullOrEmpty(logLevelString))
{
textWriter.Write(logLevelString);
}

CreateDefaultLogMessage(textWriter, in logEntry, message, scopeProvider);

_onWrite(sb.ToString());
}

private void CreateDefaultLogMessage<TState>(TextWriter textWriter, in LogEntry<TState> logEntry, string message, IExternalScopeProvider? scopeProvider)
{
bool singleLine = _options.SingleLine;
int eventId = logEntry.EventId.Id;
Exception? exception = logEntry.Exception;

// Example:
// info: ConsoleApp.Program[10]
// Request received

// category and event id
textWriter.Write(LogLevelPadding);
textWriter.Write(logEntry.Category);
textWriter.Write('[');

#if NETCOREAPP
Span<char> span = stackalloc char[10];
if (eventId.TryFormat(span, out int charsWritten))
{
textWriter.Write(span.Slice(0, charsWritten));
}
else
#endif
{
textWriter.Write(eventId.ToString());
}

textWriter.Write(']');
if (!singleLine)
{
textWriter.Write(Environment.NewLine);
}

// scope information
WriteScopeInformation(textWriter, scopeProvider, singleLine);
WriteMessage(textWriter, message, singleLine);

// Example:
// System.InvalidOperationException
// at Namespace.Class.Function() in File:line X
if (exception is not null)
{
// exception message
WriteMessage(textWriter, exception.ToString(), singleLine);
}

if (singleLine)
{
textWriter.Write(Environment.NewLine);
}
}

private static void WriteMessage(TextWriter textWriter, string message, bool singleLine)
{
if (!string.IsNullOrEmpty(message))
{
if (singleLine)
{
textWriter.Write(' ');
WriteReplacing(textWriter, Environment.NewLine, " ", message);
}
else
{
textWriter.Write(MessagePadding);
WriteReplacing(textWriter, Environment.NewLine, NewLineWithMessagePadding, message);
textWriter.Write(Environment.NewLine);
}
}

static void WriteReplacing(TextWriter writer, string oldValue, string newValue, string message)
{
string newMessage = message.Replace(oldValue, newValue);
writer.Write(newMessage);
}
}

private void WriteScopeInformation(TextWriter textWriter, IExternalScopeProvider? scopeProvider, bool singleLine)
{
if (_options.IncludeScopes && scopeProvider is not null)
{
bool paddingNeeded = !singleLine;
scopeProvider.ForEachScope((scope, state) =>
{
if (paddingNeeded)
{
paddingNeeded = false;
state.Write(MessagePadding);
state.Write("=> ");
}
else
{
state.Write(" => ");
}
state.Write(scope);
},
textWriter);

if (!paddingNeeded && !singleLine)
{
textWriter.Write(Environment.NewLine);
}
}
}

private static string GetLogLevelString(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "trce",
LogLevel.Debug => "dbug",
LogLevel.Information => "info",
LogLevel.Warning => "warn",
LogLevel.Error => "fail",
LogLevel.Critical => "crit",
_ => throw new ArgumentOutOfRangeException(nameof(logLevel))
};
}

private DateTimeOffset GetCurrentDateTime()
{
return _options.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
}
}

private class ConsoleCaptureOptions : ConsoleFormatterOptions
{
/// <summary>
/// When <see langword="true" />, the entire message gets logged in a single line.
/// </summary>
public bool SingleLine { get; set; }
}
}
53 changes: 53 additions & 0 deletions test/MockHttp.Server.Tests/Fixtures/LoggerFactoryFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace MockHttp.Fixtures;

public abstract class LoggerFactoryFixture : IAsyncLifetime, IAsyncDisposable
{
private readonly ServiceProvider _services;

protected LoggerFactoryFixture(Action<ILoggingBuilder>? configure = null)
{
_services = new ServiceCollection()
.AddLogging(builder =>
{
builder.SetMinimumLevel(LogLevel.Trace);
builder
.AddDebug()
#if NET6_0_OR_GREATER
.AddSimpleConsole(opts => opts.IncludeScopes = true)
#endif
;
configure?.Invoke(builder);
})
.BuildServiceProvider();

Factory = _services.GetRequiredService<ILoggerFactory>();
}

public ILoggerFactory Factory { get; }

public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
GC.SuppressFinalize(this);
}

Task IAsyncLifetime.InitializeAsync()
{
return Task.CompletedTask;
}

Task IAsyncLifetime.DisposeAsync()
{
return DisposeAsync().AsTask();
}

protected virtual async ValueTask DisposeAsyncCore()
{
await _services.DisposeAsync();
}
}
Loading

0 comments on commit 85b2c58

Please sign in to comment.