Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Sep 16, 2023
1 parent 44a8d64 commit 6b8ef89
Show file tree
Hide file tree
Showing 20 changed files with 338 additions and 15 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
51 changes: 40 additions & 11 deletions Src/FluentAssertions/Common/Services.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Reflection;
using FluentAssertions.Execution;
using FluentAssertions.Extensibility;
using JetBrains.Annotations;

namespace FluentAssertions.Common;
Expand Down Expand Up @@ -46,22 +47,50 @@ public static void ResetToDefaults()

internal static void EnsureInitialized()
{
lock (Lockable)
if (!isInitialized)
{
if (!isInitialized)
lock (Lockable)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
if (!isInitialized)
{
InitializeExtensionAssemblies();

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

isInitialized = true;
isInitialized = true;
}
}
}
}

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

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<ExtensionAssemblyAttribute>());

foreach (var attribute in attributes)
{
// SMELL: What happens if this throws? Is that our responsibility?
attribute.Initialize();
}
}

private static bool IsFramework(Assembly assembly)
{
#pragma warning disable CA1310
return assembly.FullName.StartsWith("Microsoft") || assembly.FullName.StartsWith("System");
#pragma warning restore CA1310
}
}
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;
}
}
}
40 changes: 40 additions & 0 deletions Src/FluentAssertions/Extensibility/ExtensionAssemblyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Reflection;

namespace FluentAssertions.Extensibility;

/// <summary>
/// Marks an assembly as containing extension methods for FluentAssertions, optionally allowing the assembly to
/// be initialized before the first assertion happens.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class ExtensionAssemblyAttribute : Attribute
{
private readonly string methodName;
private readonly Type type;

/// <summary>
/// Marks the assembly as containing extension methods for FluentAssertions and which does not require any initialization.
/// </summary>
public ExtensionAssemblyAttribute()
{
}

/// <summary>
/// Marks the assembly as containing extension methods for FluentAssertions and which requires initialization.
/// </summary>
/// <param name="type">The static type that contains the initialization method.</param>
/// <param name="methodName">The static parameterless and void-returning method to invoke before the first initialization</param>
#pragma warning disable CA1019
public ExtensionAssemblyAttribute(Type type, string methodName)
#pragma warning restore CA1019
{
this.type = type;
this.methodName = methodName;
}

public 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,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,16 @@ namespace FluentAssertions.Execution
public string FormattedMessage { get; set; }
}
}
namespace FluentAssertions.Extensibility
{
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)]
public sealed class ExtensionAssemblyAttribute : System.Attribute
{
public ExtensionAssemblyAttribute() { }
public ExtensionAssemblyAttribute(System.Type type, string methodName) { }
public void Initialize() { }
}
}
namespace FluentAssertions.Extensions
{
public static class FluentDateTimeExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<ItemGroup>
<PackageReference Include="Chill" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSubstitute" Version="5.0.0" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="Xunit.StaFact" Version="1.1.11" />
<PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all">
Expand Down
52 changes: 52 additions & 0 deletions Tests/FluentAssertions.Extensibility.Specs/ExtensibilitySpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using Xunit.Sdk;

namespace FluentAssertions.Extensibility.Specs;

public class ExtensibilitySpecs
{
[Fact]
public void Methods_in_extension_libraries_do_not_need_to_be_annoted_as_custom_assertions()
{
// Arrange
var myClient = new MyCustomer
{
Active = false
};

// Act
Action act = () => myClient.Should().BeActive("because we don't work with old clients");

// Assert
act.Should().Throw<XunitException>().WithMessage(
"Expected myClient to be true because we don't work with old clients, but found False.");
}
}

public class MyCustomer
{
public bool Active { get; set; }
}

public static class MyCustomerExtensions
{
public static MyCustomerAssertions Should(this MyCustomer customer)
{
return new MyCustomerAssertions(customer);
}
}

public class MyCustomerAssertions
{
private readonly MyCustomer customer;

public MyCustomerAssertions(MyCustomer customer)
{
this.customer = customer;
}

public void BeActive(string because = "", params object[] becauseArgs)
{
customer.Active.Should().BeTrue(because, becauseArgs);
}
}
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 = () => FileContainingAssemblyLevelAttributes.ShouldBeCalledOnlyOnce.Should().Be(1);

act.Should().NotThrow();
}
}
}
Loading

0 comments on commit 6b8ef89

Please sign in to comment.