Skip to content

Commit

Permalink
Work on Matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
justindbaur committed Apr 21, 2024
1 parent 400ffe1 commit 8abc736
Show file tree
Hide file tree
Showing 21 changed files with 141 additions and 36 deletions.
2 changes: 1 addition & 1 deletion example/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void Test3()
var pretend = Pretend.That<IInterface>();

var setup = pretend
.Setup(i => i.Greeting("Hello", It.IsAny<int>()));
.Setup(i => i.Greeting("Hello", It.Is<int>(n => n > 10)));

setup.Returns("2");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Pretender.SourceGenerator.SetupArguments;
using Pretender.SourceGenerator.Writing;

namespace Pretender.SourceGenerator.Emitter
{
internal class CaptureInvocationArgumentEmitter : SetupArgumentEmitter
{
public CaptureInvocationArgumentEmitter(SetupArgumentSpec argumentSpec) : base(argumentSpec)
{
}

public override bool NeedsCapturer => true;

public override void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken)
{
EmitArgumentAccessor(writer);
//writer.WriteLine($"var {ArgumentSpec.Parameter.Name}_capture = ({ArgumentSpec.Parameter.Type.ToUnknownTypeString()})captured[{ArgumentSpec.Parameter.Ordinal}];");
//EmitIfReturnFalseCheck(writer, $"{ArgumentSpec.Parameter.Name}_arg", $"{ArgumentSpec.Parameter.Name}_capture");

writer.WriteLine($"if (!{Parameter.Name}_capturedMatcher.Matches({Parameter.Name}_arg))");
using (writer.WriteBlock())
{
writer.WriteLine("return false;");
}
}
}
}
8 changes: 4 additions & 4 deletions src/Pretender.SourceGenerator/Emitter/PretendEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ public void Emit(IndentedTextWriter writer, CancellationToken token)
writer.WriteLine();

// instance fields
writer.WriteLine($"private readonly Pretend<{_pretendType.ToFullDisplayString()}> _pretend;");
writer.WriteLine($"private readonly ICallHandler _callHandler;");
writer.WriteLine();

// main constructor
writer.WriteLine($"public {_knownTypeSymbols.GetPretendName(PretendType)}(Pretend<{_pretendType.ToFullDisplayString()}> pretend)");
writer.WriteLine($"public {_knownTypeSymbols.GetPretendName(PretendType)}(ICallHandler callHandler)");
using (writer.WriteBlock())
{
writer.WriteLine("_pretend = pretend;");
writer.WriteLine("_callHandler = callHandler;");
}

