From df5dbea51294a09410c080c3ca875e31ac7700db Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Mon, 5 Nov 2018 22:17:46 -0500 Subject: [PATCH] Selecting members during object graph assertions now honors the declaring type Fixes #956 --- .../GenericCollectionAssertions.cs | 2 +- .../Common/ExpressionExtensions.cs | 18 ++++++-- .../Selection => Common}/MemberPath.cs | 29 +++++++++--- .../Common/StringExtensions.cs | 9 ---- .../EquivalencyAssertionOptions.cs | 2 +- .../ExcludeMemberByPathSelectionRule.cs | 15 ++++--- .../IncludeMemberByPathSelectionRule.cs | 15 ++++--- Tests/Shared.Specs/BasicEquivalencySpecs.cs | 45 +++++++++++++++++++ Tests/Shared.Specs/ExceptionAssertionSpecs.cs | 1 + Tests/Shared.Specs/ThrowAssertionsSpecs.cs | 1 + 10 files changed, 105 insertions(+), 32 deletions(-) rename Src/FluentAssertions/{Equivalency/Selection => Common}/MemberPath.cs (55%) diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 12e9a60e5c..0edd8ab971 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -254,7 +254,7 @@ public AndConstraint> OnlyHaveUniqueItems(E ? unordered.OrderBy(keySelector, comparer) : unordered.OrderByDescending(keySelector, comparer); - var orderString = propertyExpression.GetMemberPath(); + string orderString = propertyExpression.GetMemberPath().ToString(); orderString = orderString == "\"\"" ? string.Empty : " by " + orderString; Execute.Assertion diff --git a/Src/FluentAssertions/Common/ExpressionExtensions.cs b/Src/FluentAssertions/Common/ExpressionExtensions.cs index eb2cd38cb2..c99b0f552a 100644 --- a/Src/FluentAssertions/Common/ExpressionExtensions.cs +++ b/Src/FluentAssertions/Common/ExpressionExtensions.cs @@ -5,10 +5,11 @@ using System.Reflection; using FluentAssertions.Equivalency; +using FluentAssertions.Equivalency.Selection; namespace FluentAssertions.Common { - public static class ExpressionExtensions + internal static class ExpressionExtensions { public static SelectedMemberInfo GetSelectedMemberInfo(this Expression> expression) { @@ -79,9 +80,12 @@ public static class ExpressionExtensions } /// - /// Gets a dotted path of property names representing the property expression. E.g. Parent.Child.Sibling.Name. + /// Gets a dotted path of property names representing the property expression, including the declaring type. /// - public static string GetMemberPath( + /// + /// E.g. Parent.Child.Sibling.Name. + /// + public static MemberPath GetMemberPath( this Expression> expression) { if (expression == null) @@ -90,6 +94,7 @@ public static class ExpressionExtensions } var segments = new List(); + var declaringTypes = new List(); Expression node = expression; var unsupportedExpressionMessage = $"Expression <{expression.Body}> cannot be used to select a member."; @@ -113,6 +118,7 @@ public static class ExpressionExtensions node = memberExpression.Expression; segments.Add(memberExpression.Member.Name); + declaringTypes.Add(memberExpression.Member.DeclaringType); break; case ExpressionType.ArrayIndex: @@ -144,9 +150,13 @@ public static class ExpressionExtensions } } + // If any members were accessed in the expression, the first one found is the last member. + Type declaringType = declaringTypes.FirstOrDefault( ) ?? typeof(TDeclaringType); + string[] reversedSegments = segments.AsEnumerable().Reverse().ToArray(); string segmentPath = string.Join(".", reversedSegments); - return segmentPath.Replace(".[", "["); + + return new MemberPath(declaringType, segmentPath.Replace(".[", "[")); } internal static string GetMethodName(Expression action) diff --git a/Src/FluentAssertions/Equivalency/Selection/MemberPath.cs b/Src/FluentAssertions/Common/MemberPath.cs similarity index 55% rename from Src/FluentAssertions/Equivalency/Selection/MemberPath.cs rename to Src/FluentAssertions/Common/MemberPath.cs index 05975e8e46..143032bec4 100644 --- a/Src/FluentAssertions/Equivalency/Selection/MemberPath.cs +++ b/Src/FluentAssertions/Common/MemberPath.cs @@ -2,17 +2,22 @@ using System.Collections.Generic; using System.Linq; -namespace FluentAssertions.Equivalency.Selection +namespace FluentAssertions.Common { /// - /// Encapsulates a dotted candidate to a (nested) member of a type. + /// Encapsulates a dotted candidate to a (nested) member of a type as well as the + /// declaring type of the deepest member. /// internal class MemberPath { + private readonly Type declaringType; + private readonly string dottedPath; private readonly List segments = new List(); - public MemberPath(string dottedPath) + public MemberPath(Type declaringType, string dottedPath) { + this.declaringType = declaringType; + this.dottedPath = dottedPath; segments.AddRange(Segmentize(dottedPath)); } @@ -21,23 +26,37 @@ public bool IsParentOrChildOf(string candidate) return IsParent(candidate) || IsChild(candidate); } + public bool IsSameAs(string candidate, Type memberDeclaringType) + { + string[] candidateSegments = Segmentize(candidate); + + return candidateSegments.SequenceEqual(segments) && memberDeclaringType == declaringType; + } + private bool IsChild(string candidate) { string[] candidateSegments = Segmentize(candidate); - return candidateSegments.Take(segments.Count).SequenceEqual(segments); + return candidateSegments.Take(segments.Count).SequenceEqual(segments) && + candidateSegments.Length > segments.Count; } private bool IsParent(string candidate) { string[] candidateSegments = Segmentize(candidate); - return candidateSegments.SequenceEqual(segments.Take(candidateSegments.Length)); + return candidateSegments.SequenceEqual(segments.Take(candidateSegments.Length)) + && candidateSegments.Length < segments.Count; } private static string[] Segmentize(string dottedPath) { return dottedPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries); } + + public override string ToString() + { + return dottedPath; + } } } diff --git a/Src/FluentAssertions/Common/StringExtensions.cs b/Src/FluentAssertions/Common/StringExtensions.cs index 41df7e9ffb..6f72357adc 100644 --- a/Src/FluentAssertions/Common/StringExtensions.cs +++ b/Src/FluentAssertions/Common/StringExtensions.cs @@ -5,15 +5,6 @@ namespace FluentAssertions.Common { internal static class StringExtensions { - /// - /// Finds the first index at which the does not match the - /// string anymore, including the exact casing. - /// - public static int IndexOfFirstMismatch(this string value, string expected) - { - return IndexOfFirstMismatch(value, expected, StringComparison.CurrentCulture); - } - /// /// Finds the first index at which the does not match the /// string anymore, accounting for the specified . diff --git a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs index 98a3d62d80..ec3114a9c4 100644 --- a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs @@ -68,7 +68,7 @@ public EquivalencyAssertionOptions Including(Expression WithStrictOrderingFor( Expression> expression) { - string expressionMemberPath = expression.GetMemberPath(); + string expressionMemberPath = expression.GetMemberPath().ToString(); orderingRules.Add(new PathBasedOrderingRule(expressionMemberPath)); return this; } diff --git a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs index 634396e4fa..12d0645d6d 100644 --- a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs +++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions.Common; @@ -9,23 +10,25 @@ namespace FluentAssertions.Equivalency.Selection /// internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule { - private readonly string pathToExclude; + private readonly MemberPath memberToExclude; - public ExcludeMemberByPathSelectionRule(string pathToExclude) - : base(pathToExclude) + public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude) + : base(pathToExclude.ToString()) { - this.pathToExclude = pathToExclude; + this.memberToExclude = pathToExclude; } protected override IEnumerable OnSelectMembers(IEnumerable selectedMembers, string currentPath, IMemberInfo context) { - return selectedMembers.Where(memberInfo => currentPath.Combine(memberInfo.Name) != pathToExclude).ToArray(); + return selectedMembers + .Where(memberInfo => !memberToExclude.IsSameAs(currentPath.Combine(memberInfo.Name), memberInfo.DeclaringType)) + .ToArray(); } public override string ToString() { - return "Exclude member root." + pathToExclude; + return "Exclude member root." + memberToExclude; } } } diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs index 3dcc2e451d..f428475108 100644 --- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs +++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions.Common; @@ -9,12 +10,12 @@ namespace FluentAssertions.Equivalency.Selection /// internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule { - private readonly MemberPath pathToInclude; + private readonly MemberPath memberToInclude; - public IncludeMemberByPathSelectionRule(string pathToInclude) - : base(pathToInclude) + public IncludeMemberByPathSelectionRule(MemberPath pathToInclude) + : base(pathToInclude.ToString()) { - this.pathToInclude = new MemberPath(pathToInclude); + memberToInclude = pathToInclude; } public override bool IncludesMembers => true; @@ -24,7 +25,9 @@ public IncludeMemberByPathSelectionRule(string pathToInclude) { var matchingMembers = from member in context.RuntimeType.GetNonPrivateMembers() - where pathToInclude.IsParentOrChildOf(currentPath.Combine(member.Name)) + let memberPath = currentPath.Combine(member.Name) + where memberToInclude.IsSameAs(memberPath, member.DeclaringType) || + memberToInclude.IsParentOrChildOf(memberPath) select member; return selectedMembers.Concat(matchingMembers).ToArray(); @@ -32,7 +35,7 @@ where pathToInclude.IsParentOrChildOf(currentPath.Combine(member.Name)) public override string ToString() { - return "Include member root." + pathToInclude; + return "Include member root." + memberToInclude; } } } diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index 8b9320a83e..23089812cb 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -1191,6 +1191,51 @@ public void When_a_property_is_hidden_in_a_derived_class_it_should_ignore_it() action.Should().NotThrow(); } + [Fact] + public void When_including_a_property_that_is_hidden_in_a_derived_class_it_should_select_the_correct_one() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var b1 = new ClassThatHidesBaseClassProperty(); + var b2 = new ClassThatHidesBaseClassProperty(); + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + b1.Should().BeEquivalentTo(b2, config => config.Including(b => b.Property)); + } + + [Fact] + public void When_excluding_a_property_that_is_hidden_in_a_derived_class_it_should_select_the_correct_one() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var b1 = new ClassThatHidesBaseClassProperty(); + var b2 = new ClassThatHidesBaseClassProperty(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => b1.Should().BeEquivalentTo(b2, config => config.Excluding(b => b.Property)); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage("Expected member Property*-*-*-*-*but*"); + } + + class ClassWithGuidProperty + { + public string Property { get; set; } = Guid.NewGuid().ToString(); + } + + class ClassThatHidesBaseClassProperty: ClassWithGuidProperty + { + public new string[] Property { get; set; } + } + [Fact] public void When_a_property_is_an_indexer_it_should_be_ignored() { diff --git a/Tests/Shared.Specs/ExceptionAssertionSpecs.cs b/Tests/Shared.Specs/ExceptionAssertionSpecs.cs index 40f2853525..a60438bc20 100644 --- a/Tests/Shared.Specs/ExceptionAssertionSpecs.cs +++ b/Tests/Shared.Specs/ExceptionAssertionSpecs.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using FakeItEasy; using Xunit; using Xunit.Sdk; diff --git a/Tests/Shared.Specs/ThrowAssertionsSpecs.cs b/Tests/Shared.Specs/ThrowAssertionsSpecs.cs index 6a0fe0d5f3..2c98fa4df2 100644 --- a/Tests/Shared.Specs/ThrowAssertionsSpecs.cs +++ b/Tests/Shared.Specs/ThrowAssertionsSpecs.cs @@ -1,4 +1,5 @@ using System; +using FakeItEasy; using Xunit; using Xunit.Sdk;