From 3ca4dbebb0391d16f189adaa5825b65680ae4869 Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Mon, 8 Oct 2018 07:10:20 +0200 Subject: [PATCH 1/5] Fix ConversionSelector's cloning of the description --- .../Equivalency/ConversionSelector.cs | 12 ++++++------ Tests/Shared.Specs/BasicEquivalencySpecs.cs | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Src/FluentAssertions/Equivalency/ConversionSelector.cs b/Src/FluentAssertions/Equivalency/ConversionSelector.cs index ee85bf7e71..58f5e34445 100644 --- a/Src/FluentAssertions/Equivalency/ConversionSelector.cs +++ b/Src/FluentAssertions/Equivalency/ConversionSelector.cs @@ -14,24 +14,24 @@ public class ConversionSelector { private List> inclusions = new List>(); private List> exclusions = new List>(); - private StringBuilder description = new StringBuilder(); + private StringBuilder descriptionBuilder = new StringBuilder(); public void IncludeAll() { inclusions.Add(_ => true); - description.Append("Try conversion of all members. "); + descriptionBuilder.Append("Try conversion of all members. "); } public void Include(Expression> predicate) { inclusions.Add(predicate.Compile()); - description.Append("Try conversion of member ").Append(predicate.Body).Append(". "); + descriptionBuilder.Append("Try conversion of member ").Append(predicate.Body).Append(". "); } public void Exclude(Expression> predicate) { exclusions.Add(predicate.Compile()); - description.Append("Do not convert member ").Append(predicate.Body).Append(". "); + descriptionBuilder.Append("Do not convert member ").Append(predicate.Body).Append(". "); } public bool RequiresConversion(IMemberInfo info) @@ -41,7 +41,7 @@ public bool RequiresConversion(IMemberInfo info) public override string ToString() { - string result = description.ToString(); + string result = descriptionBuilder.ToString(); return (result.Length > 0) ? result : "Without automatic conversion."; } @@ -51,7 +51,7 @@ public ConversionSelector Clone() { inclusions = new List>(inclusions), exclusions = new List>(exclusions), - description = description + descriptionBuilder = new StringBuilder(descriptionBuilder.ToString()) }; } } diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index 2f08c3f43f..d4a694ae43 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -2581,7 +2581,10 @@ public void When_a_specific_mismatching_property_is_excluded_from_conversion_it_ //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - act.Should().Throw().WithMessage("Expected*<1973-09-20>*\"1973-09-20\"*"); + string exceptionMessage = act.Should().Throw().Which.Message; + exceptionMessage.Should().Match("Expected*<1973-09-20>*\"1973-09-20\"*", "{0} field is of mismatched type", nameof(expectation.Birthdate)); + exceptionMessage.Should().NotMatch("*Try conversion of all members*", "conversion description should be present"); + exceptionMessage.Should().NotMatch("*Try conversion of all members*Try conversion of all members*", "conversion description should not be duplicated"); } [Fact] From f717ebf5dffbd94c14c686b2dd880d6320825d76 Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Tue, 9 Oct 2018 21:06:22 +0200 Subject: [PATCH 2/5] Make ConvertionSelector immutable and injectable into IEquivalencyAssertionOptions.GetUserEquivalencySteps --- ...llectionMemberAssertionOptionsDecorator.cs | 4 +- .../Equivalency/ConversionSelector.cs | 57 +++++++++++++------ .../IEquivalencyAssertionOptions.cs | 2 +- .../RunAllUserStepsEquivalencyStep.cs | 4 +- ...elfReferenceEquivalencyAssertionOptions.cs | 9 +-- .../Primitives/ObjectAssertions.cs | 1 - Tests/Shared.Specs/BasicEquivalencySpecs.cs | 2 +- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/Src/FluentAssertions/Equivalency/CollectionMemberAssertionOptionsDecorator.cs b/Src/FluentAssertions/Equivalency/CollectionMemberAssertionOptionsDecorator.cs index e035699ed9..90527e5135 100644 --- a/Src/FluentAssertions/Equivalency/CollectionMemberAssertionOptionsDecorator.cs +++ b/Src/FluentAssertions/Equivalency/CollectionMemberAssertionOptionsDecorator.cs @@ -39,9 +39,9 @@ public OrderingRuleCollection OrderingRules public ConversionSelector ConversionSelector => inner.ConversionSelector; - public IEnumerable UserEquivalencySteps + public IEnumerable GetUserEquivalencySteps(ConversionSelector conversionSelector) { - get { return inner.UserEquivalencySteps.Select(step => new CollectionMemberAssertionRuleDecorator(step)).ToArray(); } + return inner.GetUserEquivalencySteps(conversionSelector).Select(step => new CollectionMemberAssertionRuleDecorator(step)).ToArray(); } public bool IsRecursive => inner.IsRecursive; diff --git a/Src/FluentAssertions/Equivalency/ConversionSelector.cs b/Src/FluentAssertions/Equivalency/ConversionSelector.cs index 58f5e34445..318915373c 100644 --- a/Src/FluentAssertions/Equivalency/ConversionSelector.cs +++ b/Src/FluentAssertions/Equivalency/ConversionSelector.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Linq.Expressions; using System.Text; -using FluentAssertions.Execution; namespace FluentAssertions.Equivalency { @@ -12,46 +11,72 @@ namespace FluentAssertions.Equivalency /// public class ConversionSelector { - private List> inclusions = new List>(); - private List> exclusions = new List>(); - private StringBuilder descriptionBuilder = new StringBuilder(); + private class ConversionSelectorRule + { + public Func Predicate { get; } + public string Description { get; } + + public ConversionSelectorRule(Func predicate, string description) + { + Predicate = predicate; + Description = description; + } + } + private List inclusions = new List(); + private List exclusions = new List(); public void IncludeAll() { - inclusions.Add(_ => true); - descriptionBuilder.Append("Try conversion of all members. "); + inclusions.Add(new ConversionSelectorRule(_ => true, "Try conversion of all members. ")); } public void Include(Expression> predicate) { - inclusions.Add(predicate.Compile()); - descriptionBuilder.Append("Try conversion of member ").Append(predicate.Body).Append(". "); + inclusions.Add(new ConversionSelectorRule( + predicate.Compile(), + $"Try conversion of member {predicate.Body}. ")); } public void Exclude(Expression> predicate) { - exclusions.Add(predicate.Compile()); - descriptionBuilder.Append("Do not convert member ").Append(predicate.Body).Append(". "); + exclusions.Add(new ConversionSelectorRule( + predicate.Compile(), + $"Do not convert member {predicate.Body}.")); } public bool RequiresConversion(IMemberInfo info) { - return inclusions.Any(p => p(info)) && !exclusions.Any(p => p(info)); + return inclusions.Any(p => p.Predicate(info)) && !exclusions.Any(p => p.Predicate(info)); } public override string ToString() { - string result = descriptionBuilder.ToString(); - return (result.Length > 0) ? result : "Without automatic conversion."; + if(inclusions.Count == 0 && exclusions.Count == 0) + { + return "Without automatic conversion."; + } + + StringBuilder descriptionBuilder = new StringBuilder(); + + foreach (var inclusion in inclusions) + { + descriptionBuilder.Append(inclusion.Description); + } + + foreach (var exclusion in exclusions) + { + descriptionBuilder.Append(exclusion.Description); + } + + return descriptionBuilder.ToString(); } public ConversionSelector Clone() { return new ConversionSelector { - inclusions = new List>(inclusions), - exclusions = new List>(exclusions), - descriptionBuilder = new StringBuilder(descriptionBuilder.ToString()) + inclusions = new List(inclusions), + exclusions = new List(exclusions), }; } } diff --git a/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs index e61a9bf519..bb58993dd3 100644 --- a/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/IEquivalencyAssertionOptions.cs @@ -53,7 +53,7 @@ public interface IEquivalencyAssertionOptions /// /// Gets an ordered collection of Equivalency steps how a subject is compared with the expectation. /// - IEnumerable UserEquivalencySteps { get; } + IEnumerable GetUserEquivalencySteps(ConversionSelector conversionSelector); /// /// Gets a value indicating whether the runtime type of the expectation should be used rather than the declared type. diff --git a/Src/FluentAssertions/Equivalency/RunAllUserStepsEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/RunAllUserStepsEquivalencyStep.cs index 8f8b1aec7b..c166fdbf70 100644 --- a/Src/FluentAssertions/Equivalency/RunAllUserStepsEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/RunAllUserStepsEquivalencyStep.cs @@ -10,13 +10,13 @@ public class RunAllUserStepsEquivalencyStep : IEquivalencyStep { public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config) { - return config.UserEquivalencySteps.Any(s => s.CanHandle(context, config)); + return config.GetUserEquivalencySteps(config.ConversionSelector).Any(s => s.CanHandle(context, config)); } public bool Handle(IEquivalencyValidationContext context, IEquivalencyValidator parent, IEquivalencyAssertionOptions config) { - return config.UserEquivalencySteps + return config.GetUserEquivalencySteps(config.ConversionSelector) .Any(step => step.CanHandle(context, config) && step.Handle(context, parent, config)); } } diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs index 666d2927c2..80f0d67fe8 100644 --- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs +++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs @@ -77,11 +77,12 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions includeProperties = defaults.IncludeProperties; includeFields = defaults.IncludeFields; + ConversionSelector = defaults.ConversionSelector.Clone(); + selectionRules.AddRange(defaults.SelectionRules); - userEquivalencySteps.AddRange(defaults.UserEquivalencySteps); + userEquivalencySteps.AddRange(defaults.GetUserEquivalencySteps(ConversionSelector)); matchingRules.AddRange(defaults.MatchingRules); orderingRules = new OrderingRuleCollection(defaults.OrderingRules); - ConversionSelector = defaults.ConversionSelector.Clone(); getDefaultEqualityStrategy = defaults.GetEqualityStrategy; TraceWriter = defaults.TraceWriter; @@ -125,8 +126,8 @@ IEnumerable IEquivalencyAssertionOptions.SelectionRules /// /// Gets an ordered collection of Equivalency steps how a subject is compared with the expectation. /// - IEnumerable IEquivalencyAssertionOptions.UserEquivalencySteps => - userEquivalencySteps.Concat(new[] { new TryConversionStep(ConversionSelector) }); + IEnumerable IEquivalencyAssertionOptions.GetUserEquivalencySteps(ConversionSelector convertionSelector) => + userEquivalencySteps.Concat(new[] {new TryConversionStep(convertionSelector) }); public ConversionSelector ConversionSelector { get; } = new ConversionSelector(); diff --git a/Src/FluentAssertions/Primitives/ObjectAssertions.cs b/Src/FluentAssertions/Primitives/ObjectAssertions.cs index eb85341951..1a666ee1ad 100644 --- a/Src/FluentAssertions/Primitives/ObjectAssertions.cs +++ b/Src/FluentAssertions/Primitives/ObjectAssertions.cs @@ -12,7 +12,6 @@ namespace FluentAssertions.Primitives /// /// Contains a number of methods to assert that an is in the expected state. /// - [DebuggerNonUserCode] public class ObjectAssertions : ReferenceTypeAssertions { public ObjectAssertions(object value) diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index d4a694ae43..0f4ce90999 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -2583,7 +2583,7 @@ public void When_a_specific_mismatching_property_is_excluded_from_conversion_it_ //----------------------------------------------------------------------------------------------------------- string exceptionMessage = act.Should().Throw().Which.Message; exceptionMessage.Should().Match("Expected*<1973-09-20>*\"1973-09-20\"*", "{0} field is of mismatched type", nameof(expectation.Birthdate)); - exceptionMessage.Should().NotMatch("*Try conversion of all members*", "conversion description should be present"); + exceptionMessage.Should().Match("*Try conversion of all members*", "conversion description should be present"); exceptionMessage.Should().NotMatch("*Try conversion of all members*Try conversion of all members*", "conversion description should not be duplicated"); } From 3ae98867285ad1b9383545f2da2fe0bd7d649755 Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Wed, 10 Oct 2018 06:57:18 +0200 Subject: [PATCH 3/5] Use chaining in newly added test --- Tests/Shared.Specs/BasicEquivalencySpecs.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index 0f4ce90999..a62ee35daf 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -2581,10 +2581,10 @@ public void When_a_specific_mismatching_property_is_excluded_from_conversion_it_ //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - string exceptionMessage = act.Should().Throw().Which.Message; - exceptionMessage.Should().Match("Expected*<1973-09-20>*\"1973-09-20\"*", "{0} field is of mismatched type", nameof(expectation.Birthdate)); - exceptionMessage.Should().Match("*Try conversion of all members*", "conversion description should be present"); - exceptionMessage.Should().NotMatch("*Try conversion of all members*Try conversion of all members*", "conversion description should not be duplicated"); + act.Should().Throw().Which.Message + .Should().Match("Expected*<1973-09-20>*\"1973-09-20\"*", "{0} field is of mismatched type", nameof(expectation.Birthdate)) + .And.Subject.Should().Match("*Try conversion of all members*", "conversion description should be present") + .And.Subject.Should().NotMatch("*Try conversion of all members*Try conversion of all members*", "conversion description should not be duplicated"); } [Fact] From 06c1410f7d7647dabaea8e42d7a31c41a245f2b6 Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Wed, 10 Oct 2018 20:43:04 +0200 Subject: [PATCH 4/5] Add test for case od displaying "Without automatic conversion" --- Tests/Shared.Specs/BasicEquivalencySpecs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index a62ee35daf..f3d1bf3bc5 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -2689,7 +2689,7 @@ public void When_the_expectation_contains_a_nested_null_it_should_properly_repor // Assert //----------------------------------------------------------------------------------------------------------- act.Should().Throw() - .WithMessage("*Expected*Level.Level to be , but found*Level2*"); + .WithMessage("*Expected*Level.Level to be , but found*Level2*Without automatic conversion*"); } [Fact] From b28f9be8ef2e680b1b9b54ea0909a8b282535bed Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Wed, 10 Oct 2018 21:43:15 +0200 Subject: [PATCH 5/5] Add test checking whole message with description of all the equivalency steps --- Tests/Shared.Specs/BasicEquivalencySpecs.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/Shared.Specs/BasicEquivalencySpecs.cs b/Tests/Shared.Specs/BasicEquivalencySpecs.cs index f3d1bf3bc5..011c2b7c69 100644 --- a/Tests/Shared.Specs/BasicEquivalencySpecs.cs +++ b/Tests/Shared.Specs/BasicEquivalencySpecs.cs @@ -2790,9 +2790,12 @@ public void When_not_all_the_properties_of_the_nested_objects_are_equal_it_shoul // Assert //----------------------------------------------------------------------------------------------------------- act - .Should().Throw() - .WithMessage( - "Expected member Level.Text to be \"Level2\", but \"Level1\" differs near \"1\" (index 5)*"); + .Should().Throw().Which.Message + // Checking exception message exactly is against general guidelines + // but in that case it was done on purpose, so that we have at least single + // test confirming that whole mechanism of gathering description from + // equivalency steps works. + .Should().Be("Expected member Level.Text to be \"Level2\", but \"Level1\" differs near \"1\" (index 5).\r\n\nWith configuration:\n- Use declared types and members\r\n- Compare enums by value\r\n- Match member by name (or throw)\r\n- Without automatic conversion.\r\n- Be strict about the order of items in byte arrays\r\n"); } [Fact]