From 36b612aeb260c284d92a596f5cf406e311e625ee Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sat, 1 Jan 2022 13:52:08 +0100 Subject: [PATCH 01/25] Update releases.md --- docs/_pages/releases.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index b7846665a0..efad6be024 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -9,6 +9,12 @@ sidebar: ## Unreleased +### What's New + +### Fixes + +## 6.3.0 + ### What's New * Adding `ThatAreAsync()` and `ThatAreNotAsync()` for filtering in method assertions - [#1725](https://github.com/fluentassertions/fluentassertions/pull/1725) * Adding `ThatAreVirtual()` and `ThatAreNotVirtual()` for filtering in method assertions - [#1744](https://github.com/fluentassertions/fluentassertions/pull/1744) From 50d1f00c6e76b1eee0b8aa044176861f9891fbe6 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sat, 1 Jan 2022 13:53:08 +0100 Subject: [PATCH 02/25] Update index.html --- docs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index c5349dcddc..5f556393ca 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ overlay_color: "#373737" overlay_filter: "0.7" overlay_image: "/assets/images/fluent_assertions_large_horizontal.svg" - cta_label: "v6 is out NOW!" + cta_label: "Happy 2022! And don't forget to grab 6.3..." cta_url: "https://www.continuousimprover.com/2021/08/fluent-assertions-v6" caption: "Logo by [**IUserName**](https://github.com/IUsername) and icons by [**Zlatko Najdenovski**](https://www.flaticon.com/authors/zlatko-najdenovski) from [Flaticon](https://www.flaticon.com/) " From 885f873973a638f4ce9a35a46f150175a64d015f Mon Sep 17 00:00:00 2001 From: chvollm <37902729+chvollm@users.noreply.github.com> Date: Thu, 6 Jan 2022 14:21:36 +0100 Subject: [PATCH 03/25] Adds ThatAreStatic and ThatAreNotStatic to method-level Reflection assertions (#1740) --- .../Types/MethodInfoSelector.cs | 20 ++++++++++- .../FluentAssertions/net47.verified.txt | 2 ++ .../netcoreapp2.1.verified.txt | 2 ++ .../netcoreapp3.0.verified.txt | 2 ++ .../netstandard2.0.verified.txt | 2 ++ .../netstandard2.1.verified.txt | 2 ++ .../Types/MethodInfoSelectorSpecs.cs | 35 +++++++++++++++++++ docs/_pages/releases.md | 3 +- 8 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions/Types/MethodInfoSelector.cs b/Src/FluentAssertions/Types/MethodInfoSelector.cs index e3ba8ca102..f800d0d656 100644 --- a/Src/FluentAssertions/Types/MethodInfoSelector.cs +++ b/Src/FluentAssertions/Types/MethodInfoSelector.cs @@ -35,7 +35,7 @@ public MethodInfoSelector(IEnumerable types) Guard.ThrowIfArgumentContainsNull(types, nameof(types)); selectedMethods = types.SelectMany(t => t - .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Where(method => !HasSpecialName(method))); } @@ -151,6 +151,24 @@ public MethodInfoSelector ThatAreNotAsync() return this; } + /// + /// Only return methods that are static. + /// + public MethodInfoSelector ThatAreStatic() + { + selectedMethods = selectedMethods.Where(method => method.IsStatic); + return this; + } + + /// + /// Only return methods that are not static. + /// + public MethodInfoSelector ThatAreNotStatic() + { + selectedMethods = selectedMethods.Where(method => !method.IsStatic); + return this; + } + /// /// Only return methods that are virtual. /// diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index ee73949aa0..0f7328406c 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -2363,7 +2363,9 @@ namespace FluentAssertions.Types where TAttribute : System.Attribute { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotDecoratedWithOrInherit() where TAttribute : System.Attribute { } + public FluentAssertions.Types.MethodInfoSelector ThatAreNotStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotVirtual() { } + public FluentAssertions.Types.MethodInfoSelector ThatAreStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreVirtual() { } public FluentAssertions.Types.MethodInfoSelector ThatDoNotReturn() { } public FluentAssertions.Types.MethodInfoSelector ThatReturn() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index d5b927c7d6..8427581cf0 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -2365,7 +2365,9 @@ namespace FluentAssertions.Types where TAttribute : System.Attribute { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotDecoratedWithOrInherit() where TAttribute : System.Attribute { } + public FluentAssertions.Types.MethodInfoSelector ThatAreNotStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotVirtual() { } + public FluentAssertions.Types.MethodInfoSelector ThatAreStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreVirtual() { } public FluentAssertions.Types.MethodInfoSelector ThatDoNotReturn() { } public FluentAssertions.Types.MethodInfoSelector ThatReturn() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 62d7b60d9a..d0c715644a 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -2365,7 +2365,9 @@ namespace FluentAssertions.Types where TAttribute : System.Attribute { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotDecoratedWithOrInherit() where TAttribute : System.Attribute { } + public FluentAssertions.Types.MethodInfoSelector ThatAreNotStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotVirtual() { } + public FluentAssertions.Types.MethodInfoSelector ThatAreStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreVirtual() { } public FluentAssertions.Types.MethodInfoSelector ThatDoNotReturn() { } public FluentAssertions.Types.MethodInfoSelector ThatReturn() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 45c197d938..9b13b251b7 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -2316,7 +2316,9 @@ namespace FluentAssertions.Types where TAttribute : System.Attribute { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotDecoratedWithOrInherit() where TAttribute : System.Attribute { } + public FluentAssertions.Types.MethodInfoSelector ThatAreNotStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotVirtual() { } + public FluentAssertions.Types.MethodInfoSelector ThatAreStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreVirtual() { } public FluentAssertions.Types.MethodInfoSelector ThatDoNotReturn() { } public FluentAssertions.Types.MethodInfoSelector ThatReturn() { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 49ebf2d8c8..ea94174c5a 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -2365,7 +2365,9 @@ namespace FluentAssertions.Types where TAttribute : System.Attribute { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotDecoratedWithOrInherit() where TAttribute : System.Attribute { } + public FluentAssertions.Types.MethodInfoSelector ThatAreNotStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreNotVirtual() { } + public FluentAssertions.Types.MethodInfoSelector ThatAreStatic() { } public FluentAssertions.Types.MethodInfoSelector ThatAreVirtual() { } public FluentAssertions.Types.MethodInfoSelector ThatDoNotReturn() { } public FluentAssertions.Types.MethodInfoSelector ThatReturn() { } diff --git a/Tests/FluentAssertions.Specs/Types/MethodInfoSelectorSpecs.cs b/Tests/FluentAssertions.Specs/Types/MethodInfoSelectorSpecs.cs index f32f2c9911..e56e41f242 100644 --- a/Tests/FluentAssertions.Specs/Types/MethodInfoSelectorSpecs.cs +++ b/Tests/FluentAssertions.Specs/Types/MethodInfoSelectorSpecs.cs @@ -332,6 +332,34 @@ public void When_selecting_methods_that_are_not_virtual_it_should_only_return_th .And.NotContain(m => m.Name == "PublicVirtualVoidMethodWithAttribute") .And.NotContain(m => m.Name == "ProtectedVirtualVoidMethodWithAttribute"); } + + [Fact] + public void When_selecting_methods_that_are_static_it_should_only_return_the_applicable_methods() + { + // Arrange + Type type = typeof(TestClassForMethodSelectorWithStaticAndNonStaticMethod); + + // Act + MethodInfo[] methods = type.Methods().ThatAreStatic().ToArray(); + + // Assert + methods.Should().ContainSingle() + .Which.Name.Should().Be("PublicStaticMethod"); + } + + [Fact] + public void When_selecting_methods_that_are_not_static_it_should_only_return_the_applicable_methods() + { + // Arrange + Type type = typeof(TestClassForMethodSelectorWithStaticAndNonStaticMethod); + + // Act + MethodInfo[] methods = type.Methods().ThatAreNotStatic().ToArray(); + + // Assert + methods.Should().ContainSingle() + .Which.Name.Should().Be("PublicNonStaticMethod"); + } [Fact] public void When_selecting_methods_not_decorated_with_or_inheriting_a_noninheritable_attribute_it_should_only_return_the_applicable_methods() @@ -435,6 +463,13 @@ internal class TestClassForMethodSelectorWithAsyncAndNonAsyncMethod public Task PublicNonAsyncMethod() => Task.CompletedTask; } + internal class TestClassForMethodSelectorWithStaticAndNonStaticMethod + { + public static void PublicStaticMethod() { } + + public void PublicNonStaticMethod() { } + } + internal class TestClassForMethodReturnTypesSelector { public void SomeMethod() { } diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index efad6be024..be1359083f 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -18,10 +18,11 @@ sidebar: ### What's New * Adding `ThatAreAsync()` and `ThatAreNotAsync()` for filtering in method assertions - [#1725](https://github.com/fluentassertions/fluentassertions/pull/1725) * Adding `ThatAreVirtual()` and `ThatAreNotVirtual()` for filtering in method assertions - [#1744](https://github.com/fluentassertions/fluentassertions/pull/1744) +* Adding `ThatAreStatic()` and `ThatAreNotStatic()` for filtering in method assertions - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Adding collection content to assertion messages for `HaveCountGreaterThan()`, `HaveCountGreaterThanOrEqualTo()`, `HaveCountLessThan()` and `HaveCountLessThanOrEqualTo()` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) - ### Fixes * Prevent multiple enumeration of `IEnumerable`s in parameter-less `ContainSingle()` - [#1753](https://github.com/fluentassertions/fluentassertions/pull/1753) +* Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Change `HaveCount()` assertion message order to state expected and actual collection count before dumping its content` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) * `CompleteWithinAsync` did not take initial sync computation into account when measuring execution time - [1762](https://github.com/fluentassertions/fluentassertions/pull/1762). From 648428eb66e92eb767dff27f6002ca0950c5c600 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Fri, 7 Jan 2022 11:43:35 +0100 Subject: [PATCH 04/25] Fixup of the release notes --- docs/_pages/releases.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index be1359083f..eb5b94e687 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -10,19 +10,19 @@ sidebar: ## Unreleased ### What's New +* Adding `ThatAreStatic()` and `ThatAreNotStatic()` for filtering in method assertions - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) ### Fixes +* Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) ## 6.3.0 ### What's New * Adding `ThatAreAsync()` and `ThatAreNotAsync()` for filtering in method assertions - [#1725](https://github.com/fluentassertions/fluentassertions/pull/1725) * Adding `ThatAreVirtual()` and `ThatAreNotVirtual()` for filtering in method assertions - [#1744](https://github.com/fluentassertions/fluentassertions/pull/1744) -* Adding `ThatAreStatic()` and `ThatAreNotStatic()` for filtering in method assertions - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Adding collection content to assertion messages for `HaveCountGreaterThan()`, `HaveCountGreaterThanOrEqualTo()`, `HaveCountLessThan()` and `HaveCountLessThanOrEqualTo()` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) ### Fixes * Prevent multiple enumeration of `IEnumerable`s in parameter-less `ContainSingle()` - [#1753](https://github.com/fluentassertions/fluentassertions/pull/1753) -* Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Change `HaveCount()` assertion message order to state expected and actual collection count before dumping its content` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) * `CompleteWithinAsync` did not take initial sync computation into account when measuring execution time - [1762](https://github.com/fluentassertions/fluentassertions/pull/1762). From 208638d4d56fe4127e10fc035fe2defb7bbed956 Mon Sep 17 00:00:00 2001 From: MEmanuelsson Date: Fri, 7 Jan 2022 16:51:53 +0100 Subject: [PATCH 05/25] Trailing slash results in 404 page (#1767) Noticed that when going to https://fluentassertions.com/introduction/ I get a 404-page in response. If I remove the trailing slash it works. I guess the best solution is to make it work with and without trailing slash but this is at least one way of making the Issue template not pointing to a 404-page. --- ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 6c9fc5255c..b4449db0ce 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ * Tried upgrading to newest version of Fluent Assertions, to see if your issue has already been resolved and released? * Checked existing open *and* closed [issues](https://github.com/fluentassertions/fluentassertions/issues?utf8=%E2%9C%93&q=is%3Aissue), to see if the issue has already been reported? * Tried reproducing your problem in a new isolated project? -* Read the [documentation](https://fluentassertions.com/introduction/)? +* Read the [documentation](https://fluentassertions.com/introduction)? * Considered if this is a general question and not a bug?. For general questions please use [StackOverflow](https://stackoverflow.com/questions/tagged/fluent-assertions?mixed=1). ### Description @@ -43,4 +43,4 @@ result.Should().Be('M'); ### Additional Information -Any additional information, configuration or data that might be necessary to reproduce the issue. \ No newline at end of file +Any additional information, configuration or data that might be necessary to reproduce the issue. From c7fe3e6eb438d9cecba5d24e270d4f31a61f2faf Mon Sep 17 00:00:00 2001 From: mu88 <4560672+mu88@users.noreply.github.com> Date: Mon, 10 Jan 2022 10:20:03 +0100 Subject: [PATCH 06/25] Introduce assertions for StatusCode of HttpResponseMessage (#1737) --- Src/FluentAssertions/AssertionExtensions.cs | 11 + Src/FluentAssertions/FluentAssertions.csproj | 1 + .../HttpResponseMessageAssertions.cs | 234 +++++++++++++++ .../FluentAssertions/net47.verified.txt | 17 ++ .../netcoreapp2.1.verified.txt | 17 ++ .../netcoreapp3.0.verified.txt | 17 ++ .../netstandard2.0.verified.txt | 17 ++ .../netstandard2.1.verified.txt | 17 ++ Tests/Benchmarks/Benchmarks.csproj | 2 +- .../HttpResponseMessageAssertionSpecs.cs | 271 ++++++++++++++++++ docs/_data/navigation.yml | 2 + docs/_pages/httpresponsemessages.md | 28 ++ docs/_pages/releases.md | 1 + 13 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs create mode 100644 Tests/FluentAssertions.Specs/Primitives/HttpResponseMessageAssertionSpecs.cs create mode 100644 docs/_pages/httpresponsemessages.md diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs index 9eb3e85dfc..4940cffe48 100644 --- a/Src/FluentAssertions/AssertionExtensions.cs +++ b/Src/FluentAssertions/AssertionExtensions.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Linq.Expressions; +using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using System.Xml.Linq; @@ -302,6 +303,16 @@ public static NullableBooleanAssertions Should(this bool? actualValue) return new NullableBooleanAssertions(actualValue); } + /// + /// Returns an object that can be used to assert the + /// current . + /// + [Pure] + public static HttpResponseMessageAssertions Should(this HttpResponseMessage actualValue) + { + return new HttpResponseMessageAssertions(actualValue); + } + /// /// Returns an object that can be used to assert the /// current . diff --git a/Src/FluentAssertions/FluentAssertions.csproj b/Src/FluentAssertions/FluentAssertions.csproj index 2204810ff1..330b2da149 100644 --- a/Src/FluentAssertions/FluentAssertions.csproj +++ b/Src/FluentAssertions/FluentAssertions.csproj @@ -75,6 +75,7 @@ + diff --git a/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs b/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs new file mode 100644 index 0000000000..45010cb0ce --- /dev/null +++ b/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs @@ -0,0 +1,234 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using FluentAssertions.Execution; + +namespace FluentAssertions.Primitives +{ + /// + /// Contains a number of methods to assert that a is in the expected state. + /// + [DebuggerNonUserCode] + public class HttpResponseMessageAssertions : HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(HttpResponseMessage value) + : base(value) + { + } + } + + /// + /// Contains a number of methods to assert that a is in the expected state. + /// + [DebuggerNonUserCode] + public class HttpResponseMessageAssertions : ObjectAssertions + where TAssertions : HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(HttpResponseMessage value) + : base(value) + { + } + + /// + /// Asserts that the is successful (2xx). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be successful (2xx){reason}, but HttpResponseMessage was ."); + + if (success) + { + Execute.Assertion + .ForCondition(Subject!.IsSuccessStatusCode) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be successful (2xx){reason}, but found {0}.", Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is redirection (3xx). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeRedirection(string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be redirection (3xx){reason}, but HttpResponseMessage was ."); + + if (success) + { + Execute.Assertion + .ForCondition((int)Subject!.StatusCode is >= 300 and <= 399) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be redirection (3xx){reason}, but found {0}.", Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is either client (4xx) or server error (5xx). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveError(string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be an error{reason}, but HttpResponseMessage was ."); + + if (success) + { + Execute.Assertion + .ForCondition(IsClientError() || IsServerError()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be an error{reason}, but found {0}.", Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is client error (4xx). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveClientError(string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be client error (4xx){reason}, but HttpResponseMessage was ."); + + if (success) + { + Execute.Assertion + .ForCondition(IsClientError()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be client error (4xx){reason}, but found {0}.", Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is server error (5xx). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveServerError(string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be server error (5xx){reason}, but HttpResponseMessage was ."); + + if (success) + { + Execute.Assertion + .ForCondition(IsServerError()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be server error (5xx){reason}, but found {0}.", Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is equal to the specified value. + /// + /// The expected value + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be {0}{reason}, but HttpResponseMessage was .", expected); + + if (success) + { + Execute.Assertion + .ForCondition(Subject!.StatusCode == expected) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the is not equal to the specified value. + /// + /// The unexpected value + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotHaveStatusCode(HttpStatusCode unexpected, string because = "", params object[] becauseArgs) + { + var success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode not to be {0}{reason}, but HttpResponseMessage was .", unexpected); + + if (success) + { + Execute.Assertion + .ForCondition(Subject!.StatusCode != unexpected) + .BecauseOf(because, becauseArgs) + .FailWith("Expected HttpStatusCode not to be {0}{reason}, but found {1}.", unexpected, Subject.StatusCode); + } + + return new AndConstraint((TAssertions)this); + } + + private bool IsServerError() => (int)Subject.StatusCode is >= 500 and <= 599; + + private bool IsClientError() => (int)Subject.StatusCode is >= 400 and <= 499; + } +} diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 0f7328406c..8f749545dd 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -66,6 +66,7 @@ namespace FluentAssertions public static FluentAssertions.Primitives.NullableGuidAssertions Should(this System.Guid? actualValue) { } public static FluentAssertions.Streams.BufferedStreamAssertions Should(this System.IO.BufferedStream actualValue) { } public static FluentAssertions.Streams.StreamAssertions Should(this System.IO.Stream actualValue) { } + public static FluentAssertions.Primitives.HttpResponseMessageAssertions Should(this System.Net.Http.HttpResponseMessage actualValue) { } public static FluentAssertions.Reflection.AssemblyAssertions Should(this System.Reflection.Assembly assembly) { } public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { } public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { } @@ -1909,6 +1910,22 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(string unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) { } } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.ObjectAssertions + where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveServerError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveStatusCode(System.Net.HttpStatusCode expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveStatusCode(System.Net.HttpStatusCode unexpected, string because = "", params object[] becauseArgs) { } + } public class NullableBooleanAssertions : FluentAssertions.Primitives.NullableBooleanAssertions { public NullableBooleanAssertions(bool? value) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 8427581cf0..74366f32aa 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -66,6 +66,7 @@ namespace FluentAssertions public static FluentAssertions.Primitives.NullableGuidAssertions Should(this System.Guid? actualValue) { } public static FluentAssertions.Streams.BufferedStreamAssertions Should(this System.IO.BufferedStream actualValue) { } public static FluentAssertions.Streams.StreamAssertions Should(this System.IO.Stream actualValue) { } + public static FluentAssertions.Primitives.HttpResponseMessageAssertions Should(this System.Net.Http.HttpResponseMessage actualValue) { } public static FluentAssertions.Reflection.AssemblyAssertions Should(this System.Reflection.Assembly assembly) { } public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { } public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { } @@ -1909,6 +1910,22 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(string unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) { } } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.ObjectAssertions + where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveServerError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveStatusCode(System.Net.HttpStatusCode expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveStatusCode(System.Net.HttpStatusCode unexpected, string because = "", params object[] becauseArgs) { } + } public class NullableBooleanAssertions : FluentAssertions.Primitives.NullableBooleanAssertions { public NullableBooleanAssertions(bool? value) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index d0c715644a..742058b3f4 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -66,6 +66,7 @@ namespace FluentAssertions public static FluentAssertions.Primitives.NullableGuidAssertions Should(this System.Guid? actualValue) { } public static FluentAssertions.Streams.BufferedStreamAssertions Should(this System.IO.BufferedStream actualValue) { } public static FluentAssertions.Streams.StreamAssertions Should(this System.IO.Stream actualValue) { } + public static FluentAssertions.Primitives.HttpResponseMessageAssertions Should(this System.Net.Http.HttpResponseMessage actualValue) { } public static FluentAssertions.Reflection.AssemblyAssertions Should(this System.Reflection.Assembly assembly) { } public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { } public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { } @@ -1909,6 +1910,22 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(string unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) { } } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.ObjectAssertions + where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveServerError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveStatusCode(System.Net.HttpStatusCode expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveStatusCode(System.Net.HttpStatusCode unexpected, string because = "", params object[] becauseArgs) { } + } public class NullableBooleanAssertions : FluentAssertions.Primitives.NullableBooleanAssertions { public NullableBooleanAssertions(bool? value) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 9b13b251b7..9a7e925e43 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -65,6 +65,7 @@ namespace FluentAssertions public static FluentAssertions.Primitives.NullableGuidAssertions Should(this System.Guid? actualValue) { } public static FluentAssertions.Streams.BufferedStreamAssertions Should(this System.IO.BufferedStream actualValue) { } public static FluentAssertions.Streams.StreamAssertions Should(this System.IO.Stream actualValue) { } + public static FluentAssertions.Primitives.HttpResponseMessageAssertions Should(this System.Net.Http.HttpResponseMessage actualValue) { } public static FluentAssertions.Reflection.AssemblyAssertions Should(this System.Reflection.Assembly assembly) { } public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { } public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { } @@ -1862,6 +1863,22 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(string unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) { } } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.ObjectAssertions + where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveServerError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveStatusCode(System.Net.HttpStatusCode expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveStatusCode(System.Net.HttpStatusCode unexpected, string because = "", params object[] becauseArgs) { } + } public class NullableBooleanAssertions : FluentAssertions.Primitives.NullableBooleanAssertions { public NullableBooleanAssertions(bool? value) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index ea94174c5a..27d7e7a226 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -66,6 +66,7 @@ namespace FluentAssertions public static FluentAssertions.Primitives.NullableGuidAssertions Should(this System.Guid? actualValue) { } public static FluentAssertions.Streams.BufferedStreamAssertions Should(this System.IO.BufferedStream actualValue) { } public static FluentAssertions.Streams.StreamAssertions Should(this System.IO.Stream actualValue) { } + public static FluentAssertions.Primitives.HttpResponseMessageAssertions Should(this System.Net.Http.HttpResponseMessage actualValue) { } public static FluentAssertions.Reflection.AssemblyAssertions Should(this System.Reflection.Assembly assembly) { } public static FluentAssertions.Types.ConstructorInfoAssertions Should(this System.Reflection.ConstructorInfo constructorInfo) { } public static FluentAssertions.Types.MethodInfoAssertions Should(this System.Reflection.MethodInfo methodInfo) { } @@ -1909,6 +1910,22 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(string unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEmpty(string because = "", params object[] becauseArgs) { } } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + public HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + } + public class HttpResponseMessageAssertions : FluentAssertions.Primitives.ObjectAssertions + where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions + { + protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveServerError(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint HaveStatusCode(System.Net.HttpStatusCode expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotHaveStatusCode(System.Net.HttpStatusCode unexpected, string because = "", params object[] becauseArgs) { } + } public class NullableBooleanAssertions : FluentAssertions.Primitives.NullableBooleanAssertions { public NullableBooleanAssertions(bool? value) { } diff --git a/Tests/Benchmarks/Benchmarks.csproj b/Tests/Benchmarks/Benchmarks.csproj index 51b8040720..3e61198905 100644 --- a/Tests/Benchmarks/Benchmarks.csproj +++ b/Tests/Benchmarks/Benchmarks.csproj @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/Tests/FluentAssertions.Specs/Primitives/HttpResponseMessageAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/HttpResponseMessageAssertionSpecs.cs new file mode 100644 index 0000000000..d0513cacc7 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Primitives/HttpResponseMessageAssertionSpecs.cs @@ -0,0 +1,271 @@ +using System; +using System.Net; +using System.Net.Http; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs.Primitives +{ + public class HttpResponseMessageAssertionSpecs + { + [Theory] + [InlineData(HttpStatusCode.OK)] + [InlineData(HttpStatusCode.Accepted)] + public void Should_succeed_when_status_code_is_successful(HttpStatusCode statusCodeOfResponse) + { + // Arrange + var testee = new HttpResponseMessage(statusCodeOfResponse); + + // Act / Assert + testee.Should().BeSuccessful(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_error_is_successful() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.Gone).Should() + .BeSuccessful("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be successful (2xx) because we want to test the failure message, but found HttpStatusCode.Gone {value: 410}."); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_success_but_response_is_null() + { + // Arrange + Action action = () => + ((HttpResponseMessage)null).Should().BeSuccessful("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be successful (2xx) because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Theory] + [InlineData(HttpStatusCode.Moved)] + public void Should_succeed_when_status_code_is_redirect(HttpStatusCode statusCodeOfResponse) + { + // Arrange + var testee = new HttpResponseMessage(statusCodeOfResponse); + + // Act / Assert + testee.Should().BeRedirection(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_error_is_redirection() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.Gone).Should() + .BeRedirection("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be redirection (3xx) because we want to test the failure message, but found HttpStatusCode.Gone {value: 410}."); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_redirect_but_response_is_null() + { + // Arrange + Action action = () => + ((HttpResponseMessage)null).Should().BeRedirection("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be redirection (3xx) because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Theory] + [InlineData(HttpStatusCode.Gone)] + [InlineData(HttpStatusCode.BadRequest)] + public void Should_succeed_when_status_code_is_client_error(HttpStatusCode statusCodeOfResponse) + { + // Arrange + var testee = new HttpResponseMessage(statusCodeOfResponse); + + // Act / Assert + testee.Should().HaveClientError(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_success_is_client_error() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should() + .HaveClientError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be client error (4xx) because we want to test the failure message, but found HttpStatusCode.OK {value: 200}."); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_client_error_but_response_is_null() + { + // Arrange + Action action = () => + ((HttpResponseMessage)null).Should().HaveClientError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be client error (4xx) because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + public void Should_succeed_when_status_code_is_server_error(HttpStatusCode statusCodeOfResponse) + { + // Arrange + var testee = new HttpResponseMessage(statusCodeOfResponse); + + // Act / Assert + testee.Should().HaveServerError(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_success_is_server_error() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should() + .HaveServerError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be server error (5xx) because we want to test the failure message, but found HttpStatusCode.OK {value: 200}."); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_server_error_but_response_is_null() + { + // Arrange + Action action = () => + ((HttpResponseMessage)null).Should().HaveServerError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be server error (5xx) because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Theory] + [InlineData(HttpStatusCode.BadRequest)] + [InlineData(HttpStatusCode.InternalServerError)] + public void Should_succeed_when_status_code_is_error(HttpStatusCode statusCodeOfResponse) + { + // Arrange + var testee = new HttpResponseMessage(statusCodeOfResponse); + + // Act / Assert + testee.Should().HaveError(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_success_is_error() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should() + .HaveError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be an error because we want to test the failure message, but found HttpStatusCode.OK {value: 200}."); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_error_but_response_is_null() + { + // Arrange + Action action = () => + ((HttpResponseMessage)null).Should().HaveError("because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be an error because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Fact] + public void Should_succeed_when_status_code_to_be_equal_to_the_same_value() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should().HaveStatusCode(HttpStatusCode.OK); + + // Act / Assert + action.Should().NotThrow(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_value_to_be_equal_to_a_different_value() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should().HaveStatusCode(HttpStatusCode.Gone, + "because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be HttpStatusCode.Gone {value: 410} because we want to test the failure message, but found HttpStatusCode.OK {value: 200}.*"); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_certain_status_code_but_response_is_null() + { + // Arrange + Action action = () => ((HttpResponseMessage)null).Should() + .HaveStatusCode(HttpStatusCode.Gone, "because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode to be HttpStatusCode.Gone {value: 410} because we want to test the failure message, but HttpResponseMessage was ."); + } + + [Fact] + public void Should_succeed_when_status_code_value_not_to_be_equal_to_the_same_value() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should().NotHaveStatusCode(HttpStatusCode.Gone); + + // Act / Assert + action.Should().NotThrow(); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_status_code_value_not_to_be_equal_to_a_different_value() + { + // Arrange + Action action = () => new HttpResponseMessage(HttpStatusCode.OK).Should().NotHaveStatusCode(HttpStatusCode.OK, + "because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode not to be HttpStatusCode.OK {value: 200} because we want to test the failure message, but found HttpStatusCode.OK {value: 200}.*"); + } + + [Fact] + public void Should_fail_with_descriptive_message_when_asserting_against_certain_status_code_but_response_is_null() + { + // Arrange + Action action = () => ((HttpResponseMessage)null).Should() + .NotHaveStatusCode(HttpStatusCode.Gone, "because we want to test the failure {0}", "message"); + + // Act / Assert + action.Should().Throw() + .WithMessage( + "Expected HttpStatusCode not to be HttpStatusCode.Gone {value: 410} because we want to test the failure message, but HttpResponseMessage was ."); + } + } +} diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index 51c26a1224..0ec09d306d 100644 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -73,6 +73,8 @@ sidebar: url: /xml - title: Execution Time url: /executiontime + - title: HTTP response messages + url: /httpresponsemessages - title: Extensibility url: /extensibility diff --git a/docs/_pages/httpresponsemessages.md b/docs/_pages/httpresponsemessages.md new file mode 100644 index 0000000000..96575c1087 --- /dev/null +++ b/docs/_pages/httpresponsemessages.md @@ -0,0 +1,28 @@ +--- +title: HttpResponseMessages +permalink: /httpresponsemessages/ +layout: single +classes: wide +sidebar: +nav: "sidebar" +--- + +```csharp +var successfulResponse = new HttpResponseMessage(HttpStatusCode.OK); +successfulResponse.Should().BeSuccessful("it's set to OK"); // (HttpStatusCode = 2xx) + +var redirectResponse = new HttpResponseMessage(HttpStatusCode.Moved); +redirectResponse.Should().BeRedirection("it's set to Moved"); // (HttpStatusCode = 3xx) + +var clientErrorResponse = new HttpResponseMessage(HttpStatusCode.BadRequest); +clientErrorResponse.Should().HaveClientError("it's set to BadRequest"); // (HttpStatusCode = 4xx) +clientErrorResponse.Should().HaveError("it's set to BadRequest"); // (HttpStatusCode = 4xx or 5xx) + +var serverErrorResponse = new HttpResponseMessage(HttpStatusCode.InternalServerError); +serverErrorResponse.Should().HaveServerError("it's set to InternalServerError"); // (HttpStatusCode = 5xx) +serverErrorResponse.Should().HaveError("it's set to InternalServerError"); // (HttpStatusCode = 4xx or 5xx) + +var anotherResponse = new HttpResponseMessage(HttpStatusCode.Moved); +anotherResponse.Should().HaveStatusCode(HttpStatusCode.Moved); +anotherResponse.Should().NotHaveStatusCode(HttpStatusCode.OK); +``` diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index eb5b94e687..094c140bc6 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -11,6 +11,7 @@ sidebar: ### What's New * Adding `ThatAreStatic()` and `ThatAreNotStatic()` for filtering in method assertions - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) +* Adding new assertions for the `HttpStatusCode` of an `HttpResponseMessage` - [#1737](https://github.com/fluentassertions/fluentassertions/pull/1737) ### Fixes * Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) From 305b36ade4787fbf3afd100e810d57443a912c41 Mon Sep 17 00:00:00 2001 From: Leandro Muller Date: Mon, 10 Jan 2022 14:51:33 -0300 Subject: [PATCH 07/25] `ContainItemsAssignableTo` Now Expects At Least One Item Assignable to `T` (#1765) --- .../GenericCollectionAssertions.cs | 33 ++++------- Src/FluentAssertions/Common/Configuration.cs | 2 +- .../Execution/GivenSelector.cs | 2 +- ...AssertionSpecs.ContainItemsAssignableTo.cs | 58 +++++++------------ docs/_pages/releases.md | 1 + 5 files changed, 34 insertions(+), 62 deletions(-) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index ff75c7090d..54a10c802c 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -946,7 +946,7 @@ public AndConstraint ContainInOrder(IEnumerable expected, string } /// - /// Asserts that the current collection only contains items that are assignable to the type . + /// Asserts that the current collection contains at least one element that is assignable to the type . /// /// /// A formatted phrase as is supported by explaining why the assertion @@ -957,29 +957,18 @@ public AndConstraint ContainInOrder(IEnumerable expected, string /// public AndConstraint ContainItemsAssignableTo(string because = "", params object[] becauseArgs) { - bool success = Execute.Assertion + Execute.Assertion .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:collection} to contain at least one element assignable to type {0}{reason}, ", + typeof(TExpectation).FullName) .ForCondition(Subject is not null) - .FailWith("Expected {context:collection} to contain element assignable to type {0}{reason}, but found .", - typeof(TExpectation)); - - if (success) - { - int index = 0; - foreach (T item in Subject) - { - if (item is not TExpectation) - { - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:collection} to contain only items of type {0}{reason}" + - ", but item {1} at index {2} is of type {3}.", typeof(TExpectation), item, index, item.GetType()); - } - - ++index; - } - } + .FailWith("but found .") + .Then + .Given(() => Subject.ConvertOrCastToCollection()) + .ForCondition(subject => subject.Any(x => typeof(TExpectation).IsAssignableFrom(GetType(x)))) + .FailWith("but found {0}.", subject => subject.Select(x => GetType(x))) + .Then + .ClearExpectation(); return new AndConstraint((TAssertions)this); } diff --git a/Src/FluentAssertions/Common/Configuration.cs b/Src/FluentAssertions/Common/Configuration.cs index a59e40be46..d46724dd7e 100644 --- a/Src/FluentAssertions/Common/Configuration.cs +++ b/Src/FluentAssertions/Common/Configuration.cs @@ -76,7 +76,7 @@ private ValueFormatterDetectionMode DetermineFormatterDetectionMode() /// /// Gets or sets the assembly name to scan for custom value formatters in case - /// is set to . + /// is set to . /// public string ValueFormatterAssembly { diff --git a/Src/FluentAssertions/Execution/GivenSelector.cs b/Src/FluentAssertions/Execution/GivenSelector.cs index 395e2aeb12..58d46f2647 100644 --- a/Src/FluentAssertions/Execution/GivenSelector.cs +++ b/Src/FluentAssertions/Execution/GivenSelector.cs @@ -49,7 +49,7 @@ public GivenSelector ForCondition(Func predicate) /// The will not be invoked if the prior assertion failed, /// nor will throw any exceptions. /// - /// + /// public GivenSelector Given(Func selector) { Guard.ThrowIfArgumentIsNull(selector, nameof(selector)); diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainItemsAssignableTo.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainItemsAssignableTo.cs index 524608e968..43f3518c2a 100644 --- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainItemsAssignableTo.cs +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.ContainItemsAssignableTo.cs @@ -24,7 +24,7 @@ public void Should_succeed_when_asserting_collection_with_all_items_of_same_type } [Fact] - public void Should_fail_when_asserting_collection_with_items_of_different_types_only_contains_item_of_one_type() + public void Should_succeed_when_asserting_collection_with_items_of_different_types_contains_item_of_expected_type() { // Arrange var collection = new List @@ -33,68 +33,50 @@ public void Should_fail_when_asserting_collection_with_items_of_different_types_ "2" }; - // Act - Action act = () => collection.Should().ContainItemsAssignableTo(); - - // Assert - act.Should().Throw(); + // Act / Assert + collection.Should().ContainItemsAssignableTo(); } [Fact] - public void When_a_collection_contains_anything_other_than_strings_it_should_throw_and_report_details() + public void When_asserting_collection_contains_item_assignable_to_against_null_collection_it_should_throw() { // Arrange - var collection = new List - { - 1, - "2" - }; + int[] collection = null; // Act - Action act = () => collection.Should().ContainItemsAssignableTo(); + Action act = () => + { + using var _ = new AssertionScope(); + collection.Should().ContainItemsAssignableTo("because we want to test the behaviour with a null subject"); + }; // Assert act.Should().Throw().WithMessage( - "Expected collection to contain only items of type System.String, but item 1 at index 0 is of type System.Int32."); + "Expected collection to contain at least one element assignable to type \"System.String\" because we want to test the behaviour with a null subject, but found ."); } [Fact] - public void When_a_collection_contains_anything_other_than_strings_it_should_use_the_reason() + public void When_a_collection_is_empty_an_exception_should_be_thrown() { // Arrange - var collection = new List - { - 1, - "2" - }; + int[] collection = Array.Empty(); // Act - Action act = () => collection.Should().ContainItemsAssignableTo( - "because we want to test the failure {0}", "message"); + Action act = () => collection.Should().ContainItemsAssignableTo(); // Assert - act.Should().Throw() - .WithMessage( - "Expected collection to contain only items of type System.String because we want to test the failure message" + - ", but item 1 at index 0 is of type System.Int32."); + act.Should().Throw().WithMessage("Expected collection to contain at least one element assignable to type \"System.Int32\", but found {empty}."); } [Fact] - public void When_asserting_collection_contains_item_assignable_to_against_null_collection_it_should_throw() + public void Should_throw_exception_when_asserting_collection_for_missing_item_type() { - // Arrange - int[] collection = null; + var collection = new object[] { "1", 1.0m }; - // Act - Action act = () => - { - using var _ = new AssertionScope(); - collection.Should().ContainItemsAssignableTo("because we want to test the behaviour with a null subject"); - }; + Action act = () => collection.Should().ContainItemsAssignableTo(); - // Assert - act.Should().Throw().WithMessage( - "Expected collection to contain element assignable to type System.String because we want to test the behaviour with a null subject, but found ."); + act.Should().Throw() + .WithMessage("Expected collection to contain at least one element assignable to type \"System.Int32\", but found {System.String, System.Decimal}."); } #endregion diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 094c140bc6..41a15a2547 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -14,6 +14,7 @@ sidebar: * Adding new assertions for the `HttpStatusCode` of an `HttpResponseMessage` - [#1737](https://github.com/fluentassertions/fluentassertions/pull/1737) ### Fixes +* `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765) * Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) ## 6.3.0 From 96a8fc728c8e263847d91569caee8605f0d3a05d Mon Sep 17 00:00:00 2001 From: Leandro Muller Date: Tue, 11 Jan 2022 03:52:54 -0300 Subject: [PATCH 08/25] Variable name is not captured after await assertion (#1770) --- Src/FluentAssertions/CallerIdentifier.cs | 10 ++++------ .../Execution/CallerIdentifierSpecs.cs | 16 ++++++++++++++++ docs/_pages/releases.md | 1 + 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Src/FluentAssertions/CallerIdentifier.cs b/Src/FluentAssertions/CallerIdentifier.cs index cbf3f2fa88..5dc0956a8b 100644 --- a/Src/FluentAssertions/CallerIdentifier.cs +++ b/Src/FluentAssertions/CallerIdentifier.cs @@ -40,13 +40,11 @@ public static string DetermineCallerIdentity() frame => !IsCurrentAssembly(frame)); } - int firstFluentAssertionsCodeIndex = Array.FindLastIndex( + int lastUserStackFrameBeforeFluentAssertionsCodeIndex = Array.FindIndex( allStackFrames, - searchStart, - frame => IsCurrentAssembly(frame)); - - int lastUserStackFrameBeforeFluentAssertionsCodeIndex = - firstFluentAssertionsCodeIndex + 1; + startIndex: 0, + count: searchStart + 1, + frame => !IsCurrentAssembly(frame) && !IsDotNet(frame)); for (int i = lastUserStackFrameBeforeFluentAssertionsCodeIndex; i < allStackFrames.Length; i++) { diff --git a/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs b/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs index 96098ebde3..a539f4f942 100644 --- a/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs +++ b/Tests/FluentAssertions.Specs/Execution/CallerIdentifierSpecs.cs @@ -452,6 +452,22 @@ public void When_the_method_has_Should_prefix_it_should_read_whole_method() act.Should().Throw() .WithMessage("Expected foo.ShouldReturnSomeBool() to be false*"); } + + [UIFact] + public async Task Caller_identification_should_also_work_for_statements_following_async_code() + { + // Arrange + const string someText = "Hello"; + Func task = async () => await Task.Yield(); + + // Act + await task.Should().NotThrowAsync(); + Action act = () => someText.Should().Be("Hi"); + + // Assert + act.Should().Throw() + .WithMessage("*someText*", "it should capture the variable name"); + } } [SuppressMessage("The name of a C# element does not begin with an upper-case letter", "SA1300")] diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 41a15a2547..833e2674ee 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -16,6 +16,7 @@ sidebar: ### Fixes * `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765) * Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) +* Variable name is not captured after await assertion - [#1770](https://github.com/fluentassertions/fluentassertions/pull/1770) ## 6.3.0 From 7b09e10c6e821d453e16f7bf817993e6e2a90a67 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Wed, 12 Jan 2022 09:06:35 +0100 Subject: [PATCH 09/25] Fix typos --- .../Data/DataEquivalencyAssertionOptions.cs | 2 +- .../Selection/SelectMemberByPathSelectionRule.cs | 2 +- Src/FluentAssertions/Execution/IAssertionScope.cs | 2 +- .../FluentAssertions.Specs/Execution/GivenSelectorSpecs.cs | 4 ++-- .../Specialized/AssemblyAssertionSpecs.cs | 6 +++--- docs/_pages/releases.md | 1 + 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Src/FluentAssertions/Data/DataEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Data/DataEquivalencyAssertionOptions.cs index e73cebbba3..e69b9c6597 100644 --- a/Src/FluentAssertions/Data/DataEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Data/DataEquivalencyAssertionOptions.cs @@ -177,7 +177,7 @@ private static MemberInfo GetMemberAccessTargetMember(Expression expression) && (unaryExpression.NodeType == ExpressionType.Convert)) { // If the expression is a value type, then accessing it will involve an - // implicit boxing convertion to type object that we need to ignore. + // implicit boxing conversion to type object that we need to ignore. expression = unaryExpression.Operand; } diff --git a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs index 62da346822..8a32b13ea3 100644 --- a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs +++ b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs @@ -22,7 +22,7 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable - /// Sets the failure message when the assertion is not met, or completes the failure message set to aprior call to + /// Sets the failure message when the assertion is not met, or completes the failure message set to a prior call to /// . /// will not be called unless the assertion is not met. /// diff --git a/Tests/FluentAssertions.Specs/Execution/GivenSelectorSpecs.cs b/Tests/FluentAssertions.Specs/Execution/GivenSelectorSpecs.cs index df431da601..d2a73ae8ae 100644 --- a/Tests/FluentAssertions.Specs/Execution/GivenSelectorSpecs.cs +++ b/Tests/FluentAssertions.Specs/Execution/GivenSelectorSpecs.cs @@ -10,7 +10,7 @@ public class GivenSelectorSpecs [Fact] public void A_consecutive_subject_should_be_selected() { - // Arange + // Arrange string value = string.Empty; // Act @@ -26,7 +26,7 @@ public void A_consecutive_subject_should_be_selected() [Fact] public void After_a_failed_condition_a_consecutive_subject_should_be_ignored() { - // Arange + // Arrange string value = string.Empty; // Act diff --git a/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs index 6ccd38ca5e..b5bbaebb70 100644 --- a/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs @@ -63,7 +63,7 @@ public void When_subject_is_null_not_reference_should_fail() Assembly assemblyB = FindAssembly.Containing(); // Act - Action act = () => assemblyA.Should().NotReference(assemblyB, "we want to to test the failure {0}", "message"); + Action act = () => assemblyA.Should().NotReference(assemblyB, "we want to test the failure {0}", "message"); // Assert act.Should().Throw() @@ -140,7 +140,7 @@ public void When_subject_is_null_reference_should_fail() Assembly assemblyB = FindAssembly.Containing(); // Act - Action act = () => assemblyA.Should().Reference(assemblyB, "we want to to test the failure {0}", "message"); + Action act = () => assemblyA.Should().Reference(assemblyB, "we want to test the failure {0}", "message"); // Assert act.Should().Throw() @@ -207,7 +207,7 @@ public void When_subject_is_null_define_type_should_fail() // Act Action act = () => thisAssembly.Should().DefineType(GetType().Namespace, "WellKnownClassWithAttribute", - "we want to to test the failure {0}", "message"); + "we want to test the failure {0}", "message"); // Assert act.Should().Throw() diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 833e2674ee..69c28105d9 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -24,6 +24,7 @@ sidebar: * Adding `ThatAreAsync()` and `ThatAreNotAsync()` for filtering in method assertions - [#1725](https://github.com/fluentassertions/fluentassertions/pull/1725) * Adding `ThatAreVirtual()` and `ThatAreNotVirtual()` for filtering in method assertions - [#1744](https://github.com/fluentassertions/fluentassertions/pull/1744) * Adding collection content to assertion messages for `HaveCountGreaterThan()`, `HaveCountGreaterThanOrEqualTo()`, `HaveCountLessThan()` and `HaveCountLessThanOrEqualTo()` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) + ### Fixes * Prevent multiple enumeration of `IEnumerable`s in parameter-less `ContainSingle()` - [#1753](https://github.com/fluentassertions/fluentassertions/pull/1753) * Change `HaveCount()` assertion message order to state expected and actual collection count before dumping its content` - [#1760](https://github.com/fluentassertions/fluentassertions/pull/1760) From b02abf4ae0f6dbe071725d73f10227247b096c87 Mon Sep 17 00:00:00 2001 From: Karen Farnes <32389511+karenfarnes@users.noreply.github.com> Date: Thu, 13 Jan 2022 16:21:17 +0100 Subject: [PATCH 10/25] Non-generic overload for WithInnerExceptionExactly (#1769) --- .../ExceptionAssertionsExtensions.cs | 44 +++++++ .../Specialized/ExceptionAssertions.cs | 120 ++++++++++++------ .../FluentAssertions/net47.verified.txt | 6 + .../netcoreapp2.1.verified.txt | 6 + .../netcoreapp3.0.verified.txt | 6 + .../netstandard2.0.verified.txt | 6 + .../netstandard2.1.verified.txt | 6 + .../AsyncFunctionExceptionAssertionSpecs.cs | 61 +++++++++ .../Exceptions/ExceptionAssertionSpecs.cs | 103 +++++++++++++++ docs/_pages/releases.md | 1 + 10 files changed, 323 insertions(+), 36 deletions(-) diff --git a/Src/FluentAssertions/ExceptionAssertionsExtensions.cs b/Src/FluentAssertions/ExceptionAssertionsExtensions.cs index 13376a3a46..5572cebbc3 100644 --- a/Src/FluentAssertions/ExceptionAssertionsExtensions.cs +++ b/Src/FluentAssertions/ExceptionAssertionsExtensions.cs @@ -80,6 +80,28 @@ public static async Task> WithInnerExceptio return (await task).WithInnerException(because, becauseArgs); } + /// + /// Asserts that the thrown exception contains an inner exception of type . + /// + /// The expected type of the exception. + /// The containing the thrown exception. + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public static async Task> WithInnerException( + this Task> task, + Type innerException, + string because = "", + params object[] becauseArgs) + where TException : Exception + { + return (await task).WithInnerException(innerException, because, becauseArgs); + } + /// /// Asserts that the thrown exception contains an inner exception of the exact type (and not a derived exception type). /// @@ -103,6 +125,28 @@ public static async Task> WithInnerExceptio return (await task).WithInnerExceptionExactly(because, becauseArgs); } + /// + /// Asserts that the thrown exception contains an inner exception of the exact type (and not a derived exception type). + /// + /// The expected type of the exception. + /// The containing the thrown exception. + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public static async Task> WithInnerExceptionExactly( + this Task> task, + Type innerException, + string because = "", + params object[] becauseArgs) + where TException : Exception + { + return (await task).WithInnerExceptionExactly(innerException, because, becauseArgs); + } + /// /// Asserts that the thrown exception has a parameter which name matches . /// diff --git a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs index cb559a624b..f5b735193a 100644 --- a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs +++ b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs @@ -3,9 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; - using FluentAssertions.Common; -using FluentAssertions.Equivalency; using FluentAssertions.Equivalency.Steps; using FluentAssertions.Execution; using FluentAssertions.Formatting; @@ -89,7 +87,8 @@ public virtual ExceptionAssertions WithMessage(string expectedWildca .ForCondition(Subject.Any()) .FailWith("Expected exception with message {0}{reason}, but no exception was thrown.", expectedWildcardPattern); - OuterMessageAssertion.Execute(Subject.Select(exc => exc.Message).ToArray(), expectedWildcardPattern, because, becauseArgs); + OuterMessageAssertion.Execute(Subject.Select(exc => exc.Message).ToArray(), expectedWildcardPattern, because, + becauseArgs); return this; } @@ -109,28 +108,24 @@ public virtual ExceptionAssertions WithInnerException e.InnerException is not null)) - .FailWith("the thrown exception has no inner exception.") - .Then - .ClearExpectation(); - - TInnerException[] expectedInnerExceptions = Subject - .Select(e => e.InnerException) - .OfType() - .ToArray(); - - Execute.Assertion - .ForCondition(expectedInnerExceptions.Any()) - .BecauseOf(because, becauseArgs) - .FailWith("Expected inner {0}{reason}, but found {1}.", typeof(TInnerException), SingleSubject.InnerException); + var expectedInnerExceptions = AssertInnerExceptions(typeof(TInnerException), because, becauseArgs); + return new ExceptionAssertions(expectedInnerExceptions.Cast()); + } - return new ExceptionAssertions(expectedInnerExceptions); + /// + /// Asserts that the thrown exception contains an inner exception of type . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public ExceptionAssertions WithInnerException(Type innerException, string because = null, + params object[] becauseArgs) + { + return new ExceptionAssertions(AssertInnerExceptions(innerException, because, becauseArgs)); } /// @@ -148,19 +143,24 @@ public virtual ExceptionAssertions WithInnerExceptionExactly(because, becauseArgs); - - TInnerException[] expectedExceptions = Subject - .Select(e => e.InnerException) - .OfType() - .Where(e => e?.GetType() == typeof(TInnerException)).ToArray(); - - Execute.Assertion - .ForCondition(expectedExceptions.Any()) - .BecauseOf(because, becauseArgs) - .FailWith("Expected inner {0}{reason}, but found {1}.", typeof(TInnerException), SingleSubject.InnerException); + var exceptionExpression = AssertInnerExceptionExactly(typeof(TInnerException), because, becauseArgs); + return new ExceptionAssertions(exceptionExpression.Cast()); + } - return new ExceptionAssertions(expectedExceptions); + /// + /// Asserts that the thrown exception contains an inner exception of the exact type (and not a derived exception type). + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public ExceptionAssertions WithInnerExceptionExactly(Type innerException, string because = null, + params object[] becauseArgs) + { + return new ExceptionAssertions(AssertInnerExceptionExactly(innerException, because, becauseArgs)); } /// @@ -191,6 +191,54 @@ public ExceptionAssertions Where(Expression> return this; } + private IEnumerable AssertInnerExceptionExactly(Type innerException, string because = null, + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(innerException, nameof(innerException)); + + AssertInnerExceptions(innerException, because, becauseArgs); + + Exception[] expectedExceptions = Subject + .Select(e => e.InnerException) + .Where(e => e?.GetType() == innerException).ToArray(); + + Execute.Assertion + .ForCondition(expectedExceptions.Any()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected inner {0}{reason}, but found {1}.", innerException, SingleSubject.InnerException); + + return expectedExceptions; + } + + private IEnumerable AssertInnerExceptions(Type innerException, string because = null, + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(innerException, nameof(innerException)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected inner {0}{reason}, but ", innerException) + .ForCondition(Subject is not null) + .FailWith("no exception was thrown.") + .Then + .ForCondition(Subject.Any(e => e.InnerException is not null)) + .FailWith("the thrown exception has no inner exception.") + .Then + .ClearExpectation(); + + Exception[] expectedInnerExceptions = Subject + .Select(e => e.InnerException) + .Where(e => e != null && e.GetType().IsSameOrInherits(innerException)) + .ToArray(); + + Execute.Assertion + .ForCondition(expectedInnerExceptions.Any()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected inner {0}{reason}, but found {1}.", innerException, SingleSubject.InnerException); + + return expectedInnerExceptions; + } + private TException SingleSubject { get diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 8f749545dd..dbc62d6212 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -242,9 +242,13 @@ namespace FluentAssertions { public static System.Threading.Tasks.Task> Where(this System.Threading.Tasks.Task> task, System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) where TException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } @@ -2209,8 +2213,10 @@ namespace FluentAssertions.Specialized protected override string Identifier { get; } public TException Which { get; } public FluentAssertions.Specialized.ExceptionAssertions Where(System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerException(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerException(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 74366f32aa..4cf77e0872 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -242,9 +242,13 @@ namespace FluentAssertions { public static System.Threading.Tasks.Task> Where(this System.Threading.Tasks.Task> task, System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) where TException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } @@ -2209,8 +2213,10 @@ namespace FluentAssertions.Specialized protected override string Identifier { get; } public TException Which { get; } public FluentAssertions.Specialized.ExceptionAssertions Where(System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerException(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerException(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 742058b3f4..f95ce03797 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -242,9 +242,13 @@ namespace FluentAssertions { public static System.Threading.Tasks.Task> Where(this System.Threading.Tasks.Task> task, System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) where TException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } @@ -2209,8 +2213,10 @@ namespace FluentAssertions.Specialized protected override string Identifier { get; } public TException Which { get; } public FluentAssertions.Specialized.ExceptionAssertions Where(System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerException(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerException(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 9a7e925e43..f213fcd302 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -235,9 +235,13 @@ namespace FluentAssertions { public static System.Threading.Tasks.Task> Where(this System.Threading.Tasks.Task> task, System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) where TException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } @@ -2162,8 +2166,10 @@ namespace FluentAssertions.Specialized protected override string Identifier { get; } public TException Which { get; } public FluentAssertions.Specialized.ExceptionAssertions Where(System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerException(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerException(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 27d7e7a226..a92ed9ff80 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -242,9 +242,13 @@ namespace FluentAssertions { public static System.Threading.Tasks.Task> Where(this System.Threading.Tasks.Task> task, System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) where TException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerException(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } + public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, System.Type innerException, string because = "", params object[] becauseArgs) + where TException : System.Exception { } public static System.Threading.Tasks.Task> WithInnerExceptionExactly(this System.Threading.Tasks.Task> task, string because = "", params object[] becauseArgs) where TException : System.Exception where TInnerException : System.Exception { } @@ -2209,8 +2213,10 @@ namespace FluentAssertions.Specialized protected override string Identifier { get; } public TException Which { get; } public FluentAssertions.Specialized.ExceptionAssertions Where(System.Linq.Expressions.Expression> exceptionExpression, string because = "", params object[] becauseArgs) { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerException(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerException(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } + public FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(System.Type innerException, string because = null, params object[] becauseArgs) { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithInnerExceptionExactly(string because = null, params object[] becauseArgs) where TInnerException : System.Exception { } public virtual FluentAssertions.Specialized.ExceptionAssertions WithMessage(string expectedWildcardPattern, string because = "", params object[] becauseArgs) { } diff --git a/Tests/FluentAssertions.Specs/Exceptions/AsyncFunctionExceptionAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Exceptions/AsyncFunctionExceptionAssertionSpecs.cs index 4c2558ffb5..f454271169 100644 --- a/Tests/FluentAssertions.Specs/Exceptions/AsyncFunctionExceptionAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Exceptions/AsyncFunctionExceptionAssertionSpecs.cs @@ -832,6 +832,21 @@ public async Task When_async_method_throws_the_expected_inner_exception_it_shoul await action.Should().NotThrowAsync(); } + [Fact] + public async Task When_async_method_throws_the_expected_inner_exception_from_argument_it_should_succeed() + { + // Arrange + Func task = () => Throw.Async(new AggregateException(new InvalidOperationException())); + + // Act + Func action = () => task + .Should().ThrowAsync() + .WithInnerException(typeof(InvalidOperationException)); + + // Assert + await action.Should().NotThrowAsync(); + } + [Fact] public async Task When_async_method_throws_the_expected_inner_exception_exactly_it_should_succeed() { @@ -847,6 +862,21 @@ public async Task When_async_method_throws_the_expected_inner_exception_exactly_ await action.Should().NotThrowAsync(); } + [Fact] + public async Task When_async_method_throws_the_expected_inner_exception_exactly_defined_in_arguments_it_should_succeed() + { + // Arrange + Func task = () => Throw.Async(new AggregateException(new ArgumentException())); + + // Act + Func action = () => task + .Should().ThrowAsync() + .WithInnerExceptionExactly(typeof(ArgumentException)); + + // Assert + await action.Should().NotThrowAsync(); + } + [Fact] public async Task When_async_method_throws_aggregate_exception_containing_expected_exception_it_should_succeed() { @@ -890,6 +920,21 @@ public async Task When_async_method_does_not_throw_the_expected_inner_exception_ await action.Should().ThrowAsync().WithMessage("*InvalidOperation*Argument*"); } + [Fact] + public async Task When_async_method_does_not_throw_the_expected_inner_exception_from_argument_it_should_fail() + { + // Arrange + Func task = () => Throw.Async(new AggregateException(new ArgumentException())); + + // Act + Func action = () => task + .Should().ThrowAsync() + .WithInnerException(typeof(InvalidOperationException)); + + // Assert + await action.Should().ThrowAsync().WithMessage("*InvalidOperation*Argument*"); + } + [Fact] public async Task When_async_method_does_not_throw_the_expected_inner_exception_exactly_it_should_fail() { @@ -905,6 +950,22 @@ public async Task When_async_method_does_not_throw_the_expected_inner_exception_ await action.Should().ThrowAsync().WithMessage("*ArgumentException*ArgumentNullException*"); } + [Fact] + public async Task + When_async_method_does_not_throw_the_expected_inner_exception_exactly_defined_in_arguments_it_should_fail() + { + // Arrange + Func task = () => Throw.Async(new AggregateException(new ArgumentNullException())); + + // Act + Func action = () => task + .Should().ThrowAsync() + .WithInnerExceptionExactly(typeof(ArgumentException)); + + // Assert + await action.Should().ThrowAsync().WithMessage("*ArgumentException*ArgumentNullException*"); + } + [Fact] public async Task When_async_method_does_not_throw_the_expected_exception_it_should_fail() { diff --git a/Tests/FluentAssertions.Specs/Exceptions/ExceptionAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Exceptions/ExceptionAssertionSpecs.cs index 36085cbe62..8562dc40e2 100644 --- a/Tests/FluentAssertions.Specs/Exceptions/ExceptionAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Exceptions/ExceptionAssertionSpecs.cs @@ -409,6 +409,22 @@ public void When_asserting_with_an_aggregate_exception_type_the_asserts_should_o .WithMessage("Inner Message"); } + [Fact] + public void When_asserting_with_an_aggregate_exception_and_inner_exception_type_from_argument_the_asserts_should_occur_against_the_aggregate_exception() + { + // Arrange + Does testSubject = Does.Throw(new AggregateException("Outer Message", new Exception("Inner Message"))); + + // Act + Action act = testSubject.Do; + + // Assert + act.Should().Throw() + .WithMessage("Outer Message*") + .WithInnerException(typeof(Exception)) + .WithMessage("Inner Message"); + } + #endregion #region Inner Exceptions @@ -426,6 +442,32 @@ public void When_subject_throws_an_exception_with_the_expected_inner_exception_i .WithInnerException(); } + [Fact] + public void When_subject_throws_an_exception_with_the_expected_inner_base_exception_it_should_not_do_anything() + { + // Arrange + Does testSubject = Does.Throw(new Exception("", new ArgumentNullException())); + + // Act / Assert + testSubject + .Invoking(x => x.Do()) + .Should().Throw() + .WithInnerException(); + } + + [Fact] + public void When_subject_throws_an_exception_with_the_expected_inner_exception_from_argument_it_should_not_do_anything() + { + // Arrange + Does testSubject = Does.Throw(new Exception("", new ArgumentException())); + + // Act / Assert + testSubject + .Invoking(x => x.Do()) + .Should().Throw() + .WithInnerException(typeof(ArgumentException)); + } + [Fact] public void WithInnerExceptionExactly_no_parameters_when_subject_throws_subclass_of_expected_inner_exception_it_should_throw_with_clear_description() { @@ -487,6 +529,53 @@ public void WithInnerExceptionExactly_when_subject_throws_subclass_of_expected_i } } + [Fact] + public void WithInnerExceptionExactly_with_type_exception_when_subject_throws_expected_inner_exception_it_should_not_do_anything() + { + // Arrange + Action act = () => throw new BadImageFormatException("", new ArgumentNullException()); + + // Act / Assert + act.Should().Throw() + .WithInnerExceptionExactly(typeof(ArgumentNullException), "because {0} should do just that", "the action"); + } + + [Fact] + public void WithInnerExceptionExactly_with_type_exception_no_parameters_when_subject_throws_expected_inner_exception_it_should_not_do_anything() + { + // Arrange + Action act = () => throw new BadImageFormatException("", new ArgumentNullException()); + + // Act / Assert + act.Should().Throw() + .WithInnerExceptionExactly(typeof(ArgumentNullException)); + } + + [Fact] + public void WithInnerExceptionExactly_with_type_exception_when_subject_throws_subclass_of_expected_inner_exception_it_should_throw_with_clear_description() + { + // Arrange + var innerException = new ArgumentNullException(); + + Action act = () => throw new BadImageFormatException("", innerException); + + try + { + // Act + act.Should().Throw() + .WithInnerExceptionExactly(typeof(ArgumentException), "because {0} should do just that", "the action"); + + throw new XunitException("This point should not be reached."); + } + catch (XunitException ex) + { + // Assert + var expectedMessage = BuildExpectedMessageForWithInnerExceptionExactly("Expected inner System.ArgumentException because the action should do just that, but found System.ArgumentNullException with message", innerException.Message); + + ex.Message.Should().Be(expectedMessage); + } + } + [Fact] public void WithInnerExceptionExactly_when_subject_throws_expected_inner_exception_it_should_not_do_anything() { @@ -585,6 +674,20 @@ public void When_an_inner_exception_matches_exactly_it_should_allow_chaining_mor .Where(i => i.Message == "InnerMessage"); } + [Fact] + public void When_an_inner_exception_matches_exactly_it_should_allow_chaining_more_asserts_on_that_exception_type_from_argument() + { + // Act + Action act = () => + throw new ArgumentException("OuterMessage", new InvalidOperationException("InnerMessage")); + + // Assert + act + .Should().ThrowExactly() + .WithInnerExceptionExactly(typeof(InvalidOperationException)) + .Where(i => i.Message == "InnerMessage"); + } + [Fact] public void When_injecting_a_null_predicate_it_should_throw() { diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 69c28105d9..0709e6cfb5 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -12,6 +12,7 @@ sidebar: ### What's New * Adding `ThatAreStatic()` and `ThatAreNotStatic()` for filtering in method assertions - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Adding new assertions for the `HttpStatusCode` of an `HttpResponseMessage` - [#1737](https://github.com/fluentassertions/fluentassertions/pull/1737) +* Adding non-generic overloads for `WithInnerExceptionExactly` and `WithInnerException` - [#1769](https://github.com/fluentassertions/fluentassertions/pull/1769) ### Fixes * `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765) From f78276b1b6e79ad81023f50c0548f2d6a70431e6 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 15 Jan 2022 12:38:36 +0100 Subject: [PATCH 11/25] Fix determining caller identity for NET Native --- FluentAssertions.sln | 6 ++ Src/FluentAssertions/CallerIdentifier.cs | 18 ++-- Tests/UWP.Specs/App.xaml | 4 + Tests/UWP.Specs/App.xaml.cs | 37 +++++++ .../Assets/LockScreenLogo.scale-200.png | Bin 0 -> 1430 bytes .../Assets/SplashScreen.scale-200.png | Bin 0 -> 7700 bytes .../Assets/Square150x150Logo.scale-200.png | Bin 0 -> 2937 bytes .../Assets/Square44x44Logo.scale-200.png | Bin 0 -> 1647 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 1255 bytes Tests/UWP.Specs/Assets/StoreLogo.png | Bin 0 -> 1451 bytes .../Assets/Wide310x150Logo.scale-200.png | Bin 0 -> 3204 bytes Tests/UWP.Specs/Package.appxmanifest | 25 +++++ Tests/UWP.Specs/Properties/AssemblyInfo.cs | 17 +++ Tests/UWP.Specs/Properties/Default.rd.xml | 5 + Tests/UWP.Specs/UWP.Specs.csproj | 100 ++++++++++++++++++ Tests/UWP.Specs/UwpSpecs.cs | 23 ++++ docs/_pages/releases.md | 1 + 17 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 Tests/UWP.Specs/App.xaml create mode 100644 Tests/UWP.Specs/App.xaml.cs create mode 100644 Tests/UWP.Specs/Assets/LockScreenLogo.scale-200.png create mode 100644 Tests/UWP.Specs/Assets/SplashScreen.scale-200.png create mode 100644 Tests/UWP.Specs/Assets/Square150x150Logo.scale-200.png create mode 100644 Tests/UWP.Specs/Assets/Square44x44Logo.scale-200.png create mode 100644 Tests/UWP.Specs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 Tests/UWP.Specs/Assets/StoreLogo.png create mode 100644 Tests/UWP.Specs/Assets/Wide310x150Logo.scale-200.png create mode 100644 Tests/UWP.Specs/Package.appxmanifest create mode 100644 Tests/UWP.Specs/Properties/AssemblyInfo.cs create mode 100644 Tests/UWP.Specs/Properties/Default.rd.xml create mode 100644 Tests/UWP.Specs/UWP.Specs.csproj create mode 100644 Tests/UWP.Specs/UwpSpecs.cs diff --git a/FluentAssertions.sln b/FluentAssertions.sln index 3973759a76..c69957349c 100644 --- a/FluentAssertions.sln +++ b/FluentAssertions.sln @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Approval.Tests", "Tests\App EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Specs", "Tests\FluentAssertions.Specs\FluentAssertions.Specs.csproj", "{6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UWP.Specs", "Tests\UWP.Specs\UWP.Specs.csproj", "{08087654-2C32-4818-95E4-45362373441D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CI|Any CPU = CI|Any CPU @@ -117,6 +119,9 @@ Global {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD}.Release|Any CPU.Build.0 = Release|Any CPU + {08087654-2C32-4818-95E4-45362373441D}.CI|Any CPU.ActiveCfg = Debug|x64 + {08087654-2C32-4818-95E4-45362373441D}.Debug|Any CPU.ActiveCfg = Debug|x64 + {08087654-2C32-4818-95E4-45362373441D}.Release|Any CPU.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,6 +138,7 @@ Global {FCAFB0F1-79EA-4D49-813B-188D4BC4BE71} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3} {F5115158-A038-4D14-A04E-46E7863E40B9} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3} {6D31FFF8-E7FD-41D2-996C-CA8DDFDAE4FD} = {963262D0-9FD5-4741-8C0E-E2F34F110EF3} + {08087654-2C32-4818-95E4-45362373441D} = {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 5dc0956a8b..3985f68194 100644 --- a/Src/FluentAssertions/CallerIdentifier.cs +++ b/Src/FluentAssertions/CallerIdentifier.cs @@ -27,7 +27,7 @@ public static string DetermineCallerIdentity() var stack = new StackTrace(fNeedFileInfo: true); var allStackFrames = stack.GetFrames() - .Where(frame => !IsCompilerServices(frame)) + .Where(frame => frame is not null && !IsCompilerServices(frame)) .ToArray(); int searchStart = allStackFrames.Length - 1; @@ -82,7 +82,7 @@ public StackFrameReference() var stack = new StackTrace(); var allStackFrames = stack.GetFrames() - .Where(frame => !IsCompilerServices(frame)) + .Where(frame => frame is not null && !IsCompilerServices(frame)) .ToArray(); int firstUserCodeFrameIndex = 0; @@ -115,7 +115,7 @@ internal static IDisposable OverrideStackSearchUsingCurrentScope() internal static bool OnlyOneFluentAssertionScopeOnCallStack() { var allStackFrames = new StackTrace().GetFrames() - .Where(frame => !IsCompilerServices(frame)) + .Where(frame => frame is not null && !IsCompilerServices(frame)) .ToArray(); int firstNonFluentAssertionsStackFrameIndex = Array.FindIndex( @@ -130,29 +130,29 @@ internal static bool OnlyOneFluentAssertionScopeOnCallStack() int startOfSecondFluentAssertionsScopeStackFrameIndex = Array.FindIndex( allStackFrames, startIndex: firstNonFluentAssertionsStackFrameIndex + 1, - IsCurrentAssembly); + frame => IsCurrentAssembly(frame)); return startOfSecondFluentAssertionsScopeStackFrameIndex < 0; } private static bool IsCustomAssertion(StackFrame frame) { - return frame.GetMethod().IsDecoratedWithOrInherit(); + return frame.GetMethod()?.IsDecoratedWithOrInherit() == true; } private static bool IsDynamic(StackFrame frame) { - return frame.GetMethod().DeclaringType is null; + return frame.GetMethod() is { DeclaringType: null }; } private static bool IsCurrentAssembly(StackFrame frame) { - return frame.GetMethod().DeclaringType?.Assembly == typeof(CallerIdentifier).Assembly; + return frame.GetMethod()?.DeclaringType?.Assembly == typeof(CallerIdentifier).Assembly; } private static bool IsDotNet(StackFrame frame) { - var frameNamespace = frame.GetMethod().DeclaringType.Namespace; + var frameNamespace = frame.GetMethod()?.DeclaringType?.Namespace; var comparisonType = StringComparison.OrdinalIgnoreCase; return frameNamespace?.StartsWith("system.", comparisonType) == true || @@ -161,7 +161,7 @@ private static bool IsDotNet(StackFrame frame) private static bool IsCompilerServices(StackFrame frame) { - return frame.GetMethod().DeclaringType?.Namespace == "System.Runtime.CompilerServices"; + return frame.GetMethod()?.DeclaringType?.Namespace == "System.Runtime.CompilerServices"; } private static string ExtractVariableNameFrom(StackFrame frame) diff --git a/Tests/UWP.Specs/App.xaml b/Tests/UWP.Specs/App.xaml new file mode 100644 index 0000000000..0d5a0c69ec --- /dev/null +++ b/Tests/UWP.Specs/App.xaml @@ -0,0 +1,4 @@ + diff --git a/Tests/UWP.Specs/App.xaml.cs b/Tests/UWP.Specs/App.xaml.cs new file mode 100644 index 0000000000..a96acf33e0 --- /dev/null +++ b/Tests/UWP.Specs/App.xaml.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.VisualStudio.TestPlatform.TestExecutor; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Navigation; + +namespace UWP.Specs +{ + internal sealed partial class App : Application + { + public App() + { + InitializeComponent(); + Suspending += OnSuspending; + } + + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + if (Window.Current.Content is not Frame rootFrame) + { + rootFrame = new Frame(); + rootFrame.NavigationFailed += OnNavigationFailed; + Window.Current.Content = rootFrame; + } + + UnitTestClient.Run(e.Arguments); + } + + private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) => + throw new Exception("Failed to load Page " + e.SourcePageType.FullName); + + private void OnSuspending(object sender, SuspendingEventArgs e) => + e.SuspendingOperation.GetDeferral().Complete(); + } +} diff --git a/Tests/UWP.Specs/Assets/LockScreenLogo.scale-200.png b/Tests/UWP.Specs/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..735f57adb5dfc01886d137b4e493d7e97cf13af3 GIT binary patch literal 1430 zcmaJ>TTC2P7~aKltDttVHYH6u8Io4i*}3fO&d$gd*bA_<3j~&e7%8(eXJLfhS!M@! zKrliY>>6yT4+Kr95$!DoD(Qn-5TP|{V_KS`k~E6(LGS@#`v$hQo&^^BKsw3HIsZBT z_y6C2n`lK@apunKojRQ^(_P}Mgewt$(^BBKCTZ;*xa?J3wQ7~@S0lUvbcLeq1Bg4o zH-bvQi|wt~L7q$~a-gDFP!{&TQfc3fX*6=uHv* zT&1&U(-)L%Xp^djI2?~eBF2cxC@YOP$+9d?P&h?lPy-9M2UT9fg5jKm1t$m#iWE{M zIf%q9@;fyT?0UP>tcw-bLkz;s2LlKl2qeP0w zECS7Ate+Awk|KQ+DOk;fl}Xsy4o^CY=pwq%QAAKKl628_yNPsK>?A>%D8fQG6IgdJ ztnxttBz#NI_a@fk7SU`WtrpsfZsNs9^0(2a z@C3#YO3>k~w7?2hipBf{#b6`}Xw1hlG$yi?;1dDs7k~xDAw@jiI*+tc;t2Lflg&bM)0!Y;0_@=w%`LW^8DsYpS#-bLOklX9r?Ei}TScw|4DbpW%+7 zFgAI)f51s}{y-eWb|vrU-Ya!GuYKP)J7z#*V_k^Xo>4!1Yqj*m)x&0L^tg3GJbVAJ zJ-Pl$R=NAabouV=^z_t;^K*0AvFs!vYU>_<|I^#c?>>CR<(T?=%{;U=aI*SbZADLH z&(f2wz_Y0??Tf|g;?|1Znw6}6U43Q#qNRwv1vp9uFn1)V#*4p&%$mP9x&15^OaBiDS(XppT|z^>;B{PLVEbS3IFYV yGvCsSX*m literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/SplashScreen.scale-200.png b/Tests/UWP.Specs/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..023e7f1feda78d5100569825acedfd213a0d84e9 GIT binary patch literal 7700 zcmeHLYj~4Yw%(;oxoEH#Kxq-eR|+VkP17b#Vk;?4QwkI+A{L04G+#<<(x#Un1#+h5>eArRq zTw$)ZvTWW_Y?bDho0nPVTh08+s`sp!j74rJTTtXIDww0SILedFv?sZ?yb@@}GN;#8 znk_b~Q(A0YR#uV4ef!osoV1M3;vQ8N$O|fStfgf$S5;ddUNv`tWtGjM;koG#N;7M< zP*84lnx(bn_KF&9Z5Ai$)#Cs3a|$OFw>WKCT$of*L7_CqQEinflT|W{JT+aKp-E0v zsxmYg)1(T>DROm+LN1eQw8}KCTp=C!$H7`PU!t9_Hw@TsTI2`udRZv*!a5`#A9hK6Y95L(CDUX&_@QxKV z_feX{UhA#ZWlvgpL$#w^D#lq`_A4AzDqd|Zv6y9PX&DNcN|l}_D^{q@GG&H^Pg583 z8FI6N8^H7b5WjGp;urW)d7F+_lcp%KsLX0viCmE(OHH+=%ZfD_=`voUuoUxFO^L;- z;!;2{g-YiiO6m4bs89OuF9!p{FGtH-f%8<2gY!h9s)4ciN%{Kh1+`}{^}M~+TDH9N z^Z5PlgVXMC&2&k*Hw^Lb9gny#ro$MOIxIt{+r)EA10$VR3 zanN8D{TUkl+v0CQ_>ZoHP<M-x#8@8ZiT#$Kh`(uRaX1g$Bg|qy$<#7 zSSAi{Nb8Y=lvNVeio+UGLCAtoLBfL`iOv`)yoJMDJBN>4IH@(l7YRF;61@>qq1iM9 zr@b#OC~SAxSle?5Pp8Z78{VO0YFr1x7kZU64Z23eLf2T2#6J_t;-E}DkB?NufZ0Ug zi?J&byXeaB-uTNVhuiM!UVQw}bZrJ3GtAETYp->!{q#zfN7D3AS9@Q7*V^85jGx#R z(QxYV(wW#F0XF9^^s>>H8pPlVJ>)3Oz z&_X8Sf@~?cH_O*cgi$U#`v`RRfv#y3m(ZpKk^5uLup+lVs$~}FZU$r_+}#hl%?g5m z-u-}-666ssp-xWQak~>PPy$mRc|~?pVSs1_@mBEXpPVfLF6(Ktf1S* zPPh@QZ=tFMs?LM2(5P3L2;l_6XX6s&cYsP1ip#eg0`ZEP0HGYh{UmS@o`MihLLvkU zgyAG0G`b1|qjxxh1(ODKFE%AP}Dq=3vK$P7TXP4GrM1kQ72!GUVMDl`rDC&2;TA}*nF z8$nQD&6ys_nc1*E7$*1S@R8$ymy(sQV}imGSedB@{!QR5P&N_H=-^o!?LsWs+2|mH z-e=)T^SvI)=_JIm7}j4;@*Z17=(#}m=~YF~z~CLI+vdAGlJDcdF$TM?CVI1%LhUrN zaa6DJ=Yh$)$k&Oz{-~8yw^GM^8prYxSxo zvI4k#ibryMa%%*8oI-5m61Koa_A_xg=(fwp0aBX{;X4Q;NXUhtaoJDo1>TqhWtn=_ zd5~chq#&6~c%8JZK#t_&J(9EVUU&upYeIovLt1>vaHe}UUq>#RGQj!EN#5+0@T`(@ z^g~>*c`VGRiSt;!$_4+0hk^I!@O3``5=sZ8IwlxWW7km1B&_t&E*u0_9UBa#VqwY* zz>nxv?FAsVnRaD(Bui=6i==BFUw0k4n$>`umU`F2l?7CYTD^)c2X+d9X&ddS9|gj? zM?knGkGCX&W8offw8aLC2$D{PjC3nVZwd4k?eZH8*mZ)U@3Qk8RDFOz_#WUA#vnzy zyP>KrCfKwSXea7}jgJjBc}PGY+4#6%lbZyjhy`5sZd_Vy6Wz;ixa?czkN}J9It1K6 zY!eu>|AwF^fwZlLAYyQI*lM@^>O>Iu6Vf6i>Q$?v!SeUS<{>UYMwz$*%Aq?w^`j{h z!$GZbhu=^D{&ET8;))LL%ZBDZkQqRd2;u~!d9bHGmLRhLDctNgYyjsuvoSZ#iVdoB z2!f--UUA#U;<{je#?cYt^{PIyKa%hW>}uepWMyAI{{Zo7?2>?$c9;whJae%oN|I-kpTQSx_C$Z&;f zi2i)qmEn=y4U0uvk)$m;zKfjPK@oc?I`}1Jzl$Q~aoKBd3kt7L#7gyt|A_qgz6ai< z=X%D1i!d2h?rHR^R8SUj&G||dkC?DT>{o#Yau<@uqVT{Xef&XG}5*E4aPk{}~ zplx&XhaV)&1EfI3Em;Bw#O5SV^c;{twb-1Rw)+=0!e_BLbd7tYmXCH0wrlOSS+~`7He8Iqx0{CN+DVit9;*6L~JAN zD&cyT)2?h}xnYmL?^)<7YyzZ3$FHU^Eg;DLqAV{#wv#Wj7S`Jdl1pX&{3(uZ?!uh} zDc$ZTNV*7le_W6}Hju~GMTxZQ1aWCeUc%!jv3MHAzt>Y-nQK%zfT*3ebDQA5b?iGn; zBjv3B+GhLTexd_(CzZDP4|#n5^~scvB6#Pk%Ho!kQ>yYw((Dv{6=$g3jT1!u6gORW zx5#`7Wy-ZHRa~IxGHdrp(bm%lf>2%J660nj$fCqN(epv@y!l9s7@k6EvxS{AMP>WY zX4$@F8^kayphIx-RGO$+LYl9YdoI5d|4#q9##`_F5Xnx`&GPzp2fB{-{P@ATw=X@~ z_|&^UMWAKD;jjBKTK(~o?cUFRK8EX=6>cXpfzg4ZpMB>*w_^8GSiT-Jp|xBOnzM+j z*09-@-~qJ(eqWq5@R4i^u4^{McCP(!3}C|v_WsTR*bIUxN(Nx`u##3B4{sE`Z`v8w zAwIG`?1~PkID~W{uDzmqH98Pew_1(;x2%8r^vY{)_&J2K)cN{W+h5+g)ZcjP&Ci#O zgy|8K@4kyMfwilHd&6TDlhb%++Pk!>9HRld6HT7gwyZGrxS$}CsD6`>6!!2K1@Mjf z(P0WYB7V_OFZyeWrbOFb>O54BNXf~K&?}3=^v;v_wT{DKr?jN^DtN&DXwX%u?s*c6`%8>WFz z7}YW^tp0bp^NriE)AB6M2l<7rn7fzePtR*omOevpfm9n?}2V*+0iW;S)C zhg`NAjL?D=W#k*$aR{>pGf~lD-rVtD;5jW1_*Jn1j1=es@Kcx4ySM_bwcQCT=d+DV z>Sz~L=Hj@(X%31nK$mWI@7d>}ORB`K(p=+`UD)+99YUGQc7y^bHZ1F(8|tL0 zdK*DT0kSXG_{BKTpP2*2PecdKV9;dq$^ZZDP;Nyq1kp-&GI5eAyZsK!e3V zK@rPy*{(`KIfo+lc878mDKk^V#`VT05}64kBtk%DgwLrOvLMj5-;*GNKv6c6pzMuL z6EP%ob|_0IW}lLRXCP2!9wWhEw3LA7iF#1O1mIZ@Z=6&bz41F;@S_GvYAG-#CW3z{ zP3+6vHhvP&A3$##Vo9$dT^#MoGg^|MDm=Bt1d2RRwSZ<;ZHICpLBv5Xs!D?BH^(9_ z7`H=N&^v|Z-%mP}wNzG{aiFCsRgwzwq!N6obW9+7(R; z(SZ=23`|`>qil!LMGG{_Heq!BD>(Y-zV9wD)}hz25JA37YR%39;kI4y9pgtcUass6 zP24}ZY$vvYeI`zy&)A_X#nY3017ap*0&jx|mVwyGhg3;!keU53a}Uhm3BZI$N$6Se zLWlAmy1S0xKJm4G_U@sN_Tm=`$xWJSEwKU98rZ&)1R^*$$1vA3oG#&*%SMxY_~oGP zP&PFJatFLM-Ps%84IV-+Ow)T{C7cqUAvauy4C z(FRz&?6$Rypj{xO!`y=*J5o4@U8Q-(y5(*=YoKeZ+-1YdljXxkA#B)zo=FeQH#?Le zycNUmEEHWO9a=X^pb#&cOq7-`7UA87#|S22)<7RUtZo|(zibX=w;K3qur9vy#`MNV z6UUcf9ZwEnKCCp+OoBnF@OdbvH)ANXO0o~Pi9l8=x3))}L<#vO0-~O4!~--Ket?d} zJaqsj<@CD1%S2cTW%rOP{Vto%0sGW~1RMa_j^)5nil0Yw- z0EE#bP+l4#P^%PQ+N*oxu1Zq05xZ!bXfYTg>9c{(Iw*lnjR^>kz%lAN^zFce7rppy zY8zA~3GD=A6d*hze&l4D_wA~+O!56)BZTe_rEu}Ezi<4!kG|W#amBZ5{&XS2@6R~H z{9o^y*BkH4$~yX9U&@CgbOzX1bn9xqF|zh$Dh0Y5y*E0e90*$!ObrHY3Ok0`2=O~r zCuke6KrP9KOf?V(YDsM<6pX2nVoN%M$LT^q#FmtaF?1^27F*IcNX~XRB(|hCFvdcc zc)$=S-)acdk$g4?_>jRqxpI6M3vHZk?0c^3=byamYDNf;uB{3NlKW5IhnOS3DNkMV z?tK8?kJ}pmvp%&&eTVOVjHP`q34hN1@!aK}H(K!vI`~gf|Gv+FNEQD5Yd<~yX7k_l h&G-K)@HZb3BABY{)U1?^%I#E6`MGoTtustd{~yM6srvu` literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/Square150x150Logo.scale-200.png b/Tests/UWP.Specs/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..af49fec1a5484db1d52a7f9b5ec90a27c7030186 GIT binary patch literal 2937 zcma)84OCO-8BSud5)jwMLRVKgX(S?$n?Ld|vrsm<$CF7)&zTbyy1FE5bU`Q17MRv`9ue$;R(@8kR;#vJ*IM0>cJIAOte!d7oRgdH zd%ySjdB6L9=gX^A6)VzH7p2l@v~3zJAMw|DFy#^)F@@F*`mqUn=Il>l)8_+ab;nOW{%+iPx z+s{Eu|&pIs)Z7{La9~?xKfyl z#43?gjEL15d4WbOZo#SiP%>DB^+BcnJ=7dHEe;r#G=tuw|ka z%q@}##Uh7;tc%L_64m(kHtw74ty%BJMb)_1)#S0j`)F8_1jF7vScpsnH=0V19bO8y zR`0SjIdCUo&=>JwMQF8KHA<{ODHTiQh}0^@5QRmCA?gOH6_H3K^-_sNB^RrdNuK-R zOO*vOrKCVvDwgUck`kF(E7j{I#iiN;b*ZdCt4m@HPA`EuEqGGf4%!K<;(=I=&Vyrw z%TwcWtxa}8mCZ%Cyf&ActJ6_$ox5z6-D!0-dvnRx6t7y3d+h6QYpKWO;8OdnvERo7 zuEf>ih5`wqY)~o@OeVt-wM?Q!>QzdGRj!bz6fzYrfw$hZfAKzr2-M+D+R>}~oT574c;_3zquHcElqKIsryILt3g8n3jcMb+j?i?-L3FpZJ z2WRVBRdDPc+G5aaYg#5hpE+6nQ|(VSoxT3|biF;BUq#==-27Xi=gihDPYP$7?=9cP zYKE$jeQ|3~_L0VG-(F~2ZPyD0=k{J4Q~h(t__{-mz_w8{JDY9{`1ouzz!Vr5!ECdE z6U~O1k8c}24V7~zzXWTV-Pe4)y}wQJS&q%H5`Fo_f_JvIU489aCX$;P`u#!I-=^4ijC2{&9!O&h>mi?9oYD=GC#%)6{GzN6nQYw+Fal50!#x^asjBBR50i`+mho*ttoqV)ubM2KD9S~k7+FR4>{29?6 z{!l6kDdyTN0YJ9LgkPWeXm|gyi@zM3?0@{&pXT12w|78&W-q!RRF)&iLCEZVH<|fR zN0fr2^t8H(>L?>K#>^+jWROLral(Qy-xoBq1U7A&DV||wClb)Otd9?(gZ|8znMF}D zf<1haWz^s0qgecz;RFGt0C-B4g`jNGHsFU+;{<%t65v^sjk^h$lmWn#B0#_)9ij&d z-~lc`A)YYExi^7sBuPM^Y|wA2g*5?`K?#7tzELQYNxGo$UB$4J8RJp1k(8Jj+~hMT zlN~>M@KTTh^--8y3PK_NZ@AC!{PT=CziBzGd+wTJ^@icH!Bd}%)g8V)%K?|c&WTUk zy}qv1C%(fjRoZ4ozC3{O%@5?)XzH35zHns$pgU*Q?fj4v?fp1Qbm+j;3l;9jam9Da zXVcKjPlQ73x78QPu|Ffm6x?`~e3oD=gl=4kYK?={kD5j~QCXU)`HSdduNNENzA*2$ zOm3PzF!lN5e*06-f1Uot67wY#{o-S1!KZ7E=!~7ynnk9_iJR#kFoNbAOT#^2Gd17F zMmvU6>lndZQGd|ax9kUoXXO+$N?|j@6qpsF&_j7YXvwo_C{JpmLw5&#e6k>atv%es z5)7r*Wvv_JkUpT}M!_o!nVlEk1Zbl=a*2hQ*<|%*K1Glj^FcF`6kTzGQ3lz~2tCc@ z&x|tj;aH&1&9HwcJBcT`;{?a+pnej;M1HO(6Z{#J!cZA04hnFl;NXA+&`=7bjW_^o zfC40u3LMG?NdPtwGl>Tq6u}*QG)}-y;)lu-_>ee3kibW(69n0$0Zy!}9rQz%*v1iO zT9_H>99yIrSPYVy6^);rR}7Yo=J_T@hi+qhTZXnVWyf;JDYm5#eYLTxr*?kiNn!+Y zQ+LUkBafNJ#rH#C(?d5^;gw9o#%daEI{mA*LHPIHPU`#|H$hD zwm>0&+kahQ)E#%~k>&5@&#Vg82H?s%71=)(soi@174pi9--2{w{1$}Sz4zGn3Du&x bht0Iza^2ykEt4(epJ78uh5nDlX8(TxzDYwP literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/Square44x44Logo.scale-200.png b/Tests/UWP.Specs/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..ce342a2ec8a61291ba76c54604aea7e9d20af11b GIT binary patch literal 1647 zcmaJ?eM}Q)7(e+G1Q(|`V9JhTI2>MkceK4;p;PR&$Pi?ejk3YQ_3o`S&|W_dsOZ8# zWPTt69g`t$ab`0cj-Y0yiBSOqmd)tG7G(}M5aP0_%&9TijB#&)I{zSE^4@#z^FF`l z`8{8`o%wlL(UI|y2!cdsuVamHH~H86F!*-15em4)NqUpCQM5?aoC_eCf@lV4wvF2a zjDQn1JBL69f&@2M3rvzJcfE!eZ8FZUBlFlC5RD)it33{mF9#B82AiyQE%w)`vlwa> zv{<1sm&kSKK$&%2jSFn7$t&P%%6Ue>R=EAnG8N7fqynWG8L3p!4801a;8{+nliO(qd(jNJ_?+9W3#hLIDLoT6~3fx9=`CC-D}-AMrpEO7HK zt3$GicGPc?GmDjy7K2P@La;eu4!$zWCZ`ym{Z$b zu-O6RM&K4JT|BIZB`E-gxqG%FzanI#+2FFmqHqXG7yxWB=w55RGOM)$xMb(>kSNR z2w=1AZi%z=AmG~yea~XaXJR!v7vLn(RUnELfiB1|6D84ICOS}^Zo2AdN}<&*h}G_u z{xZ!(%>tLT3J3<5XhWy-tg+6)0nmUUENLW8TWA{R6bgVd3X;anYFZ^IRis*_P-C-r z;i>%1^eL3UI2-{w8nuFFcs0e~7J{O2k^~Ce%+Ly4U?|=!0LH=t6()xi<^I-rs+9sF z*q{E-CxZbGPeu#a;XJwE;9S1?#R&uns>^0G3p`hEUF*v`M?@h%T%J%RChmD|EVydq zmHWh*_=S%emRC*mhxaVLzT@>Z2SX0u9v*DIJ@WC^kLVdlGV6LpK$KIrlJqc zpJ921)+3JJdTx|<`G&kXpKkjGJv=76R`yYIQ{#c-`%+`#V(7}Q;&@6U8!Td1`d;?N z_9mnI#?AA}4J!r)LN4!E-@H5eXauuB7TOawS>Y|{-P?NNx-lq+z1W-+y(;39P&&LP zL{N80?&=C*qKmdA^moMZRuPcD!B<*mq$ch=0Cnlitw#txRWhb3%TQvPqjkC`F69G4b! ze7z9MZ#+;_#l?H37UqUhDFb^l&s2{oM$3I0o^Q!yx;;V)QmCMo)Tb_ui|mit8MS?U zm##6$sZZ1$@|s%?l@>4Z<*Q}sRBSKMhb4I{e5LdEhsHIHTe8Bod5c>6QtT>$XgUBz z6MK`kO$=jmt@FqggOhJ5j~e@ygRbG;<{Vu)*+nn9aQeo0;$#j;|MS=S$&L?BeV25z xs3B`@=#`5TF{^6(A1rvdY@|-RtQ|iS5{tyX+wH?;n8E)G$kykv-D^wh{{!TZT%7;_ literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Tests/UWP.Specs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c02ce97e0a802b85f6021e822c89f8bf57d5cd GIT binary patch literal 1255 zcmaJ>TWs4@7*5+{G#S+&C!qC#> zf>5N3P6jO*Cz>ug*(_DmW=)kea&m$gZ^+nyiF`;j%w@}y8)>p*SH}C`m?DXeieF2U zyQHecc_L%Gh!7GMt+hG06y;+|p4>m~}PjA}rKViGiEnn7G0ZO<>G|7q;2?NwGCM3s?eued6%hd$B+ z*kQJ{#~$S=DFE(%=E+UkmlEI*%3llUf~8Ja9YU1Vui0IbGBkW_gHB%Rd&!!ioX zs40O?i9I{};kle7GMvE7(rk`la=gTI)47=>%?q@^iL-nUo3}h4S}N-KHn8t5mVP8w z&bSErwp+37 zNJJ8?a|{r5Q3R0Z5s-LB1WHOwYC@7pCHWND#cL1cZ?{kJ368_*(UDWUDyb<}0y@o# zfMF016iMWPCb6obAxT$JlB6(2DrlXDTB&!0`!m??4F(qWMhjVZo?JXQmz`1*58Z=& zcDmB|S-E@j?BoFGix0flckqdS4jsPNzhfWyWIM98GxcLs89C(~dw%$_t;JjX-SD}E zfiGV;{8Q%8r}w9x>EEigW81>`kvnU@pK)4+xk9@+bNj9L!AAZ@SZ@q|)&BmY3+HZx zul~BeG4|}-;L%cHViQGQX?^zFfO0&#cHwel=d`lH9sJ-@Sl@n*(8J2>%Ac`IxyY?Q z{=GhWvC#gu-~Ia7*n{=+;qM?Ul_wy1+u7ho;=`>EwP^g~R@{unBds`!#@}tluZQpS zm)M~nYEifJWJGx?_6DcTy>#uh%>!H9=hb^(v`=m3F1{L>db=<5_tm+_&knAQ2EU$s Mu9UqpbNZeC0BbUo^Z)<= literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/StoreLogo.png b/Tests/UWP.Specs/Assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..7385b56c0e4d3c6b0efe3324aa1194157d837826 GIT binary patch literal 1451 zcmaJ>eN5D57_Z|bH;{0+1#mbl)eTU3{h)Wf7EZV?;HD@XL@{B`Ui%(2aMxQ~xdXSv z5nzWi(LW)U2=Vc-cY@s7nPt{i0hc6!7xN4NNHI#EQl>YNBy8l4%x9gr_W-j zEZMQmmTIy(>;lblRfh`dIyTgc9W5d!VP$L4(kKrN1c5G~(O_#xG zAJCNTstD^5SeXFB+&$h=ToJP2H>xr$iqPs-#O*;4(!Fjw25-!gEb*)mU}=)J;Iu>w zxK(5XoD0wrPSKQ~rbL^Cw6O_03*l*}i=ydbu7adJ6y;%@tjFeXIXT+ms30pmbOP%Q zX}S;+LBh8Tea~TSkHzvX6$rYb)+n&{kSbIqh|c7hmlxmwSiq5iVhU#iEQ<>a18|O^Sln-8t&+t`*{qBWo5M?wFM(JuimAOb5!K#D}XbslM@#1ZVz_;!9U zpfEpLAOz=0g@bd6Xj_ILi-x^!M}73h^o@}hM$1jflTs|Yuj9AL@A3<-?MV4!^4q`e z)fO@A;{9K^?W?DbnesnPr6kK>$zaKo&;FhFd(GYFCIU^T+OIMb%Tqo+P%oq(IdX7S zf6+HLO?7o0m+p>~Tp5UrXWh!UH!wZ5kv!E`_w)PTpI(#Iw{AS`gH4^b(bm^ZCq^FZ zY9DD7bH}rq9mg88+KgA$Zp!iWncuU2n1AuIa@=sWvUR-s`Qb{R*kk(SPU^`$6BXz8 zn#7yaFOIK%qGxyi`dYtm#&qqox0$h=pNi#u=M8zUG@bpiZ=3sT=1}Trr}39cC)H|v zbL?W)=&s4zrh)7>L(|cc%$1#!zfL?HjpeP%T+x_a+jZ16b^iKOHxFEX$7d|8${H-* zIrOJ5w&i$>*D>AKaIoYg`;{L@jM((Kt?$N$5OnuPqVvq**Nm}(f0wwOF%iX_Pba;V z;m@wxX&NcV3?<1+u?A{y_DIj7#m3Af1rCE)o`D&Y3}0%7E;iX1yMDiS)sh0wKi!36 zL!Wmq?P^Ku&rK~HJd97KkLTRl>ScGFYZNlYytWnhmuu|)L&ND8_PmkayQb{HOY640 bno1(wj@u8DCVuFR|31B*4ek@pZJqxCDDe1x literal 0 HcmV?d00001 diff --git a/Tests/UWP.Specs/Assets/Wide310x150Logo.scale-200.png b/Tests/UWP.Specs/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..288995b397fdbef1fb7e85afd71445d5de1952c5 GIT binary patch literal 3204 zcmbVPeQXow8NYmBd90>}0NP?GhXW~VaeThm=a0tV#EwJMI!)6M3}|c4_Bl3=Kd>G0 z(GHx1wl<7(tP?FsOQkTilSo*iIvF%uArExJ73~P zSv1xEy!U(Wd4A9D`FQV@W3@F^qJ@PEF$@z`Z!*BbFsS(^?B zyiAzJ+q})bkgiQHWqEb*jJD-coHYr1^iocg)l!Qa{Xqs-l~6J}p-|##ZHYofskQ3$ zI0;xzXyhazBeXhIsg5A=%ufo@f)1yy&ScKS0;HF^!r_2UE^lpZEom(+@duma3awTv zCrCL-%D_SvYWIcdHkmI}#50(fkUi)Qgx!80ju>g1za^}ff>JI8Z@^-iCiaCgg@TgF z+vtE?Q9{VQUX&MW9SYYmGcxA14%N2@7FwBTD4N<(2{nWgV8$e3?-F=L^&FrtWn~(U_Q~~^uYiyeY6-KoTnfh9AWz@ zIKje0)u!_Lw)E}G!#kEfwKVdNt(UAf9*f>tEL_(=xco-T%jTi@7YlC3hs2ik%Le0H ztj}RTeCF(5mwvi3_56>-yB?l;J>-1%!9~=fs|QcNG3J~a@JCu`4SB460s0ZO+##4fFUSGLcj_ja^fL4&BKALfb#$6$O?>P@qx2Agl^x0i&ugt zsy5Pyu=()`7HRMG3IB7F1@`_ z+-!J%#i6e^U$e#+C%Q>_qVRzWRsG^W_n+@OcX@vzI&z;mzHNb!GQ?LWA(wtpqHqTM z1OFw_{Zn?fD)p)`c`kOgv{de=v@suGRqY{N^U7gI1VF3*F=obwaXI6ob5__Yn zVTguS!%(NI09J8x#AO_aW!9W7k*UvB;IWDFC3srwftr{kHj%g)fvnAm;&h_dnl~

