Skip to content

Commit

Permalink
Add equality comparison support
Browse files Browse the repository at this point in the history
  • Loading branch information
stakx committed Mar 4, 2019
1 parent e9cf7ab commit 48d42a4
Show file tree
Hide file tree
Showing 6 changed files with 419 additions and 4 deletions.
17 changes: 16 additions & 1 deletion src/Moq/ExpressionComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq.Expressions;

namespace Moq
Expand All @@ -18,7 +19,7 @@ private ExpressionComparer()

public bool Equals(Expression x, Expression y)
{
if (x == null && y == null)
if (object.ReferenceEquals(x, y))
{
return true;
}
Expand Down Expand Up @@ -98,6 +99,11 @@ public bool Equals(Expression x, Expression y)
}
}

if (x.NodeType == ExpressionType.Extension || y.NodeType == ExpressionType.Extension)
{
return this.EqualsExtension(x, y);
}

return false;
}

Expand Down Expand Up @@ -243,5 +249,14 @@ private bool EqualsUnary(UnaryExpression x, UnaryExpression y)
{
return x.Method == y.Method && this.Equals(x.Operand, y.Operand);
}

private bool EqualsExtension(Expression x, Expression y)
{
// For now, we only care about our own `MatchExpression` extension;
// if we wanted to be more thorough, we'd try to reduce `x` and `y`,
// then compare the reduced nodes.

return x.IsMatch(out var xm) && y.IsMatch(out var ym) && object.Equals(xm, ym);
}
}
}
37 changes: 37 additions & 0 deletions src/Moq/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

using Moq.Properties;

Expand All @@ -31,6 +32,12 @@ internal static TDelegate CompileUsingExpressionCompiler<TDelegate>(this Express

public static bool IsMatch(this Expression expression, out Match match)
{
if (expression is MatchExpression matchExpression)
{
match = matchExpression.Match;
return true;
}

using (var observer = AmbientObserver.Activate())
{
Expression.Lambda<Action>(expression).CompileUsingExpressionCompiler().Invoke();
Expand Down Expand Up @@ -356,5 +363,35 @@ public static string ToStringFixed(this Expression expression)
{
return new ExpressionStringBuilder().Append(expression).ToString();
}

public static Expression EvaluateCapturedVariables(this Expression expression)
{
return CapturedVariablesEvaluator.Instance.Visit(expression);
}

/// This is a more limited but much cheaper variant of `Evaluator`. It only evaluates
/// captured variables in lambdas to remove weird field accesses on "display class" instances.
private sealed class CapturedVariablesEvaluator : ExpressionVisitor
{
public static readonly CapturedVariablesEvaluator Instance = new CapturedVariablesEvaluator();

private CapturedVariablesEvaluator()
{
}

protected override Expression VisitMember(MemberExpression node)
{
if (node.Member is FieldInfo fi
&& node.Expression is ConstantExpression ce
&& node.Member.DeclaringType.GetTypeInfo().IsDefined(typeof(CompilerGeneratedAttribute)))
{
return Expression.Constant(fi.GetValue(ce.Value), node.Type);
}
else
{
return base.VisitMember(node);
}
}
}
}
}
4 changes: 3 additions & 1 deletion src/Moq/It.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using static System.Runtime.InteropServices.Marshal;
#endif

using Moq.Protected;

namespace Moq
{
/// <include file='It.xdoc' path='docs/doc[@for="It"]/*'/>
Expand Down Expand Up @@ -65,7 +67,7 @@ public static TValue Is<TValue>(Expression<Func<TValue, bool>> match)
{
return Match<TValue>.Create(
value => match.CompileUsingExpressionCompiler().Invoke(value),
() => It.Is<TValue>(match));
Expression.Lambda<Func<TValue>>(ItExpr.Is<TValue>(match)));
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsInRange"]/*'/>
Expand Down
37 changes: 35 additions & 2 deletions src/Moq/Match.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ internal static T Create<T>(Match<T> match)
}

/// <include file='Match.xdoc' path='docs/doc[@for="Match{T}"]/*'/>
public class Match<T> : Match
public class Match<T> : Match, IEquatable<Match<T>>
{
internal Predicate<T> Condition { get; set; }

internal Match(Predicate<T> condition, Expression<Func<T>> renderExpression)
{
this.Condition = condition;
this.RenderExpression = renderExpression.Body;
this.RenderExpression = renderExpression.Body.EvaluateCapturedVariables();
}

internal override bool Matches(object value)
Expand All @@ -96,5 +96,38 @@ internal override bool Matches(object value)
}
return this.Condition((T)value);
}

