Skip to content

Commit

Permalink
Merge 0774341 into 418a405
Browse files Browse the repository at this point in the history
  • Loading branch information
whymatter committed Feb 21, 2022
2 parents 418a405 + 0774341 commit fc70829
Show file tree
Hide file tree
Showing 14 changed files with 493 additions and 8 deletions.
22 changes: 17 additions & 5 deletions Src/FluentAssertions/Common/MemberPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ internal class MemberPath

private string[] segments;

private static readonly MemberPathSegmentEqualityComparer MemberPathSegmentEqualityComparer = new();

public MemberPath(IMember member, string parentPath)
: this(member.ReflectedType, member.DeclaringType, parentPath.Combine(member.Name))
{
Expand Down Expand Up @@ -53,7 +55,7 @@ public bool IsSameAs(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;

return candidateSegments.SequenceEqual(Segments);
return candidateSegments.SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}

return false;
Expand All @@ -64,15 +66,22 @@ private bool IsParentOf(MemberPath candidate)
string[] candidateSegments = candidate.Segments;

return candidateSegments.Length > Segments.Length &&
candidateSegments.Take(Segments.Length).SequenceEqual(Segments);
candidateSegments.Take(Segments.Length).SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}

private bool IsChildOf(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;

return candidateSegments.Length < Segments.Length
&& candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length));
&& candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length),
MemberPathSegmentEqualityComparer);
}

public MemberPath Combine(MemberPath nextPath, string separator = ".")
{
var extendedDottedPath = dottedPath.Combine(nextPath.dottedPath, separator);
return new MemberPath(declaringType, nextPath.reflectedType, extendedDottedPath);
}

/// <summary>
Expand All @@ -86,7 +95,7 @@ public bool IsEquivalentTo(string path)
public bool HasSameParentAs(MemberPath path)
{
return Segments.Length == path.Segments.Length
&& GetParentSegments().SequenceEqual(path.GetParentSegments());
&& GetParentSegments().SequenceEqual(path.GetParentSegments(), MemberPathSegmentEqualityComparer);
}

private IEnumerable<string> GetParentSegments() => Segments.Take(Segments.Length - 1);
Expand All @@ -96,7 +105,10 @@ public bool HasSameParentAs(MemberPath path)
/// </summary>
public bool GetContainsSpecificCollectionIndex() => dottedPath.ContainsSpecificCollectionIndex();

private string[] Segments => segments ??= dottedPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
private string[] Segments =>
segments ??= dottedPath
.Replace("[]", "[*]")
.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);

/// <summary>
/// Returns the name of the member the current path points to without its parent path.
Expand Down
54 changes: 54 additions & 0 deletions Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace FluentAssertions.Common
{
/// <summary>
/// Compares two segments of a <see cref="MemberPath"/>.
/// Sets the <see cref="AnyIndexQualifier"/> equal with any numeric index qualifier.
/// All other comparisons are default string equality.
/// </summary>
internal class MemberPathSegmentEqualityComparer : IEqualityComparer<string>
{
private const string AnyIndexQualifier = "*";
private static readonly Regex IndexQualifierRegex = new(@"^\d+$");

/// <summary>
/// Compares two segments of a <see cref="MemberPath"/>.
/// </summary>
/// <param name="x">Left part of the comparison.</param>
/// <param name="y">Right part of the comparison.</param>
/// <returns>True if segments are equal, false if not.</returns>
public bool Equals(string x, string y)
{
if (x is null || y is null)
{
return x == y;
}

if (x == AnyIndexQualifier)
{
return EqualsAnyIndexQualifier(y);
}

if (y == AnyIndexQualifier)
{
return EqualsAnyIndexQualifier(x);
}

return x == y;
}

private static bool EqualsAnyIndexQualifier(string segment)
=> segment == AnyIndexQualifier || IndexQualifierRegex.IsMatch(segment);

public int GetHashCode(string obj)
{
#if NETCOREAPP2_1_OR_GREATER
return obj.GetHashCode(System.StringComparison.Ordinal);
#else
return obj.GetHashCode();
#endif
}
}
}
12 changes: 11 additions & 1 deletion Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Execution;
Expand Down Expand Up @@ -40,6 +39,17 @@ public EquivalencyAssertionOptions<TExpectation> Excluding(Expression<Func<TExpe
return this;
}