token.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -147,7 +147,7 @@ private void EmitMethodBody(IndentedTextWriter writer, IMethodSymbol methodSymbo
writer.WriteLine($"object?[] __arguments__ = [{string.Join(", ", methodSymbol.Parameters.Select(p => p.Name))}];");
// TODO: Probably create an Argument object
writer.WriteLine($"var __callInfo__ = new CallInfo({_methodStrategies[methodSymbol].UniqueName}_MethodInfo, __arguments__);");
writer.WriteLine("_pretend.Handle(__callInfo__);");
writer.WriteLine("_callHandler.Handle(__callInfo__);");

foreach (var parameter in methodSymbol.Parameters)
{
Expand Down
25 changes: 23 additions & 2 deletions src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,31 @@ public void Emit(IndentedTextWriter writer, CancellationToken cancellationToken)
if (anyEmitMatcherStatements)
{
matcherName = "matchCall";
writer.WriteLine("Matcher matchCall = (callInfo, target) =>");
writer.WriteLine("Matcher matchCall = (callInfo, setup) =>");
writer.WriteLine("{");
writer.IncreaseIndent();

if (_setupArgumentEmitters.Any(a => a.NeedsCapturer))
{
// TODO: Create single use call handler
writer.WriteLine("var singleUseCallHandler = new SingleUseCallHandler();");
writer.WriteLine($"var fake = new {_knownTypeSymbols.GetPretendName(PretendType)}(singleUseCallHandler);");
// Emit and run capturer
writer.WriteLine("var listener = MatcherListener.StartListening();");
writer.WriteLine("setup.Method.Invoke(setup.Target, [fake]);");

writer.WriteLine("listener.Dispose();");
writer.WriteLine("var capturedArguments = singleUseCallHandler.Arguments;");
writer.WriteLine();
}

int index = 0;
foreach (var a in _setupArgumentEmitters.Where(a => a.NeedsCapturer))
{
writer.WriteLine($"var {a.Parameter.Name}_capturedMatcher = listener.Matchers[{index}];");
index++;
}

foreach (var argumentEmitter in _setupArgumentEmitters)
{
argumentEmitter.EmitArgumentMatcher(writer, cancellationToken);
Expand All @@ -68,7 +89,7 @@ public void Emit(IndentedTextWriter writer, CancellationToken cancellationToken)
var methodStrategy = _knownTypeSymbols.GetSingleMethodStrategy(SetupMethod);

// TODO: default value
writer.WriteLine($"return new ReturningCompiledSetup<{PretendType.ToFullDisplayString()}, {returnType.ToUnknownTypeString()}>(pretend, {_knownTypeSymbols.GetPretendName(PretendType)}.{methodStrategy.UniqueName}_MethodInfo, {matcherName}, expr.Target, defaultValue: default);");
writer.WriteLine($"return new ReturningCompiledSetup<{PretendType.ToFullDisplayString()}, {returnType.ToUnknownTypeString()}>(pretend, {_knownTypeSymbols.GetPretendName(PretendType)}.{methodStrategy.UniqueName}_MethodInfo, {matcherName}, expr, defaultValue: default);");
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ protected SetupArgumentEmitter(SetupArgumentSpec argumentSpec)

protected SetupArgumentSpec ArgumentSpec { get; }

public IParameterSymbol Parameter => ArgumentSpec.Parameter;

public virtual bool EmitsMatcher => true;
public ImmutableArray<ILocalSymbol> NeededLocals { get; }
public bool NeedsCapturer { get; }
public virtual bool NeedsCapturer { get; }

public abstract void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public override void EmitArgumentMatcher(IndentedTextWriter writer, Cancellation
{
var localVariableName = $"{ArgumentSpec.Parameter.Name}_local";
EmitArgumentAccessor(writer);
writer.WriteLine(@$"var {localVariableName} = target.GetType().GetField(""{_localReferenceOperation.Local.Name}"").GetValue(target);");
writer.WriteLine(@$"var {localVariableName} = setup.Target!.GetType().GetField(""{_localReferenceOperation.Local.Name}"").GetValue(setup.Target!);");
EmitIfReturnFalseCheck(writer, $"{ArgumentSpec.Parameter.Name}_arg", localVariableName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public SetupArgumentParser(SetupArgumentSpec setupArgumentSpec)
}

// TODO: Parse args passed into the invocation
if (invocation.Arguments.Length > 0)
{
// TODO: Some of these might be safe to rewrite
return (new CaptureInvocationArgumentEmitter(_setupArgumentSpec), null);
}

return (new MatcherArgumentEmitter(matcherType, _setupArgumentSpec), null);
}
else
Expand Down
6 changes: 3 additions & 3 deletions src/Pretender/Internals/BaseCompiledSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ public abstract class BaseCompiledSetup<T>(
Pretend<T> pretend,
MethodInfo methodInfo,
Matcher matcher,
object? target)
Delegate setup)
{
private readonly MethodInfo _methodInfo = methodInfo;
private readonly Matcher _matcher = matcher;
private readonly object? _target = target;
private readonly Delegate _setup = setup;
protected Behavior? _behavior;

public Pretend<T> Pretend { get; } = pretend;
Expand Down Expand Up @@ -46,7 +46,7 @@ public bool Matches(CallInfo callInfo)
return false;
}

if (!_matcher(callInfo, _target))
if (!_matcher(callInfo, _setup))
{
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Pretender/Internals/Cache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class Cache
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
public static readonly Matcher NoOpMatcher = delegate (CallInfo callInfo, object? target)
public static readonly Matcher NoOpMatcher = delegate (CallInfo callInfo, Delegate setup)
{
return true;
};
Expand Down
14 changes: 14 additions & 0 deletions src/Pretender/Internals/ICallHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel;

namespace Pretender.Internals
{
/// <summary>
/// **FOR INTERNAL USE ONLY**
/// </summary>
public interface ICallHandler
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
void Handle(CallInfo callInfo);
}
}
2 changes: 1 addition & 1 deletion src/Pretender/Internals/Matcher.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace Pretender.Internals
{
// TODO: Can I make this a delegate of the unsafe wrapper around this?
public delegate bool Matcher(CallInfo callInfo, object? target);
public delegate bool Matcher(CallInfo callInfo, Delegate setup);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Pretender.Matchers;

namespace Pretender.Matchers
namespace Pretender.Internals
{
public sealed class MatcherListener : IDisposable
{
Expand All @@ -26,9 +27,8 @@ public static bool IsListening([MaybeNullWhen(false)] out MatcherListener listen
{
var listeners = s_listeners;

if (listeners != null && listeners.Count > 0)
if (listeners != null && listeners.TryPeek(out listener))
{
listener = listeners.Peek();
return true;
}

Expand All @@ -48,14 +48,17 @@ public void OnMatch(IMatcher matcher)
_matchers.Add(matcher);
}

public IEnumerable<IMatcher> GetMatchers()
public IReadOnlyList<IMatcher> Matchers
{
if (_matchers == null)
get
{
return [];
}
if (_matchers == null)
{
return [];
}

return _matchers;
return _matchers;
}
}

public void Dispose()
Expand Down
4 changes: 2 additions & 2 deletions src/Pretender/Internals/ReturningCompiledSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Pretender.Internals

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
public class ReturningCompiledSetup<T, TResult>(Pretend<T> pretend, MethodInfo methodInfo, Matcher matcher, object? target, TResult defaultValue)
: BaseCompiledSetup<T>(pretend, methodInfo, matcher, target), IPretendSetup<T, TResult>
public class ReturningCompiledSetup<T, TResult>(Pretend<T> pretend, MethodInfo methodInfo, Matcher matcher, Delegate setup, TResult defaultValue)
: BaseCompiledSetup<T>(pretend, methodInfo, matcher, setup), IPretendSetup<T, TResult>
{
private readonly TResult _defaultValue = defaultValue;

Expand Down
23 changes: 23 additions & 0 deletions src/Pretender/Internals/SingleUseCallHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.ComponentModel;

namespace Pretender.Internals
{
/// <summary>
/// **FOR INTERNAL USE ONLY**
/// </summary>
public class SingleUseCallHandler : ICallHandler
{
public object?[] Arguments { get; private set; } = null!;

/// <summary>
/// **FOR INTERNAL USE ONLY** Method signature subject to change.
/// </summary>
/// <param name="callInfo"></param>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
public void Handle(CallInfo callInfo)
{
Arguments = callInfo.Arguments;
}
}
}
4 changes: 2 additions & 2 deletions src/Pretender/Internals/VoidCompiledSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace Pretender.Internals
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
public class VoidCompiledSetup<T>(Pretend<T> pretend, MethodInfo methodInfo, Matcher matcher, object? target)
: BaseCompiledSetup<T>(pretend, methodInfo, matcher, target), IPretendSetup<T>
public class VoidCompiledSetup<T>(Pretend<T> pretend, MethodInfo methodInfo, Matcher matcher, Delegate setup)
: BaseCompiledSetup<T>(pretend, methodInfo, matcher, setup), IPretendSetup<T>
{
[DebuggerStepThrough]
public void Execute(CallInfo callInfo)
Expand Down
9 changes: 7 additions & 2 deletions src/Pretender/It.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Pretender.Matchers;
using Pretender.Internals;
using Pretender.Matchers;

namespace Pretender
{
Expand All @@ -12,8 +13,12 @@ public static T IsAny<T>()
}

[Matcher(typeof(AnonymousMatcher<>))]
public static T Is<T>(Func<T, bool> matcher)
public static T Is<T>(Func<T?, bool> matcher)
{
if (MatcherListener.IsListening(out var listener))
{
listener.OnMatch(new AnonymousMatcher<T>(matcher));
}
return default!;
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/Pretender/Matchers/AnonymousMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Pretender.Matchers
{
public readonly struct AnonymousMatcher<T>
public readonly struct AnonymousMatcher<T> : IMatcher
{
private readonly Func<T?, bool> _matcher;

Expand All @@ -13,5 +13,10 @@ public bool Matches(T? argument)
{
return _matcher(argument);
}

bool IMatcher.Matches(object? argument)
{
return _matcher((T)argument!);
}
}
}
7 changes: 3 additions & 4 deletions src/Pretender/Pretend.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.ComponentModel;
using System.Diagnostics;
using Pretender.Internals;

namespace Pretender;

[DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed partial class Pretend<T>
public sealed partial class Pretend<T> : ICallHandler
{
// TODO: Should we minimize allocations for rarely called mocks?
private List<CallInfo>? _calls;
Expand Down Expand Up @@ -62,9 +63,7 @@ public void Verify(IPretendSetup<T> pretendSetup, Called called)
called.Validate(timesCalled);
}

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method is only meant to be used by source generators")]
public void Handle(CallInfo callInfo)
void ICallHandler.Handle(CallInfo callInfo)
{
_calls ??= [];
_calls.Add(callInfo);
Expand Down
2 changes: 1 addition & 1 deletion test/Pretender.Tests/Matchers/MatcherListenerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Pretender.Matchers;
using Pretender.Internals;

namespace Pretender.Tests.Matchers
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ file static class SetupInterceptors
{
return pretend.GetOrCreateSetup<string>(0, static (pretend, expr) =>
{
return new ReturningCompiledSetup<global::ISimpleInterface, string>(pretend, PretendISimpleInterface.get_Bar_MethodInfo, Cache.NoOpMatcher, expr.Target, defaultValue: default);
return new ReturningCompiledSetup<global::ISimpleInterface, string>(pretend, PretendISimpleInterface.get_Bar_MethodInfo, Cache.NoOpMatcher, expr, defaultValue: default);
}, setupExpression);
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/SourceGeneratorTests/MainTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public TestClass()
{
var pretend = Pretend.That<IMyInterface>();
pretend.Setup(i => i.MethodAsync("Hi"));
pretend.Setup(i => i.MethodAsync(It.Is<string>(v => v == "Hi!")));
}
}
""");
Expand Down

0 comments on commit 8abc736

Please sign in to comment.