From b8c7c7a4f865f3a455c324e717d05168242aff54 Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 2 Oct 2018 17:59:41 +0300 Subject: [PATCH 1/2] EquivalencyValidator performance fixes --- .../Common/ExpressionExtensions.cs | 5 -- .../EnumerableEquivalencyValidator.cs | 2 +- .../GenericDictionaryEquivalencyStep.cs | 45 ++++++------------ .../GenericEnumerableEquivalencyStep.cs | 47 +++++++------------ .../Equivalency/ObjectReference.cs | 32 +++++++------ 5 files changed, 52 insertions(+), 79 deletions(-) diff --git a/Src/FluentAssertions/Common/ExpressionExtensions.cs b/Src/FluentAssertions/Common/ExpressionExtensions.cs index c99b0f552a..82a22d8e46 100644 --- a/Src/FluentAssertions/Common/ExpressionExtensions.cs +++ b/Src/FluentAssertions/Common/ExpressionExtensions.cs @@ -158,10 +158,5 @@ internal static class ExpressionExtensions return new MemberPath(declaringType, segmentPath.Replace(".[", "[")); } - - internal static string GetMethodName(Expression action) - { - return ((MethodCallExpression)action.Body).Method.Name; - } } } diff --git a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs index 5af82aeccf..279dcd88cd 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs @@ -53,7 +53,7 @@ public void Execute(object[] subject, T[] expectation) private bool AssertIsNotNull(object expectation, object[] subject) { - return AssertionScope.Current + return expectation != null || AssertionScope.Current .ForCondition(!(expectation is null)) .FailWith("Expected {context:subject} to be , but found {0}.", new object[] { subject }); } diff --git a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs index 225a4a8065..c08b704043 100644 --- a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs @@ -1,9 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; -using FluentAssertions.Common; using FluentAssertions.Execution; namespace FluentAssertions.Equivalency @@ -52,14 +51,14 @@ private static bool PreconditionsAreMet(IEquivalencyValidationContext context, I private static bool AssertSubjectIsNotNull(object subject) { - return AssertionScope.Current + return subject != null || AssertionScope.Current .ForCondition(!(subject is null)) .FailWith("Expected {context:Subject} not to be {0}.", new object[] { null }); } private static bool AssertExpectationIsNotNull(object subject, object expectation) { - return AssertionScope.Current + return expectation != null || AssertionScope.Current .ForCondition(!(expectation is null)) .FailWith("Expected {context:Subject} to be {0}, but found {1}.", null, subject); } @@ -135,22 +134,17 @@ private static Type GetDictionaryKeyType(Type expectedType) private static bool AssertSameLength(object subject, Type expectationType, object expectation) { - string methodName = - ExpressionExtensions.GetMethodName(() => AssertSameLength(null, null)); + if(subject is ICollection subjectCollection + && expectation is ICollection expectationCollection + && subjectCollection.Count == expectationCollection.Count) + return true; Type subjectType = subject.GetType(); Type[] subjectTypeArguments = GetDictionaryTypeArguments(subjectType); Type[] expectationTypeArguments = GetDictionaryTypeArguments(expectationType); Type[] typeArguments = subjectTypeArguments.Concat(expectationTypeArguments).ToArray(); - MethodCallExpression assertSameLength = Expression.Call( - typeof(GenericDictionaryEquivalencyStep), - methodName, - typeArguments, - Expression.Constant(subject, GetIDictionaryInterface(subjectType)), - Expression.Constant(expectation, GetIDictionaryInterface(expectationType))); - - return (bool)Expression.Lambda(assertSameLength).Compile().DynamicInvoke(); + return (bool)AssertSameLengthMethod.MakeGenericMethod(typeArguments).Invoke(null, new[] { subject, expectation }); } private static Type[] GetDictionaryTypeArguments(Type type) @@ -165,6 +159,9 @@ private static Type GetIDictionaryInterface(Type expectedType) return GetIDictionaryInterfaces(expectedType).Single(); } + private static readonly MethodInfo AssertSameLengthMethod = new Func, IDictionary, bool> + (AssertSameLength).GetMethodInfo().GetGenericMethodDefinition(); + private static bool AssertSameLength( IDictionary subject, IDictionary expectation) where TExpectedKey : TSubjectKey @@ -233,29 +230,17 @@ private static Type GetIDictionaryInterface(Type expectedType) IEquivalencyValidator parent, IEquivalencyAssertionOptions config) { Type expectationType = config.GetExpectationType(context); - - string methodName = - ExpressionExtensions.GetMethodName( - () => AssertDictionaryEquivalence(null, null, null, null, null)); - Type subjectType = context.Subject.GetType(); Type[] subjectTypeArguments = GetDictionaryTypeArguments(subjectType); Type[] expectationTypeArguments = GetDictionaryTypeArguments(expectationType); Type[] typeArguments = subjectTypeArguments.Concat(expectationTypeArguments).ToArray(); - MethodCallExpression assertDictionaryEquivalence = Expression.Call( - typeof(GenericDictionaryEquivalencyStep), - methodName, - typeArguments, - Expression.Constant(context), - Expression.Constant(parent), - Expression.Constant(config), - Expression.Constant(context.Subject, GetIDictionaryInterface(subjectType)), - Expression.Constant(context.Expectation, GetIDictionaryInterface(expectationType))); - - Expression.Lambda(assertDictionaryEquivalence).Compile().DynamicInvoke(); + AssertDictionaryEquivalenceMethod.MakeGenericMethod(typeArguments).Invoke(null, new[] { context, parent, config, context.Subject, context.Expectation }); } + private static readonly MethodInfo AssertDictionaryEquivalenceMethod = new Action, IDictionary> + (AssertDictionaryEquivalence).GetMethodInfo().GetGenericMethodDefinition(); + private static void AssertDictionaryEquivalence( EquivalencyValidationContext context, IEquivalencyValidator parent, diff --git a/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs index d80929327e..5427d87906 100644 --- a/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using FluentAssertions.Common; using FluentAssertions.Execution; @@ -36,14 +35,16 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti { Type expectedType = config.GetExpectationType(context); - var interfaceTypes = GetIEnumerableInterfaces(expectedType) - .Select(type => "IEnumerable<" + type.GetGenericArguments().Single() + ">") - .ToList(); + var interfaceTypes = GetIEnumerableInterfaces(expectedType); - AssertionScope.Current - .ForCondition(interfaceTypes.Count == 1) - .FailWith("{context:Expectation} implements {0}, so cannot determine which one " + - "to use for asserting the equivalency of the collection. ", interfaceTypes); + if (interfaceTypes.Length != 1) + { + AssertionScope.Current + .ForCondition(interfaceTypes.Length == 1) + .FailWith("{context:Expectation} implements {0}, so cannot determine which one " + + "to use for asserting the equivalency of the collection. ", + interfaceTypes.Select(type => "IEnumerable<" + type.GetGenericArguments().Single() + ">").ToList()); + } if (AssertSubjectIsCollection(context.Expectation, context.Subject)) { @@ -55,20 +56,11 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti Type typeOfEnumeration = GetTypeOfEnumeration(expectedType); - MethodCallExpression expectationAsArray = ToArray(context.Expectation, typeOfEnumeration); - ConstantExpression subjectAsArray = - Expression.Constant(EnumerableEquivalencyStep.ToArray(context.Subject)); - - MethodCallExpression executeExpression = Expression.Call( - Expression.Constant(validator), - ExpressionExtensions.GetMethodName(() => validator.Execute(null, null)), - new[] {typeOfEnumeration}, - subjectAsArray, - expectationAsArray); + var subjectAsArray = EnumerableEquivalencyStep.ToArray(context.Subject); try { - Expression.Lambda(executeExpression).Compile().DynamicInvoke(); + HandleMethod.MakeGenericMethod(typeOfEnumeration).Invoke(null, new[] { validator, subjectAsArray, context.Expectation }); } catch (TargetInvocationException e) { @@ -79,9 +71,15 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti return true; } + private static readonly MethodInfo HandleMethod = new Action> + (HandleImpl).GetMethodInfo().GetGenericMethodDefinition(); + + private static void HandleImpl(EnumerableEquivalencyValidator validator, object[] subject, IEnumerable expectation) + => validator.Execute(subject, expectation?.ToArray()); + private static bool AssertSubjectIsCollection(object expectation, object subject) { - bool conditionMet = AssertionScope.Current + bool conditionMet = subject != null || AssertionScope.Current .ForCondition(!(subject is null)) .FailWith("Expected {context:subject} not to be {0}.", new object[] {null}); @@ -120,14 +118,5 @@ private static Type GetTypeOfEnumeration(Type enumerableType) return interfaceType.GetGenericArguments().Single(); } - - private static MethodCallExpression ToArray(object value, Type typeOfEnumeration) - { - return Expression.Call( - typeof(Enumerable), - "ToArray", - new[] {typeOfEnumeration}, - Expression.Constant(value, typeof(IEnumerable<>).MakeGenericType(typeOfEnumeration))); - } } } diff --git a/Src/FluentAssertions/Equivalency/ObjectReference.cs b/Src/FluentAssertions/Equivalency/ObjectReference.cs index 68c6382301..3fc779b8a5 100644 --- a/Src/FluentAssertions/Equivalency/ObjectReference.cs +++ b/Src/FluentAssertions/Equivalency/ObjectReference.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using FluentAssertions.Common; namespace FluentAssertions.Equivalency @@ -10,18 +9,13 @@ namespace FluentAssertions.Equivalency internal class ObjectReference { private readonly object @object; - private readonly string[] path; + private readonly string path; private readonly bool? isComplexType; public ObjectReference(object @object, string path, bool? isComplexType = null) { this.@object = @object; - - this.path = path - .ToLower() - .Replace("][", "].[") - .Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - + this.path = path; this.isComplexType = isComplexType; } @@ -42,9 +36,22 @@ public override bool Equals(object obj) return ReferenceEquals(@object, other.@object) && IsParentOf(other); } + string[] pathElements; + + string[] GetPathElements() => pathElements + ?? (pathElements = path.ToLowerInvariant().Replace("][", "].[").Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)); + private bool IsParentOf(ObjectReference other) { - return (other.path.Length > path.Length) && other.path.Take(path.Length).SequenceEqual(path); + string[] path = GetPathElements(), otherPath = other.GetPathElements(); + + if (otherPath.Length <= path.Length) + return false; + + for (int i = 0; i < path.Length; i++) + if (path[i] != otherPath[i]) + return false; + return true; } /// @@ -56,15 +63,12 @@ private bool IsParentOf(ObjectReference other) /// 2 public override int GetHashCode() { - unchecked - { - return (@object.GetHashCode() * 397) ^ path.GetHashCode(); - } + return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(@object); } public override string ToString() { - return $"{{\"{string.Join(".", path)}\", {@object}}}"; + return $"{{\"{path}\", {@object}}}"; } public bool IsComplexType => isComplexType ?? (!(@object is null) && !@object.GetType().OverridesEquals()); From bf43168691d84866d775d372fff6af9661bf437e Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Fri, 9 Nov 2018 14:19:23 +0200 Subject: [PATCH 2/2] Address PR feedback --- .../EnumerableEquivalencyValidator.cs | 2 +- .../GenericDictionaryEquivalencyStep.cs | 16 +++++++------- .../GenericEnumerableEquivalencyStep.cs | 21 ++++++++----------- .../Equivalency/ObjectReference.cs | 18 ++++++---------- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs index 279dcd88cd..5af82aeccf 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs @@ -53,7 +53,7 @@ public void Execute(object[] subject, T[] expectation) private bool AssertIsNotNull(object expectation, object[] subject) { - return expectation != null || AssertionScope.Current + return AssertionScope.Current .ForCondition(!(expectation is null)) .FailWith("Expected {context:subject} to be , but found {0}.", new object[] { subject }); } diff --git a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs index c08b704043..9744027415 100644 --- a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs @@ -13,6 +13,12 @@ namespace FluentAssertions.Equivalency /// public class GenericDictionaryEquivalencyStep : IEquivalencyStep { + private static readonly MethodInfo AssertSameLengthMethod = new Func, IDictionary, bool> + (AssertSameLength).GetMethodInfo().GetGenericMethodDefinition(); + + private static readonly MethodInfo AssertDictionaryEquivalenceMethod = new Action, IDictionary> + (AssertDictionaryEquivalence).GetMethodInfo().GetGenericMethodDefinition(); + public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAssertionOptions config) { Type expectationType = config.GetExpectationType(context); @@ -51,14 +57,14 @@ private static bool PreconditionsAreMet(IEquivalencyValidationContext context, I private static bool AssertSubjectIsNotNull(object subject) { - return subject != null || AssertionScope.Current + return AssertionScope.Current .ForCondition(!(subject is null)) .FailWith("Expected {context:Subject} not to be {0}.", new object[] { null }); } private static bool AssertExpectationIsNotNull(object subject, object expectation) { - return expectation != null || AssertionScope.Current + return AssertionScope.Current .ForCondition(!(expectation is null)) .FailWith("Expected {context:Subject} to be {0}, but found {1}.", null, subject); } @@ -159,9 +165,6 @@ private static Type GetIDictionaryInterface(Type expectedType) return GetIDictionaryInterfaces(expectedType).Single(); } - private static readonly MethodInfo AssertSameLengthMethod = new Func, IDictionary, bool> - (AssertSameLength).GetMethodInfo().GetGenericMethodDefinition(); - private static bool AssertSameLength( IDictionary subject, IDictionary expectation) where TExpectedKey : TSubjectKey @@ -238,9 +241,6 @@ private static Type GetIDictionaryInterface(Type expectedType) AssertDictionaryEquivalenceMethod.MakeGenericMethod(typeArguments).Invoke(null, new[] { context, parent, config, context.Subject, context.Expectation }); } - private static readonly MethodInfo AssertDictionaryEquivalenceMethod = new Action, IDictionary> - (AssertDictionaryEquivalence).GetMethodInfo().GetGenericMethodDefinition(); - private static void AssertDictionaryEquivalence( EquivalencyValidationContext context, IEquivalencyValidator parent, diff --git a/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs index 5427d87906..adf5cf5482 100644 --- a/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs @@ -10,6 +10,9 @@ namespace FluentAssertions.Equivalency { public class GenericEnumerableEquivalencyStep : IEquivalencyStep { + private static readonly MethodInfo HandleMethod = new Action> + (HandleImpl).GetMethodInfo().GetGenericMethodDefinition(); + /// /// Gets a value indicating whether this step can handle the verificationScope subject and/or expectation. /// @@ -37,14 +40,11 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti var interfaceTypes = GetIEnumerableInterfaces(expectedType); - if (interfaceTypes.Length != 1) - { - AssertionScope.Current - .ForCondition(interfaceTypes.Length == 1) - .FailWith("{context:Expectation} implements {0}, so cannot determine which one " + - "to use for asserting the equivalency of the collection. ", - interfaceTypes.Select(type => "IEnumerable<" + type.GetGenericArguments().Single() + ">").ToList()); - } + AssertionScope.Current + .ForCondition(interfaceTypes.Length == 1) + .FailWith(() => new FailReason("{context:Expectation} implements {0}, so cannot determine which one " + + "to use for asserting the equivalency of the collection. ", + interfaceTypes.Select(type => "IEnumerable<" + type.GetGenericArguments().Single() + ">"))); if (AssertSubjectIsCollection(context.Expectation, context.Subject)) { @@ -71,15 +71,12 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti return true; } - private static readonly MethodInfo HandleMethod = new Action> - (HandleImpl).GetMethodInfo().GetGenericMethodDefinition(); - private static void HandleImpl(EnumerableEquivalencyValidator validator, object[] subject, IEnumerable expectation) => validator.Execute(subject, expectation?.ToArray()); private static bool AssertSubjectIsCollection(object expectation, object subject) { - bool conditionMet = subject != null || AssertionScope.Current + bool conditionMet = AssertionScope.Current .ForCondition(!(subject is null)) .FailWith("Expected {context:subject} not to be {0}.", new object[] {null}); diff --git a/Src/FluentAssertions/Equivalency/ObjectReference.cs b/Src/FluentAssertions/Equivalency/ObjectReference.cs index 3fc779b8a5..d23ec6c754 100644 --- a/Src/FluentAssertions/Equivalency/ObjectReference.cs +++ b/Src/FluentAssertions/Equivalency/ObjectReference.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using FluentAssertions.Common; namespace FluentAssertions.Equivalency @@ -11,6 +12,7 @@ internal class ObjectReference private readonly object @object; private readonly string path; private readonly bool? isComplexType; + private string[] pathElements; public ObjectReference(object @object, string path, bool? isComplexType = null) { @@ -36,22 +38,14 @@ public override bool Equals(object obj) return ReferenceEquals(@object, other.@object) && IsParentOf(other); } - string[] pathElements; - - string[] GetPathElements() => pathElements + private string[] GetPathElements() => pathElements ?? (pathElements = path.ToLowerInvariant().Replace("][", "].[").Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries)); private bool IsParentOf(ObjectReference other) { - string[] path = GetPathElements(), otherPath = other.GetPathElements(); - - if (otherPath.Length <= path.Length) - return false; - - for (int i = 0; i < path.Length; i++) - if (path[i] != otherPath[i]) - return false; - return true; + string[] path = GetPathElements(); + string[] otherPath = other.GetPathElements(); + return (otherPath.Length > path.Length) && otherPath.Take(path.Length).SequenceEqual(path); } ///