/// <summary>
/// Excludes the specified (nested) member from the structural equality check.
/// Allows to navigate deeper by using <see cref="NestedExclusionOptionBuilder{TExpectation,TCurrent}.ThenExcluding"/>.
/// </summary>
public NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(Expression<Func<TExpectation, IEnumerable<TNext>>> expression)
{
var selectionRule = new ExcludeMemberByPathSelectionRule(expression.GetMemberPath());
AddSelectionRule(selectionRule);
return new NestedExclusionOptionBuilder<TExpectation, TNext>(this, selectionRule);
}

/// <summary>
/// Includes the specified member in the equality check.
/// </summary>
Expand Down
45 changes: 45 additions & 0 deletions Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Selection;

namespace FluentAssertions.Equivalency
{
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : EquivalencyAssertionOptions<TExpectation>
{
/// <summary>
/// The selected path starting at the first <see cref="EquivalencyAssertionOptions{TExpectation}.Excluding{TNext}"/>.
/// </summary>
private readonly ExcludeMemberByPathSelectionRule currentPathSelectionRule;

internal NestedExclusionOptionBuilder(EquivalencyAssertionOptions<TExpectation> equivalencyAssertionOptions,
ExcludeMemberByPathSelectionRule currentPathSelectionRule)
: base(equivalencyAssertionOptions)
{
this.currentPathSelectionRule = currentPathSelectionRule;
}

/// <summary>
/// Selects a property to use. This ends the <see cref="ThenExcluding"/> chain.
/// </summary>
public EquivalencyAssertionOptions<TExpectation> ThenExcluding(Expression<Func<TCurrent, object>> expression)
{
var nextPath = expression.GetMemberPath();
currentPathSelectionRule.CombinePath(nextPath);
return this;
}

/// <summary>
/// Adds the selected collection to the <see cref="ThenExcluding{TNext}"/> chain.
/// If this is the last call to <see cref="ThenExcluding{TNext}"/>, this ends the chain.
/// </summary>
public NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(
Expression<Func<TCurrent, IEnumerable<TNext>>> expression)
{
var nextPath = expression.GetMemberPath();
currentPathSelectionRule.CombinePath(nextPath);
return new NestedExclusionOptionBuilder<TExpectation, TNext>(this, currentPathSelectionRule);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace FluentAssertions.Equivalency.Selection
/// </summary>
internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
{
private readonly MemberPath memberToExclude;
private MemberPath memberToExclude;

public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
: base(pathToExclude.ToString())
Expand All @@ -23,6 +23,12 @@ public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
memberToExclude.IsSameAs(new MemberPath(member, parentPath)));
}

public void CombinePath(MemberPath nextPath)
{
memberToExclude = memberToExclude.Combine(nextPath, "[]");
SetSelectedPath(memberToExclude.ToString());
}

public override string ToString()
{
return "Exclude member " + memberToExclude;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection
{
internal abstract class SelectMemberByPathSelectionRule : IMemberSelectionRule
{
private readonly string selectedPath;
private string selectedPath;

protected SelectMemberByPathSelectionRule(string selectedPath)
{
Expand All @@ -16,6 +16,11 @@ protected SelectMemberByPathSelectionRule(string selectedPath)

public virtual bool IncludesMembers => false;

protected void SetSelectedPath(string path)
{
this.selectedPath = path;
}

public IEnumerable<IMember> SelectMembers(INode currentNode, IEnumerable<IMember> selectedMembers,
MemberSelectionContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -924,6 +925,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> ThenExcluding(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -924,6 +925,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> ThenExcluding(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -924,6 +925,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> ThenExcluding(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -917,6 +918,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> ThenExcluding(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<System.Collections.Generic.IEnumerable<TExpectation>> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Excluding(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> Excluding<TNext>(System.Linq.Expressions.Expression<System.Func<TExpectation, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> Including(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expectationMemberPath, System.Linq.Expressions.Expression<System.Func<TSubject, object>> subjectMemberPath) { }
Expand Down Expand Up @@ -924,6 +925,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
public class NestedExclusionOptionBuilder<TExpectation, TCurrent> : FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation>
{
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> ThenExcluding(System.Linq.Expressions.Expression<System.Func<TCurrent, object>> expression) { }
public FluentAssertions.Equivalency.NestedExclusionOptionBuilder<TExpectation, TNext> ThenExcluding<TNext>(System.Linq.Expressions.Expression<System.Func<TCurrent, System.Collections.Generic.IEnumerable<TNext>>> expression) { }
}
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
Expand Down
Loading

0 comments on commit fc70829

Please sign in to comment.