From b63742336a623812b81e01c7f9e39a143eb85960 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Tue, 23 Aug 2016 21:36:57 +0200 Subject: [PATCH] Ensured that using the Including construct always starts at the root. Fixes #462. --- Src/Core/Core.projitems | 1 + ...llectionMemberAssertionOptionsDecorator.cs | 9 +- Src/Core/Equivalency/EquivalencyValidator.cs | 15 ++- Src/Core/Equivalency/IConditionalRule.cs | 10 ++ .../IEquivalencyAssertionOptions.cs | 7 +- .../CollectionMemberSelectionRuleDecorator.cs | 5 +- .../IncludeMemberByPathSelectionRule.cs | 7 +- .../IncludeMemberByPredicateSelectionRule.cs | 5 +- Src/Core/Equivalency/Selection/MemberPath.cs | 2 + ...elfReferenceEquivalencyAssertionOptions.cs | 105 +++++++----------- .../StructuralEqualityEquivalencyStep.cs | 2 +- .../BasicEquivalencySpecs.cs | 43 +++++++ 12 files changed, 127 insertions(+), 84 deletions(-) create mode 100644 Src/Core/Equivalency/IConditionalRule.cs diff --git a/Src/Core/Core.projitems b/Src/Core/Core.projitems index 3e2a71a122..6f45d1209a 100644 --- a/Src/Core/Core.projitems +++ b/Src/Core/Core.projitems @@ -66,6 +66,7 @@ + diff --git a/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs b/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs index cd51676bd7..e657bfed9c 100644 --- a/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs +++ b/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs @@ -16,14 +16,13 @@ public CollectionMemberAssertionOptionsDecorator(IEquivalencyAssertionOptions in this.inner = inner; } - public IEnumerable SelectionRules + public IEnumerable 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 SelectionRules => inner.SelectionRules; + public IEnumerable MatchingRules { get { return inner.MatchingRules.Select(rule => new CollectionMemberMatchingRuleDecorator(rule)).ToArray(); } diff --git a/Src/Core/Equivalency/EquivalencyValidator.cs b/Src/Core/Equivalency/EquivalencyValidator.cs index 3e3f0e9599..dd311d5669 100644 --- a/Src/Core/Equivalency/EquivalencyValidator.cs +++ b/Src/Core/Equivalency/EquivalencyValidator.cs @@ -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) { diff --git a/Src/Core/Equivalency/IConditionalRule.cs b/Src/Core/Equivalency/IConditionalRule.cs new file mode 100644 index 0000000000..8c4dff97cd --- /dev/null +++ b/Src/Core/Equivalency/IConditionalRule.cs @@ -0,0 +1,10 @@ +namespace FluentAssertions.Equivalency +{ + /// + /// Allows a to indicate it applies only in a certain context. + /// + public interface IConditionalRule + { + bool Applies(IEquivalencyValidationContext context); + } +} \ No newline at end of file diff --git a/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs b/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs index 14aae4fd62..b0486debb6 100644 --- a/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs +++ b/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs @@ -9,7 +9,12 @@ namespace FluentAssertions.Equivalency public interface IEquivalencyAssertionOptions { /// - /// 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. + /// + IEnumerable GetActiveSelectionRules(IEquivalencyValidationContext context); + + /// + /// Gets an ordered collection of all selection rules that have been configured. /// IEnumerable SelectionRules { get; } diff --git a/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs b/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs index 8fe395db07..ae3d504a21 100644 --- a/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs +++ b/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs @@ -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 SelectMembers(IEnumerable selectedMembers, ISubjectInfo context, IEquivalencyAssertionOptions config) diff --git a/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs index 641981b48b..bf59f3303c 100644 --- a/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs +++ b/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs @@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection /// /// Selection rule that includes a particular property in the structural comparison. /// - internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule + internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule, IConditionalRule { private readonly MemberPath pathToInclude; @@ -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 OnSelectMembers(IEnumerable selectedMembers, string currentPath, ISubjectInfo context) { diff --git a/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs b/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs index 41786a3b77..9dd272ef96 100644 --- a/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs +++ b/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs @@ -20,10 +20,7 @@ public IncludeMemberByPredicateSelectionRule(Expression this.predicate = predicate.Compile(); } - public bool IncludesMembers - { - get { return true; } - } + public bool IncludesMembers => true; public IEnumerable SelectMembers(IEnumerable selectedMembers, ISubjectInfo context, IEquivalencyAssertionOptions config) { diff --git a/Src/Core/Equivalency/Selection/MemberPath.cs b/Src/Core/Equivalency/Selection/MemberPath.cs index 87499b5e2d..9fc908323b 100644 --- a/Src/Core/Equivalency/Selection/MemberPath.cs +++ b/Src/Core/Equivalency/Selection/MemberPath.cs @@ -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; } } \ No newline at end of file diff --git a/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs index cb75ced0f2..7aee8638db 100644 --- a/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs +++ b/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs @@ -88,95 +88,71 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions /// /// Gets an ordered collection of selection rules that define what members are included. /// - IEnumerable IEquivalencyAssertionOptions.SelectionRules + /// + IEnumerable IEquivalencyAssertionOptions.GetActiveSelectionRules(IEquivalencyValidationContext context) { - get + IEnumerable 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 SelectionRules => selectionRules; + /// /// Gets an ordered collection of matching rules that determine which subject members are matched with which /// expectation members. /// - IEnumerable IEquivalencyAssertionOptions.MatchingRules - { - get { return matchingRules; } - } + IEnumerable IEquivalencyAssertionOptions.MatchingRules => matchingRules; /// /// Gets an ordered collection of Equivalency steps how a subject is compared with the expectation. /// - IEnumerable IEquivalencyAssertionOptions.UserEquivalencySteps - { - get { return userEquivalencySteps; } - } + IEnumerable IEquivalencyAssertionOptions.UserEquivalencySteps => userEquivalencySteps; /// /// Gets an ordered collection of rules that determine whether or not the order of collections is important. By default, /// ordering is irrelevant. /// - OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules - { - get { return orderingRules; } - } + OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules => orderingRules; /// /// Gets value indicating whether the equality check will include nested collections and complex types. /// - bool IEquivalencyAssertionOptions.IsRecursive - { - get { return isRecursive; } - } + bool IEquivalencyAssertionOptions.IsRecursive => isRecursive; - bool IEquivalencyAssertionOptions.AllowInfiniteRecursion - { - get { return allowInfiniteRecursion; } - } + bool IEquivalencyAssertionOptions.AllowInfiniteRecursion => allowInfiniteRecursion; /// /// Gets value indicating how cyclic references should be handled. By default, it will throw an exception. /// - 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; /// /// Gets a value indicating whether the should be treated as having value semantics. @@ -360,7 +336,6 @@ public TSelf IgnoringCyclicReferences() return (TSelf)this; } - /// /// Disables limitations on recursion depth when the structural equality check is configured to include nested objects /// @@ -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; } /// @@ -462,7 +437,7 @@ public TSelf WithoutStrictOrderingFor(Expression> predi public TSelf ComparingEnumsByName() { enumEquivalencyHandling = EnumEquivalencyHandling.ByName; - return (TSelf) this; + return (TSelf)this; } /// @@ -474,7 +449,7 @@ public TSelf ComparingEnumsByName() public TSelf ComparingEnumsByValue() { enumEquivalencyHandling = EnumEquivalencyHandling.ByValue; - return (TSelf) this; + return (TSelf)this; } /// @@ -484,7 +459,7 @@ public TSelf ComparingEnumsByValue() public TSelf ComparingByValue() { valueTypes.Add(typeof(T)); - return (TSelf) this; + return (TSelf)this; } #region Non-fluent API @@ -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 @@ -612,4 +587,4 @@ public TSelf When(Expression> predicate) } } } -} +} \ No newline at end of file diff --git a/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs b/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs index 899278f68a..598af4f815 100644 --- a/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs +++ b/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs @@ -78,7 +78,7 @@ private static SelectedMemberInfo FindMatchFor(SelectedMemberInfo selectedMember { IEnumerable members = Enumerable.Empty(); - foreach (var selectionRule in config.SelectionRules) + foreach (var selectionRule in config.GetActiveSelectionRules(context)) { members = selectionRule.SelectMembers(members, context, config); } diff --git a/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs b/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs index 51771a4bdd..a0e8187e29 100644 --- a/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs @@ -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 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 + { + new CustomType {Name = "A"}, + new CustomType {Name = "B"} + }; + + var list2 = new List + { + 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(). + WithMessage("*C*but*A*D*but*B*"); + } + [TestMethod] public void When_null_is_provided_as_property_expression_it_should_throw() {