Skip to content

Commit

Permalink
EquivalencyValidator performance fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
pentp committed Oct 2, 2018
1 parent 65f6753 commit 08a6f6e
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 81 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Expand Up @@ -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
5 changes: 0 additions & 5 deletions Src/FluentAssertions/Common/ExpressionExtensions.cs
Expand Up @@ -148,10 +148,5 @@ public static class ExpressionExtensions
string segmentPath = string.Join(".", reversedSegments);
return segmentPath.Replace(".[", "[");
}

internal static string GetMethodName(Expression<Action> action)
{
return ((MethodCallExpression)action.Body).Method.Name;
}
}
}
Expand Up @@ -54,7 +54,7 @@ public void Execute<T>(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 <null>, but found {0}.", new object[] { subject });
}
Expand Down
@@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<object, object, object, object>(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)
Expand All @@ -165,6 +159,9 @@ private static Type GetIDictionaryInterface(Type expectedType)
return GetIDictionaryInterfaces(expectedType).Single();
}

private static readonly MethodInfo AssertSameLengthMethod = new Func<IDictionary<object, object>, IDictionary<object, object>, bool>
(AssertSameLength).GetMethodInfo().GetGenericMethodDefinition();

private static bool AssertSameLength<TSubjectKey, TSubjectValue, TExpectedKey, TExpectedValue>(
IDictionary<TSubjectKey, TSubjectValue> subject, IDictionary<TExpectedKey, TExpectedValue> expectation)
where TExpectedKey : TSubjectKey
Expand Down Expand Up @@ -233,29 +230,17 @@ private static Type GetIDictionaryInterface(Type expectedType)
IEquivalencyValidator parent, IEquivalencyAssertionOptions config)
{
Type expectationType = config.GetExpectationType(context);

string methodName =
ExpressionExtensions.GetMethodName(
() => AssertDictionaryEquivalence<object, object, object, object>(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<EquivalencyValidationContext, IEquivalencyValidator, IEquivalencyAssertionOptions, IDictionary<object, object>, IDictionary<object, object>>
(AssertDictionaryEquivalence).GetMethodInfo().GetGenericMethodDefinition();

private static void AssertDictionaryEquivalence<TSubjectKey, TSubjectValue, TExpectedKey, TExpectedValue>(
EquivalencyValidationContext context,
IEquivalencyValidator parent,
Expand Down
@@ -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;
Expand Down Expand Up @@ -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))
{
Expand All @@ -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<object>(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)
{
Expand All @@ -78,9 +70,15 @@ public bool CanHandle(IEquivalencyValidationContext context, IEquivalencyAsserti
return true;
}

private static readonly MethodInfo HandleMethod = new Action<EnumerableEquivalencyValidator, object[], IEnumerable<object>>
(HandleImpl).GetMethodInfo().GetGenericMethodDefinition();

private static void HandleImpl<T>(EnumerableEquivalencyValidator validator, object[] subject, IEnumerable<T> 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 });

Expand Down Expand Up @@ -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)));
}
}
}
32 changes: 18 additions & 14 deletions Src/FluentAssertions/Equivalency/ObjectReference.cs
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using FluentAssertions.Common;

namespace FluentAssertions.Equivalency
Expand All @@ -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;
}

Expand All @@ -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;
}

/// <summary>
Expand All @@ -56,15 +63,12 @@ private bool IsParentOf(ObjectReference other)
/// <filterpriority>2</filterpriority>
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());
Expand Down

0 comments on commit 08a6f6e

Please sign in to comment.