Skip to content

Commit

Permalink
SAVEPOINT
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisdoomen committed Jan 22, 2022
1 parent 9b2d8e2 commit 4af5e23
Show file tree
Hide file tree
Showing 15 changed files with 704 additions and 23 deletions.
35 changes: 34 additions & 1 deletion Src/FluentAssertions/Common/MemberPath.cs
@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Reflection;
using FluentAssertions.Equivalency;

namespace FluentAssertions.Common
Expand All @@ -23,9 +22,18 @@ public MemberPath(IMember member, string parentPath)
}

public MemberPath(Type reflectedType, Type declaringType, string dottedPath)
: this(dottedPath)
{
this.reflectedType = reflectedType;
this.declaringType = declaringType;
}

public MemberPath(string dottedPath)
{
Guard.ThrowIfArgumentIsNullOrEmpty(
dottedPath, nameof(dottedPath),
"A member path cannot be null or empty");

this.dottedPath = dottedPath;
}

Expand Down Expand Up @@ -66,8 +74,33 @@ private bool IsChildOf(MemberPath candidate)
&& candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length));
}

/// <summary>
/// Determines whether the current path is the same as <paramref name="path"/> when ignoring any specific indexes.
/// </summary>
public bool IsEquivalentTo(string path)
{
return path.WithoutSpecificCollectionIndex() == dottedPath.WithoutSpecificCollectionIndex();
}

public bool HasSameParentAs(MemberPath path)
{
return ParentSegments.SequenceEqual(path.ParentSegments);
}

private string[] ParentSegments => Segments.Take(Segments.Length - 1).ToArray();

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

/// <summary>
/// Returns the name of the member the current path points to without its parent path.
/// </summary>
public string MemberName => Segments.Last();

/// <summary>
/// Gets a value indicating whether the current path contains an indexer like `[1]` instead of `[]`.
/// </summary>
public bool ContainsSpecificCollectionIndex => dottedPath.WithoutSpecificCollectionIndex() != dottedPath;

public override string ToString()
{
return dottedPath;
Expand Down
9 changes: 9 additions & 0 deletions Src/FluentAssertions/Common/StringExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using FluentAssertions.Formatting;

namespace FluentAssertions.Common
Expand Down Expand Up @@ -41,6 +42,14 @@ public static string IndexedSegmentAt(this string value, int index)
return $"{formattedString} (index {index})".EscapePlaceholders();
}

/// <summary>
/// Replaces the numeric index from a path like "property[0].nested" and returns "property[].nested"
/// </summary>
public static string WithoutSpecificCollectionIndex(this string indexedPath)
{
return Regex.Replace(indexedPath, @"(\[)\d+(\])", "$1$2");
}

/// <summary>
/// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts.
/// </summary>
Expand Down
40 changes: 40 additions & 0 deletions Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
Expand Up @@ -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;

Expand Down Expand Up @@ -70,6 +72,44 @@ public EquivalencyAssertionOptions<IEnumerable<TExpectation>> AsCollection()
return new EquivalencyAssertionOptions<IEnumerable<TExpectation>>(
new CollectionMemberAssertionOptionsDecorator(this));
}

public EquivalencyAssertionOptions<TExpectation> WithMapping<TSubject>(
Expression<Func<TExpectation, object>> expectationMemberPath,
Expression<Func<TSubject, object>> subjectMemberPath)
{
return WithMapping(
expectationMemberPath.GetMemberPath().ToString().WithoutSpecificCollectionIndex(),
subjectMemberPath.GetMemberPath().ToString().WithoutSpecificCollectionIndex());
}

public EquivalencyAssertionOptions<TExpectation> WithMapping(
string expectationMemberPath,
string subjectMemberPath)
{
AddMatchingRule(new MappedPathMatchingRule(expectationMemberPath, subjectMemberPath));

return this;
}

public EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(
Expression<Func<TNestedExpectation, object>> expectationMember,
Expression<Func<TNestedSubject, object>> subjectMember)
{
return WithMapping<TNestedExpectation, TNestedSubject>(
expectationMember.GetMemberPath().ToString(),
subjectMember.GetMemberPath().ToString());
}

public EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(
string expectationMemberName,
string subjectMemberName)
{
AddMatchingRule(new MappedMemberMatchingRule<TNestedExpectation, TNestedSubject>(
expectationMemberName,
subjectMemberName));

return this;
}
}

