Skip to content

Commit

Permalink
WIP: detect wrong-typed argument constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaslevesque committed Oct 17, 2017
1 parent 7521ea7 commit 41e6011
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 36 deletions.
4 changes: 3 additions & 1 deletion src/FakeItEasy/Core/DefaultArgumentConstraintManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FakeItEasy.Core
namespace FakeItEasy.Core
{
using System;

Expand Down Expand Up @@ -56,6 +56,8 @@ public MatchesConstraint(Func<T, bool> predicate, Action<IOutputWriter> descript
this.descriptionWriter = descriptionWriter;
}

public Type Type => typeof(T);

void IArgumentConstraint.WriteDescription(IOutputWriter writer)
{
writer.Write("<");
Expand Down
7 changes: 7 additions & 0 deletions src/FakeItEasy/Core/IArgumentConstraint.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
namespace FakeItEasy.Core
{
using System;

/// <summary>
/// Validates an argument, checks that it's valid in a specific fake call.
/// </summary>
internal interface IArgumentConstraint
{
/// <summary>
/// Gets the type of the argument constraint.
/// </summary>
Type Type { get; }

/// <summary>
/// Writes a description of the argument constraint to the specified writer.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion src/FakeItEasy/ExceptionMessages.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FakeItEasy
namespace FakeItEasy
{
using System;
using FakeItEasy.Core;
Expand Down Expand Up @@ -36,5 +36,8 @@ public static string CallToUnconfiguredMethodOfStrictFake(IFakeObjectCall call)
var callFormatter = ServiceLocator.Current.Resolve<IFakeObjectCallFormatter>();
return $"Call to unconfigured method of strict fake: {callFormatter.GetDescription(call)}.";
}

public static string ArgumentConstraintHasWrongType(Type constraintType, Type parameterType) =>
$"Argument constraint is of type {constraintType}, but parameter is of type {parameterType}. No call can match this constraint.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ namespace FakeItEasy.Expressions.ArgumentConstraints
internal class AggregateArgumentConstraint
: IArgumentConstraint
{
private IArgumentConstraint[] constraintsField;
private readonly IArgumentConstraint[] constraintsField;

public AggregateArgumentConstraint(IEnumerable<IArgumentConstraint> constraints)
public AggregateArgumentConstraint(IEnumerable<IArgumentConstraint> constraints, Type constraintType)
{
this.Type = constraintType;
this.constraintsField = constraints.ToArray();
}

public IEnumerable<IArgumentConstraint> Constraints => this.constraintsField;

public Type Type { get; }

public void WriteDescription(IOutputWriter writer)
{
writer.Write("[");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ namespace FakeItEasy.Expressions.ArgumentConstraints
internal class EqualityArgumentConstraint
: IArgumentConstraint
{
public EqualityArgumentConstraint(object expectedValue)
public EqualityArgumentConstraint(object expectedValue, Type constraintType)
{
this.ExpectedValue = expectedValue;
this.Type = constraintType;
}

public object ExpectedValue { get; }

public string ConstraintDescription => this.ToString();

public Type Type { get; }

public bool IsValid(object argument)
{
return object.Equals(this.ExpectedValue, argument);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
namespace FakeItEasy.Expressions.ArgumentConstraints
{
using System;
using FakeItEasy.Core;

internal class OutArgumentConstraint : IArgumentConstraint, IArgumentValueProvider
{
public OutArgumentConstraint(object value)
public OutArgumentConstraint(object value, Type constraintType)
{
this.Value = value;
this.Type = constraintType;
}

/// <summary>
Expand All @@ -18,6 +20,8 @@ public OutArgumentConstraint(object value)
/// </summary>
public object Value { get; }

public Type Type { get; }

public void WriteDescription(IOutputWriter writer)
{
writer.Write("<out parameter>");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace FakeItEasy.Expressions.ArgumentConstraints
{
using System;
using FakeItEasy.Core;

internal class RefArgumentConstraint : IArgumentConstraint, IArgumentValueProvider
Expand All @@ -26,6 +27,8 @@ public RefArgumentConstraint(IArgumentConstraint baseConstraint, object value)
/// </summary>
public object Value { get; }

public Type Type => this.baseConstraint.Type;

public void WriteDescription(IOutputWriter writer)
{
this.baseConstraint.WriteDescription(writer);
Expand Down
45 changes: 34 additions & 11 deletions src/FakeItEasy/Expressions/ExpressionArgumentConstraintFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace FakeItEasy.Expressions
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using FakeItEasy.Configuration;
using FakeItEasy.Core;
using FakeItEasy.Expressions.ArgumentConstraints;

Expand All @@ -20,19 +21,21 @@ public ExpressionArgumentConstraintFactory(IArgumentConstraintTrapper argumentCo

public virtual IArgumentConstraint GetArgumentConstraint(ParsedArgumentExpression argument)
{
var parameterType = argument.ArgumentInformation.ParameterType;

if (IsParamArrayExpression(argument))
{
return this.CreateParamArrayConstraint((NewArrayExpression)argument.Expression);
return this.CreateParamArrayConstraint((NewArrayExpression)argument.Expression, parameterType);
}

var isByRefArgument = IsByRefArgument(argument);

var constraint = this.GetArgumentConstraintFromExpression(argument.Expression, out var argumentValue);
var constraint = this.GetArgumentConstraintFromExpression(argument.Expression, parameterType, out var argumentValue);
if (isByRefArgument)
{
if (IsOutArgument(argument))
{
constraint = new OutArgumentConstraint(argumentValue);
constraint = new OutArgumentConstraint(argumentValue, parameterType);
}
else
{
Expand Down Expand Up @@ -68,9 +71,9 @@ private static bool IsByRefArgument(ParsedArgumentExpression argument)
return argument.ArgumentInformation.ParameterType.IsByRef;
}

private static IArgumentConstraint CreateEqualityConstraint(object expressionValue)
private static IArgumentConstraint CreateEqualityConstraint(object expressionValue, Type parameterType)
{
return new EqualityArgumentConstraint(expressionValue);
return new EqualityArgumentConstraint(expressionValue, expressionValue?.GetType() ?? parameterType);
}

private static object InvokeExpression(Expression expression)
Expand Down Expand Up @@ -139,7 +142,20 @@ private static Type GetGenericTypeDefinition(Type type)
return type;
}

private IArgumentConstraint GetArgumentConstraintFromExpression(Expression expression, out object value)
private static void CheckConstraintIsCompatibleWithParameterType(IArgumentConstraint constraint, Type parameterType)
{
if (parameterType.IsByRef)
{
parameterType = parameterType.GetElementType();
}

if (!parameterType.IsAssignableFrom(constraint.Type))
{
throw new FakeConfigurationException(ExceptionMessages.ArgumentConstraintHasWrongType(constraint.Type, parameterType));
}
}

private IArgumentConstraint GetArgumentConstraintFromExpression(Expression expression, Type parameterType, out object value)
{
CheckArgumentExpressionIsValid(expression);

Expand All @@ -148,23 +164,30 @@ private IArgumentConstraint GetArgumentConstraintFromExpression(Expression expre
var trappedConstraints = this.argumentConstraintTrapper.TrapConstraints(() =>
{
expressionValue = InvokeExpression(expression);
});
}).ToList();

foreach (var constraint in trappedConstraints)
{
CheckConstraintIsCompatibleWithParameterType(constraint, parameterType);
}

value = expressionValue;

return TryCreateConstraintFromTrappedConstraints(trappedConstraints.ToArray()) ?? CreateEqualityConstraint(expressionValue);
return TryCreateConstraintFromTrappedConstraints(trappedConstraints.ToArray())
?? CreateEqualityConstraint(expressionValue, parameterType);
}

private IArgumentConstraint CreateParamArrayConstraint(NewArrayExpression expression)
private IArgumentConstraint CreateParamArrayConstraint(NewArrayExpression expression, Type parameterType)
{
var result = new List<IArgumentConstraint>();
var itemType = parameterType.GetElementType();

foreach (var argumentExpression in expression.Expressions)
{
result.Add(this.GetArgumentConstraintFromExpression(argumentExpression, out _));
result.Add(this.GetArgumentConstraintFromExpression(argumentExpression, itemType, out _));
}

return new AggregateArgumentConstraint(result);
return new AggregateArgumentConstraint(result, expression.Type);
}

private class ArgumentConstraintExpressionVisitor : ExpressionVisitor
Expand Down
11 changes: 8 additions & 3 deletions src/FakeItEasy/Expressions/ExpressionCallMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ public override string ToString()
public virtual void UsePredicateToValidateArguments(Func<ArgumentCollection, bool> predicate)
{
this.argumentsPredicate = predicate;

var numberOfValidators = this.argumentConstraints.Count();
this.argumentConstraints = Enumerable.Repeat<IArgumentConstraint>(new PredicatedArgumentConstraint(), numberOfValidators);
this.argumentConstraints = this.argumentConstraints.Select(a => new PredicatedArgumentConstraint(a.Type)).ToArray();
}

public Func<IFakeObjectCall, ICollection<object>> GetOutAndRefParametersValueProducer()
Expand Down Expand Up @@ -156,6 +154,13 @@ private bool ArgumentsMatchesArgumentConstraints(ArgumentCollection argumentColl
private class PredicatedArgumentConstraint
: IArgumentConstraint
{
public PredicatedArgumentConstraint(Type constraintType)
{
this.Type = constraintType;
}

public Type Type { get; }

public bool IsValid(object argument)
{
return true;
Expand Down
16 changes: 7 additions & 9 deletions tests/FakeItEasy.Specs/CallMatchingSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace FakeItEasy.Specs
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using FakeItEasy.Configuration;
using FakeItEasy.Tests.TestHelpers;
using FluentAssertions;
using Xbehave;
Expand Down Expand Up @@ -609,22 +610,19 @@ public static void UnusedArgumentMatcherDescriptionNotUsed(IHaveNoGenericParamet

[Scenario]
public static void PassingIgnoredConstraintWithWrongTypeToAMethod(
IHaveNoGenericParameters fake, Exception exception, bool wasCalled)
IHaveNoGenericParameters fake, Exception exception)
{
"Given a fake"
.x(() => fake = A.Fake<IHaveNoGenericParameters>());

"When I try to configure a method of the fake with an Ignored constraint of the wrong type"
.x(() => exception = Record.Exception(() => A.CallTo(() => fake.Bar(A<byte>.Ignored)).Invokes(() => wasCalled = true)));

"And I call the method with any value"
.x(() => fake.Bar(default(byte)));
.x(() => exception = Record.Exception(() => A.CallTo(() => fake.Bar(A<byte>.Ignored))));

"Then the call configuration doesn't throw"
.x(() => exception.Should().BeNull());
"Then the call configuration throws a FakeConfigurationException"
.x(() => exception.Should().BeAnExceptionOfType<FakeConfigurationException>());

"And the configured call isn't matched"
.x(() => wasCalled.Should().BeFalse());
"And the message should indicate the actual and expected type"
.x(() => exception.Message.Should().Be($"Argument constraint is of type System.Byte, but parameter is of type System.Int32. No call can match this constraint."));
}

[Scenario]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ public class AggregateArgumentConstraintTests
{
public AggregateArgumentConstraintTests()
{
this.ConstraintField = new AggregateArgumentConstraint(new[] { new EqualityArgumentConstraint("foo"), new EqualityArgumentConstraint("bar") });
this.ConstraintField = new AggregateArgumentConstraint(
new[]
{
new EqualityArgumentConstraint("foo", typeof(string)),
new EqualityArgumentConstraint("bar", typeof(string))
},
typeof(string[]));
}

public interface ITypeWithMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class EqualityArgumentConstraintTests
{
public EqualityArgumentConstraintTests()
{
this.ConstraintField = new EqualityArgumentConstraint(1);
this.ConstraintField = new EqualityArgumentConstraint(1, typeof(int));
}

protected override string ExpectedDescription => "1";
Expand Down Expand Up @@ -55,15 +55,15 @@ public override void Constraint_should_provide_correct_description()
[Fact]
public void ToString_should_return_NULL_when_expected_value_is_null()
{
var validator = new EqualityArgumentConstraint(null);
var validator = new EqualityArgumentConstraint(null, typeof(object));

validator.ToString().Should().Be("<NULL>");
}

[Fact]
public void ToString_should_put_accents_when_expected_value_is_string()
{
var validator = new EqualityArgumentConstraint("foo");
var validator = new EqualityArgumentConstraint("foo", typeof(string));

validator.ToString().Should().Be("\"foo\"");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace FakeItEasy.Tests.Expressions
using System.Linq;
using System.Linq.Expressions;
using FakeItEasy.Core;
using FakeItEasy.Creation;
using FakeItEasy.Expressions;
using FakeItEasy.Expressions.ArgumentConstraints;
using FakeItEasy.Tests.Builders;
Expand All @@ -30,7 +31,8 @@ public ExpressionArgumentConstraintFactoryTests()
public void Should_return_constraint_from_trapper_when_available()
{
// Arrange
var constraint = A.Dummy<IArgumentConstraint>();
var constraint = A.Fake<IArgumentConstraint>();
A.CallTo(() => constraint.Type).Returns(typeof(object));

A.CallTo(this.trapper).WithReturnType<IEnumerable<IArgumentConstraint>>().Returns(new[] { constraint });

Expand Down Expand Up @@ -97,9 +99,9 @@ public void Should_not_invoke_expression_more_than_once()
public void Should_get_aggregate_constraint_when_multiple_items_are_passed_to_parameters_array()
{
// Arrange
var constraintForFirst = A.CollectionOfFake<IArgumentConstraint>(1);
var constraintForFirst = A.CollectionOfFake<IArgumentConstraint>(1, ConfigureFakeArgumentConstraint);
var noConstraintForSecond = Enumerable.Empty<IArgumentConstraint>();
var constraintForThird = A.CollectionOfFake<IArgumentConstraint>(1);
var constraintForThird = A.CollectionOfFake<IArgumentConstraint>(1, ConfigureFakeArgumentConstraint);

A.CallTo(() => this.trapper.TrapConstraints(A<Action>._))
.ReturnsNextFromSequence(constraintForFirst, noConstraintForSecond, constraintForThird);
Expand All @@ -116,6 +118,14 @@ public void Should_get_aggregate_constraint_when_multiple_items_are_passed_to_pa
aggregate.Constraints.ElementAt(0).Should().BeSameAs(constraintForFirst.Single());
aggregate.Constraints.ElementAt(1).Should().BeOfType<EqualityArgumentConstraint>().Which.ExpectedValue.Should().Be("foo");
aggregate.Constraints.ElementAt(2).Should().BeSameAs(constraintForThird.Single());

void ConfigureFakeArgumentConstraint(IFakeOptions<IArgumentConstraint> options)
{
options.ConfigureFake(constraint =>
{
A.CallTo(() => constraint.Type).Returns(typeof(string));
});
}
}

[Fact]
Expand Down

0 comments on commit 41e6011

Please sign in to comment.