Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/NSubstitute/Arg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

// Disable nullability for client API, so it does not affect clients.
#nullable disable annotations
#pragma warning disable CS1574
#pragma warning disable CS0419

namespace NSubstitute
{
Expand All @@ -12,6 +14,10 @@ namespace NSubstitute
/// </summary>
public static class Arg
{
public class AnyType
{
}

/// <summary>
/// Match any argument value compatible with type <typeparamref name="T"/>.
/// </summary>
Expand All @@ -29,7 +35,7 @@ public static ref T Is<T>(T value)
}

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/>.
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// </summary>
public static ref T Is<T>(Expression<Predicate<T>> predicate)
Expand Down Expand Up @@ -87,14 +93,23 @@ public static ref TDelegate InvokeDelegate<TDelegate>(params object[] arguments)
}

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// </summary>
public static ref T Do<T>(Action<T> useArgument)
{
return ref ArgumentMatcher.Enqueue<T>(new AnyArgumentMatcher(typeof(T)), x => useArgument((T) x!));
}

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// </summary>
public static ref AnyType Do<T>(Action<object> useArgument) where T : AnyType
{
return ref ArgumentMatcher.Enqueue<AnyType>(new AnyArgumentMatcher(typeof(AnyType)), x => useArgument(x!));
}

/// <summary>
/// Alternate version of <see cref="Arg"/> matchers for compatibility with pre-C#7 compilers
/// which do not support <c>ref</c> return types. Do not use unless you are unable to use <see cref="Arg"/>.
Expand All @@ -119,7 +134,7 @@ public static class Compat
public static T Is<T>(T value) => Arg.Is(value);

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/>.
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// This is provided for compatibility with older compilers --
/// if possible use <see cref="Arg.Is{T}(Expression{Predicate{T}})" /> instead.
Expand Down Expand Up @@ -170,12 +185,18 @@ public static class Compat
public static TDelegate InvokeDelegate<TDelegate>(params object[] arguments) => Arg.InvokeDelegate<TDelegate>(arguments);

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// This is provided for compatibility with older compilers --
/// if possible use <see cref="Arg.Do{T}" /> instead.
/// </summary>
public static T Do<T>(Action<T> useArgument) => Arg.Do(useArgument);
public static T Do<T>(Action<T> useArgument) => Arg.Do<T>(useArgument);

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// </summary>
public static AnyType Do<T>(Action<object> useArgument) where T : AnyType => Arg.Do<T>(useArgument);
}

private static Action<object> InvokeDelegateAction(params object[] arguments)
Expand Down
14 changes: 11 additions & 3 deletions src/NSubstitute/Compatibility/CompatArg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

// Disable nullability for client API, so it does not affect clients.
#nullable disable annotations
#pragma warning disable CS1574
#pragma warning disable CS0419

namespace NSubstitute.Compatibility
{
/// <summary>
/// Alternate version of <see cref="Arg"/> matchers for compatibility with pre-C#7 compilers
/// which do not support <c>ref</c> return types. Do not use unless you are unable to use <see cref="Arg"/>.
///
///
/// <see cref="CompatArg"/> provides a non-static version of <see cref="Arg.Compat"/>, which can make it easier
/// to use from an abstract base class. You can get a reference to this instance using the static
/// <see cref="Instance" /> field.
Expand Down Expand Up @@ -41,7 +43,7 @@ private CompatArg() { }
public T Is<T>(T value) => Arg.Is(value);

/// <summary>
/// Match argument that satisfies <paramref name="predicate"/>.
/// Match argument that satisfies <paramref name="predicate"/>.
/// If the <paramref name="predicate"/> throws an exception for an argument it will be treated as non-matching.
/// This is provided for compatibility with older compilers --
/// if possible use <see cref="Arg.Is{T}(Expression{Predicate{T}})" /> instead.
Expand Down Expand Up @@ -92,11 +94,17 @@ private CompatArg() { }
public TDelegate InvokeDelegate<TDelegate>(params object[] arguments) => Arg.InvokeDelegate<TDelegate>(arguments);

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// This is provided for compatibility with older compilers --
/// if possible use <see cref="Arg.Do{T}" /> instead.
/// </summary>
public T Do<T>(Action<T> useArgument) => Arg.Do(useArgument);

/// <summary>
/// Capture any argument compatible with type <typeparamref name="T"/> and use it to call the <paramref name="useArgument"/> function
/// whenever a matching call is made to the substitute.
/// </summary>
public static Arg.AnyType Do<T>(Action<object> useArgument) where T : Arg.AnyType => Arg.Do<T>(useArgument);
}
}
9 changes: 5 additions & 4 deletions src/NSubstitute/Core/CallSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public bool IsSatisfiedBy(ICall call)
{
return false;
}

