Skip to content

Commit

Permalink
Add xunit->ILogger adapter, target net8.0 AOT, packages.lock.json. Bu…
Browse files Browse the repository at this point in the history
…mp to v1.1.0
  • Loading branch information
Mads Klinkby committed Nov 17, 2023
1 parent 3436145 commit 18548d7
Show file tree
Hide file tree
Showing 11 changed files with 4,098 additions and 57 deletions.
15 changes: 15 additions & 0 deletions Directory.Build.props
@@ -0,0 +1,15 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<LangVersion>10</LangVersion>

<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

<ImplicitUsings>enable</ImplicitUsings>
<Configurations>Debug;Release</Configurations>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Deterministic>true</Deterministic>
</PropertyGroup>
</Project>
5 changes: 3 additions & 2 deletions README.md
Expand Up @@ -5,15 +5,16 @@

## Summary

Just an evolving collection of general purpose coding productivity tools.
Just an evolving collection of general purpose coding productivity tools.
\>95% test coverage.

## Contents

- Explicit parameter validation guard extension methods
- Thread safe event raiser extension methods
- Apply, Curry, Uncurry for `Func<T>`
- Apply for `Action<T>`
- Apply for `Action<T>`
- Adapter for passing Extensions.ILogger to XUnit

## License

Expand Down
51 changes: 37 additions & 14 deletions src/Klinkby.Toolkitt/EventHandlerExtensions.cs
Expand Up @@ -3,38 +3,61 @@
// hat tip to Jon Skeet
// https://codeblog.jonskeet.uk/2015/01/30/clean-event-handlers-invocation-with-c-6/

/// <summary>Raises events</summary>
/// <summary>Raises events</summary>
public static class EventHandlerExtensions
{
/// <summary>
/// Invoke an event handler, thread safe.
/// Invoke an event handler, thread safe.
/// </summary>
/// <param name="eh">The event handler to invoke</param>
/// <param name="sender">Event emitter instance</param>
/// <param name="args"><see cref="EventArgs.Empty"/></param>
/// <example><code>
/// <param name="args">
/// <see cref="EventArgs" />
/// </param>
/// <example>
/// <code>
/// public EventHandler Click;
/// protected void OnClick()
/// {
/// Click.Raise(this, new ClickEventArgs(x, y));
/// }
/// </code>
/// </example>
public static void Raise(this EventHandler? eh, object sender, EventArgs args)
=> Interlocked.CompareExchange(ref eh, null, null)?.Invoke(sender, args);

/// <summary>
/// Invoke an event handler, thread safe.
/// </summary>
/// <param name="eh">The event handler to invoke</param>
/// <param name="sender">Event emitter instance</param>
/// <example>
/// <code>
/// public EventHandler Click;
/// protected void OnClick()
/// {
/// Click.Raise(this);
/// }
/// </code></example>
public static void Raise(this EventHandler? eh, object sender, EventArgs? args = null) =>
Interlocked.CompareExchange(ref eh, null, null)?.Invoke(sender, args ?? EventArgs.Empty);
/// </code>
/// </example>
public static void Raise(this EventHandler? eh, object sender)
=> Interlocked.CompareExchange(ref eh, null, null)?.Invoke(sender, EventArgs.Empty);

/// <summary>
/// Invoke an event handler, thread safe.
/// Invoke an event handler, thread safe.
/// </summary>
/// <param name="eh">The event handler to invoke</param>
/// <param name="sender">Event emitter instance</param>
/// <param name="args"><see cref="EventArgs.Empty"/></param>
/// <example><code><!--
/// <param name="args"><see cref="EventArgs" /> subclass</param>
/// <example>
/// <code><!--
/// public EventHandler<ClickEventArgs> Click;
/// protected void OnClick(int x, int y)
/// {
/// Click.Raise(this, new ClickEventArgs(x, y));
/// }
/// --></code></example>
public static void Raise<T>(this EventHandler<T>? eh, object sender, T args) where T: EventArgs =>
Interlocked.CompareExchange(ref eh, null, null)?.Invoke(sender, args);
}
/// --></code>
/// </example>
public static void Raise<T>(this EventHandler<T>? eh, object sender, T args) where T : EventArgs
=> Interlocked.CompareExchange(ref eh, null, null)?.Invoke(sender, args);
}
68 changes: 31 additions & 37 deletions src/Klinkby.Toolkitt/Klinkby.Toolkitt.csproj
@@ -1,44 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<LangVersion>10</LangVersion>
<RootNamespace>Klinkby.Toolkitt</RootNamespace>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<PropertyGroup>
<RootNamespace>Klinkby.Toolkitt</RootNamespace>
<IsAotCompatible>true</IsAotCompatible>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<PropertyGroup>
<Authors>@klinkby</Authors>
<RepositoryUrl>https://github.com/klinkby/toolkitt</RepositoryUrl>
<AssemblyName>Klinkby.Toolkitt</AssemblyName>
<AssemblyTitle>Klinkby.Toolkitt</AssemblyTitle>
<PackageId>Klinkby.Toolkitt</PackageId>
<PackageVersion>1.1.0</PackageVersion>
<PackageIcon>kitt.png</PackageIcon>
<PackageTags>Tool Utility Extension Guard String EventHandler</PackageTags>
<Description>Just an evolving collection of general purpose coding productivity tools</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup>
<Authors>@klinkby</Authors>
<RepositoryUrl>https://github.com/klinkby/toolkitt</RepositoryUrl>
<AssemblyName>Klinkby.Toolkitt</AssemblyName>
<AssemblyTitle>Klinkby.Toolkitt</AssemblyTitle>
<RootNamespace>Klinkby.Toolkitt</RootNamespace>
<PackageId>Klinkby.Toolkitt</PackageId>
<PackageVersion>1.0.5</PackageVersion>
<PackageIcon>kitt.png</PackageIcon>
<PackageTags>Tool Utility Extension Guard String EventHandler</PackageTags>
<Description>Just an evolving collection of general purpose coding productivity tools</Description>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<StripSymbols>false</StripSymbols>
</PropertyGroup>

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="../../LICENSE" Pack="true" PackagePath=""/>
<None Include="../../README.md" Pack="true" PackagePath=""/>
<None Include="../../kitt.png" Pack="true" PackagePath=""/>
</ItemGroup>

