diff --git a/FluentAssertions.sln b/FluentAssertions.sln index 31ee369de2..52e93a3c91 100644 --- a/FluentAssertions.sln +++ b/FluentAssertions.sln @@ -51,6 +51,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}") = "ExampleExtensions", "Tests\ExampleExtensions\ExampleExtensions.csproj", "{8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CI|Any CPU = CI|Any CPU @@ -141,6 +143,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 + {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 + {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,6 +168,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} + {8DF4A6FE-AAD0-41E5-B2F4-34166D1B139C} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {75DDA3D8-9D6F-4865-93F4-DDE11DEE8290} diff --git a/Src/FluentAssertions/CallerIdentifier.cs b/Src/FluentAssertions/CallerIdentifier.cs index ff9c66f547..525bba71d1 100644 --- a/Src/FluentAssertions/CallerIdentifier.cs +++ b/Src/FluentAssertions/CallerIdentifier.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using FluentAssertions.CallerIdentification; @@ -137,7 +138,16 @@ internal static bool OnlyOneFluentAssertionScopeOnCallStack() private static bool IsCustomAssertion(StackFrame frame) { - return frame.GetMethod()?.IsDecoratedWithOrInherit() == true; + MethodBase getMethod = frame.GetMethod(); + + if (getMethod is not null) + { + return + getMethod.IsDecoratedWithOrInherit() || + getMethod.ReflectedType?.Assembly.IsDefined(typeof(CustomAssertionsAssemblyAttribute)) == true; + } + + return false; } private static bool IsDynamic(StackFrame frame) diff --git a/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs b/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs new file mode 100644 index 0000000000..69d40f4d7a --- /dev/null +++ b/Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace FluentAssertions; + +/// +/// Marks an assembly as containing extensions to Fluent Assertions that either uses the built-in assertions +/// internally, or directly uses the Execute.Assertion. +/// +[AttributeUsage(AttributeTargets.Assembly)] +#pragma warning disable CA1813 // Avoid unsealed attributes. This type has shipped. +public class CustomAssertionsAssemblyAttribute : Attribute +{ +} diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 8be364f24e..fde6e654e3 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -193,6 +193,11 @@ namespace FluentAssertions { public CustomAssertionAttribute() { } } + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public class CustomAssertionsAssemblyAttribute : System.Attribute + { + public CustomAssertionsAssemblyAttribute() { } + } public static class EnumAssertionsExtensions { public static FluentAssertions.Primitives.EnumAssertions Should(this TEnum @enum) diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index d319c0fc3f..982c5f4865 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -206,6 +206,11 @@ namespace FluentAssertions { public CustomAssertionAttribute() { } } + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public class CustomAssertionsAssemblyAttribute : System.Attribute + { + public CustomAssertionsAssemblyAttribute() { } + } public static class EnumAssertionsExtensions { public static FluentAssertions.Primitives.EnumAssertions Should(this TEnum @enum) diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index a2e54fe7a8..6432da77c8 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -192,6 +192,11 @@ namespace FluentAssertions { public CustomAssertionAttribute() { } } + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public class CustomAssertionsAssemblyAttribute : System.Attribute + { + public CustomAssertionsAssemblyAttribute() { } + } public static class EnumAssertionsExtensions { public static FluentAssertions.Primitives.EnumAssertions Should(this TEnum @enum) diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 44ff600418..5e5e418e92 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -193,6 +193,11 @@ namespace FluentAssertions { public CustomAssertionAttribute() { } } + [System.AttributeUsage(System.AttributeTargets.Assembly)] + public class CustomAssertionsAssemblyAttribute : System.Attribute + { + public CustomAssertionsAssemblyAttribute() { } + } public static class EnumAssertionsExtensions { public static FluentAssertions.Primitives.EnumAssertions Should(this TEnum @enum) diff --git a/Tests/ExampleExtensions/AssemblyInfo.cs b/Tests/ExampleExtensions/AssemblyInfo.cs new file mode 100644 index 0000000000..520561d30e --- /dev/null +++ b/Tests/ExampleExtensions/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using FluentAssertions; + +[assembly: CustomAssertionsAssembly] diff --git a/Tests/ExampleExtensions/ExampleExtensions.csproj b/Tests/ExampleExtensions/ExampleExtensions.csproj new file mode 100644 index 0000000000..8c3772298e --- /dev/null +++ b/Tests/ExampleExtensions/ExampleExtensions.csproj @@ -0,0 +1,21 @@ + + + + net47;net6.0 + enable + disable + false + 11 + True + ..\..\Src\FluentAssertions\FluentAssertions.snk + + + + + + + + + + + diff --git a/Tests/ExampleExtensions/StringAssertionExtensions.cs b/Tests/ExampleExtensions/StringAssertionExtensions.cs new file mode 100644 index 0000000000..d751de5b00 --- /dev/null +++ b/Tests/ExampleExtensions/StringAssertionExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using FluentAssertions.Primitives; + +namespace ExampleExtensions; + +public static class StringAssertionExtensions +{ + public static void BePalindrome(this StringAssertions assertions) + { + char[] charArray = assertions.Subject.ToCharArray(); + Array.Reverse(charArray); + string reversedSubject = new string(charArray); + + assertions.Subject.Should().Be(reversedSubject); + } +} diff --git a/Tests/FluentAssertions.Specs/ExtensibilitySpecs.cs b/Tests/FluentAssertions.Specs/ExtensibilitySpecs.cs index e4ad77ede5..1064bd65dd 100644 --- a/Tests/FluentAssertions.Specs/ExtensibilitySpecs.cs +++ b/Tests/FluentAssertions.Specs/ExtensibilitySpecs.cs @@ -1,4 +1,5 @@ using System; +using ExampleExtensions; using Xunit; using Xunit.Sdk; @@ -7,7 +8,7 @@ namespace FluentAssertions.Specs; public class ExtensibilitySpecs { [Fact] - public void When_a_method_is_marked_as_custom_assertion_it_should_be_ignored_during_caller_identification() + public void Methods_marked_as_custom_assertion_are_ignored_during_caller_identification() { // Arrange var myClient = new MyCustomer @@ -22,14 +23,28 @@ public void When_a_method_is_marked_as_custom_assertion_it_should_be_ignored_dur act.Should().Throw().WithMessage( "Expected myClient to be true because we don't work with old clients, but found False."); } + + [Fact] + public void Methods_in_assemblies_marked_as_custom_assertion_are_ignored_during_caller_identification() + { + // Arrange + string palindrome = "fluent"; + + // Act + Action act = () => palindrome.Should().BePalindrome(); + + // Assert + act.Should().Throw().WithMessage( + "Expected palindrome to be*tneulf*"); + } } -public class MyCustomer +internal class MyCustomer { public bool Active { get; set; } } -public static class MyCustomerExtensions +internal static class MyCustomerExtensions { public static MyCustomerAssertions Should(this MyCustomer customer) { @@ -37,7 +52,7 @@ public static MyCustomerAssertions Should(this MyCustomer customer) } } -public class MyCustomerAssertions +internal class MyCustomerAssertions { private readonly MyCustomer customer; diff --git a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj index 48297cf9c3..e93bf2ad17 100644 --- a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj +++ b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj @@ -1,4 +1,4 @@ - + net47;net6.0 @@ -45,6 +45,7 @@ + diff --git a/docs/_pages/introduction.md b/docs/_pages/introduction.md index d2333c7156..8f98ede3f1 100644 --- a/docs/_pages/introduction.md +++ b/docs/_pages/introduction.md @@ -124,6 +124,8 @@ myClient.Should().BeActive("because we don't work with old clients"); Without the `[CustomAssertion]` attribute, Fluent Assertions would find the line that calls `Should().BeTrue()` and treat the `customer` variable as the subject-under-test (SUT). But by applying this attribute, it will ignore this invocation and instead find the SUT by looking for a call to `Should().BeActive()` and use the `myClient` variable instead. +Alternatively, you can add the `[assembly:CustomAssertionsAssembly]` attribute to a file within the project to tell Fluent Assertions that all code in that assembly should be treated as custom assertion code. + ## Assertion Scopes You can batch multiple assertions into an `AssertionScope` so that FluentAssertions throws one exception at the end of the scope with all failures. diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index b63bca7de2..1e3fbdbd06 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -13,6 +13,7 @@ sidebar: ### Improvements * Improve failure message for string assertions when checking for equality - [#2307](https://github.com/fluentassertions/fluentassertions/pull/2307) +* You can mark all assertions in an assembly as custom assertions using the `[CustomAssertionsAssembly]` attribute - [#2389](https://github.com/fluentassertions/fluentassertions/pull/2389) ### Fixes * Fixed formatting error when checking nullable `DateTimeOffset` with