return true;
}

Expand All @@ -47,12 +47,12 @@ private static bool AreComparable(MethodInfo a, MethodInfo b)
{
return true;
}

if (a.IsGenericMethod && b.IsGenericMethod)
{
return CanCompareGenericMethods(a, b);
}

return false;
}

Expand All @@ -77,7 +77,8 @@ private static bool TypesAreAllEquivalent(Type[] aArgs, Type[] bArgs)
var first = aArgs[i];
var second = bArgs[i];

var areEquivalent = first.IsAssignableFrom(second) || second.IsAssignableFrom(first);
var areEquivalent = first.IsAssignableFrom(second) || second.IsAssignableFrom(first) ||
first == typeof(Arg.AnyType) || second == typeof(Arg.AnyType);
if (!areEquivalent) return false;
}
return true;
Expand Down
5 changes: 5 additions & 0 deletions src/NSubstitute/Core/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ internal static class Extensions
/// </summary>
public static bool IsCompatibleWith(this object? instance, Type type)
{
if (type == typeof(Arg.AnyType))
{
return true;
}

var requiredType = type.IsByRef ? type.GetElementType()! : type;
return instance == null ? TypeCanBeNull(requiredType) : requiredType.IsInstanceOfType(instance);
}
Expand Down
7 changes: 7 additions & 0 deletions src/NSubstitute/Exceptions/DoAnyTypeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NSubstitute.Exceptions;

public class DoAnyTypeException : SubstituteException
{
private const string FixedMessage = "Use DoForAny() instead of Do<AnyType>()";
public DoAnyTypeException() : base(FixedMessage) { }
}
40 changes: 40 additions & 0 deletions tests/NSubstitute.Acceptance.Specs/GenericArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using NSubstitute.Exceptions;
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs;

[TestFixture]
public class GenericArguments
{
public interface ISomethingWithGenerics
{
void Log<TState>(int level, TState state);
}

[Test]
public void Return_result_for_any_argument()
{
string argDoResult = null;
int? whenDoResult = null;
bool whenDoCalled = false;
ISomethingWithGenerics something = Substitute.For<ISomethingWithGenerics>();
something.Log(Arg.Any<int>(), Arg.Do<Arg.AnyType>(a => argDoResult = ">>" + ((int)a).ToString("P")));
something
.When(substitute => substitute.Log(Arg.Any<int>(), Arg.Any<Arg.AnyType>()))
.Do(info =>
{
whenDoResult = info.ArgAt<int>(1);
whenDoCalled = true;
});

something.Log(7, 3409);

something.Received().Log(Arg.Any<int>(), Arg.Any<Arg.AnyType>());
something.Received().Log(7, 3409);
Assert.That(whenDoCalled, Is.True);
Assert.That(argDoResult, Is.EqualTo(">>340 900.00 %"));
Assert.That(whenDoResult, Is.EqualTo(3409));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NSubstitute.Acceptance.Specs.Infrastructure;

public interface ISomethingWithGenericMethods
{
void Log<TState>(int level, TState state);
string Format<T>(T state);
}