Skip to content

Commit

Permalink
Introduced a new attribute to allow global initialization of the asse…
Browse files Browse the repository at this point in the history
…rtion engine
  • Loading branch information
dennisdoomen committed Oct 22, 2023
1 parent 0ae4aa3 commit ea77cd4
Show file tree
Hide file tree
Showing 18 changed files with 283 additions and 8 deletions.
9 changes: 9 additions & 0 deletions FluentAssertions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Specs", "Tests\FShar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleExtensions", "Tests\ExampleExtensions\ExampleExtensions.csproj", "{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertions.Extensibility.Specs", "Tests\FluentAssertions.Extensibility.Specs\FluentAssertions.Extensibility.Specs.csproj", "{450FC408-A4E2-4483-B064-2007024D6CF1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CI|Any CPU = CI|Any CPU
Expand Down Expand Up @@ -143,6 +145,12 @@ Global
{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}.Release|Any CPU.Build.0 = Release|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.CI|Any CPU.ActiveCfg = Debug|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.CI|Any CPU.Build.0 = Debug|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{450FC408-A4E2-4483-B064-2007024D6CF1}.Release|Any CPU.Build.0 = Release|Any CPU
{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.CI|Any CPU.ActiveCfg = Debug|Any CPU
{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.CI|Any CPU.Build.0 = Debug|Any CPU
{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -169,6 +177,7 @@ Global
{0C0211B6-D185-4518-A15A-38AC092EDC50} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{450FC408-A4E2-4483-B064-2007024D6CF1} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {75DDA3D8-9D6F-4865-93F4-DDE11DEE8290}
Expand Down
5 changes: 5 additions & 0 deletions Src/FluentAssertions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public static class AssertionExtensions
{
private static readonly AggregateExceptionExtractor Extractor = new();

static AssertionExtensions()
{
Services.EnsureInitialized();
}

/// <summary>
/// Invokes the specified action on a subject so that you can chain it
/// with any of the assertions from <see cref="ActionAssertions"/>
Expand Down
1 change: 1 addition & 0 deletions Src/FluentAssertions/AssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static class AssertionOptions
static AssertionOptions()
{
EquivalencyPlan = new EquivalencyPlan();
Services.EnsureInitialized();
}

/// <summary>
Expand Down
80 changes: 74 additions & 6 deletions Src/FluentAssertions/Common/Services.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentAssertions.Execution;
using FluentAssertions.Extensibility;
using JetBrains.Annotations;

namespace FluentAssertions.Common;

Expand All @@ -10,10 +15,11 @@ public static class Services
{
private static readonly object Lockable = new();
private static Configuration configuration;
private static bool isInitialized;

static Services()
{
ResetToDefaults();
EnsureInitialized();
}

public static IConfigurationStore ConfigurationStore { get; set; }
Expand All @@ -33,14 +39,76 @@ public static Configuration Configuration

public static IReflector Reflector { get; set; }

[PublicAPI]
public static void ResetToDefaults()
{
Reflector = new FullFrameworkReflector();
isInitialized = false;
EnsureInitialized();
}

internal static void EnsureInitialized()
{
if (isInitialized)
{
return;
}

lock (Lockable)
{
if (!isInitialized)
{
ExecuteCustomInitializers();

Reflector = new FullFrameworkReflector();
#if NETFRAMEWORK || NET6_0_OR_GREATER
ConfigurationStore = new ConfigurationStoreExceptionInterceptor(new AppSettingsConfigurationStore());
ConfigurationStore = new ConfigurationStoreExceptionInterceptor(new AppSettingsConfigurationStore());
#else
ConfigurationStore = new NullConfigurationStore();
ConfigurationStore = new NullConfigurationStore();
#endif
ThrowException = new TestFrameworkProvider(Configuration).Throw;
ThrowException = new TestFrameworkProvider(Configuration).Throw;

isInitialized = true;
}
}
}

private static void ExecuteCustomInitializers()
{
var currentAssembly = Assembly.GetExecutingAssembly();
var currentAssemblyName = currentAssembly.GetName();

var attributes = Array.Empty<AssertionEngineInitializerAttribute>();

try
{
attributes = AppDomain.CurrentDomain
.GetAssemblies()
.Where(assembly => assembly != currentAssembly && !assembly.IsDynamic && !IsFramework(assembly))
.Where(a => a.GetReferencedAssemblies().Any(r => r.FullName == currentAssemblyName.FullName))
.SelectMany(a => a.GetCustomAttributes<AssertionEngineInitializerAttribute>())
.ToArray();
}
catch
{
// Just ignore any exceptions that might happen while trying to find the attributes
}

foreach (var attribute in attributes)
{
try
{
attribute.Initialize();
}
catch
{
// Just ignore any exceptions that might happen while trying to find the attributes
}
}
}

private static bool IsFramework(Assembly assembly)
{
return assembly?.FullName?.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) == true ||
assembly?.FullName?.StartsWith("System", StringComparison.OrdinalIgnoreCase) == true;
}
}
13 changes: 11 additions & 2 deletions Src/FluentAssertions/Execution/Execute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace FluentAssertions.Execution;
using FluentAssertions.Common;

namespace FluentAssertions.Execution;

/// <summary>
/// Helper class for verifying a condition and/or throwing a test harness specific exception representing an assertion failure.
Expand All @@ -8,5 +10,12 @@ public static class Execute
/// <summary>
/// Gets an object that wraps and executes a conditional or unconditional assertion.
/// </summary>
public static AssertionScope Assertion => AssertionScope.Current;
public static AssertionScope Assertion
{
get
{
Services.EnsureInitialized();
return AssertionScope.Current;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Reflection;

namespace FluentAssertions.Extensibility;

/// <summary>
/// Can be added to an assembly so it gets a change to initialize Fluent Assertions before the first assertion happens.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class AssertionEngineInitializerAttribute : Attribute
{
private readonly string methodName;
private readonly Type type;

/// <summary>
/// Defines the static void-returned and parameterless method that should be invoked before the first assertion happens.
/// </summary>
#pragma warning disable CA1019
public AssertionEngineInitializerAttribute(Type type, string methodName)
#pragma warning restore CA1019
{
this.type = type;
this.methodName = methodName;
}

internal void Initialize()
{
type?.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static)?.Invoke(obj: null, parameters: null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,14 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class AssertionEngineInitializerAttribute : System.Attribute
{
public AssertionEngineInitializerAttribute(System.Type type, string methodName) { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,14 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class AssertionEngineInitializerAttribute : System.Attribute
{
public AssertionEngineInitializerAttribute(System.Type type, string methodName) { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,14 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class AssertionEngineInitializerAttribute : System.Attribute
{
public AssertionEngineInitializerAttribute(System.Type type, string methodName) { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,14 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class AssertionEngineInitializerAttribute : System.Attribute
{
public AssertionEngineInitializerAttribute(System.Type type, string methodName) { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading;

// With specific initialization code to invoke before the first assertion happens
[assembly: FluentAssertions.Extensibility.AssertionEngineInitializer(
typeof(FluentAssertions.Extensibility.Specs.AssertionEngineInitializer),
nameof(FluentAssertions.Extensibility.Specs.AssertionEngineInitializer.InitializeBeforeFirstAssertion))]

namespace FluentAssertions.Extensibility.Specs;

public static class AssertionEngineInitializer
{
private static int shouldBeCalledOnlyOnce;

public static int ShouldBeCalledOnlyOnce => shouldBeCalledOnlyOnce;

public static void InitializeBeforeFirstAssertion()
{
Interlocked.Increment(ref shouldBeCalledOnlyOnce);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace FluentAssertions.Extensibility.Specs;

public class ExtensionAssemblyAttributeSpecs
{
[Fact]
public void Calls_assembly_initialization_code_only_once()
{
for (int i = 0; i < 10; i++)
{
var act = () => AssertionEngineInitializer.ShouldBeCalledOnlyOnce.Should().Be(1);

act.Should().NotThrow();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net47</TargetFrameworks>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\Src\FluentAssertions\FluentAssertions.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn),IDE0052,1573,1591,1712</NoWarn>
<DebugType>full</DebugType>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net47'">
<!--
(SRCU = System.Runtime.CompilerServices.Unsafe)
FluentAssertions.csproj depends on SRCU 4.5.0 (AssemblyVersion 4.0.4.0)
System.Collections.Immutable 1.6.0+ depends on SRCU 4.5.2+ (AssemblyVersion 4.0.4.1)
FluentAssertions.Specs.csproj for net47 does not generate a binding redirect from 4.0.4.0 to 4.0.4.1
Upgrading to 1.6.0 gives "Could not load file or assembly 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.0"
-->
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="PolySharp" Version="1.13.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Src\FluentAssertions\FluentAssertions.csproj" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions Tests/FluentAssertions.Extensibility.Specs/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
2 changes: 2 additions & 0 deletions docs/_data/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ sidebar:
url: /introduction#detecting-test-frameworks
- title: Subject Identification
url: /introduction#subject-identification
- title: Global Configuration
url: /introduction#global-configuration
- title: Assertion Scopes
url: /introduction#assertion-scopes
- title: Basic Assertions
Expand Down
15 changes: 15 additions & 0 deletions docs/_pages/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,21 @@ internal static class Initializer
}
```

Unfortunately, this only works for .NET 5 and higher. That's why Fluent Assertions supports its own "module initializer" through the `[AssertionEngineInitializer]` attribute. It can be used multiple times.

```csharp
[assembly: AssertionEngineInitializer(typeof(Initializer), nameof(Initializer.Initialize))]

public static class Initializer
{
public static void Initialize()
{
AssertionOptions.AssertEquivalencyUsing(options => options
.ComparingByValue<DirectoryInfo>());
}
}
```

### MSTest

MSTest provides the `AssemblyInitializeAttribute` to annotate that a method in a `TestClass` should be run once per assembly.
Expand Down
Loading

0 comments on commit ea77cd4

Please sign in to comment.