/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is Match<T> other && this.Equals(other);
}

/// <inheritdoc/>
public bool Equals(Match<T> other)
{
if (this.Condition == other.Condition)
{
return true;
}
else if (this.Condition.GetMethodInfo() != other.Condition.GetMethodInfo())
{
return false;
}
else if (!(this.RenderExpression is MethodCallExpression ce && ce.Method.DeclaringType == typeof(Match)))
{
return ExpressionComparer.Default.Equals(this.RenderExpression, other.RenderExpression);
}
else
{
return false; // The test documented in `MatchFixture.Equality_ambiguity` is caused by this.
// Returning true would break equality even worse. The only way to resolve the
// ambiguity is to either add a render expression to your custom matcher, or
// to test both `Condition.Target` objects for structural equality.
}
}

/// <inheritdoc/>
public override int GetHashCode() => 0;
}
}
63 changes: 63 additions & 0 deletions tests/Moq.Tests/MatchExpressionFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Diagnostics;
using System.Linq.Expressions;

using Moq.Protected;

using Xunit;

namespace Moq.Tests
Expand Down Expand Up @@ -55,6 +57,38 @@ public void Is_not_evaluated_by_PartialMatcherAwareEval()
Assert.Same(matchExpression, FindMatchExpression(evaluatedExpression));
}

[Fact]
public void Can_be_compared_by_ExpressionComparer()
{
var left = GetExpression();
var right = GetExpression();
Assert.Equal(left, right, ExpressionComparer.Default);
}

[Fact]
public void Can_be_compared_by_ExpressionComparer_2()
{
var left = GetItIsAnyExpression();
var right = GetItIsAnyExpression();
Assert.Equal(left, right, ExpressionComparer.Default);
}

[Fact]
public void Can_be_compared_by_ExpressionComparer_3()
{
var left = GetItIsAnyExpression();
var right = GetItIsAnyMatchExpression();
Assert.Equal(left, right, ExpressionComparer.Default);
}

[Fact]
public void Can_be_compared_by_ExpressionComparer_4()
{
var left = GetItIsAnyMatchExpression();
var right = GetItIsAnyExpression();
Assert.Equal(left, right, ExpressionComparer.Default);
}

[Fact]
public void Is_correctly_handled_by_MatcherFactory()
{
Expand Down Expand Up @@ -89,6 +123,35 @@ private Expression<Action<IX>> GetExpression()
x);
}

private Expression<Action<IX>> GetItIsAnyExpression()
{
var x = Expression.Parameter(typeof(IX), "x");
return Expression.Lambda<Action<IX>>(
Expression.Call(
x,
typeof(IX).GetMethod(nameof(IX.M)),
ItExpr.IsAny<int>()),
x);
}

private Expression<Action<IX>> GetItIsAnyMatchExpression()
{
Match itIsAnyMatch;
using (var observer = AmbientObserver.Activate())
{
_ = It.IsAny<int>();
_ = observer.LastIsMatch(out itIsAnyMatch);
}

var x = Expression.Parameter(typeof(IX), "x");
return Expression.Lambda<Action<IX>>(
Expression.Call(
x,
typeof(IX).GetMethod(nameof(IX.M)),
new MatchExpression(itIsAnyMatch)),
x);
}

private static MatchExpression FindMatchExpression(Expression expression)
{
switch (expression.NodeType)
Expand Down
Loading

0 comments on commit 48d42a4

Please sign in to comment.