Skip to content

Commit

Permalink
Selecting members during object graph assertions now honors the decla…
Browse files Browse the repository at this point in the history
…ring type

Fixes #956
  • Loading branch information
dennisdoomen committed Nov 6, 2018
1 parent 31b959d commit 900b645
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 32 deletions.
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
Expand Up @@ -2,17 +2,22 @@
using System.Collections.Generic;
using System.Linq;

namespace FluentAssertions.Equivalency.Selection
namespace FluentAssertions.Common
{
/// <summary>
/// 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.
/// </summary>
internal class MemberPath
{
private readonly Type declaringType;
private readonly string dottedPath;
private readonly List<string> segments = new List<string>();

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

Expand All @@ -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;
}
}
}
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;
}
}
}
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

0 comments on commit 900b645

Please sign in to comment.