NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Argument Matcher for Boolean Type Out of Order with Multiple Named Parameters

Open mszeqli opened this issue 4 years ago • 2 comments

Describe the bug bool argument type when combined with argument matcher Arg.Any<bool> yields wrong call specifications. In particular, false with Arg.any<bool>() end up out of order call specification.

For example, A call argument list: false, Arg.any<bool>(), Arg.any<bool> will yield any Boolean, any Boolean, False call specs.

To Reproduce Below tests can reproduce the bug.


  public interface IInterface
  {
      void MethodWithMultipleOptionalParameter(object obligatory, bool opt1 = false, bool opt2 = false, bool opt3 = false);
  }

  // Failed Test
  [Test]
  public void ArgCheckForBooleanNamedParametersFail()
  {
      var mock = Substitute.For<IInterface>();
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), false, true, true);
      mock.Received(3).MethodWithMultipleOptionalParameter(Arg.Any<object>(), false, Arg.Any<bool>(), Arg.Any<bool>());
  }

  // Good Test 1
  [Test]
  public void ArgCheckForBooleanNamedParametersSuccess1()
  {
      var mock = Substitute.For<IInterface>();
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), true, false, false);
      mock.Received(1).MethodWithMultipleOptionalParameter(Arg.Any<object>(), true, Arg.Any<bool>(), Arg.Any<bool>());
  }
  // Good Test 2
  [Test]
  public void ArgCheckForBooleanNamedParameterSuccess2()
  {
      var mock = Substitute.For<IInterface>();
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), false, false, false);
      mock.MethodWithMultipleOptionalParameter(new object(), false, true, true);
      mock.Received(3).MethodWithMultipleOptionalParameter(Arg.Any<object>(), Arg.Is<bool>(x => x == false), Arg.Any<bool>(), Arg.Any<bool>());
  }

Stack trace

  Message: 
    NSubstitute.Exceptions.ReceivedCallsException : Expected to receive exactly 3 calls matching:
    	MethodWithMultipleOptionalParameter(any Object, any Boolean, any Boolean, False)
    Actually received 2 matching calls:
    	MethodWithMultipleOptionalParameter(Object, False, False, False)
    	MethodWithMultipleOptionalParameter(Object, False, False, False)
    Received 1 non-matching call (non-matching arguments indicated with '*' characters):
    	MethodWithMultipleOptionalParameter(Object, False, True, *True*)
    
  Stack Trace: 
    ReceivedCallsExceptionThrower.Throw(ICallSpecification callSpecification, IEnumerable`1 matchingCalls, IEnumerable`1 nonMatchingCalls, Quantity requiredQuantity) line 24
    CheckReceivedCallsHandler.Handle(ICall call) line 35
    Route.Handle(ICall call) line 24
    CallRouter.Route(ICall call) line 69
    CastleForwardingInterceptor.Intercept(IInvocation invocation) line 25
    AbstractInvocation.Proceed()
    ProxyIdInterceptor.Intercept(IInvocation invocation) line 27
    AbstractInvocation.Proceed()
    Issue114_ArgumentCheckOfOptionalParameter.ArgCheckForBooleanNamedParametersFail() line 19

Expected behaviour A clear and concise description of what you expected to happen, compared to what actually happened.

Expect a call argument list: false, Arg.any<bool>(), Arg.any<bool> correctly yields False, any Boolean, any Boolean call specs, yet actual result is any Boolean, any Boolean, False. This causes surprise behavior.

Environment:

  • NSubstitute version: 4.2.2
  • NSubstitute.Analyzers version: CSharp 1.0.13
  • Platform: netcoreapp2.0 project on Windows

mszeqli avatar Apr 24 '21 03:04 mszeqli

Thanks very much for this report and the great reproduction case.

Arg.Any<bool> will return false and NSubstitute will try to match up its queued arg matchers with any false values in the call. In this case I would expect an AmbiguousArgumentsException to be thrown. 🤔

dtchepak avatar May 10 '21 23:05 dtchepak

Hey, I spend a while trying to find a way to solve this, but I am hitting a wall because it seems that this is a really complex case.

What's happening is the literal false is never known by the args match queue, but its position being there makes it problematic because for the queue, it's as if we had any object, any boolean, any boolean which would appear to the ArgumentSpecificationFactory as if we had a case of specifying the first 3 arguments, but not the last optional one which would not throw (the logic for this was introduced due to #114 ).

Even more annoying: it seems that even if we don't specify the optional args, they will be present in the argument with the default value. This means that for the factory, both cases I just talked about are effectively indistinguishable: it simply does not have enough information to tell which one it is. The queue has 3 elements, but the actual arguments passed had 4 which is the key information we would need to extract to fix this. The issue is since the queue has 3 elements, it's the same than if we JUST passed 3 specs and omitted the fourth one.

I am not familiar with the dynamic proxy this project uses, but it seems to default every argument even if they aren't actually sent. Maybe we could do something if we has more information about the arguments actually sent vs the ones that were defaulted out of not being sent, but for now, I don't really know how this one can be solved.

ghost avatar Mar 15 '23 20:03 ghost