diff --git a/src/NSubstitute/Arg.cs b/src/NSubstitute/Arg.cs index 458499bf..2ff1e931 100644 --- a/src/NSubstitute/Arg.cs +++ b/src/NSubstitute/Arg.cs @@ -58,6 +58,16 @@ public static ref T Is(Expression> predicate) where T : Any return ref ArgumentMatcher.Enqueue(new ExpressionArgumentMatcher(predicate)); } + /// + /// Match argument that satisfies and use it to call the function + /// whenever a matching call is or was made to the substitute. + /// If the throws an exception for an argument it will be treated as non-matching. + /// + public static ref T IsAndDo(Expression> predicate, Action useArgument) + { + return ref ArgumentMatcher.Enqueue(new ExpressionArgumentMatcher(predicate), x => useArgument((T)x!)); + } + /// /// Invoke any argument whenever a matching call is made to the substitute. /// @@ -164,6 +174,13 @@ public static class Compat /// public static AnyType Is(Expression> predicate) where T : AnyType => Arg.Is(predicate); + /// + /// Match argument that satisfies and use it to call the function + /// whenever a matching call is made to the substitute. + /// If the throws an exception for an argument it will be treated as non-matching. + /// + public static T IsAndDo(Expression> predicate, Action useArgument) => Arg.IsAndDo(predicate, useArgument); + /// /// Invoke any argument whenever a matching call is made to the substitute. /// This is provided for compatibility with older compilers -- diff --git a/src/NSubstitute/Compatibility/CompatArg.cs b/src/NSubstitute/Compatibility/CompatArg.cs index 7d1a84f4..10f14772 100644 --- a/src/NSubstitute/Compatibility/CompatArg.cs +++ b/src/NSubstitute/Compatibility/CompatArg.cs @@ -49,6 +49,13 @@ private CompatArg() { } /// public T Is(Expression> predicate) => Arg.Is(predicate); + /// + /// Match argument that satisfies and use it to call the function + /// whenever a matching call is or was made to the substitute. + /// If the throws an exception for an argument it will be treated as non-matching. + /// + public static T IsAndDo(Expression> predicate, Action useArgument) => Arg.IsAndDo(predicate, useArgument); + /// /// Match argument that satisfies . /// If the throws an exception for an argument it will be treated as non-matching. diff --git a/src/NSubstitute/Match.cs b/src/NSubstitute/Match.cs new file mode 100644 index 00000000..8d6c7ea5 --- /dev/null +++ b/src/NSubstitute/Match.cs @@ -0,0 +1,43 @@ +using NSubstitute.Core.Arguments; +using System.Linq.Expressions; + +namespace NSubstitute; + +/// +/// Argument matcher allowing a match predicate and optional action to be called for each match to be specified separately. +/// +public class Match +{ + private Expression> predicate; + private Action useArgument; + + internal Match(Expression> predicate, Action useArgument) + { + this.predicate = predicate; + this.useArgument = useArgument; + } + + /// + /// The function to be invoked + /// for each matching call made to the substitute. + /// + public Match AndDo(Action useArgument) => new Match(predicate, x => { this.useArgument(x); useArgument(x); }); + + public static implicit operator T? (Match match) + { + return ArgumentMatcher.Enqueue( + new ExpressionArgumentMatcher(match.predicate), + x => match.useArgument((T?)x) + ); + } +} + +public static class Match +{ + /// + /// Match argument that satisfies . + /// If the throws an exception for an argument it will be treated as non-matching. + /// + public static Match When(Expression> predicate) => + new Match(predicate, x => { }); +} diff --git a/src/NSubstitute/Routing/Handlers/CheckReceivedCallsHandler.cs b/src/NSubstitute/Routing/Handlers/CheckReceivedCallsHandler.cs index de7c5816..30119bed 100644 --- a/src/NSubstitute/Routing/Handlers/CheckReceivedCallsHandler.cs +++ b/src/NSubstitute/Routing/Handlers/CheckReceivedCallsHandler.cs @@ -34,6 +34,18 @@ public RouteAction Handle(ICall call) _exceptionThrower.Throw(callSpecification, matchingCalls, relatedCalls, _requiredQuantity); } + InvokePerArgumentActionsForMatchingCalls(callSpecification, matchingCalls); + return RouteAction.Continue(); } + + private static void InvokePerArgumentActionsForMatchingCalls(ICallSpecification callSpecification, List matchingCalls) + { + var callInfoFactory = new CallInfoFactory(); + + foreach (var matchingCall in matchingCalls) + { + callSpecification.InvokePerArgumentActions(callInfoFactory.Create(matchingCall)); + } + } } \ No newline at end of file diff --git a/tests/NSubstitute.Acceptance.Specs/ArgDoFromMatcher.cs b/tests/NSubstitute.Acceptance.Specs/ArgDoFromMatcher.cs index cd49cc26..5c5be66a 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgDoFromMatcher.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgDoFromMatcher.cs @@ -56,6 +56,32 @@ public void Should_call_action_with_each_matching_call() Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "world" })); } + [Test] + public void Should_call_action_with_each_call_matching_predicate_using_isanddo() + { + var stringArgs = new List(); + _sub.Bar(Arg.IsAndDo(x => x.StartsWith("h"), x => stringArgs.Add(x)), Arg.Any(), _someObject); + + _sub.Bar("hello", 1, _someObject); + _sub.Bar("hello2", 2, _someObject); + _sub.Bar("don't use this because call doesn't match", -123, _someObject); + + Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "hello2" })); + } + + [Test] + public void Should_call_action_with_each_call_matching_predicate() + { + var stringArgs = new List(); + _sub.Bar(Match.When(x => x.StartsWith("h")).AndDo(stringArgs.Add), Arg.Any(), _someObject); + + _sub.Bar("hello", 1, _someObject); + _sub.Bar("hello2", 2, _someObject); + _sub.Bar("don't use this because call doesn't match", -123, _someObject); + + Assert.That(stringArgs, Is.EqualTo(new[] { "hello", "hello2" })); + } + [Test] public void Arg_do_with_when_for_any_args() { diff --git a/tests/NSubstitute.Acceptance.Specs/ReceivedCalls.cs b/tests/NSubstitute.Acceptance.Specs/ReceivedCalls.cs index d7de58b4..6ceb0522 100644 --- a/tests/NSubstitute.Acceptance.Specs/ReceivedCalls.cs +++ b/tests/NSubstitute.Acceptance.Specs/ReceivedCalls.cs @@ -315,6 +315,40 @@ public void Throw_when_negative_min_range_given() StringAssert.Contains("minInclusive must be >= 0, but was -1.", ex.Message); } + [Test] + public void Should_call_action_for_each_call_matching_predicate_using_isanddo() + { + var suitCaseLuggage = new List(); + + _car.StoreLuggage(new SuitCase()); + _car.StoreLuggage(new SuitCase()); + _car.StoreLuggage(new object()); + + _car.Received(2).StoreLuggage( + Arg.IsAndDo( + x => x.All(l => l is SuitCase), + suitCaseLuggage.Add)); + + Assert.That(suitCaseLuggage, Has.Count.EqualTo(2)); + } + + [Test] + public void Should_call_action_for_each_call_matching_predicate() + { + var suitCaseLuggage = new List(); + + _car.StoreLuggage(new SuitCase()); + _car.StoreLuggage(new SuitCase()); + _car.StoreLuggage(new object()); + + _car.Received(2).StoreLuggage( + Match.When( + x => x.All(l => l is SuitCase)) + .AndDo(suitCaseLuggage.Add)); + + Assert.That(suitCaseLuggage, Has.Count.EqualTo(2)); + } + public interface ICar { void Start(); @@ -328,4 +362,6 @@ public interface ICar float GetCapacityInLitres(); event Action Started; } + + public class SuitCase; } \ No newline at end of file