/// <summary>
Expand Down
14 changes: 13 additions & 1 deletion Src/FluentAssertions/Equivalency/INode.cs
Expand Up @@ -16,7 +16,10 @@ public interface INode
/// <summary>
/// Gets the name of this node.
/// </summary>
string Name { get; }
/// <example>
/// "Property2"
/// </example>
string Name { get; set; }

/// <summary>
/// Gets the type of this node.
Expand All @@ -26,11 +29,17 @@ public interface INode
/// <summary>
/// Gets the path from the root object UNTIL the current node, separated by dots or index/key brackets.
/// </summary>
/// <example>
/// "Parent[0].Property2"
/// </example>
string Path { get; }

/// <summary>
/// Gets the full path from the root object up to and including the name of the node.
/// </summary>
/// <example>
/// "Parent[0]"
/// </example>
string PathAndName { get; }

/// <summary>
Expand All @@ -41,6 +50,9 @@ public interface INode
/// <summary>
/// Gets the path including the description of the subject.
/// </summary>
/// <example>
/// "property subject.Parent[0].Property2"
/// </example>
string Description { get; }

/// <summary>
Expand Down
@@ -0,0 +1,50 @@
using System;
using System.Text.RegularExpressions;
using FluentAssertions.Common;

namespace FluentAssertions.Equivalency.Matching
{
internal class MappedMemberMatchingRule<TExpectation, TSubject> : IMemberMatchingRule
{
private readonly string expectationPropertyName;
private readonly string subjectPropertyName;

/// <summary>
/// Creates an instance of this rule that matches the two direct properties of the specified subject and expectation types.
/// </summary>
public MappedMemberMatchingRule(string expectationPropertyName, string subjectPropertyName)
{
// TODO: declaredtype vs reflectedtype

if (Regex.IsMatch(@"(\.|\[|\])", expectationPropertyName))
{
throw new ArgumentException("The expectation's member name cannot be a nested path");
}

if (Regex.IsMatch(@"(\.|\[|\])", subjectPropertyName))
{
throw new ArgumentException("The subject's member name cannot be a nested path");
}

this.expectationPropertyName = expectationPropertyName;
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 expectation property does not exist
// TODO: What if the expectation is null
// TODO: What if the subject path is invalid. Can we even verify that?
if (expectedMember.ReflectedType.IsSameOrInherits(typeof(TExpectation)) && subject is TSubject)
{
if (expectedMember.Name == expectationPropertyName)
{
return MemberFactory.Create(subject.GetType().GetProperty(subjectPropertyName), parent);
}
}

return null;
}
}
}
@@ -0,0 +1,61 @@
using System;
using System.Reflection;
using FluentAssertions.Common;

namespace FluentAssertions.Equivalency.Matching
{
/// <summary>
/// Allows mapping a member (property or field) of the expectation to a differently named member
/// of the subject-under-test using a nested member path in the form of "Parent.NestedCollection[].Member"
/// </summary>
internal class MappedPathMatchingRule : IMemberMatchingRule
{
private readonly MemberPath expectationPath;
private readonly MemberPath subjectPath;

public MappedPathMatchingRule(string expectationMemberPath, string subjectMemberPath)
{
expectationPath = new MemberPath(expectationMemberPath);
subjectPath = new MemberPath(subjectMemberPath);

if (expectationPath.ContainsSpecificCollectionIndex || subjectPath.ContainsSpecificCollectionIndex)
{
throw new ArgumentException("Mapping properties containing a collection index must use the [] format without specific index.");
}

if (!expectationPath.HasSameParentAs(subjectPath))
{
throw new ArgumentException("The member paths must have the same parent.");
}
}

public IMember Match(IMember expectedMember, object subject, INode parent, IEquivalencyAssertionOptions options)
{
if (expectationPath.IsEquivalentTo(expectedMember.PathAndName))
{
var member = FindSubjectMember(subject, subjectPath.MemberName, expectedMember.Type, parent);
if (member is null)
{
throw new ArgumentException(
$"Subject of type {subject?.GetType().Name} does not have member {subjectPath.MemberName}");
}

return member;
}

return null;
}

private static IMember FindSubjectMember(object subject, string memberName, Type expectedMemberType, INode parent)
{
PropertyInfo property = subject.GetType().FindProperty(memberName, expectedMemberType);
if ((property is not null) && !property.IsIndexer())
{
return new Property(property, parent);
}

FieldInfo field = subject.GetType().FindField(memberName, expectedMemberType);
return (field is not null) ? new Field(field, parent) : null;
}
}
}
2 changes: 1 addition & 1 deletion Src/FluentAssertions/Equivalency/Node.cs
Expand Up @@ -21,7 +21,7 @@ public class Node : INode

