From b91d5d0bc395d0e76fd4497b87b47d7a4e866a44 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 17 Oct 2021 09:21:15 +0200 Subject: [PATCH] SAVEPOINT --- .../EquivalencyAssertionOptions.cs | 38 ++++ Src/FluentAssertions/Equivalency/INode.cs | 12 + .../Matching/MappedMemberMatchingRule.cs | 56 +++++ ...elfReferenceEquivalencyAssertionOptions.cs | 2 +- .../FluentAssertions/net47.verified.txt | 4 + .../netcoreapp2.1.verified.txt | 4 + .../netcoreapp3.0.verified.txt | 4 + .../netstandard2.0.verified.txt | 4 + .../netstandard2.1.verified.txt | 4 + .../Equivalency/BasicEquivalencySpecs.cs | 215 ++++++++++++++++-- 10 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs diff --git a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs index d073d581fd..54c5771722 100644 --- a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs @@ -2,9 +2,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using FluentAssertions.Common; using FluentAssertions.Equivalency.Execution; +using FluentAssertions.Equivalency.Matching; using FluentAssertions.Equivalency.Ordering; using FluentAssertions.Equivalency.Selection; @@ -70,6 +72,42 @@ public EquivalencyAssertionOptions> AsCollection() return new EquivalencyAssertionOptions>( new CollectionMemberAssertionOptionsDecorator(this)); } + + public EquivalencyAssertionOptions WithMapping( + Expression> expectationPropertyPath, + Expression> subjectPropertyPath) + { + return WithMapping( + expectationPropertyPath.GetMemberPath().ToString(), + subjectPropertyPath.GetMemberPath().ToString()); + } + + public EquivalencyAssertionOptions WithMapping( + Expression> expectationProperty, + Expression> subjectProperty) + { + return WithMapping( + expectationProperty.GetMemberPath().ToString(), + subjectProperty.GetMemberPath().ToString()); + } + + public EquivalencyAssertionOptions WithMapping( + string expectationPropertyPath, + string subjectPropertyPath) + { + AddMatchingRule(new MappedMemberMatchingRule(expectationPropertyPath, subjectPropertyPath)); + + return this; + } + + public EquivalencyAssertionOptions WithMapping( + string expectationPropertyName, + string subjectPropertyName) + { + AddMatchingRule(new MappedMemberMatchingRule(typeof(TNestedExpectation), expectationPropertyName, typeof(TNestedSubject), subjectPropertyName)); + + return this; + } } /// diff --git a/Src/FluentAssertions/Equivalency/INode.cs b/Src/FluentAssertions/Equivalency/INode.cs index de06fb501d..4516896c07 100644 --- a/Src/FluentAssertions/Equivalency/INode.cs +++ b/Src/FluentAssertions/Equivalency/INode.cs @@ -16,6 +16,9 @@ public interface INode /// /// Gets the name of this node. /// + /// + /// "Property2" + /// string Name { get; } /// @@ -26,11 +29,17 @@ public interface INode /// /// Gets the path from the root object UNTIL the current node, separated by dots or index/key brackets. /// + /// + /// "Parent[0].Property2" + /// string Path { get; } /// /// Gets the full path from the root object up to and including the name of the node. /// + /// + /// "Parent[0]" + /// string PathAndName { get; } /// @@ -41,6 +50,9 @@ public interface INode /// /// Gets the path including the description of the subject. /// + /// + /// "property subject.Parent[0].Property2" + /// string Description { get; } /// diff --git a/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs b/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs new file mode 100644 index 0000000000..572b53fbe3 --- /dev/null +++ b/Src/FluentAssertions/Equivalency/Matching/MappedMemberMatchingRule.cs @@ -0,0 +1,56 @@ + +// +using System; +using System.Reflection; +using FluentAssertions.Common; + +namespace FluentAssertions.Equivalency.Matching +{ + internal class MappedMemberMatchingRule : IMemberMatchingRule + { + private readonly string expectationPropertyPath; + private readonly string subjectPropertyPath; + private Type nestedExpectationType = null; + private readonly string expectationPropertyName; + private Type nestedSubjectType = null; + private readonly string subjectPropertyName; + + public MappedMemberMatchingRule(string expectationPropertyPath, string subjectPropertyPath) + { + // TODO: What if the property path is empty or null? + // TODO: What if the two paths have a different parent? + + this.expectationPropertyPath = expectationPropertyPath; + this.subjectPropertyPath = subjectPropertyPath; + } + + public MappedMemberMatchingRule(Type expectationType, string expectationPropertyName, Type subjectType, string subjectPropertyName) + { + // TODO: What if the property is more than just a member name + // TODO: declaredtype vs reflectedtype + + nestedExpectationType = expectationType; + this.expectationPropertyName = expectationPropertyName; + nestedSubjectType = subjectType; + this.subjectPropertyName = subjectPropertyName; + } + + public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions options) + { + // TODO: What if the subject property does not exist + // TODO: What if the subject is null + // TODO: What if the expectation property does not exist + // TODO: What if the expectation is null + + if (expectedMember.ReflectedType.IsSameOrInherits(nestedExpectationType) && subject.GetType() == nestedSubjectType) + { + if (expectedMember.Name == expectationPropertyName) + { + return MemberFactory.Create(subject.GetType().GetProperty(subjectPropertyName), parent); + } + } + + return null; + } + } +} diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs index 2dc043b6c9..fff0bdafc6 100644 --- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs @@ -820,7 +820,7 @@ protected TSelf AddSelectionRule(IMemberSelectionRule selectionRule) return (TSelf)this; } - private TSelf AddMatchingRule(IMemberMatchingRule matchingRule) + protected TSelf AddMatchingRule(IMemberMatchingRule matchingRule) { matchingRules.Insert(0, matchingRule); return (TSelf)this; diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index f4ea4fb655..85aa8c3b8f 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -751,6 +751,10 @@ namespace FluentAssertions.Equivalency public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyPath, string subjectPropertyPath) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyName, string subjectPropertyName) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithStrictOrderingFor(System.Linq.Expressions.Expression> expression) { } } public enum EquivalencyResult diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index a3a805d23d..9369caa185 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -751,6 +751,10 @@ namespace FluentAssertions.Equivalency public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyPath, string subjectPropertyPath) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyName, string subjectPropertyName) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithStrictOrderingFor(System.Linq.Expressions.Expression> expression) { } } public enum EquivalencyResult diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index 94a2362b0a..f5ea09f03c 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -751,6 +751,10 @@ namespace FluentAssertions.Equivalency public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyPath, string subjectPropertyPath) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyName, string subjectPropertyName) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithStrictOrderingFor(System.Linq.Expressions.Expression> expression) { } } public enum EquivalencyResult diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index 8b379c216f..97d58f5917 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -744,6 +744,10 @@ namespace FluentAssertions.Equivalency public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyPath, string subjectPropertyPath) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyName, string subjectPropertyName) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithStrictOrderingFor(System.Linq.Expressions.Expression> expression) { } } public enum EquivalencyResult diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index f4709a87bd..bdfee94803 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -751,6 +751,10 @@ namespace FluentAssertions.Equivalency public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyPath, string subjectPropertyPath) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationProperty, System.Linq.Expressions.Expression> subjectProperty) { } + public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationPropertyName, string subjectPropertyName) { } public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithStrictOrderingFor(System.Linq.Expressions.Expression> expression) { } } public enum EquivalencyResult diff --git a/Tests/FluentAssertions.Specs/Equivalency/BasicEquivalencySpecs.cs b/Tests/FluentAssertions.Specs/Equivalency/BasicEquivalencySpecs.cs index 3375bc7a01..d7f01c6755 100644 --- a/Tests/FluentAssertions.Specs/Equivalency/BasicEquivalencySpecs.cs +++ b/Tests/FluentAssertions.Specs/Equivalency/BasicEquivalencySpecs.cs @@ -16,18 +16,6 @@ namespace FluentAssertions.Specs.Equivalency { public class BasicEquivalencySpecs { - public enum LocalOtherType : byte - { - Default, - NonDefault - } - - public enum LocalType : byte - { - Default, - NonDefault - } - #region General [Fact] @@ -746,6 +734,18 @@ public void When_the_graph_contains_guids_it_should_properly_format_them() #region Selection Rules + public enum LocalOtherType : byte + { + Default, + NonDefault + } + + public enum LocalType : byte + { + Default, + NonDefault + } + [Fact] public void When_specific_properties_have_been_specified_it_should_ignore_the_other_properties() { @@ -2155,8 +2155,6 @@ public DerivedWithCovariantOverride(DerivedWithProperty prop) #endif - #endregion - public interface IInterfaceWithTwoProperties { int Value1 { get; set; } @@ -2215,6 +2213,8 @@ public void Including_an_interface_property_through_inheritance_should_work() .RespectingRuntimeTypes()); } + #endregion + #region Matching Rules [Fact] @@ -3431,6 +3431,193 @@ public void When_a_property_of_a_nested_object_doesnt_match_it_should_clearly_in #endregion + #region Mapping + + [Fact] + public void Properties_mapped_by_name_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new SubjectWithProperty1 + { + Property1 = "Hello" + }; + + var expectation = new ExpectationWithProperty2 + { + Property2 = "Hello" + }; + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping("Property2", "Property1")); + } + + [Fact] + public void Properties_mapped_using_an_expression_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new SubjectWithProperty1 + { + Property1 = "Hello" + }; + + var expectation = new ExpectationWithProperty2 + { + Property2 = "Hello" + }; + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping(e => e.Property2, e => e.Property1)); + } + + [Fact] + public void Nested_properties_mapped_by_name_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new + { + Parent = new[] + { + new SubjectWithProperty1 + { + Property1 = "Hello" + } + } + }; + + var expectation = new + { + Parent = new[] + { + new ExpectationWithProperty2 + { + Property2 = "Hello" + } + } + }; + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping("Parent.Property2", "Parent.Property1")); + } + + [Fact] + public void Nested_properties_mapped_using_an_expression_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new ParentOfSubjectWithProperty1(new[] + { + new SubjectWithProperty1 + { + Property1 = "Hello" + } + }); + + var expectation = new ParentOfExpectationWithProperty2(new[] + { + new ExpectationWithProperty2 + { + Property2 = "Hello" + } + }); + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping( + e => e.Parent[0].Property2, + s => s.Parent[0].Property1)); + } + + [Fact] + public void Nested_properties_mapped_by_the_nested_name_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new ParentOfSubjectWithProperty1(new[] + { + new SubjectWithProperty1 + { + Property1 = "Hello" + } + }); + + var expectation = new ParentOfExpectationWithProperty2(new[] + { + new ExpectationWithProperty2 + { + Property2 = "Hello" + } + }); + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping("Property2", "Property1")); + } + + [Fact] + public void Nested_properties_mapped_by_using_a_nested_expression_with_equivalent_values_should_not_fail_the_assertion() + { + // Arrange + var subject = new ParentOfSubjectWithProperty1(new[] + { + new SubjectWithProperty1 + { + Property1 = "Hello" + } + }); + + var expectation = new ParentOfExpectationWithProperty2(new[] + { + new ExpectationWithProperty2 + { + Property2 = "Hello" + } + }); + + // Act / Assert + subject.Should() + .BeEquivalentTo(expectation, opt => opt + .WithMapping( + e => e.Property2, s => s.Property1)); + } + + internal class ParentOfExpectationWithProperty2 + { + public ExpectationWithProperty2[] Parent { get; } + + public ParentOfExpectationWithProperty2(ExpectationWithProperty2[] parent) + { + Parent = parent; + } + } + + internal class ParentOfSubjectWithProperty1 + { + public SubjectWithProperty1[] Parent { get; } + + public ParentOfSubjectWithProperty1(SubjectWithProperty1[] parent) + { + Parent = parent; + } + } + + internal class SubjectWithProperty1 + { + public string Property1 { get; set; } + } + + internal class ExpectationWithProperty2 + { + public string Property2 { get; set; } + } + + #endregion + #region Cyclic References [Fact]