From 08a6f6ec1a8f6ace1d8880c608f202d418acc280 Mon Sep 17 00:00:00 2001 From: Pent Ploompuu Date: Tue, 2 Oct 2018 17:59:41 +0300 Subject: [PATCH] EquivalencyValidator performance fixes --- .editorconfig | 4 +- .../Common/ExpressionExtensions.cs | 5 -- .../EnumerableEquivalencyValidator.cs | 2 +- .../GenericDictionaryEquivalencyStep.cs | 45 ++++++------------ .../GenericEnumerableEquivalencyStep.cs | 47 +++++++------------ .../Equivalency/ObjectReference.cs | 32 +++++++------ 6 files changed, 54 insertions(+), 81 deletions(-) diff --git a/.editorconfig b/.editorconfig index 26015fa746..a32088be5e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,8 +21,8 @@ tab_width = 2 [*.{md,json}] indent_style = space -tab_width = 4 +indent_size = 4 [*.cs] indent_style = space -tab_width = 4 +indent_size = 4 diff --git a/Src/FluentAssertions/Common/ExpressionExtensions.cs b/Src/FluentAssertions/Common/ExpressionExtensions.cs index eb2cd38cb2..03417edc9e 100644 --- a/Src/FluentAssertions/Common/ExpressionExtensions.cs +++ b/Src/FluentAssertions/Common/ExpressionExtensions.cs @@ -148,10 +148,5 @@ public static class ExpressionExtensions string segmentPath = string.Join(".", reversedSegments); return 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 f8d5b52ce8..c98cf9bd2a 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs @@ -54,7 +54,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 7c66ba7717..21475611ac 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 e584047988..8f2df05b6f 100644 --- a/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericEnumerableEquivalencyStep.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using FluentAssertions.Common; using FluentAssertions.Execution; @@ -35,14 +34,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)) { @@ -54,20 +55,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) { @@ -78,9 +70,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 }); @@ -114,14 +112,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());