public string PathAndName => Path.Combine(Name);

public string Name { get; protected set; }
public string Name { get; set; }

public virtual string Description => $"{GetSubjectId().Combine(PathAndName)}";

Expand Down
Expand Up @@ -822,7 +822,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;
Expand Down
Expand Up @@ -62,6 +62,13 @@ public class StructuralEqualityEquivalencyStep : IEquivalencyStep
CompileTimeType = selectedMember.Type
};

if (selectedMember.Name != matchingMember.Name)
{
// In case the matching process selected a different member on the subject,
// adjust the current member so that assertion failures report the proper name.
selectedMember.Name = matchingMember.Name;
}

parent.RecursivelyAssertEquality(nestedComparands, context.AsNestedMember(selectedMember));
}
}
Expand Down
Expand Up @@ -756,6 +756,10 @@ namespace FluentAssertions.Equivalency
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.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) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(System.Linq.Expressions.Expression<System.Func<TNestedExpectation, object>> expectationMember, System.Linq.Expressions.Expression<System.Func<TNestedSubject, object>> subjectMember) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(string expectationMemberName, string subjectMemberName) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithStrictOrderingFor(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
}
public enum EquivalencyResult
Expand Down Expand Up @@ -880,7 +884,7 @@ namespace FluentAssertions.Equivalency
string Description { get; }
FluentAssertions.Equivalency.GetSubjectId GetSubjectId { get; }
bool IsRoot { get; }
string Name { get; }
string Name { get; set; }
string Path { get; }
string PathAndName { get; }
bool RootIsCollection { get; }
Expand Down Expand Up @@ -968,6 +972,7 @@ namespace FluentAssertions.Equivalency
[System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
protected FluentAssertions.Equivalency.OrderingRuleCollection OrderingRules { get; }
public FluentAssertions.Equivalency.Tracing.ITraceWriter TraceWriter { get; }
protected TSelf AddMatchingRule(FluentAssertions.Equivalency.IMemberMatchingRule matchingRule) { }
protected TSelf AddSelectionRule(FluentAssertions.Equivalency.IMemberSelectionRule selectionRule) { }
public TSelf AllowingInfiniteRecursion() { }
public TSelf ComparingByMembers(System.Type type) { }
Expand Down
Expand Up @@ -756,6 +756,10 @@ namespace FluentAssertions.Equivalency
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.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) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(System.Linq.Expressions.Expression<System.Func<TNestedExpectation, object>> expectationMember, System.Linq.Expressions.Expression<System.Func<TNestedSubject, object>> subjectMember) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithMapping<TNestedExpectation, TNestedSubject>(string expectationMemberName, string subjectMemberName) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions<TExpectation> WithStrictOrderingFor(System.Linq.Expressions.Expression<System.Func<TExpectation, object>> expression) { }
}
public enum EquivalencyResult
Expand Down Expand Up @@ -880,7 +884,7 @@ namespace FluentAssertions.Equivalency
string Description { get; }
FluentAssertions.Equivalency.GetSubjectId GetSubjectId { get; }
bool IsRoot { get; }
string Name { get; }
string Name { get; set; }
string Path { get; }
string PathAndName { get; }
bool RootIsCollection { get; }
Expand Down Expand Up @@ -968,6 +972,7 @@ namespace FluentAssertions.Equivalency
[System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
protected FluentAssertions.Equivalency.OrderingRuleCollection OrderingRules { get; }
public FluentAssertions.Equivalency.Tracing.ITraceWriter TraceWriter { get; }
protected TSelf AddMatchingRule(FluentAssertions.Equivalency.IMemberMatchingRule matchingRule) { }
protected TSelf AddSelectionRule(FluentAssertions.Equivalency.IMemberSelectionRule selectionRule) { }
public TSelf AllowingInfiniteRecursion() { }
public TSelf ComparingByMembers(System.Type type) { }
Expand Down

0 comments on commit 4af5e23

Please sign in to comment.