Skip to content

Commit

Permalink
Ensured that using the Including construct always starts at the root.
Browse files Browse the repository at this point in the history
Fixes #462.
  • Loading branch information
dennisdoomen committed Aug 23, 2016
1 parent 3abfc14 commit b637423
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 84 deletions.
1 change: 1 addition & 0 deletions Src/Core/Core.projitems
Expand Up @@ -66,6 +66,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\GenericEnumerableEquivalencyStep.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IAssertionContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IAssertionRule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IConditionalRule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IEquivalencyAssertionOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IEquivalencyStep.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Equivalency\IEquivalencyValidationContext.cs" />
Expand Down
Expand Up @@ -16,14 +16,13 @@ public CollectionMemberAssertionOptionsDecorator(IEquivalencyAssertionOptions in
this.inner = inner;
}

public IEnumerable<IMemberSelectionRule> SelectionRules
public IEnumerable<IMemberSelectionRule> GetActiveSelectionRules(IEquivalencyValidationContext context)
{
get
{
return inner.SelectionRules.Select(rule => new CollectionMemberSelectionRuleDecorator(rule)).ToArray();
}
return inner.GetActiveSelectionRules(context).Select(rule => new CollectionMemberSelectionRuleDecorator(rule)).ToArray();
}

public IEnumerable<IMemberSelectionRule> SelectionRules => inner.SelectionRules;

