Skip to content

Commit

Permalink
Minor refactors and additional tests.
Browse files Browse the repository at this point in the history
Added logging output wrappers as utilities.
  • Loading branch information
CZEMacLeod committed Aug 11, 2023
1 parent 89a6886 commit c602413
Show file tree
Hide file tree
Showing 15 changed files with 388 additions and 183 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.CopyOnWrite" Version="1.0.263" PrivateAssets="All" />
<PackageReference Include="Microsoft.Build.CopyOnWrite" Version="1.0.265" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
</PackageReference>
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.extensibility.execution" Version="2.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='net7.0'">
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,31 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Configuration;
using System.Diagnostics.CodeAnalysis;
using Xunit.Abstractions;

namespace Microsoft.Extensions.Logging;

public static class MELXunitLoggerExtensions
{
private static bool TryGetService<T>(this IServiceProvider services, [NotNullWhen(returnValue: true)] out T? service) where T : class
{
service = services.GetService<T>();
return service is not null;
}

#region "ILoggingBuilder"
public static ILoggingBuilder AddXunit(this ILoggingBuilder builder)
public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, Action<XunitLoggerOptions>? configure = null)
{
LoggerProviderOptions.RegisterProviderOptions<XunitLoggerOptions, XunitLoggerProvider>(builder.Services);
if (configure is not null) { builder.Services.Configure(configure); }
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, XunitLoggerProvider>());
return builder;
}

public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, Action<XunitLoggerOptions>? configure)
{
var optionsBuilder = builder.Services.AddOptions<XunitLoggerOptions>();
if (configure is not null) { optionsBuilder.Configure(configure); }
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider>(sp =>
{
if (sp.TryGetService<ITestOutputHelper>(out var testOutput)) { ActivatorUtilities.CreateInstance<XunitLoggerProvider>(sp,testOutput); }
if (sp.TryGetService<IMessageSink>(out var messageSink)) { ActivatorUtilities.CreateInstance<XunitLoggerProvider>(sp, messageSink); }
return Microsoft.Extensions.Logging.Abstractions.NullLoggerProvider.Instance;
}));

return builder;
}

public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, Action<XunitLoggerOptions>? configure = null)
{
var optionsBuilder = builder.Services.AddOptions<XunitLoggerOptions>();
if (configure is not null) { optionsBuilder.Configure(configure); }
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider>(sp=> ActivatorUtilities.CreateInstance<XunitLoggerProvider>(sp, output)));
return builder;
builder.Services.AddSingleton(output);
return builder.AddXunit(configure);
}

public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, IMessageSink output, Action<XunitLoggerOptions>? configure = null)
{
var optionsBuilder = builder.Services.AddOptions<XunitLoggerOptions>();
if (configure is not null) { optionsBuilder.Configure(configure); }
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider>(sp => ActivatorUtilities.CreateInstance<XunitLoggerProvider>(sp, output)));
return builder;
builder.Services.AddSingleton(output);
return builder.AddXunit(configure);
}
#endregion

Expand Down
12 changes: 12 additions & 0 deletions src/C3D/Extensions/Logging/Xunit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ logging
});
```

N.B. This will add the `output` object as a singleton to the `ILoggingBuilder` services.

### Direct injection

```c#
Expand Down Expand Up @@ -77,3 +79,13 @@ var loggerFactory = LoggerFactory.Create(builder => builder.AddXunit(output));
The same mechanisms used in tests based on injecting `ITestOutputHelper` into the test class, can be used in fixtures.
However, in fixtures you inject `IMessageSink`. The same extension methods are available for both interfaces.

### Utilities

As you may want to check the logged output for content in your tests (either for something existing or not existing), there are 2 utility classes provided.

- `LoggingMessageSink` which implements `IMessageSink`
- `LoggingTestOutputHelper` which implements `ITestOutputHelper`

These both have a `ClearMessages` function (which may be required in a fixture to ensure output from one test does not affect another), and a `Messages` property.