MY- zf+K}sCe8qU6Ujs`3ua{U0Of$R_gVQBuUA za0v=mu#vIOqiiAZOr&h*$WyOw&k-xr$;G4Ixa!#TJNr>95(h>l%)PUy4p+^SgR(uR zta%k*?ny-+nAr8spEk1fo{J4i!b^Fia`N{_F6@zidA2ZTTrjl#^5Z-2KfB@Cu}l9s z(*|Z2jc?p~vn2f)3y9i*7zJV1L{$?|&q)4oaT;uXi6>1GkRXVTOzAz(RHEmr=eFIi z`}<>-Q?K0GN8!IYxeP1XKXO+jsJbp~o^);Bc;%b7Flpe7;1`Ny@3r7ZR;?R)aJt8C ziNlEC<@3f_lIV4TwV}&e;D!Ee5_|e#g0LUh=5vmYWYm7&2h*M>QPKvGh9-)wfMMW3 z8J9b%1k7dzPzO0_NGQy92BZ^FR6R~6;^6?lqO;-QUP4BY%cG%3vEhbm#>4vIhPBh3 z-+pZGjh$x%Hp{?=FHsMp0&wNPlj00us{&`1ZOZTqs8%4X&xH=UDr*xyBW(Zp&Em94 zf)ZSfn#yg0N)>!1kWdkqJ^S*z0FF5|fj&qcE#Na|%OY0$uO>!&hP+1ywfD_WXk@4J(?MBftK7>$Nvqh@tDuarN%PrTLQ2Uzysx>UV=V zk^RrDSvdQ?0;=hY67EgII-f4`t=+i*yS=Y~!XlqIy_4x&%+OdfbKOFPXS2X5%4R{N z$SQMX^AK6(fA + + + + UWP.Specs + FluentAssertions + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/UWP.Specs/Properties/AssemblyInfo.cs b/Tests/UWP.Specs/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f7cdff3769 --- /dev/null +++ b/Tests/UWP.Specs/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("UWP.Specs")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UWP.Specs")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyMetadata("TargetPlatform","UAP")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/Tests/UWP.Specs/Properties/Default.rd.xml b/Tests/UWP.Specs/Properties/Default.rd.xml new file mode 100644 index 0000000000..f3d2603c7e --- /dev/null +++ b/Tests/UWP.Specs/Properties/Default.rd.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Tests/UWP.Specs/UWP.Specs.csproj b/Tests/UWP.Specs/UWP.Specs.csproj new file mode 100644 index 0000000000..45a4d06107 --- /dev/null +++ b/Tests/UWP.Specs/UWP.Specs.csproj @@ -0,0 +1,100 @@ + + + + Debug + x86 + {08087654-2C32-4818-95E4-45362373441D} + AppContainerExe + Properties + UWP.Specs + UWP.Specs + 9.0 + en-US + UAP + 10.0.19041.0 + 10.0.18362.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + $(VisualStudioVersion) + false + $(NoWarn);2008 + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + pdbonly + x64 + false + prompt + true + + + PackageReference + + + + + + + + App.xaml + + + + + + MSBuild:Compile + Designer + + + + + Designer + + + + + + + + + + + + + + + Src\FluentAssertions\bin\$(Configuration)\netstandard2.0\FluentAssertions.dll + {34E3713D-C02F-4868-BBE7-47DAD2C7F03D} + FluentAssertions + + + + + 6.2.13 + + + 2.2.8 + + + 2.2.8 + + + 4.3.0 + + + + \ No newline at end of file diff --git a/Tests/UWP.Specs/UwpSpecs.cs b/Tests/UWP.Specs/UwpSpecs.cs new file mode 100644 index 0000000000..07b6878949 --- /dev/null +++ b/Tests/UWP.Specs/UwpSpecs.cs @@ -0,0 +1,23 @@ +using System; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UWP.Specs +{ + [TestClass] + public class UwpSpecs + { + [TestMethod] + public void Determining_caller_identity_should_not_throw_for_native_programs() + { + // Arrange + Action someAction = () => throw new Exception(); + + // Act + Action act = () => someAction.Should().NotThrow(); + + // Assert + act.Should().Throw(); + } + } +} diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 0709e6cfb5..6e1426a6bb 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -18,6 +18,7 @@ sidebar: * `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765) * Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Variable name is not captured after await assertion - [#1770](https://github.com/fluentassertions/fluentassertions/pull/1770) +* Avoid a `NullReferenceException` when testing an application compiled with .NET Native - [#1776](https://github.com/fluentassertions/fluentassertions/pull/1776) ## 6.3.0 From 2b25db1966c7f7a61c0e5cbfaf3b0d1abb3a87d4 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Jan 2022 10:10:32 +0100 Subject: [PATCH 12/25] Adjust Identifier property --- .../Primitives/HttpResponseMessageAssertions.cs | 2 ++ Src/FluentAssertions/Types/MethodBaseAssertions.cs | 2 -- Src/FluentAssertions/Xml/XmlElementAssertions.cs | 2 ++ Src/FluentAssertions/Xml/XmlNodeAssertions.cs | 2 +- .../ApprovedApi/FluentAssertions/net47.verified.txt | 3 ++- .../ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt | 3 ++- .../ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt | 3 ++- .../ApprovedApi/FluentAssertions/netstandard2.0.verified.txt | 3 ++- .../ApprovedApi/FluentAssertions/netstandard2.1.verified.txt | 3 ++- 9 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs b/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs index 45010cb0ce..3b3e96779f 100644 --- a/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs +++ b/Src/FluentAssertions/Primitives/HttpResponseMessageAssertions.cs @@ -230,5 +230,7 @@ public AndConstraint NotHaveStatusCode(HttpStatusCode unexpected, s private bool IsServerError() => (int)Subject.StatusCode is >= 500 and <= 599; private bool IsClientError() => (int)Subject.StatusCode is >= 400 and <= 499; + + protected override string Identifier => "HTTP response message"; } } diff --git a/Src/FluentAssertions/Types/MethodBaseAssertions.cs b/Src/FluentAssertions/Types/MethodBaseAssertions.cs index ddf91bef42..2136b6a769 100644 --- a/Src/FluentAssertions/Types/MethodBaseAssertions.cs +++ b/Src/FluentAssertions/Types/MethodBaseAssertions.cs @@ -93,8 +93,6 @@ public AndConstraint NotHaveAccessModifier(CSharpAccessModifier acc return new AndConstraint((TAssertions)this); } - protected override string Identifier => "methodBase"; - internal static string GetParameterString(MethodBase methodBase) { IEnumerable parameterTypes = methodBase.GetParameters().Select(p => p.ParameterType); diff --git a/Src/FluentAssertions/Xml/XmlElementAssertions.cs b/Src/FluentAssertions/Xml/XmlElementAssertions.cs index 11283d19be..46814dc5fe 100644 --- a/Src/FluentAssertions/Xml/XmlElementAssertions.cs +++ b/Src/FluentAssertions/Xml/XmlElementAssertions.cs @@ -165,5 +165,7 @@ public AndWhichConstraint HaveElementWithNames return new AndWhichConstraint(this, element); } + + protected override string Identifier => "XML element"; } } diff --git a/Src/FluentAssertions/Xml/XmlNodeAssertions.cs b/Src/FluentAssertions/Xml/XmlNodeAssertions.cs index 6abe32d325..a8d571f05d 100644 --- a/Src/FluentAssertions/Xml/XmlNodeAssertions.cs +++ b/Src/FluentAssertions/Xml/XmlNodeAssertions.cs @@ -80,6 +80,6 @@ public AndConstraint NotBeEquivalentTo(XmlNode unexpected, string b ///

/// Returns the type of the subject the assertion applies on. /// - protected override string Identifier => "Xml Node"; + protected override string Identifier => "XML node"; } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index dbc62d6212..4f01de4ab7 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -1922,6 +1922,7 @@ namespace FluentAssertions.Primitives where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions { protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } @@ -2348,7 +2349,6 @@ namespace FluentAssertions.Types where TAssertions : FluentAssertions.Types.MethodBaseAssertions { protected MethodBaseAssertions(TSubject subject) { } - protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } } @@ -2645,6 +2645,7 @@ namespace FluentAssertions.Xml public class XmlElementAssertions : FluentAssertions.Xml.XmlNodeAssertions { public XmlElementAssertions(System.Xml.XmlElement xmlElement) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAttribute(string expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveAttributeWithNamespace(string expectedName, string expectedNamespace, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expectedName, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 4cf77e0872..ee1154cbe1 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -1922,6 +1922,7 @@ namespace FluentAssertions.Primitives where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions { protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } @@ -2350,7 +2351,6 @@ namespace FluentAssertions.Types where TAssertions : FluentAssertions.Types.MethodBaseAssertions { protected MethodBaseAssertions(TSubject subject) { } - protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } } @@ -2647,6 +2647,7 @@ namespace FluentAssertions.Xml public class XmlElementAssertions : FluentAssertions.Xml.XmlNodeAssertions { public XmlElementAssertions(System.Xml.XmlElement xmlElement) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAttribute(string expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveAttributeWithNamespace(string expectedName, string expectedNamespace, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expectedName, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index f95ce03797..75bcd1c0bf 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -1922,6 +1922,7 @@ namespace FluentAssertions.Primitives where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions { protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } @@ -2350,7 +2351,6 @@ namespace FluentAssertions.Types where TAssertions : FluentAssertions.Types.MethodBaseAssertions { protected MethodBaseAssertions(TSubject subject) { } - protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } } @@ -2647,6 +2647,7 @@ namespace FluentAssertions.Xml public class XmlElementAssertions : FluentAssertions.Xml.XmlNodeAssertions { public XmlElementAssertions(System.Xml.XmlElement xmlElement) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAttribute(string expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveAttributeWithNamespace(string expectedName, string expectedNamespace, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expectedName, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index f213fcd302..4646a938bf 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -1875,6 +1875,7 @@ namespace FluentAssertions.Primitives where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions { protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } @@ -2301,7 +2302,6 @@ namespace FluentAssertions.Types where TAssertions : FluentAssertions.Types.MethodBaseAssertions { protected MethodBaseAssertions(TSubject subject) { } - protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } } @@ -2598,6 +2598,7 @@ namespace FluentAssertions.Xml public class XmlElementAssertions : FluentAssertions.Xml.XmlNodeAssertions { public XmlElementAssertions(System.Xml.XmlElement xmlElement) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAttribute(string expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveAttributeWithNamespace(string expectedName, string expectedNamespace, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expectedName, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index a92ed9ff80..7ec995f597 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -1922,6 +1922,7 @@ namespace FluentAssertions.Primitives where TAssertions : FluentAssertions.Primitives.HttpResponseMessageAssertions { protected HttpResponseMessageAssertions(System.Net.Http.HttpResponseMessage value) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint BeRedirection(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint BeSuccessful(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveClientError(string because = "", params object[] becauseArgs) { } @@ -2350,7 +2351,6 @@ namespace FluentAssertions.Types where TAssertions : FluentAssertions.Types.MethodBaseAssertions { protected MethodBaseAssertions(TSubject subject) { } - protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier accessModifier, string because = "", params object[] becauseArgs) { } } @@ -2647,6 +2647,7 @@ namespace FluentAssertions.Xml public class XmlElementAssertions : FluentAssertions.Xml.XmlNodeAssertions { public XmlElementAssertions(System.Xml.XmlElement xmlElement) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveAttribute(string expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveAttributeWithNamespace(string expectedName, string expectedNamespace, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expectedName, string because = "", params object[] becauseArgs) { } From fb58d8f65fbc21bbc71f0d1c83e9a01a84c56ad2 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 16 Jan 2022 14:23:35 +0100 Subject: [PATCH 13/25] Bump Nuke/GitVersion to their latest --- Build/Build.cs | 15 +++++++++++---- Build/_build.csproj | 4 ++-- build.ps1 | 18 ++++++++++++++---- build.sh | 16 +++++++++++++--- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/Build/Build.cs b/Build/Build.cs index 29b8e4ee15..1085e168c6 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -78,11 +78,18 @@ class Build : NukeBuild { if (EnvironmentInfo.IsWin) { - Xunit2(s => s - .SetFramework("net47") - .AddTargetAssemblies(GlobFiles( + Xunit2(s => + { + IReadOnlyCollection testAssemblies = GlobFiles( Solution.Specs.FluentAssertions_Specs.Directory, - "bin/Debug/net47/*.Specs.dll").NotEmpty())); + "bin/Debug/net47/*.Specs.dll"); + + Assert.NotEmpty(testAssemblies.ToList()); + + return s + .SetFramework("net47") + .AddTargetAssemblies(testAssemblies); + }); } DotNetTest(s => s diff --git a/Build/_build.csproj b/Build/_build.csproj index b2677c5693..7ad25ada24 100644 --- a/Build/_build.csproj +++ b/Build/_build.csproj @@ -8,9 +8,9 @@ ..\ - + - + diff --git a/build.ps1 b/build.ps1 index 7403af110e..b3a8824d33 100644 --- a/build.ps1 +++ b/build.ps1 @@ -14,15 +14,16 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.nuke\temp" +$TempDirectory = "$PSScriptRoot\.nuke\temp" -$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetGlobalFile = "$PSScriptRoot\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 $env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:DOTNET_ROLL_FORWARD = "Major" $env:NUKE_TELEMETRY_OPTOUT = 1 ########################################################################### @@ -34,9 +35,18 @@ function ExecSafe([scriptblock] $cmd) { if ($LASTEXITCODE) { exit $LASTEXITCODE } } +# Print environment variables +# WARNING: Make sure that secrets are actually scrambled in build log +# Get-Item -Path Env:* | Sort-Object -Property Name | ForEach-Object {"{0}={1}" -f $_.Name,$_.Value} + +# Check if any dotnet is installed +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue)) { + ExecSafe { & dotnet --info } +} + # If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` - $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { @@ -66,5 +76,5 @@ else { Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" -ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 435dac52bc..f5bf05e1c4 100755 --- a/build.sh +++ b/build.sh @@ -10,15 +10,16 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/Build/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" +TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" -DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_GLOBAL_FILE="$SCRIPT_DIR/global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 export DOTNET_MULTILEVEL_LOOKUP=0 +export DOTNET_ROLL_FORWARD="Major" export NUKE_TELEMETRY_OPTOUT=1 ########################################################################### @@ -29,6 +30,15 @@ function FirstJsonValue { perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" } +# Print environment variables +# WARNING: Make sure that secrets are actually scrambled in build log +# env | sort + +# Check if any dotnet is installed +if [[ -x "$(command -v dotnet)" ]]; then + dotnet --info +fi + # If dotnet CLI is installed globally and it matches requested version, use for execution if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then export DOTNET_EXE="$(command -v dotnet)" @@ -59,5 +69,5 @@ fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" -"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" From 4eacf0d9bcf27e3418b88c8c0f96b6a5a4418387 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Jan 2022 14:27:35 +0100 Subject: [PATCH 14/25] Add xml documentation on Identifier about user-friendliness --- Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs b/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs index b3a56a1e56..80e53e1490 100644 --- a/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs +++ b/Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs @@ -416,6 +416,7 @@ public AndConstraint Match(Expression> predicate, /// /// Returns the type of the subject the assertion applies on. + /// It should be a user-friendly name as it is included in the failure message. /// protected abstract string Identifier { get; } From ca0d5e19625c6bb848fc1fe9e6cf91082068f558 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 16 Jan 2022 14:27:36 +0100 Subject: [PATCH 15/25] PRs get a unique build number prepanded This ensures that force pushes to and rebases of the branches a PR is pointing at still result in a unique Nuget package. --- .github/workflows/build.yml | 11 +++++------ .nuke/build.schema.json | 10 ++++++++++ Build/Build.cs | 30 +++++++++++++++++++++++++++++- GitVersion.yml | 6 ++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d4e175544..d86031f9ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,13 +32,12 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 2.1.x - - - name: Install NUKE - run: | - dotnet tool install --global Nuke.GlobalTool - + - name: Run NUKE - run: nuke + run: ./build.ps1 + env: + BranchSpec: ${{ github.ref }} + BuildNumber: ${{ github.run_number}} - name: Upload artifacts uses: actions/upload-artifact@v2 diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index a2c28c228d..813d3f4446 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -6,6 +6,14 @@ "build": { "type": "object", "properties": { + "BranchSpec": { + "type": "string", + "description": "A branch specification such as develop or refs/pull/1775/merge" + }, + "BuildNumber": { + "type": "string", + "description": "An incrementing build number as provided by the build engine" + }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -64,6 +72,7 @@ "type": "string", "enum": [ "ApiChecks", + "CalculateNugetVersion", "Clean", "Compile", "Pack", @@ -84,6 +93,7 @@ "type": "string", "enum": [ "ApiChecks", + "CalculateNugetVersion", "Clean", "Compile", "Pack", diff --git a/Build/Build.cs b/Build/Build.cs index 1085e168c6..22cc7c0cf7 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -26,18 +26,45 @@ class Build : NukeBuild public static int Main() => Execute(x => x.Pack); + [Parameter("A branch specification such as develop or refs/pull/1775/merge")] + readonly string BranchSpec; + + [Parameter("An incrementing build number as provided by the build engine")] + readonly string BuildNumber; + + [Solution(GenerateProjects = true)] readonly Solution Solution; [GitVersion(Framework = "net5.0")] readonly GitVersion GitVersion; [PackageExecutable("nspec", "NSpecRunner.exe", Version = "3.1.0")] Tool NSpec3; AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts"; + string SemVer; + Target Clean => _ => _ .Executes(() => { EnsureCleanDirectory(ArtifactsDirectory); }); + Target CalculateNugetVersion => _ => _ + .Executes(() => + { + SemVer = GitVersion.SemVer; + if (IsPullRequest) + { + Serilog.Log.Information( + "Branch spec {branchspec} is a pull request. Adding build number {buildnumber}", + BranchSpec, BuildNumber); + + SemVer = string.Join('.', GitVersion.SemVer.Split('.').Take(3).Union(new [] { BuildNumber })); + } + + Serilog.Log.Information("SemVer = {semver}", SemVer); + }); + + bool IsPullRequest => BranchSpec != null && BranchSpec.Contains("pull", StringComparison.InvariantCultureIgnoreCase); + Target Restore => _ => _ .DependsOn(Clean) .Executes(() => @@ -135,6 +162,7 @@ from framework in supportedFrameworks .DependsOn(ApiChecks) .DependsOn(TestFrameworks) .DependsOn(UnitTests) + .DependsOn(CalculateNugetVersion) .Executes(() => { DotNetPack(s => s @@ -142,6 +170,6 @@ from framework in supportedFrameworks .SetOutputDirectory(ArtifactsDirectory) .SetConfiguration("Release") .EnableContinuousIntegrationBuild() // Necessary for deterministic builds - .SetVersion(GitVersion.NuGetVersionV2)); + .SetVersion(SemVer)); }); } diff --git a/GitVersion.yml b/GitVersion.yml index aec62da423..99fad930de 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -3,5 +3,11 @@ branches: release: regex: releases?[/-] tag: rc + pull-request: + mode: ContinuousDeployment + regex: ((pull|pull\-requests|pr)[/-]|[/-](merge)) + tag: pr + tag-number-pattern: '[/-]?(?\d+)' + prevent-increment-of-merged-branch-version: false ignore: sha: [] From 915dbcae7eb9e6c7b7a5a0691f73011427235452 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Jan 2022 14:43:57 +0100 Subject: [PATCH 16/25] Relax When_the_execution_time_of_an_async_action_is_less_than_a_limit_it_should_not_throw --- .../Specialized/ExecutionTimeAssertionsSpecs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FluentAssertions.Specs/Specialized/ExecutionTimeAssertionsSpecs.cs b/Tests/FluentAssertions.Specs/Specialized/ExecutionTimeAssertionsSpecs.cs index 5b7d12ff1d..64c291e032 100644 --- a/Tests/FluentAssertions.Specs/Specialized/ExecutionTimeAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Specialized/ExecutionTimeAssertionsSpecs.cs @@ -163,7 +163,7 @@ public void When_the_execution_time_of_an_async_action_is_less_than_a_limit_it_s Func someAction = () => Task.Delay(TimeSpan.FromMilliseconds(100)); // Act - Action act = () => someAction.ExecutionTime().Should().BeLessThan(2.Seconds()); + Action act = () => someAction.ExecutionTime().Should().BeLessThan(20.Seconds()); // Assert act.Should().NotThrow(); From 94d349146b7b317a363dd7ac63fe23084466fa98 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 16 Jan 2022 14:27:50 +0100 Subject: [PATCH 17/25] Move Nuget pushing from Yaml into Nuke --- .github/workflows/build.yml | 20 ++++++++----------- .nuke/build.schema.json | 6 ++++++ Build/.editorconfig | 7 +++++-- Build/Build.cs | 38 +++++++++++++++++++++++++++++++++---- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d86031f9ab..890a5ff95e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,10 +2,10 @@ name: build on: pull_request: - paths-ignore: + paths-ignore: - docs/** push: - paths-ignore: + paths-ignore: - docs/** jobs: @@ -17,17 +17,17 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - + - name: Setup .NET 5 uses: actions/setup-dotnet@v1 with: dotnet-version: 5.0.x - + - name: Setup .NET 3.1 uses: actions/setup-dotnet@v1 with: dotnet-version: 3.1.x - + - name: Setup .NET 2.1 uses: actions/setup-dotnet@v1 with: @@ -38,15 +38,11 @@ jobs: env: BranchSpec: ${{ github.ref }} BuildNumber: ${{ github.run_number}} + ApiKey: ${{ secrets.NUGETAPIKEY }} - name: Upload artifacts uses: actions/upload-artifact@v2 with: path: ./Artifacts/* - - - name: Push to Nuget - run: dotnet nuget push .\artifacts\*.nupkg --api-key "$env:ApiKey" --skip-duplicate --no-symbols true --source https://api.nuget.org/v3/index.json - if: contains(github.ref, 'refs/tags/') - env: - ApiKey: ${{ secrets.NUGETAPIKEY }} - + + diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 813d3f4446..e85daa3006 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -6,6 +6,10 @@ "build": { "type": "object", "properties": { + "ApiKey": { + "type": "string", + "description": "The key to push to Nuget" + }, "BranchSpec": { "type": "string", "description": "A branch specification such as develop or refs/pull/1775/merge" @@ -76,6 +80,7 @@ "Clean", "Compile", "Pack", + "Push", "Restore", "TestFrameworks", "UnitTests" @@ -97,6 +102,7 @@ "Clean", "Compile", "Pack", + "Push", "Restore", "TestFrameworks", "UnitTests" diff --git a/Build/.editorconfig b/Build/.editorconfig index 21da7492e9..5641e0f6df 100644 --- a/Build/.editorconfig +++ b/Build/.editorconfig @@ -9,5 +9,8 @@ csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning -dotnet_diagnostic.IDE0044.severity = none -dotnet_diagnostic.IDE0051.severity = none +dotnet_diagnostic.ide0044.severity = none +dotnet_diagnostic.ide0051.severity = none + +# ReSharper properties +resharper_place_field_attribute_on_same_line = false diff --git a/Build/Build.cs b/Build/Build.cs index 22cc7c0cf7..a09090bf5a 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Nuke.Common; using Nuke.Common.Execution; @@ -24,7 +26,7 @@ class Build : NukeBuild - Microsoft VSCode https://nuke.build/vscode */ - public static int Main() => Execute(x => x.Pack); + public static int Main() => Execute(x => x.Push); [Parameter("A branch specification such as develop or refs/pull/1775/merge")] readonly string BranchSpec; @@ -32,10 +34,17 @@ class Build : NukeBuild [Parameter("An incrementing build number as provided by the build engine")] readonly string BuildNumber; + [Parameter("The key to push to Nuget")] + readonly string ApiKey; - [Solution(GenerateProjects = true)] readonly Solution Solution; - [GitVersion(Framework = "net5.0")] readonly GitVersion GitVersion; - [PackageExecutable("nspec", "NSpecRunner.exe", Version = "3.1.0")] Tool NSpec3; + [Solution(GenerateProjects = true)] + readonly Solution Solution; + + [GitVersion(Framework = "net5.0")] + readonly GitVersion GitVersion; + + [PackageExecutable("nspec", "NSpecRunner.exe", Version = "3.1.0")] + Tool NSpec3; AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts"; @@ -172,4 +181,25 @@ from framework in supportedFrameworks .EnableContinuousIntegrationBuild() // Necessary for deterministic builds .SetVersion(SemVer)); }); + + Target Push => _ => _ + .DependsOn(Pack) + .OnlyWhenDynamic(() => IsTag) + .Executes(() => + { + var packages = GlobFiles(ArtifactsDirectory, "*.nupkg"); + + Assert.NotEmpty(packages.ToList()); + + DotNetNuGetPush(s => s + .SetApiKey(ApiKey) + .EnableSkipDuplicate() + .SetSource("https://api.nuget.org/v3/index.json") + .EnableNoSymbols() + .CombineWith(packages, + (v, path) => v.SetTargetPath(path))); + }); + + bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.InvariantCultureIgnoreCase); + } From 8165b67bd4987f71fee965ef536029102f2591b1 Mon Sep 17 00:00:00 2001 From: Leandro Muller Date: Mon, 17 Jan 2022 13:11:14 -0300 Subject: [PATCH 18/25] `OccurredEvent` ordering on monitored object is now done via thread-safe counter (#1773) Added EventRaisedOrder to correctly track order of raised events in Monitor --- Src/FluentAssertions/Events/EventMonitor.cs | 6 ++- Src/FluentAssertions/Events/EventRecorder.cs | 11 +++-- Src/FluentAssertions/Events/OccurredEvent.cs | 5 +++ Src/FluentAssertions/Events/RecordedEvent.cs | 8 +++- .../Events/ThreadSafeSequenceGenerator.cs | 20 +++++++++ .../FluentAssertions/net47.verified.txt | 1 + .../netcoreapp2.1.verified.txt | 1 + .../netcoreapp3.0.verified.txt | 1 + .../netstandard2.1.verified.txt | 1 + .../Events/EventAssertionSpecs.cs | 43 +++++++++++++++++++ docs/_pages/releases.md | 1 + 11 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs diff --git a/Src/FluentAssertions/Events/EventMonitor.cs b/Src/FluentAssertions/Events/EventMonitor.cs index 132154b758..41cc874c2c 100644 --- a/Src/FluentAssertions/Events/EventMonitor.cs +++ b/Src/FluentAssertions/Events/EventMonitor.cs @@ -30,6 +30,8 @@ public EventMonitor(object eventSource, Func utcNow) public T Subject => (T)subject.Target; + private readonly ThreadSafeSequenceGenerator threadSafeSequenceGenerator = new(); + public EventMetadata[] MonitoredEvents { get @@ -49,7 +51,7 @@ public OccurredEvent[] OccurredEvents from eventName in recorderMap.Keys let recording = GetRecordingFor(eventName) from @event in recording - orderby @event.TimestampUtc + orderby @event.Sequence select @event; return query.ToArray(); @@ -125,7 +127,7 @@ private void AttachEventHandler(EventInfo eventInfo, Func utcNow) { if (!recorderMap.TryGetValue(eventInfo.Name, out _)) { - var recorder = new EventRecorder(subject.Target, eventInfo.Name, utcNow); + var recorder = new EventRecorder(subject.Target, eventInfo.Name, utcNow, threadSafeSequenceGenerator); if (recorderMap.TryAdd(eventInfo.Name, recorder)) { recorder.Attach(subject, eventInfo); diff --git a/Src/FluentAssertions/Events/EventRecorder.cs b/Src/FluentAssertions/Events/EventRecorder.cs index 647e216f9e..abea8d3b2b 100644 --- a/Src/FluentAssertions/Events/EventRecorder.cs +++ b/Src/FluentAssertions/Events/EventRecorder.cs @@ -25,11 +25,13 @@ internal sealed class EventRecorder : IEventRecording, IDisposable /// The object events are recorded from /// The name of the event that's recorded /// A delegate to get the current date and time in UTC format. - public EventRecorder(object eventRaiser, string eventName, Func utcNow) + /// Class used to generate a sequence in a thread-safe manner. + public EventRecorder(object eventRaiser, string eventName, Func utcNow, ThreadSafeSequenceGenerator sequenceGenerator) { this.utcNow = utcNow; EventObject = eventRaiser; EventName = eventName; + this.sequenceGenerator = sequenceGenerator; } /// @@ -40,6 +42,8 @@ public EventRecorder(object eventRaiser, string eventName, Func utcNow /// public string EventName { get; } + private readonly ThreadSafeSequenceGenerator sequenceGenerator; + public Type EventHandlerType { get; private set; } public void Attach(WeakReference subject, EventInfo eventInfo) @@ -77,7 +81,7 @@ public void RecordEvent(params object[] parameters) { lock (lockable) { - raisedEvents.Add(new RecordedEvent(utcNow(), parameters)); + raisedEvents.Add(new RecordedEvent(utcNow(), sequenceGenerator.Increment(), parameters)); } } @@ -105,7 +109,8 @@ public IEnumerator GetEnumerator() { EventName = EventName, Parameters = @event.Parameters, - TimestampUtc = @event.TimestampUtc + TimestampUtc = @event.TimestampUtc, + Sequence = @event.Sequence }; } } diff --git a/Src/FluentAssertions/Events/OccurredEvent.cs b/Src/FluentAssertions/Events/OccurredEvent.cs index 937025bfd3..761341ea29 100644 --- a/Src/FluentAssertions/Events/OccurredEvent.cs +++ b/Src/FluentAssertions/Events/OccurredEvent.cs @@ -21,5 +21,10 @@ public class OccurredEvent /// The exact date and time of the occurrence in . /// public DateTime TimestampUtc { get; set; } + + /// + /// The order in which this event was raised on the monitored object. + /// + public int Sequence { get; set; } } } diff --git a/Src/FluentAssertions/Events/RecordedEvent.cs b/Src/FluentAssertions/Events/RecordedEvent.cs index 1930a605ee..69168a7692 100644 --- a/Src/FluentAssertions/Events/RecordedEvent.cs +++ b/Src/FluentAssertions/Events/RecordedEvent.cs @@ -12,10 +12,11 @@ internal class RecordedEvent /// /// Default constructor stores the parameters the event was raised with /// - public RecordedEvent(DateTime utcNow, params object[] parameters) + public RecordedEvent(DateTime utcNow, int sequence, params object[] parameters) { Parameters = parameters; TimestampUtc = utcNow; + Sequence = sequence; } /// @@ -27,5 +28,10 @@ public RecordedEvent(DateTime utcNow, params object[] parameters) /// Parameters for the event /// public object[] Parameters { get; } + + /// + /// The order in which this event was invoked on the monitored object. + /// + public int Sequence { get; } } } diff --git a/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs b/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs new file mode 100644 index 0000000000..4ccf4ad443 --- /dev/null +++ b/Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs @@ -0,0 +1,20 @@ +using System.Threading; + +namespace FluentAssertions.Events +{ + /// + /// Generates a sequence in a thread-safe manner. + /// + internal sealed class ThreadSafeSequenceGenerator + { + private int sequence = -1; + + /// + /// Increments the current sequence. + /// + public int Increment() + { + return Interlocked.Increment(ref sequence); + } + } +} diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 4f01de4ab7..3abc0f26e9 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events public OccurredEvent() { } public string EventName { get; set; } public object[] Parameters { get; set; } + public int Sequence { get; set; } public System.DateTime TimestampUtc { get; set; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index ee1154cbe1..2163da6552 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events public OccurredEvent() { } public string EventName { get; set; } public object[] Parameters { get; set; } + public int Sequence { get; set; } public System.DateTime TimestampUtc { get; set; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 75bcd1c0bf..a97c3e2b3a 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events public OccurredEvent() { } public string EventName { get; set; } public object[] Parameters { get; set; } + public int Sequence { get; set; } public System.DateTime TimestampUtc { get; set; } } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 7ec995f597..17041a01d3 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -1221,6 +1221,7 @@ namespace FluentAssertions.Events public OccurredEvent() { } public string EventName { get; set; } public object[] Parameters { get; set; } + public int Sequence { get; set; } public System.DateTime TimestampUtc { get; set; } } } diff --git a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs index 432996e070..f07bc28c78 100644 --- a/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Events/EventAssertionSpecs.cs @@ -411,6 +411,28 @@ public void When_constraints_are_specified_it_should_filter_the_events_based_on_ .Which.PropertyName.Should().Be("Boo"); } + [Fact] + public void When_events_are_raised_regardless_of_time_tick_it_should_return_by_invokation_order() + { + // Arrange + var observable = new TestEventRaisingInOrder(); + var utcNow = 11.January(2022).At(12, 00).AsUtc(); + using var monitor = observable.Monitor(() => utcNow); + + // Act + observable.RaiseAllEvents(); + + // Assert + monitor.OccurredEvents[0].EventName.Should().Be(nameof(TestEventRaisingInOrder.InterfaceEvent)); + monitor.OccurredEvents[0].Sequence.Should().Be(0); + + monitor.OccurredEvents[1].EventName.Should().Be(nameof(TestEventRaisingInOrder.Interface2Event)); + monitor.OccurredEvents[1].Sequence.Should().Be(1); + + monitor.OccurredEvents[2].EventName.Should().Be(nameof(TestEventRaisingInOrder.Interface3Event)); + monitor.OccurredEvents[2].Sequence.Should().Be(2); + } + #endregion #region Should(Not)RaisePropertyChanged events @@ -817,6 +839,22 @@ public void RaiseBothEvents() } } + private class TestEventRaisingInOrder : IEventRaisingInterface, IEventRaisingInterface2, IEventRaisingInterface3 + { + public event EventHandler Interface3Event; + + public event EventHandler Interface2Event; + + public event EventHandler InterfaceEvent; + + public void RaiseAllEvents() + { + InterfaceEvent?.Invoke(this, EventArgs.Empty); + Interface2Event?.Invoke(this, EventArgs.Empty); + Interface3Event?.Invoke(this, EventArgs.Empty); + } + } + public interface IEventRaisingInterface { event EventHandler InterfaceEvent; @@ -827,6 +865,11 @@ public interface IEventRaisingInterface2 event EventHandler Interface2Event; } + public interface IEventRaisingInterface3 + { + event EventHandler Interface3Event; + } + public interface IInheritsEventRaisingInterface : IEventRaisingInterface { } diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 6e1426a6bb..5b69bb9c9b 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -18,6 +18,7 @@ sidebar: * `ContainItemsAssignableTo` now expects at least one item assignable to `T` - [#1765](https://github.com/fluentassertions/fluentassertions/pull/1765) * Querying methods on classes, e.g. `typeof(MyController).Methods()`, now also includes static methods - [#1740](https://github.com/fluentassertions/fluentassertions/pull/1740) * Variable name is not captured after await assertion - [#1770](https://github.com/fluentassertions/fluentassertions/pull/1770) +* `OccurredEvent` ordering on monitored object is now done via thread-safe counter - [#1773](https://github.com/fluentassertions/fluentassertions/pull/1773) * Avoid a `NullReferenceException` when testing an application compiled with .NET Native - [#1776](https://github.com/fluentassertions/fluentassertions/pull/1776) ## 6.3.0 From ac04547a33db977d6edcfabcdf9fda8c055f98f2 Mon Sep 17 00:00:00 2001 From: amosonn Date: Mon, 17 Jan 2022 20:08:43 +0100 Subject: [PATCH 19/25] override Identifier for BufferedStream (#1772) Fix default Identifier of BufferedStreamAssertions Co-authored-by: Amos Onn --- AcceptApiChanges.sh | 0 Src/FluentAssertions/Streams/BufferedStreamAssertions.cs | 2 ++ .../ApprovedApi/FluentAssertions/net47.verified.txt | 1 + .../ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt | 1 + .../ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt | 1 + .../ApprovedApi/FluentAssertions/netstandard2.0.verified.txt | 1 + .../ApprovedApi/FluentAssertions/netstandard2.1.verified.txt | 1 + 7 files changed, 7 insertions(+) mode change 100644 => 100755 AcceptApiChanges.sh diff --git a/AcceptApiChanges.sh b/AcceptApiChanges.sh old mode 100644 new mode 100755 diff --git a/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs b/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs index 018e873a61..8b193ba680 100644 --- a/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs +++ b/Src/FluentAssertions/Streams/BufferedStreamAssertions.cs @@ -27,6 +27,8 @@ public BufferedStreamAssertions(BufferedStream stream) { } + protected override string Identifier => "buffered stream"; + #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 /// /// Asserts that the current has the buffer size. diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index 3abc0f26e9..64e793a717 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -2292,6 +2292,7 @@ namespace FluentAssertions.Streams where TAssertions : FluentAssertions.Streams.BufferedStreamAssertions { public BufferedStreamAssertions(System.IO.BufferedStream stream) { } + protected override string Identifier { get; } } public class StreamAssertions : FluentAssertions.Streams.StreamAssertions { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index 2163da6552..78fbc1dd87 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -2292,6 +2292,7 @@ namespace FluentAssertions.Streams where TAssertions : FluentAssertions.Streams.BufferedStreamAssertions { public BufferedStreamAssertions(System.IO.BufferedStream stream) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveBufferSize(int expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveBufferSize(int unexpected, string because = "", params object[] becauseArgs) { } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index a97c3e2b3a..a71b144a73 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -2292,6 +2292,7 @@ namespace FluentAssertions.Streams where TAssertions : FluentAssertions.Streams.BufferedStreamAssertions { public BufferedStreamAssertions(System.IO.BufferedStream stream) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveBufferSize(int expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveBufferSize(int unexpected, string because = "", params object[] becauseArgs) { } } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 4646a938bf..da28d3d67b 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -2244,6 +2244,7 @@ namespace FluentAssertions.Streams where TAssertions : FluentAssertions.Streams.BufferedStreamAssertions { public BufferedStreamAssertions(System.IO.BufferedStream stream) { } + protected override string Identifier { get; } } public class StreamAssertions : FluentAssertions.Streams.StreamAssertions { diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 17041a01d3..a2ecfc1c6f 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -2292,6 +2292,7 @@ namespace FluentAssertions.Streams where TAssertions : FluentAssertions.Streams.BufferedStreamAssertions { public BufferedStreamAssertions(System.IO.BufferedStream stream) { } + protected override string Identifier { get; } public FluentAssertions.AndConstraint HaveBufferSize(int expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveBufferSize(int unexpected, string because = "", params object[] becauseArgs) { } } From 94a210ac25a1c84500a7492f339e3c7c2455d175 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Fri, 21 Jan 2022 19:44:23 +0100 Subject: [PATCH 20/25] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03c59e98de..a8c1e9ca5f 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ See https://www.fluentassertions.com for [background information](https://fluent Originally authored by Dennis Doomen with Jonas Nyrup as the productive side-kick. Notable contributions were provided by Artur Krajewski, Lukas Grützmacher and David Omid. # How do I build this? -Install Visual Studio 2019 16.9+ or JetBrains Rider 2017.1 and Build Tools 2017 and run. -You will also need to have .NET Framework 4.7 SDK and .NET 5.0 SDK installed - check [global.json](global.json) for the current minimum required version. +Install Visual Studio 2019 16.9+ or JetBrains Rider 2021.1.0 as well as the Build Tools 2019 (including the Universal Windows Platform build tools). +You will also need to have .NET Framework 4.7 SDK and .NET 5.0 SDK installed. Check [global.json](global.json) for the current minimum required version. # What are these Approval.Tests? This is a special set of tests that use the [Verify](https://github.com/VerifyTests/Verify) project to verify whether you've introduced any breaking changes in the public API of the library. From 0b4eaa0d8d8eb5c0fcd61049e8aaeffda7d0a7e1 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 22 Jan 2022 10:01:05 +0100 Subject: [PATCH 21/25] Fix TryGetValue for dictionary like enumerables --- .../Common/DictionaryHelpers.cs | 14 +++++++--- .../GenericDictionaryAssertionSpecs.cs | 26 +++++++++++++++++-- docs/_pages/releases.md | 1 + 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/Src/FluentAssertions/Common/DictionaryHelpers.cs b/Src/FluentAssertions/Common/DictionaryHelpers.cs index 99b3e92d6d..a152c63b00 100644 --- a/Src/FluentAssertions/Common/DictionaryHelpers.cs +++ b/Src/FluentAssertions/Common/DictionaryHelpers.cs @@ -58,9 +58,17 @@ public static bool TryGetValue(this TCollection colle static bool TryGetValue(TCollection collection, TKey key, out TValue value) { Func areSameOrEqual = ObjectExtensions.GetComparer(); - KeyValuePair matchingPair = collection.FirstOrDefault(kvp => areSameOrEqual(kvp.Key, key)); - value = matchingPair.Value; - return matchingPair.Equals(default(KeyValuePair)); + foreach (var kvp in collection) + { + if (areSameOrEqual(kvp.Key, key)) + { + value = kvp.Value; + return true; + } + } + + value = default; + return false; } } diff --git a/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs index a60f261f50..86c451cf7d 100644 --- a/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Collections/GenericDictionaryAssertionSpecs.cs @@ -874,7 +874,7 @@ public void When_asserting_dictionary_with_items_is_not_empty_it_should_succeed( dictionary.Should().NotBeEmpty(); } - #if !NET5_0_OR_GREATER +#if !NET5_0_OR_GREATER [Fact] public void When_asserting_dictionary_with_items_is_not_empty_it_should_enumerate_the_dictionary_only_once() @@ -889,7 +889,7 @@ public void When_asserting_dictionary_with_items_is_not_empty_it_should_enumerat trackingDictionary.Enumerator.LoopCount.Should().Be(1); } - #endif +#endif [Fact] public void When_asserting_dictionary_without_items_is_not_empty_it_should_fail() @@ -2638,6 +2638,15 @@ public void When_a_dictionary_like_collection_contains_the_expected_key_it_shoul act.Should().NotThrow(); } + [Theory] + [MemberData(nameof(SingleDictionaryData))] + public void When_a_dictionary_like_collection_contains_the_expected_key_and_value_it_should_succeed(T subject) + where T : IEnumerable> + { + // Assert + subject.Should().Contain(1, 42); + } + [Theory] [MemberData(nameof(SingleDictionaryData))] public void When_a_dictionary_like_collection_contains_the_expected_value_it_should_succeed(T subject) @@ -2675,6 +2684,19 @@ private static object[] Dictionaries() }; } + [Fact] + public void When_a_dictionary_like_collection_contains_the_default_key_it_should_succeed() + { + // Arrange + var subject = new List>() { new(0, 0) }; + + // Act + Action act = () => subject.Should().Contain(0, 0); + + // Assert + act.Should().NotThrow(); + } + /// /// This class only implements , /// as also implements . diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 5b69bb9c9b..cf5bcc54ac 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -20,6 +20,7 @@ sidebar: * Variable name is not captured after await assertion - [#1770](https://github.com/fluentassertions/fluentassertions/pull/1770) * `OccurredEvent` ordering on monitored object is now done via thread-safe counter - [#1773](https://github.com/fluentassertions/fluentassertions/pull/1773) * Avoid a `NullReferenceException` when testing an application compiled with .NET Native - [#1776](https://github.com/fluentassertions/fluentassertions/pull/1776) +* `[Not]Contain(key, value)` for dictionary-like enumerables incorrectly checked if the key was present - [#1786](https://github.com/fluentassertions/fluentassertions/pull/1786) ## 6.3.0 From eac13736d33da443936fc449d2b8e7c79bbe18da Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 22 Jan 2022 11:15:56 +0100 Subject: [PATCH 22/25] Optimize build times --- Src/FluentAssertions/FluentAssertions.csproj | 7 +++++ .../FluentAssertions.Specs.csproj | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Src/FluentAssertions/FluentAssertions.csproj b/Src/FluentAssertions/FluentAssertions.csproj index 330b2da149..1a3d1f3de8 100644 --- a/Src/FluentAssertions/FluentAssertions.csproj +++ b/Src/FluentAssertions/FluentAssertions.csproj @@ -1,4 +1,5 @@ + net47;netcoreapp2.1;netcoreapp3.0;netstandard2.0;netstandard2.1 True @@ -11,6 +12,7 @@ ..\..\Rules.ruleset $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true + true Dennis Doomen;Jonas Nyrup @@ -29,6 +31,11 @@ Copyright Dennis Doomen 2010-2020 9.0 + + false + false + false + <_Parameter1>FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f diff --git a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj index 5c7e276c02..9845cf5918 100644 --- a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj +++ b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj @@ -1,5 +1,6 @@ + net47;net5.0;netcoreapp2.0;netcoreapp2.1;netcoreapp3.0 9.0 @@ -8,24 +9,24 @@ false ..\..\TestRules.ruleset - $(OutputPath)$(AssemblyName).xml + $(OutputPath)$(AssemblyName).xml $(NoWarn),IDE0052,1573,1591,1712 false true + full + + + + false + false + false - + - - full - - - full - - - +