<PropertyGroup>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<None Include="../../LICENSE" Pack="true" PackagePath=""/>
<None Include="../../README.md" Pack="true" PackagePath=""/>
<None Include="../../kitt.png" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0"/>
<PackageReference Include="xunit.abstractions" Version="2.0.1"/>
</ItemGroup>
</Project>
64 changes: 64 additions & 0 deletions src/Klinkby.Toolkitt/XUnitITestOutputHelperExtensions.cs
@@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Klinkby.Toolkitt;

/// <summary>
/// Extensions for <see cref="ITestOutputHelper" />
/// </summary>
public static class XUnitITestOutputHelperExtensions
{
private const string DefaultSeparator = "\t";
private const LogLevel DefaultMinimumLogLevel = LogLevel.Trace;

/// <summary>
/// Create an <see cref="ILogger" /> from an <see cref="ITestOutputHelper" />
/// </summary>
/// <param name="xunitLogger">The logger provided to test constructor</param>
/// <param name="minimumLogLevel">Filter minimum log level (defaults to Trace)</param>
/// <param name="separator">Field separator in output (defaults to Tab-character)</param>
/// <returns>An ILogger</returns>
/// <example>
/// <code><![CDATA[
/// public class MyTest
/// {
/// ILogger _logger;
/// public MyTest(ITestOutputHelper output)
/// {
/// _logger = output.ToILogger();
/// }
/// }
/// ]]></code>
/// </example>
public static ILogger ToILogger(this ITestOutputHelper xunitLogger,
LogLevel minimumLogLevel = DefaultMinimumLogLevel,
string separator = DefaultSeparator)
{
return new XUnitLoggerAdapter(xunitLogger, minimumLogLevel, separator);
}

/// <summary>
/// Create an <see cref="ILogger{T}" /> from an <see cref="ITestOutputHelper" />
/// </summary>
/// <param name="xunitLogger">The logger provided to test constructor</param>
/// <param name="minimumLogLevel">Filter minimum log level (defaults to Trace)</param>
/// <param name="separator">Field separator in output (defaults to Tab-character)</param>
/// <returns>An ILogger{T}</returns>
/// <example>
/// <code><![CDATA[
/// public class MyTest
/// {
/// ILogger<MyTest> _logger;
/// public MyTest(ITestOutputHelper output)
/// {
/// _logger = output.ToILogger<MyTest>();
/// }
/// }
/// ]]></code>
/// </example>
public static ILogger<T> ToILogger<T>(this ITestOutputHelper xunitLogger, LogLevel minimumLogLevel = LogLevel.Trace,
string separator = "\t")
{
return new XUnitLoggerAdapter<T>(xunitLogger, minimumLogLevel, separator);
}
}
61 changes: 61 additions & 0 deletions src/Klinkby.Toolkitt/XUnitLoggerAdapter.cs
@@ -0,0 +1,61 @@
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Klinkby.Toolkitt;

internal class XUnitLoggerAdapter : ILogger
{
private readonly ITestOutputHelper _logger;
private readonly LogLevel _minimumLogLevel;
private readonly string _separator;
private int _indent;

public XUnitLoggerAdapter(ITestOutputHelper logger, LogLevel minimumLogLevel, string separator)
{
_logger = logger;
_minimumLogLevel = minimumLogLevel;
_separator = separator;
}

private string Indent
=> new(' ', _indent);

public IDisposable BeginScope<TState>(TState state)
{
_logger.WriteLine(Indent + state);
Interlocked.Increment(ref _indent);
return new Scope(() => Interlocked.Decrement(ref _indent));
}

public bool IsEnabled(LogLevel logLevel)
=> logLevel switch
{
LogLevel.None => false,
_ => logLevel >= _minimumLogLevel
};

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
var message = $"{Indent}{logLevel}{_separator}{eventId}{_separator}{formatter(state, exception)}";
_logger.WriteLine(message);
}

private sealed class Scope : IDisposable
{
private readonly Action _cleanUp;

internal Scope(Action cleanup) => _cleanUp = cleanup;

public void Dispose() => _cleanUp();
}
}

internal class XUnitLoggerAdapter<T> : XUnitLoggerAdapter, ILogger<T>
{
public XUnitLoggerAdapter(ITestOutputHelper logger, LogLevel minimumLogLevel, string separator)
: base(logger, minimumLogLevel, separator)
{
}
}

0 comments on commit 18548d7

Please sign in to comment.