Examples of how to use these can be found in the unit tests at [CZEMacLeod/C3D.Extensions.Logging](https://github.com/CZEMacLeod/C3D.Extensions.Logging/tree/main/test/C3D/Extensions/Logging/Xunit/Test).
27 changes: 27 additions & 0 deletions src/C3D/Extensions/Logging/Xunit/Utilities/LoggingMessageSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Immutable;
using Xunit.Abstractions;

namespace C3D.Extensions.Logging.Xunit.Utilities;

public class LoggingMessageSink : IMessageSink
{
private readonly List<IMessageSinkMessage> messages = new();
private readonly IMessageSink output;

public LoggingMessageSink(IMessageSink output) => this.output = output;

#if NET6_0_OR_GREATER
public IReadOnlyList<IMessageSinkMessage> Messages => messages.ToImmutableList();
#else
public IReadOnlyList<IMessageSinkMessage> Messages => ImmutableList<IMessageSinkMessage>.Empty.AddRange(messages);
#endif

public bool OnMessage(IMessageSinkMessage message)
{
messages.Add(message);
return output.OnMessage(message);
}

public void Clear() => messages.Clear();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Immutable;
using Xunit.Abstractions;

namespace C3D.Extensions.Logging.Xunit.Utilities;

public class LoggingTestOutputHelper : ITestOutputHelper
{
private readonly ITestOutputHelper output;
private readonly List<string> messages = new();

#if NET6_0_OR_GREATER
public IReadOnlyList<string> Messages => messages.ToImmutableList();
#else
public IReadOnlyList<string> Messages => ImmutableList<string>.Empty.AddRange(messages);
#endif

public void ClearMessages() { messages.Clear(); }

public LoggingTestOutputHelper(ITestOutputHelper output) => this.output = output;

public void WriteLine(string message)
{
output.WriteLine(message);
messages.Add(message);
}

public void WriteLine(string format, params object[] args)
{
output.WriteLine(format, args);
messages.Add(string.Format(format, args));
}
}
6 changes: 4 additions & 2 deletions src/C3D/Extensions/Logging/Xunit/XunitLoggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ public enum XunitLoggerTimeStamp

public class XunitLoggerOptions
{
public XunitLoggerOptions(Func<DateTimeOffset>? getUtcNow = null)
public XunitLoggerOptions() : this(() => DateTimeOffset.UtcNow) { }

public XunitLoggerOptions(Func<DateTimeOffset> getUtcNow)
{
GetUtcNow = getUtcNow ?? (() => DateTimeOffset.UtcNow);
GetUtcNow = getUtcNow;
LogStart = GetUtcNow();
}

Expand Down
8 changes: 4 additions & 4 deletions src/C3D/Extensions/Logging/Xunit/XunitLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public sealed class XunitLoggerProvider : ILoggerProvider

public XunitLoggerProvider(IMessageSink output, IOptionsMonitor<XunitLoggerOptions> options)
{
output2= output;
output2 = output;
onChange = options.OnChange(SetOptions);
SetOptions(options.CurrentValue);
}
Expand Down Expand Up @@ -50,9 +50,9 @@ internal XunitLoggerProvider(ITestOutputHelper output, XunitLoggerOptions option
internal XunitLoggerOptions GetOptions() => options;

public ILogger CreateLogger(string categoryName) =>
_loggers.GetOrAdd(categoryName,
name => output1 is null ?
(output2 is null ? NullLogger.Instance :
_loggers.GetOrAdd(categoryName,
name => output1 is null ?
(output2 is null ? NullLogger.Instance :
new MessageSinkLogger(name, output2, GetOptions)) :
new TextOutputLogger(name, output1, GetOptions));

Expand Down
2 changes: 1 addition & 1 deletion src/C3D/Extensions/Logging/Xunit/version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/v3.3.37/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.1",
"version": "0.2",
"publicReleaseRefSpec": [
"^refs/heads/main$", // we release out of main
"^refs/heads/rel/v\\d+\\.\\d+" // we also release tags starting with rel/N.N
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net48</TargetFrameworks>
<TargetFrameworks>net6.0;net48;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
Expand All @@ -22,6 +22,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />

<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />

<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
12 changes: 0 additions & 12 deletions test/C3D/Extensions/Logging/Xunit/Test/FixtureTest.cs

This file was deleted.

82 changes: 82 additions & 0 deletions test/C3D/Extensions/Logging/Xunit/Test/FixtureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace C3D.Extensions.Logging.Xunit.Test;

public class FixtureTests : IClassFixture<LoggingTestFixture>
{
private readonly LoggingTestFixture fixture;
private readonly ITestOutputHelper output;

private void WriteFunctionName([CallerMemberName] string? caller = null) => output.WriteLine(caller);

public FixtureTests(LoggingTestFixture fixture, ITestOutputHelper output)
{
this.fixture = fixture;
this.output = output;
}

[Fact]
public void CreateLoggerFromServices()
{
WriteFunctionName();

fixture.ClearMessages();

var services = fixture.Services;

var log = services.GetRequiredService<ILogger<FixtureTests>>();

Assert.NotNull(log);

const string debug = "Here is some debugging";
const string info = "Here is some information";

log.LogDebug(debug);
log.LogInformation(info);

var messages = fixture.Messages;

Assert.NotNull(messages);

Assert.Equal(2, messages.Count);

var diagnosticMessages = messages.OfType<DiagnosticMessage>();

Assert.Collection(diagnosticMessages, s => s.Message.EndsWith(debug), s => s.Message.EndsWith(info));
}

[Fact]
public void CreateLoggerFromFactory()
{
WriteFunctionName();

fixture.ClearMessages();

var factory = fixture.LoggerFactory;

var log = factory.CreateLogger<TestOutputHelperTests>();

Assert.NotNull(log);

const string debug = "Here is some debugging";
const string info = "Here is some information";

log.LogDebug(debug);
log.LogInformation(info);

var messages = fixture.Messages;

var diagnosticMessages = messages.OfType<DiagnosticMessage>();

Assert.Collection(diagnosticMessages, s => s.Message.EndsWith(debug), s => s.Message.EndsWith(info));
}
}
56 changes: 49 additions & 7 deletions test/C3D/Extensions/Logging/Xunit/Test/LoggingTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
using C3D.Extensions.Logging.Xunit.Utilities;

namespace C3D.Extensions.Logging.Xunit.Net6
namespace C3D.Extensions.Logging.Xunit.Test;

public partial class LoggingTestFixture
{
internal class LoggingTestFixture
private readonly LoggingMessageSink output;
public LoggingTestFixture(IMessageSink output) => this.output = new LoggingMessageSink(output);


public IReadOnlyCollection<IMessageSinkMessage> Messages => output.Messages;
public void ClearMessages() => output.Clear();

private static IConfiguration CreateConfiguration()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
return builder.Build();

}

private ILoggingBuilder ConfigureLogger(ILoggingBuilder builder, IConfiguration configuration) => builder
.ClearProviders()
.SetMinimumLevel(LogLevel.Debug)
.AddConfiguration(configuration.GetSection("Logging"))
.AddDebug();

private IServiceCollection CreateServices()
{
var configuration = CreateConfiguration();

return new ServiceCollection()
.AddSingleton(configuration)
.AddSingleton<IMessageSink>(output)
.AddTransient(_ => output.Messages)
.AddLogging(logging => ConfigureLogger(logging, configuration).AddXunit());
}

private IServiceProvider? services;
public IServiceProvider Services => services ??= CreateServices().BuildServiceProvider();

private ILoggerFactory CreateLoggerFactory()
{
var configuration = CreateConfiguration();
return Microsoft.Extensions.Logging.LoggerFactory.Create(logging => ConfigureLogger(logging, configuration).AddXunit(output));
}

private ILoggerFactory? loggerFactory;
public ILoggerFactory LoggerFactory => loggerFactory ??= CreateLoggerFactory();
}
Loading

0 comments on commit c602413

Please sign in to comment.