Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Including and/or excluding properties in graph comparison should honor the declared type #960

Merged
merged 1 commit into from Nov 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -254,7 +254,7 @@ public AndConstraint<GenericCollectionAssertions<T>> OnlyHaveUniqueItems<TKey>(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
Expand Down
18 changes: 14 additions & 4 deletions Src/FluentAssertions/Common/ExpressionExtensions.cs
Expand Up @@ -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<T, TValue>(this Expression<Func<T, TValue>> expression)
{
Expand Down Expand Up @@ -79,9 +80,12 @@ public static class ExpressionExtensions
}

/// <summary>
/// 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.
/// </summary>
public static string GetMemberPath<TDeclaringType, TPropertyType>(
/// <example>
/// E.g. Parent.Child.Sibling.Name.
/// </example>
public static MemberPath GetMemberPath<TDeclaringType, TPropertyType>(
this Expression<Func<TDeclaringType, TPropertyType>> expression)
{
if (expression == null)
Expand All @@ -90,6 +94,7 @@ public static class ExpressionExtensions
}

var segments = new List<string>();
var declaringTypes = new List<Type>();
Expression node = expression;

var unsupportedExpressionMessage = $"Expression <{expression.Body}> cannot be used to select a member.";
Expand All @@ -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:
Expand Down Expand Up @@ -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> action)
Expand Down
62 changes: 62 additions & 0 deletions Src/FluentAssertions/Common/MemberPath.cs
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace FluentAssertions.Common
{
/// <summary>
/// Encapsulates a dotted candidate to a (nested) member of a type as well as the
/// declaring type of the deepest member.
/// </summary>
internal class MemberPath
{
private readonly Type declaringType;
private readonly string dottedPath;
private readonly List<string> segments = new List<string>();

public MemberPath(Type declaringType, string dottedPath)
{
this.declaringType = declaringType;
this.dottedPath = dottedPath;
segments.AddRange(Segmentize(dottedPath));
}

public bool IsParentOrChildOf(string candidate)
{
return IsParent(candidate) || IsChild(candidate);
}

public bool IsSameAs(string candidate, Type memberDeclaringType)
{
string[] candidateSegments = Segmentize(candidate);
dennisdoomen marked this conversation as resolved.
Show resolved Hide resolved

return memberDeclaringType == declaringType && candidateSegments.SequenceEqual(segments);
}

private bool IsChild(string candidate)
{
string[] candidateSegments = Segmentize(candidate);

return candidateSegments.Length > segments.Count &&
candidateSegments.Take(segments.Count).SequenceEqual(segments);
}

private bool IsParent(string candidate)
{
string[] candidateSegments = Segmentize(candidate);

return candidateSegments.Length < segments.Count
&& candidateSegments.SequenceEqual(segments.Take(candidateSegments.Length));
}

private static string[] Segmentize(string dottedPath)
{
return dottedPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
}

public override string ToString()
{
return dottedPath;
}
}
}
9 changes: 0 additions & 9 deletions Src/FluentAssertions/Common/StringExtensions.cs
Expand Up @@ -5,15 +5,6 @@ namespace FluentAssertions.Common
{
internal static class StringExtensions
{
/// <summary>
/// Finds the first index at which the <paramref name="value"/> does not match the <paramref name="expected"/>
/// string anymore, including the exact casing.
/// </summary>
public static int IndexOfFirstMismatch(this string value, string expected)
{
return IndexOfFirstMismatch(value, expected, StringComparison.CurrentCulture);
}

/// <summary>
/// Finds the first index at which the <paramref name="value"/> does not match the <paramref name="expected"/>
/// string anymore, accounting for the specified <paramref name="stringComparison"/>.
Expand Down
Expand Up @@ -68,7 +68,7 @@ public EquivalencyAssertionOptions<TExpectation> Including(Expression<Func<IMemb
public EquivalencyAssertionOptions<TExpectation> WithStrictOrderingFor(
Expression<Func<TExpectation, object>> expression)
{
string expressionMemberPath = expression.GetMemberPath();
string expressionMemberPath = expression.GetMemberPath().ToString();
orderingRules.Add(new PathBasedOrderingRule(expressionMemberPath));
return this;
}
Expand Down
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Common;
Expand All @@ -9,23 +10,25 @@ namespace FluentAssertions.Equivalency.Selection
/// </summary>
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<SelectedMemberInfo> OnSelectMembers(IEnumerable<SelectedMemberInfo> 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;
}
}
}
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Common;
Expand All @@ -9,12 +10,12 @@ namespace FluentAssertions.Equivalency.Selection
/// </summary>
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;
Expand All @@ -24,15 +25,17 @@ 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();
}

public override string ToString()
{
return "Include member root." + pathToInclude;
return "Include member root." + memberToInclude;
}
}
}
43 changes: 0 additions & 43 deletions Src/FluentAssertions/Equivalency/Selection/MemberPath.cs

This file was deleted.

45 changes: 45 additions & 0 deletions Tests/Shared.Specs/BasicEquivalencySpecs.cs
Expand Up @@ -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<XunitException>().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()
{
Expand Down