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 Sep 18, 2023
1 parent b0f8abb commit 3e0e13d
Show file tree
Hide file tree
Showing 23 changed files with 327 additions and 10 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("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VB.Specs", "Tests\VB.Specs\
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Specs", "Tests\FSharp.Specs\FSharp.Specs.fsproj", "{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertions.Extensibility.Specs", "Tests\FluentAssertions.Extensibility.Specs\FluentAssertions.Extensibility.Specs.csproj", "{07268C80-07CB-4B23-9113-288438499E7B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CI|Any CPU = CI|Any CPU
Expand Down Expand Up @@ -149,6 +151,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
{07268C80-07CB-4B23-9113-288438499E7B}.CI|Any CPU.ActiveCfg = Debug|Any CPU
{07268C80-07CB-4B23-9113-288438499E7B}.CI|Any CPU.Build.0 = Debug|Any CPU
{07268C80-07CB-4B23-9113-288438499E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07268C80-07CB-4B23-9113-288438499E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07268C80-07CB-4B23-9113-288438499E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07268C80-07CB-4B23-9113-288438499E7B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -169,6 +177,7 @@ Global
{A946043D-D3F8-46A4-B485-A88412C417FE} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{0C0211B6-D185-4518-A15A-38AC092EDC50} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{0A69DC62-CA14-44E5-BAF9-2EB2E2E2CADF} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3}
{07268C80-07CB-4B23-9113-288438499E7B} = {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
69 changes: 63 additions & 6 deletions Src/FluentAssertions/Common/Services.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System;
using System;
using System.Linq;
using System.Reflection;
using FluentAssertions.Execution;
using FluentAssertions.Extensibility;
using JetBrains.Annotations;

namespace FluentAssertions.Common;

Expand All @@ -10,10 +14,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 +38,66 @@ 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 || NETCOREAPP
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();

try
{
var 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>());

foreach (var attribute in attributes)
{
attribute.Initialize();
}
}
catch
{
// Just ignore any exceptions that might happen while finding and invoking initialize
}
}

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);
}
}
5 changes: 4 additions & 1 deletion Src/FluentAssertions/FluentAssertions.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<!-- To reduce build times, we only enable analyzers for the newest TFM -->
<PropertyGroup>
Expand All @@ -12,6 +12,7 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<FluentAssertionsPublicKey>00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f</FluentAssertionsPublicKey>
<Version>7.0.0</Version>
</PropertyGroup>

<PropertyGroup Label="Package info">
Expand Down Expand Up @@ -48,8 +49,10 @@
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="PolySharp" Version="1.13.2" PrivateAssets="all" />
<PackageReference Include="xunit.extensibility.core" Version="2.5.0" />
</ItemGroup>


<!-- Target framework dependent configuration -->
<Choose>
<When Condition="'$(TargetFramework)' == 'net6.0'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,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 @@ -1267,6 +1267,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 @@ -1254,6 +1254,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 @@ -1254,6 +1254,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 @@ -1205,6 +1205,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 @@ -1254,6 +1254,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();
}
}
}
Loading

0 comments on commit 3e0e13d

Please sign in to comment.