public IEnumerable<IMemberMatchingRule> MatchingRules
{
get { return inner.MatchingRules.Select(rule => new CollectionMemberMatchingRuleDecorator(rule)).ToArray(); }
Expand Down
15 changes: 12 additions & 3 deletions Src/Core/Equivalency/EquivalencyValidator.cs
Expand Up @@ -50,9 +50,18 @@ public void AssertEqualityUsing(IEquivalencyValidationContext context)

if (!objectTracker.IsCyclicReference(new ObjectReference(context.Subject, context.SelectedMemberPath)))
{
bool wasHandled = AssertionOptions.EquivalencySteps
.Where(s => s.CanHandle(context, config))
.Any(step => step.Handle(context, this, config));
bool wasHandled = false;
foreach (var step in AssertionOptions.EquivalencySteps)
{
if (step.CanHandle(context, config))
{
if (step.Handle(context, this, config))
{
wasHandled = true;
break;
}
}
}

if (!wasHandled)
{
Expand Down
10 changes: 10 additions & 0 deletions Src/Core/Equivalency/IConditionalRule.cs
@@ -0,0 +1,10 @@
namespace FluentAssertions.Equivalency
{
/// <summary>
/// Allows a <see cref="IMemberSelectionRule"/> to indicate it applies only in a certain context.
/// </summary>
public interface IConditionalRule
{
bool Applies(IEquivalencyValidationContext context);
}
}
7 changes: 6 additions & 1 deletion Src/Core/Equivalency/IEquivalencyAssertionOptions.cs
Expand Up @@ -9,7 +9,12 @@ namespace FluentAssertions.Equivalency
public interface IEquivalencyAssertionOptions
{
/// <summary>
/// Gets an ordered collection of selection rules that define what properties are included.
/// Gets an ordered collection of the selection rules that are relevant given the provided context.
/// </summary>
IEnumerable<IMemberSelectionRule> GetActiveSelectionRules(IEquivalencyValidationContext context);

/// <summary>
/// Gets an ordered collection of all selection rules that have been configured.
/// </summary>
IEnumerable<IMemberSelectionRule> SelectionRules { get; }

Expand Down
Expand Up @@ -11,10 +11,7 @@ public CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule
this.selectionRule = selectionRule;
}

public bool IncludesMembers
{
get { return selectionRule.IncludesMembers; }
}
public bool IncludesMembers => selectionRule.IncludesMembers;

public IEnumerable<SelectedMemberInfo> SelectMembers(IEnumerable<SelectedMemberInfo> selectedMembers,
ISubjectInfo context, IEquivalencyAssertionOptions config)
Expand Down
Expand Up @@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection
/// <summary>
/// Selection rule that includes a particular property in the structural comparison.
/// </summary>
internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule, IConditionalRule
{
private readonly MemberPath pathToInclude;

Expand All @@ -18,6 +18,11 @@ public IncludeMemberByPathSelectionRule(string pathToInclude) : base(pathToInclu

public override bool IncludesMembers => true;

public bool Applies(IEquivalencyValidationContext context)
{
return context.IsRoot || pathToInclude.IsDeepPath;
}

protected override IEnumerable<SelectedMemberInfo> OnSelectMembers(IEnumerable<SelectedMemberInfo> selectedMembers,
string currentPath, ISubjectInfo context)
{
Expand Down
Expand Up @@ -20,10 +20,7 @@ public IncludeMemberByPredicateSelectionRule(Expression<Func<ISubjectInfo, bool>
this.predicate = predicate.Compile();
}

public bool IncludesMembers
{
get { return true; }
}
public bool IncludesMembers => true;

public IEnumerable<SelectedMemberInfo> SelectMembers(IEnumerable<SelectedMemberInfo> selectedMembers, ISubjectInfo context, IEquivalencyAssertionOptions config)
{
Expand Down
2 changes: 2 additions & 0 deletions Src/Core/Equivalency/Selection/MemberPath.cs
Expand Up @@ -20,5 +20,7 @@ public bool StartsWith(string subPath)
string[] subPathSegments = subPath.Split('.');
return segments.Take(subPathSegments.Length).SequenceEqual(subPathSegments);
}

public bool IsDeepPath => segments.Count > 1;
}
}
105 changes: 40 additions & 65 deletions Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
Expand Up @@ -88,95 +88,71 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions
/// <summary>
/// Gets an ordered collection of selection rules that define what members are included.
/// </summary>
IEnumerable<IMemberSelectionRule> IEquivalencyAssertionOptions.SelectionRules
/// <param name="context"></param>
IEnumerable<IMemberSelectionRule> IEquivalencyAssertionOptions.GetActiveSelectionRules(IEquivalencyValidationContext context)
{
get
IEnumerable<IMemberSelectionRule> activeRules =
from rule in selectionRules
let conditionRule = rule as IConditionalRule
where (conditionRule == null) || conditionRule.Applies(context)
select rule;

bool hasConflictingRules = activeRules.ToArray().Any(rule => rule.IncludesMembers);

if (includeProperties && !hasConflictingRules)
{
bool hasConflictingRules = selectionRules.Any(rule => rule.IncludesMembers);

if (includeProperties && !hasConflictingRules)
{
yield return new AllPublicPropertiesSelectionRule();
}
yield return new AllPublicPropertiesSelectionRule();
}

if (includeFields && !hasConflictingRules)
{
yield return new AllPublicFieldsSelectionRule();
}
if (includeFields && !hasConflictingRules)
{
yield return new AllPublicFieldsSelectionRule();
}

foreach (IMemberSelectionRule rule in selectionRules)
{
yield return rule;
}
foreach (IMemberSelectionRule rule in activeRules.ToArray())
{
yield return rule;
}
}

public IEnumerable<IMemberSelectionRule> SelectionRules => selectionRules;

/// <summary>
/// Gets an ordered collection of matching rules that determine which subject members are matched with which
/// expectation members.
/// </summary>
IEnumerable<IMemberMatchingRule> IEquivalencyAssertionOptions.MatchingRules
{
get { return matchingRules; }
}
IEnumerable<IMemberMatchingRule> IEquivalencyAssertionOptions.MatchingRules => matchingRules;

/// <summary>
/// Gets an ordered collection of Equivalency steps how a subject is compared with the expectation.
/// </summary>
IEnumerable<IEquivalencyStep> IEquivalencyAssertionOptions.UserEquivalencySteps
{
get { return userEquivalencySteps; }
}
IEnumerable<IEquivalencyStep> IEquivalencyAssertionOptions.UserEquivalencySteps => userEquivalencySteps;

/// <summary>
/// Gets an ordered collection of rules that determine whether or not the order of collections is important. By default,
/// ordering is irrelevant.
/// </summary>
OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules
{
get { return orderingRules; }
}
OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules => orderingRules;

/// <summary>
/// Gets value indicating whether the equality check will include nested collections and complex types.
/// </summary>
bool IEquivalencyAssertionOptions.IsRecursive
{
get { return isRecursive; }
}
bool IEquivalencyAssertionOptions.IsRecursive => isRecursive;

bool IEquivalencyAssertionOptions.AllowInfiniteRecursion
{
get { return allowInfiniteRecursion; }
}
bool IEquivalencyAssertionOptions.AllowInfiniteRecursion => allowInfiniteRecursion;

/// <summary>
/// Gets value indicating how cyclic references should be handled. By default, it will throw an exception.
/// </summary>
CyclicReferenceHandling IEquivalencyAssertionOptions.CyclicReferenceHandling
{
get { return cyclicReferenceHandling; }
}
CyclicReferenceHandling IEquivalencyAssertionOptions.CyclicReferenceHandling => cyclicReferenceHandling;

EnumEquivalencyHandling IEquivalencyAssertionOptions.EnumEquivalencyHandling
{
get { return enumEquivalencyHandling; }
}
EnumEquivalencyHandling IEquivalencyAssertionOptions.EnumEquivalencyHandling => enumEquivalencyHandling;

bool IEquivalencyAssertionOptions.UseRuntimeTyping
{
get { return useRuntimeTyping; }
}
bool IEquivalencyAssertionOptions.UseRuntimeTyping => useRuntimeTyping;

bool IEquivalencyAssertionOptions.IncludeProperties
{
get { return includeProperties; }
}
bool IEquivalencyAssertionOptions.IncludeProperties => includeProperties;

bool IEquivalencyAssertionOptions.IncludeFields
{
get { return includeFields; }
}
bool IEquivalencyAssertionOptions.IncludeFields => includeFields;

/// <summary>
/// Gets a value indicating whether the <paramref name="type"/> should be treated as having value semantics.
Expand Down Expand Up @@ -360,7 +336,6 @@ public TSelf IgnoringCyclicReferences()
return (TSelf)this;
}


/// <summary>
/// Disables limitations on recursion depth when the structural equality check is configured to include nested objects
/// </summary>
Expand Down Expand Up @@ -409,7 +384,7 @@ public TSelf Using(IMemberMatchingRule matchingRule)
public TSelf Using(IAssertionRule assertionRule)
{
userEquivalencySteps.Insert(0, new AssertionRuleEquivalencyStepAdapter(assertionRule));
return (TSelf) this;
return (TSelf)this;
}

/// <summary>
Expand Down Expand Up @@ -462,7 +437,7 @@ public TSelf WithoutStrictOrderingFor(Expression<Func<ISubjectInfo, bool>> predi
public TSelf ComparingEnumsByName()
{
enumEquivalencyHandling = EnumEquivalencyHandling.ByName;
return (TSelf) this;
return (TSelf)this;
}

/// <summary>
Expand All @@ -474,7 +449,7 @@ public TSelf ComparingEnumsByName()
public TSelf ComparingEnumsByValue()
{
enumEquivalencyHandling = EnumEquivalencyHandling.ByValue;
return (TSelf) this;
return (TSelf)this;
}

/// <summary>
Expand All @@ -484,7 +459,7 @@ public TSelf ComparingEnumsByValue()
public TSelf ComparingByValue<T>()
{
valueTypes.Add(typeof(T));
return (TSelf) this;
return (TSelf)this;
}

#region Non-fluent API
Expand All @@ -505,19 +480,19 @@ private void ClearMatchingRules()
protected TSelf AddSelectionRule(IMemberSelectionRule selectionRule)
{
selectionRules.Add(selectionRule);
return (TSelf) this;
return (TSelf)this;
}

private TSelf AddMatchingRule(IMemberMatchingRule matchingRule)
{
matchingRules.Insert(0, matchingRule);
return (TSelf) this;
return (TSelf)this;
}

private TSelf AddEquivalencyStep(IEquivalencyStep equivalencyStep)
{
userEquivalencySteps.Add(equivalencyStep);
return (TSelf) this;
return (TSelf)this;
}

#endregion
Expand Down Expand Up @@ -612,4 +587,4 @@ public TSelf When(Expression<Func<ISubjectInfo, bool>> predicate)
}
}
}
}
}
2 changes: 1 addition & 1 deletion Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs
Expand Up @@ -78,7 +78,7 @@ private static SelectedMemberInfo FindMatchFor(SelectedMemberInfo selectedMember
{
IEnumerable<SelectedMemberInfo> members = Enumerable.Empty<SelectedMemberInfo>();

foreach (var selectionRule in config.SelectionRules)
foreach (var selectionRule in config.GetActiveSelectionRules(context))
{
members = selectionRule.SelectMembers(members, context, config);
}
Expand Down
43 changes: 43 additions & 0 deletions Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs
Expand Up @@ -574,6 +574,49 @@ public void When_including_a_property_it_should_exactly_match_the_property()
act.ShouldNotThrow();
}

public class CustomType
{
public string Name { get; set; }
}

public class ClassA
{
public List<CustomType> ListOfCustomTypes { get; set; }
}

[TestMethod]
public void When_including_a_property_using_an_expression_it_should_only_apply_it_to_the_root_level()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var list1 = new List<CustomType>
{
new CustomType {Name = "A"},
new CustomType {Name = "B"}
};

var list2 = new List<CustomType>
{
new CustomType {Name = "C"},
new CustomType {Name = "D"}
};

var objectA = new ClassA { ListOfCustomTypes = list1 };
var objectB = new ClassA { ListOfCustomTypes = list2 };

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action act = () => objectA.ShouldBeEquivalentTo(objectB, options => options.Including(x => x.ListOfCustomTypes));

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
act.ShouldThrow<AssertFailedException>().
WithMessage("*C*but*A*D*but*B*");
}

[TestMethod]
public void When_null_is_provided_as_property_expression_it_should_throw()
{
Expand Down

0 comments on commit b637423

Please sign in to comment.