From 8abc7363864ac629b004cee9a6d1936f84f3a452 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:47:56 -0400 Subject: [PATCH] Work on Matchers --- example/UnitTest1.cs | 2 +- .../CaptureInvocationArgumentEmitter.cs | 27 +++++++++++++++++++ .../Emitter/PretendEmitter.cs | 8 +++--- .../Emitter/SetupActionEmitter.cs | 25 +++++++++++++++-- .../Emitter/SetupArgumentEmitter.cs | 4 ++- .../LocalReferenceArgumentEmitter.cs | 2 +- .../SetupArguments/SetupArgumentParser.cs | 6 +++++ src/Pretender/Internals/BaseCompiledSetup.cs | 6 ++--- src/Pretender/Internals/Cache.cs | 2 +- src/Pretender/Internals/ICallHandler.cs | 14 ++++++++++ src/Pretender/Internals/Matcher.cs | 2 +- .../MatcherListener.cs | 19 +++++++------ .../Internals/ReturningCompiledSetup.cs | 4 +-- .../Internals/SingleUseCallHandler.cs | 23 ++++++++++++++++ src/Pretender/Internals/VoidCompiledSetup.cs | 4 +-- src/Pretender/It.cs | 9 +++++-- src/Pretender/Matchers/AnonymousMatcher.cs | 7 ++++- src/Pretender/Pretend.cs | 7 +++-- .../Matchers/MatcherListenerTests.cs | 2 +- .../MainTests/ReturningMethod/Pretender_g.cs | 2 +- test/SourceGeneratorTests/MainTests.cs | 2 +- 21 files changed, 141 insertions(+), 36 deletions(-) create mode 100644 src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs create mode 100644 src/Pretender/Internals/ICallHandler.cs rename src/Pretender/{Matchers => Internals}/MatcherListener.cs (80%) create mode 100644 src/Pretender/Internals/SingleUseCallHandler.cs diff --git a/example/UnitTest1.cs b/example/UnitTest1.cs index 931dc66..6d72f74 100644 --- a/example/UnitTest1.cs +++ b/example/UnitTest1.cs @@ -42,7 +42,7 @@ public void Test3() var pretend = Pretend.That(); var setup = pretend - .Setup(i => i.Greeting("Hello", It.IsAny())); + .Setup(i => i.Greeting("Hello", It.Is(n => n > 10))); setup.Returns("2"); diff --git a/src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs b/src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs new file mode 100644 index 0000000..50c0e1f --- /dev/null +++ b/src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs @@ -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;"); + } + } + } +} diff --git a/src/Pretender.SourceGenerator/Emitter/PretendEmitter.cs b/src/Pretender.SourceGenerator/Emitter/PretendEmitter.cs index 2624fba..9dfcbb3 100644 --- a/src/Pretender.SourceGenerator/Emitter/PretendEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/PretendEmitter.cs @@ -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(); @@ -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) { diff --git a/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs b/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs index 556434e..18fc0e9 100644 --- a/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs @@ -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); @@ -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 { diff --git a/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs b/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs index 026262d..96be6e5 100644 --- a/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs @@ -14,9 +14,11 @@ protected SetupArgumentEmitter(SetupArgumentSpec argumentSpec) protected SetupArgumentSpec ArgumentSpec { get; } + public IParameterSymbol Parameter => ArgumentSpec.Parameter; + public virtual bool EmitsMatcher => true; public ImmutableArray NeededLocals { get; } - public bool NeedsCapturer { get; } + public virtual bool NeedsCapturer { get; } public abstract void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken); diff --git a/src/Pretender.SourceGenerator/SetupArguments/LocalReferenceArgumentEmitter.cs b/src/Pretender.SourceGenerator/SetupArguments/LocalReferenceArgumentEmitter.cs index d451010..66b81b8 100644 --- a/src/Pretender.SourceGenerator/SetupArguments/LocalReferenceArgumentEmitter.cs +++ b/src/Pretender.SourceGenerator/SetupArguments/LocalReferenceArgumentEmitter.cs @@ -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); } diff --git a/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs b/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs index eb820af..7b5d467 100644 --- a/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs +++ b/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs @@ -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 diff --git a/src/Pretender/Internals/BaseCompiledSetup.cs b/src/Pretender/Internals/BaseCompiledSetup.cs index 34daae4..864435c 100644 --- a/src/Pretender/Internals/BaseCompiledSetup.cs +++ b/src/Pretender/Internals/BaseCompiledSetup.cs @@ -9,11 +9,11 @@ public abstract class BaseCompiledSetup( Pretend 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 Pretend { get; } = pretend; @@ -46,7 +46,7 @@ public bool Matches(CallInfo callInfo) return false; } - if (!_matcher(callInfo, _target)) + if (!_matcher(callInfo, _setup)) { return false; } diff --git a/src/Pretender/Internals/Cache.cs b/src/Pretender/Internals/Cache.cs index 01adb82..8f464cb 100644 --- a/src/Pretender/Internals/Cache.cs +++ b/src/Pretender/Internals/Cache.cs @@ -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; }; diff --git a/src/Pretender/Internals/ICallHandler.cs b/src/Pretender/Internals/ICallHandler.cs new file mode 100644 index 0000000..8d04038 --- /dev/null +++ b/src/Pretender/Internals/ICallHandler.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +namespace Pretender.Internals +{ + /// + /// **FOR INTERNAL USE ONLY** + /// + public interface ICallHandler + { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is only meant to be used by source generators")] + void Handle(CallInfo callInfo); + } +} diff --git a/src/Pretender/Internals/Matcher.cs b/src/Pretender/Internals/Matcher.cs index 1c4cbbb..239ded6 100644 --- a/src/Pretender/Internals/Matcher.cs +++ b/src/Pretender/Internals/Matcher.cs @@ -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); } \ No newline at end of file diff --git a/src/Pretender/Matchers/MatcherListener.cs b/src/Pretender/Internals/MatcherListener.cs similarity index 80% rename from src/Pretender/Matchers/MatcherListener.cs rename to src/Pretender/Internals/MatcherListener.cs index 1f96137..eadc25d 100644 --- a/src/Pretender/Matchers/MatcherListener.cs +++ b/src/Pretender/Internals/MatcherListener.cs @@ -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 { @@ -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; } @@ -48,14 +48,17 @@ public void OnMatch(IMatcher matcher) _matchers.Add(matcher); } - public IEnumerable GetMatchers() + public IReadOnlyList Matchers { - if (_matchers == null) + get { - return []; - } + if (_matchers == null) + { + return []; + } - return _matchers; + return _matchers; + } } public void Dispose() diff --git a/src/Pretender/Internals/ReturningCompiledSetup.cs b/src/Pretender/Internals/ReturningCompiledSetup.cs index 3056bd7..79191f5 100644 --- a/src/Pretender/Internals/ReturningCompiledSetup.cs +++ b/src/Pretender/Internals/ReturningCompiledSetup.cs @@ -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(Pretend pretend, MethodInfo methodInfo, Matcher matcher, object? target, TResult defaultValue) - : BaseCompiledSetup(pretend, methodInfo, matcher, target), IPretendSetup + public class ReturningCompiledSetup(Pretend pretend, MethodInfo methodInfo, Matcher matcher, Delegate setup, TResult defaultValue) + : BaseCompiledSetup(pretend, methodInfo, matcher, setup), IPretendSetup { private readonly TResult _defaultValue = defaultValue; diff --git a/src/Pretender/Internals/SingleUseCallHandler.cs b/src/Pretender/Internals/SingleUseCallHandler.cs new file mode 100644 index 0000000..8e2d4a9 --- /dev/null +++ b/src/Pretender/Internals/SingleUseCallHandler.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; + +namespace Pretender.Internals +{ + /// + /// **FOR INTERNAL USE ONLY** + /// + public class SingleUseCallHandler : ICallHandler + { + public object?[] Arguments { get; private set; } = null!; + + /// + /// **FOR INTERNAL USE ONLY** Method signature subject to change. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is only meant to be used by source generators")] + public void Handle(CallInfo callInfo) + { + Arguments = callInfo.Arguments; + } + } +} diff --git a/src/Pretender/Internals/VoidCompiledSetup.cs b/src/Pretender/Internals/VoidCompiledSetup.cs index af2723e..27e9613 100644 --- a/src/Pretender/Internals/VoidCompiledSetup.cs +++ b/src/Pretender/Internals/VoidCompiledSetup.cs @@ -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(Pretend pretend, MethodInfo methodInfo, Matcher matcher, object? target) - : BaseCompiledSetup(pretend, methodInfo, matcher, target), IPretendSetup + public class VoidCompiledSetup(Pretend pretend, MethodInfo methodInfo, Matcher matcher, Delegate setup) + : BaseCompiledSetup(pretend, methodInfo, matcher, setup), IPretendSetup { [DebuggerStepThrough] public void Execute(CallInfo callInfo) diff --git a/src/Pretender/It.cs b/src/Pretender/It.cs index f98d80b..c8bcc03 100644 --- a/src/Pretender/It.cs +++ b/src/Pretender/It.cs @@ -1,4 +1,5 @@ -using Pretender.Matchers; +using Pretender.Internals; +using Pretender.Matchers; namespace Pretender { @@ -12,8 +13,12 @@ public static T IsAny() } [Matcher(typeof(AnonymousMatcher<>))] - public static T Is(Func matcher) + public static T Is(Func matcher) { + if (MatcherListener.IsListening(out var listener)) + { + listener.OnMatch(new AnonymousMatcher(matcher)); + } return default!; } } diff --git a/src/Pretender/Matchers/AnonymousMatcher.cs b/src/Pretender/Matchers/AnonymousMatcher.cs index e18a2c0..6b44eb5 100644 --- a/src/Pretender/Matchers/AnonymousMatcher.cs +++ b/src/Pretender/Matchers/AnonymousMatcher.cs @@ -1,6 +1,6 @@ namespace Pretender.Matchers { - public readonly struct AnonymousMatcher + public readonly struct AnonymousMatcher : IMatcher { private readonly Func _matcher; @@ -13,5 +13,10 @@ public bool Matches(T? argument) { return _matcher(argument); } + + bool IMatcher.Matches(object? argument) + { + return _matcher((T)argument!); + } } } \ No newline at end of file diff --git a/src/Pretender/Pretend.cs b/src/Pretender/Pretend.cs index 08109ac..e1f7419 100644 --- a/src/Pretender/Pretend.cs +++ b/src/Pretender/Pretend.cs @@ -1,10 +1,11 @@ using System.ComponentModel; using System.Diagnostics; +using Pretender.Internals; namespace Pretender; [DebuggerDisplay("{DebuggerToString(),nq}")] -public sealed partial class Pretend +public sealed partial class Pretend : ICallHandler { // TODO: Should we minimize allocations for rarely called mocks? private List? _calls; @@ -62,9 +63,7 @@ public void Verify(IPretendSetup 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); diff --git a/test/Pretender.Tests/Matchers/MatcherListenerTests.cs b/test/Pretender.Tests/Matchers/MatcherListenerTests.cs index 64780b6..a8c8ff7 100644 --- a/test/Pretender.Tests/Matchers/MatcherListenerTests.cs +++ b/test/Pretender.Tests/Matchers/MatcherListenerTests.cs @@ -1,4 +1,4 @@ -using Pretender.Matchers; +using Pretender.Internals; namespace Pretender.Tests.Matchers { diff --git a/test/SourceGeneratorTests/Baselines/MainTests/ReturningMethod/Pretender_g.cs b/test/SourceGeneratorTests/Baselines/MainTests/ReturningMethod/Pretender_g.cs index 8c3351b..1cca5be 100644 --- a/test/SourceGeneratorTests/Baselines/MainTests/ReturningMethod/Pretender_g.cs +++ b/test/SourceGeneratorTests/Baselines/MainTests/ReturningMethod/Pretender_g.cs @@ -112,7 +112,7 @@ file static class SetupInterceptors { return pretend.GetOrCreateSetup(0, static (pretend, expr) => { - return new ReturningCompiledSetup(pretend, PretendISimpleInterface.get_Bar_MethodInfo, Cache.NoOpMatcher, expr.Target, defaultValue: default); + return new ReturningCompiledSetup(pretend, PretendISimpleInterface.get_Bar_MethodInfo, Cache.NoOpMatcher, expr, defaultValue: default); }, setupExpression); } } diff --git a/test/SourceGeneratorTests/MainTests.cs b/test/SourceGeneratorTests/MainTests.cs index 4c05ff2..288ec62 100644 --- a/test/SourceGeneratorTests/MainTests.cs +++ b/test/SourceGeneratorTests/MainTests.cs @@ -40,7 +40,7 @@ public TestClass() { var pretend = Pretend.That(); - pretend.Setup(i => i.MethodAsync("Hi")); + pretend.Setup(i => i.MethodAsync(It.Is(v => v == "Hi!"))); } } """);