Skip to content

Commit 102eccc

Browse files
committed
Add protected BaseResult() method to CallInfo.
Create CallInfo<T> to calls that return results and expose `BaseResult`. This gets messy to push the generic all the way through the code, so am just using a cast in `Returns` extensions to handle this. This should be safe as if we are in `Returns<T>` then the return value should be safe to cast to a `T`. Based off discussion here: #622 (comment)
1 parent a4e2e6d commit 102eccc

10 files changed

+124
-26
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,6 @@ docs/_site/*
304304

305305
# Ignore VIM tmp files
306306
*.swp
307+
308+
# kdiff/merge files
309+
*.orig

src/NSubstitute/Core/CallInfo.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,25 @@ namespace NSubstitute.Core
1212
public class CallInfo
1313
{
1414
private readonly Argument[] _callArguments;
15+
private readonly Func<Maybe<object>> _baseResult;
1516

16-
public CallInfo(Argument[] callArguments)
17+
public CallInfo(Argument[] callArguments, Func<Maybe<object>> baseResult)
1718
{
1819
_callArguments = callArguments;
20+
_baseResult = baseResult;
21+
}
22+
23+
protected CallInfo(CallInfo info) : this(info._callArguments, info._baseResult) {
24+
}
25+
26+
/// <summary>
27+
/// Call and returns the result from the base implementation of a substitute for a class.
28+
/// Will throw an exception if no base implementation exists.
29+
/// </summary>
30+
/// <returns>Result from base implementation</returns>
31+
/// <exception cref="NoBaseImplementationException">Throws in no base implementation exists</exception>
32+
protected object GetBaseResult() {
33+
return _baseResult().ValueOr(() => throw new NoBaseImplementationException());
1934
}
2035

2136
/// <summary>

src/NSubstitute/Core/CallInfoFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public class CallInfoFactory : ICallInfoFactory
55
public CallInfo Create(ICall call)
66
{
77
var arguments = GetArgumentsFromCall(call);
8-
return new CallInfo(arguments);
8+
return new CallInfo(arguments, () => call.TryCallBase());
99
}
1010

1111
private static Argument[] GetArgumentsFromCall(ICall call)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace NSubstitute.Core
2+
{
3+
/// <summary>
4+
/// Information for a call that returns a value of type <c>T</c>.
5+
/// </summary>
6+
/// <typeparam name="T"></typeparam>
7+
public class CallInfo<T> : CallInfo
8+
{
9+
internal CallInfo(CallInfo info) : base(info) {
10+
}
11+
12+
public T BaseResult() {
13+
return (T)GetBaseResult();
14+
}
15+
}
16+
}

src/NSubstitute/Core/IReturn.cs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,21 @@ public ReturnValue(object? value)
3939

4040
public class ReturnValueFromFunc<T> : IReturn
4141
{
42-
private readonly Func<CallInfo, T?> _funcToReturnValue;
42+
private readonly Func<CallInfo<T>, T?> _funcToReturnValue;
4343

44-
public ReturnValueFromFunc(Func<CallInfo, T?>? funcToReturnValue)
44+
public ReturnValueFromFunc(Func<CallInfo<T>, T?>? funcToReturnValue)
4545
{
4646
_funcToReturnValue = funcToReturnValue ?? ReturnNull();
4747
}
4848

49-
public object? ReturnFor(CallInfo info) => _funcToReturnValue(info);
50-
public Type TypeOrNull() => typeof (T);
51-
public bool CanBeAssignedTo(Type t) => typeof (T).IsAssignableFrom(t);
49+
public object? ReturnFor(CallInfo info) => _funcToReturnValue(new CallInfo<T>(info));
50+
public Type TypeOrNull() => typeof(T);
51+
public bool CanBeAssignedTo(Type t) => typeof(T).IsAssignableFrom(t);
5252

5353
private static Func<CallInfo, T?> ReturnNull()
5454
{
5555
if (typeof(T).GetTypeInfo().IsValueType) throw new CannotReturnNullForValueType(typeof(T));
56-
return x => default(T);
56+
return x => default;
5757
}
5858
}
5959

@@ -70,27 +70,28 @@ public ReturnMultipleValues(T?[] values)
7070

7171
public object? GetReturnValue() => GetNext();
7272
public object? ReturnFor(CallInfo info) => GetReturnValue();
73-
public Type TypeOrNull() => typeof (T);
74-
public bool CanBeAssignedTo(Type t) => typeof (T).IsAssignableFrom(t);
73+
public Type TypeOrNull() => typeof(T);
74+
public bool CanBeAssignedTo(Type t) => typeof(T).IsAssignableFrom(t);
7575

7676
private T? GetNext() => _valuesToReturn.TryDequeue(out var nextResult) ? nextResult : _lastValue;
7777
}
7878

7979
public class ReturnMultipleFuncsValues<T> : IReturn
8080
{
81-
private readonly ConcurrentQueue<Func<CallInfo, T?>> _funcsToReturn;
82-
private readonly Func<CallInfo, T?> _lastFunc;
81+
private readonly ConcurrentQueue<Func<CallInfo<T>, T?>> _funcsToReturn;
82+
private readonly Func<CallInfo<T>, T?> _lastFunc;
8383

84-
public ReturnMultipleFuncsValues(Func<CallInfo, T?>[] funcs)
84+
public ReturnMultipleFuncsValues(Func<CallInfo<T>, T?>[] funcs)
8585
{
86-
_funcsToReturn = new ConcurrentQueue<Func<CallInfo, T?>>(funcs);
86+
_funcsToReturn = new ConcurrentQueue<Func<CallInfo<T>, T?>>(funcs);
8787
_lastFunc = funcs.Last();
8888
}
8989

9090
public object? ReturnFor(CallInfo info) => GetNext(info);
91-
public Type TypeOrNull() => typeof (T);
92-
public bool CanBeAssignedTo(Type t) => typeof (T).IsAssignableFrom(t);
91+
public Type TypeOrNull() => typeof(T);
92+
public bool CanBeAssignedTo(Type t) => typeof(T).IsAssignableFrom(t);
9393

94-
private T? GetNext(CallInfo info) => _funcsToReturn.TryDequeue(out var nextFunc) ? nextFunc(info) : _lastFunc(info);
94+
private T? GetNext(CallInfo info) =>
95+
_funcsToReturn.TryDequeue(out var nextFunc) ? nextFunc(new CallInfo<T>(info)) : _lastFunc(new CallInfo<T>(info));
9596
}
9697
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace NSubstitute.Exceptions
2+
{
3+
public class NoBaseImplementationException : SubstituteException
4+
{
5+
private const string Explanation =
6+
"Cannot call the base method as the base method implementation is missing. " +
7+
"You can call base method only if you create a class substitute and the method is not abstract.";
8+
9+
public NoBaseImplementationException() : base(Explanation) { }
10+
}
11+
}

src/NSubstitute/SubstituteExtensions.Returns.Task.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static ConfiguredCall Returns<T>(this Task<T> value, Func<CallInfo, T> re
4040
var wrappedFunc = WrapFuncInTask(returnThis);
4141
var wrappedReturnThese = returnThese.Length > 0 ? returnThese.Select(WrapFuncInTask).ToArray() : null;
4242

43-
return ConfigureReturn(MatchArgs.AsSpecifiedInCall, wrappedFunc, wrappedReturnThese);
43+
return ConfigureFuncReturn(MatchArgs.AsSpecifiedInCall, wrappedFunc, wrappedReturnThese);
4444
}
4545

4646
/// <summary>
@@ -72,7 +72,7 @@ public static ConfiguredCall ReturnsForAnyArgs<T>(this Task<T> value, Func<CallI
7272
var wrappedFunc = WrapFuncInTask(returnThis);
7373
var wrappedReturnThese = returnThese.Length > 0 ? returnThese.Select(WrapFuncInTask).ToArray() : null;
7474

75-
return ConfigureReturn(MatchArgs.Any, wrappedFunc, wrappedReturnThese);
75+
return ConfigureFuncReturn(MatchArgs.Any, wrappedFunc, wrappedReturnThese);
7676
}
7777

7878
#nullable restore

src/NSubstitute/SubstituteExtensions.Returns.ValueTask.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static ConfiguredCall Returns<T>(this ValueTask<T> value, Func<CallInfo,
4040
var wrappedFunc = WrapFuncInValueTask(returnThis);
4141
var wrappedReturnThese = returnThese.Length > 0 ? returnThese.Select(WrapFuncInValueTask).ToArray() : null;
4242

43-
return ConfigureReturn(MatchArgs.AsSpecifiedInCall, wrappedFunc, wrappedReturnThese);
43+
return ConfigureFuncReturn(MatchArgs.AsSpecifiedInCall, wrappedFunc, wrappedReturnThese);
4444
}
4545

4646
/// <summary>
@@ -72,7 +72,7 @@ public static ConfiguredCall ReturnsForAnyArgs<T>(this ValueTask<T> value, Func<
7272
var wrappedFunc = WrapFuncInValueTask(returnThis);
7373
var wrappedReturnThese = returnThese.Length > 0 ? returnThese.Select(WrapFuncInValueTask).ToArray() : null;
7474

75-
return ConfigureReturn(MatchArgs.Any, wrappedFunc, wrappedReturnThese);
75+
return ConfigureFuncReturn(MatchArgs.Any, wrappedFunc, wrappedReturnThese);
7676
}
7777

7878
#nullable restore

src/NSubstitute/SubstituteExtensions.Returns.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public static ConfiguredCall Returns<T>(this T value, T returnThis, params T[] r
2424
/// <param name="value"></param>
2525
/// <param name="returnThis">Function to calculate the return value</param>
2626
/// <param name="returnThese">Optionally use these functions next</param>
27-
public static ConfiguredCall Returns<T>(this T value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese) =>
28-
ConfigureReturn(MatchArgs.AsSpecifiedInCall, returnThis, returnThese);
27+
public static ConfiguredCall Returns<T>(this T value, Func<CallInfo<T>, T> returnThis, params Func<CallInfo<T>, T>[] returnThese) =>
28+
ConfigureFuncReturn(MatchArgs.AsSpecifiedInCall, returnThis, returnThese);
2929

3030
/// <summary>
3131
/// Set a return value for this call made with any arguments.
@@ -43,8 +43,8 @@ public static ConfiguredCall ReturnsForAnyArgs<T>(this T value, T returnThis, pa
4343
/// <param name="returnThis">Function to calculate the return value</param>
4444
/// <param name="returnThese">Optionally use these functions next</param>
4545
/// <returns></returns>
46-
public static ConfiguredCall ReturnsForAnyArgs<T>(this T value, Func<CallInfo, T> returnThis, params Func<CallInfo, T>[] returnThese) =>
47-
ConfigureReturn(MatchArgs.Any, returnThis, returnThese);
46+
public static ConfiguredCall ReturnsForAnyArgs<T>(this T value, Func<CallInfo<T>, T> returnThis, params Func<CallInfo<T>, T>[] returnThese) =>
47+
ConfigureFuncReturn(MatchArgs.Any, returnThis, returnThese);
4848

4949
#nullable restore
5050
private static ConfiguredCall ConfigureReturn<T>(MatchArgs matchArgs, T? returnThis, T?[]? returnThese)
@@ -64,7 +64,7 @@ private static ConfiguredCall ConfigureReturn<T>(MatchArgs matchArgs, T? returnT
6464
.LastCallShouldReturn(returnValue, matchArgs);
6565
}
6666

67-
private static ConfiguredCall ConfigureReturn<T>(MatchArgs matchArgs, Func<CallInfo, T?> returnThis, Func<CallInfo, T?>[]? returnThese)
67+
private static ConfiguredCall ConfigureFuncReturn<T>(MatchArgs matchArgs, Func<CallInfo<T>, T?> returnThis, Func<CallInfo<T>, T?>[]? returnThese)
6868
{
6969
IReturn returnValue;
7070
if (returnThese == null || returnThese.Length == 0)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using NSubstitute.Exceptions;
3+
using NUnit.Framework;
4+
5+
namespace NSubstitute.Acceptance.Specs
6+
{
7+
public class ReturnFromBase
8+
{
9+
public class Sample
10+
{
11+
public virtual string RepeatButLouder(string s) => s + "!";
12+
public virtual void VoidMethod() { }
13+
}
14+
15+
public abstract class SampleWithAbstractMethod
16+
{
17+
public abstract string NoBaseImplementation();
18+
}
19+
20+
public interface ISample
21+
{
22+
string InterfaceMethod();
23+
}
24+
25+
[Test]
26+
public void UseBaseInReturn() {
27+
var sub = Substitute.For<Sample>();
28+
sub.RepeatButLouder(Arg.Any<string>()).Returns(x => x.BaseResult() + "?");
29+
30+
Assert.AreEqual("Hi!?", sub.RepeatButLouder("Hi"));
31+
}
32+
33+
[Test]
34+
public void CallWithNoBaseImplementation() {
35+
var sub = Substitute.For<SampleWithAbstractMethod>();
36+
sub.NoBaseImplementation().Returns(x => x.BaseResult());
37+
38+
Assert.Throws<NoBaseImplementationException>(() =>
39+
sub.NoBaseImplementation()
40+
);
41+
}
42+
43+
[Test]
44+
public void CallBaseForInterface() {
45+
var sub = Substitute.For<ISample>();
46+
sub.InterfaceMethod().Returns(x => x.BaseResult());
47+
Assert.Throws<NoBaseImplementationException>(() =>
48+
sub.InterfaceMethod()
49+
);
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)