From 58066b544e0cff4d9c7bea07bd4f353fa003aca7 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:52:14 -0400 Subject: [PATCH] Support FieldReference --- .editorconfig | 1 + example/Example.Tests.csproj | 8 +- example/FieldTests.cs | 41 ++++++++ perf/Comparison/Comparison.csproj | 2 +- .../Emitter/CapturedArgumentEmitter.cs | 26 +++++ ...cs => CapturedMatcherInvocationEmitter.cs} | 5 +- .../Emitter/SetupActionEmitter.cs | 14 ++- .../Emitter/SetupArgumentEmitter.cs | 1 + .../Pretender.SourceGenerator.csproj | 6 +- .../SetupArguments/SetupArgumentParser.cs | 9 +- src/Pretender/CallInfo.cs | 1 + src/Pretender/Internals/BaseCompiledSetup.cs | 5 +- .../Internals/ReturningCompiledSetup.cs | 8 +- .../Internals/SingleUseCallHandler.cs | 2 +- src/Pretender/Internals/VoidCompiledSetup.cs | 7 +- src/Pretender/Pretender.csproj | 2 +- test/Pretender.Tests/Pretender.Tests.csproj | 8 +- .../MainTests/FieldReference/Pretender_g.cs | 96 +++++++++++++++++++ .../MainTests/TaskOfTMethod/Pretender_g.cs | 10 +- test/SourceGeneratorTests/MainTests.cs | 33 +++++++ ...Pretender.SourceGenerator.SpecTests.csproj | 16 ++-- 21 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 example/FieldTests.cs create mode 100644 src/Pretender.SourceGenerator/Emitter/CapturedArgumentEmitter.cs rename src/Pretender.SourceGenerator/Emitter/{CaptureInvocationArgumentEmitter.cs => CapturedMatcherInvocationEmitter.cs} (84%) create mode 100644 test/SourceGeneratorTests/Baselines/MainTests/FieldReference/Pretender_g.cs diff --git a/.editorconfig b/.editorconfig index cf811d3..a381c35 100644 --- a/.editorconfig +++ b/.editorconfig @@ -387,3 +387,4 @@ dotnet_style_prefer_collection_expression = true:suggestion dotnet_style_namespace_match_folder = true:suggestion dotnet_diagnostic.CA1811.severity = warning +dotnet_diagnostic.CA1508.severity = warning diff --git a/example/Example.Tests.csproj b/example/Example.Tests.csproj index d616c20..fcac8bd 100644 --- a/example/Example.Tests.csproj +++ b/example/Example.Tests.csproj @@ -11,13 +11,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/example/FieldTests.cs b/example/FieldTests.cs new file mode 100644 index 0000000..15ddfee --- /dev/null +++ b/example/FieldTests.cs @@ -0,0 +1,41 @@ +using Pretender; + +namespace Example.Tests +{ + public static class FieldConstants + { + public const string MyConstant = "something"; + } + + public interface IFieldTest + { + int MyMethod(string myArg); + } + + + public class FieldTests + { + [Theory] + [InlineData("something", true)] + [InlineData("something_else", false)] + public void ReferenceFieldConstant(string actualArg, bool shouldMatch) + { + var pretend = Pretend.That(); + + pretend + .Setup(i => i.MyMethod(FieldConstants.MyConstant)) + .Returns(1); + + var test = pretend.Create(); + + if (false) + { + throw new Exception("something"); + } + + var result = test.MyMethod(actualArg); + + Assert.Equal(shouldMatch, result == 1); + } + } +} diff --git a/perf/Comparison/Comparison.csproj b/perf/Comparison/Comparison.csproj index 993c8c2..d6cc6fd 100644 --- a/perf/Comparison/Comparison.csproj +++ b/perf/Comparison/Comparison.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Pretender.SourceGenerator/Emitter/CapturedArgumentEmitter.cs b/src/Pretender.SourceGenerator/Emitter/CapturedArgumentEmitter.cs new file mode 100644 index 0000000..01f70df --- /dev/null +++ b/src/Pretender.SourceGenerator/Emitter/CapturedArgumentEmitter.cs @@ -0,0 +1,26 @@ +using Pretender.SourceGenerator.SetupArguments; +using Pretender.SourceGenerator.Writing; + +namespace Pretender.SourceGenerator.Emitter +{ + internal class CapturedArgumentEmitter : SetupArgumentEmitter + { + public CapturedArgumentEmitter(SetupArgumentSpec argumentSpec) : base(argumentSpec) + { + + } + + public override bool NeedsCapturer => true; + + public override void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken) + { + EmitArgumentAccessor(writer); + writer.WriteLine($"var {Parameter.Name}_capturedArg = ({Parameter.Type.ToUnknownTypeString()})capturedArguments[{Parameter.Ordinal}];"); + writer.WriteLine($"if ({Parameter.Name}_arg != {Parameter.Name}_capturedArg)"); + using (writer.WriteBlock()) + { + writer.WriteLine("return false;"); + } + } + } +} diff --git a/src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs b/src/Pretender.SourceGenerator/Emitter/CapturedMatcherInvocationEmitter.cs similarity index 84% rename from src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs rename to src/Pretender.SourceGenerator/Emitter/CapturedMatcherInvocationEmitter.cs index 50c0e1f..df4479a 100644 --- a/src/Pretender.SourceGenerator/Emitter/CaptureInvocationArgumentEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/CapturedMatcherInvocationEmitter.cs @@ -3,13 +3,14 @@ namespace Pretender.SourceGenerator.Emitter { - internal class CaptureInvocationArgumentEmitter : SetupArgumentEmitter + internal class CapturedMatcherInvocationEmitter : SetupArgumentEmitter { - public CaptureInvocationArgumentEmitter(SetupArgumentSpec argumentSpec) : base(argumentSpec) + public CapturedMatcherInvocationEmitter(SetupArgumentSpec argumentSpec) : base(argumentSpec) { } public override bool NeedsCapturer => true; + public override bool NeedsMatcher => true; public override void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken) { diff --git a/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs b/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs index b0bb5f9..65a8489 100644 --- a/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/SetupActionEmitter.cs @@ -66,8 +66,16 @@ public void Emit(IndentedTextWriter writer, CancellationToken cancellationToken) writer.WriteLine(); writer.WriteLine("var listener = MatcherListener.StartListening();"); - writer.WriteLine("setup.Method.Invoke(setup.Target, [fake]);"); - writer.WriteLine("listener.Dispose();"); + writer.WriteLine("try"); + using (writer.WriteBlock()) + { + writer.WriteLine("setup.Method.Invoke(setup.Target, [fake]);"); + } + writer.WriteLine("finally"); + using (writer.WriteBlock()) + { + writer.WriteLine("listener.Dispose();"); + } writer.WriteLine(); writer.WriteLine("var capturedArguments = singleUseCallHandler.Arguments;"); @@ -75,7 +83,7 @@ public void Emit(IndentedTextWriter writer, CancellationToken cancellationToken) } int index = 0; - foreach (var a in _setupArgumentEmitters.Where(a => a.NeedsCapturer)) + foreach (var a in _setupArgumentEmitters.Where(a => a.NeedsMatcher)) { writer.WriteLine($"var {a.Parameter.Name}_capturedMatcher = listener.Matchers[{index}];"); index++; diff --git a/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs b/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs index 96be6e5..0dc86c5 100644 --- a/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs +++ b/src/Pretender.SourceGenerator/Emitter/SetupArgumentEmitter.cs @@ -19,6 +19,7 @@ protected SetupArgumentEmitter(SetupArgumentSpec argumentSpec) public virtual bool EmitsMatcher => true; public ImmutableArray NeededLocals { get; } public virtual bool NeedsCapturer { get; } + public virtual bool NeedsMatcher { get; } public abstract void EmitArgumentMatcher(IndentedTextWriter writer, CancellationToken cancellationToken); diff --git a/src/Pretender.SourceGenerator/Pretender.SourceGenerator.csproj b/src/Pretender.SourceGenerator/Pretender.SourceGenerator.csproj index 66d80c1..2852085 100644 --- a/src/Pretender.SourceGenerator/Pretender.SourceGenerator.csproj +++ b/src/Pretender.SourceGenerator/Pretender.SourceGenerator.csproj @@ -10,12 +10,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs b/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs index 420a731..a338869 100644 --- a/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs +++ b/src/Pretender.SourceGenerator/SetupArguments/SetupArgumentParser.cs @@ -42,10 +42,17 @@ public SetupArgumentParser(SetupArgumentSpec setupArgumentSpec) OperationKind.Literal => (new LiteralArgumentEmitter((ILiteralOperation)argumentValue, _setupArgumentSpec), null), OperationKind.Invocation => ParseInvocation((IInvocationOperation)argumentValue, cancellationToken), OperationKind.LocalReference => (new LocalReferenceArgumentEmitter((ILocalReferenceOperation)argumentValue, _setupArgumentSpec), null), + OperationKind.FieldReference => ParseFieldReference((IFieldReferenceOperation)argumentValue, cancellationToken), _ => throw new NotImplementedException($"{argumentValue.Kind} is not a supported operation in setup arguments."), }; } + private (SetupArgumentEmitter? Emitter, ImmutableArray? Diagnostics) ParseFieldReference(IFieldReferenceOperation fieldReference, CancellationToken cancellationToken) + { + // For now fields references are just captured, we can change this to aggressively rewrite the callsite if there is desire + return (new CapturedArgumentEmitter(_setupArgumentSpec), null); + } + private (SetupArgumentEmitter? Emitter, ImmutableArray? Diagnostics) ParseInvocation(IInvocationOperation invocation, CancellationToken cancellationToken) { if (TryGetMatcherAttributeType(invocation, out var matcherType, cancellationToken)) @@ -62,7 +69,7 @@ public SetupArgumentParser(SetupArgumentSpec setupArgumentSpec) if (invocation.Arguments.Length > 0) { // TODO: Some of these might be safe to rewrite - return (new CaptureInvocationArgumentEmitter(_setupArgumentSpec), null); + return (new CapturedMatcherInvocationEmitter(_setupArgumentSpec), null); } return (new MatcherArgumentEmitter(matcherType, _setupArgumentSpec), null); diff --git a/src/Pretender/CallInfo.cs b/src/Pretender/CallInfo.cs index f6a9eb9..e161d0d 100644 --- a/src/Pretender/CallInfo.cs +++ b/src/Pretender/CallInfo.cs @@ -8,6 +8,7 @@ public CallInfo(MethodInfo methodInfo, object?[] arguments) { MethodInfo = methodInfo; Arguments = arguments; + } public MethodInfo MethodInfo { get; } diff --git a/src/Pretender/Internals/BaseCompiledSetup.cs b/src/Pretender/Internals/BaseCompiledSetup.cs index 864435c..e008b0c 100644 --- a/src/Pretender/Internals/BaseCompiledSetup.cs +++ b/src/Pretender/Internals/BaseCompiledSetup.cs @@ -29,13 +29,14 @@ public void SetBehavior(Behavior behavior) _behavior = behavior; } - public void ExecuteCore(CallInfo callInfo) + public bool ExecuteCore(CallInfo callInfo) { if (!Matches(callInfo)) { - return; + return false; } TimesCalled++; + return true; } public bool Matches(CallInfo callInfo) diff --git a/src/Pretender/Internals/ReturningCompiledSetup.cs b/src/Pretender/Internals/ReturningCompiledSetup.cs index 79191f5..b02d87e 100644 --- a/src/Pretender/Internals/ReturningCompiledSetup.cs +++ b/src/Pretender/Internals/ReturningCompiledSetup.cs @@ -18,7 +18,13 @@ public class ReturningCompiledSetup(Pretend pretend, MethodInfo m [DebuggerStepThrough] public void Execute(CallInfo callInfo) { - ExecuteCore(callInfo); + var matched = ExecuteCore(callInfo); + + if (!matched) + { + callInfo.ReturnValue ??= _defaultValue; + return; + } // Run behavior if (_behavior is null) diff --git a/src/Pretender/Internals/SingleUseCallHandler.cs b/src/Pretender/Internals/SingleUseCallHandler.cs index 21a8621..70c26a1 100644 --- a/src/Pretender/Internals/SingleUseCallHandler.cs +++ b/src/Pretender/Internals/SingleUseCallHandler.cs @@ -5,7 +5,7 @@ namespace Pretender.Internals /// /// **FOR INTERNAL USE ONLY** /// - public class SingleUseCallHandler : ICallHandler + public sealed class SingleUseCallHandler : ICallHandler { public object?[] Arguments { get; private set; } = null!; diff --git a/src/Pretender/Internals/VoidCompiledSetup.cs b/src/Pretender/Internals/VoidCompiledSetup.cs index 27e9613..ef91e38 100644 --- a/src/Pretender/Internals/VoidCompiledSetup.cs +++ b/src/Pretender/Internals/VoidCompiledSetup.cs @@ -12,7 +12,12 @@ public class VoidCompiledSetup(Pretend pretend, MethodInfo methodInfo, Mat [DebuggerStepThrough] public void Execute(CallInfo callInfo) { - ExecuteCore(callInfo); + var matched = ExecuteCore(callInfo); + + if (!matched) + { + return; + } // Run behavior if (_behavior is null) diff --git a/src/Pretender/Pretender.csproj b/src/Pretender/Pretender.csproj index 7598eb5..f10c84d 100644 --- a/src/Pretender/Pretender.csproj +++ b/src/Pretender/Pretender.csproj @@ -17,7 +17,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Pretender.Tests/Pretender.Tests.csproj b/test/Pretender.Tests/Pretender.Tests.csproj index 7fe5319..2249e43 100644 --- a/test/Pretender.Tests/Pretender.Tests.csproj +++ b/test/Pretender.Tests/Pretender.Tests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/SourceGeneratorTests/Baselines/MainTests/FieldReference/Pretender_g.cs b/test/SourceGeneratorTests/Baselines/MainTests/FieldReference/Pretender_g.cs new file mode 100644 index 0000000..2ed111b --- /dev/null +++ b/test/SourceGeneratorTests/Baselines/MainTests/FieldReference/Pretender_g.cs @@ -0,0 +1,96 @@ +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + using System; + using System.CodeDom.Compiler; + + [GeneratedCode("Pretender.SourceGenerator", "1.0.0.0")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int column) + { + } + } +} + +namespace Pretender.SourceGeneration +{ + using System; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Threading.Tasks; + using Pretender; + using Pretender.Internals; + + file class PretendITest : global::FieldReference.ITest + { + public static readonly MethodInfo Method_MethodInfo = typeof(PretendITest).GetMethod(nameof(Method))!; + + private readonly ICallHandler _callHandler; + + public PretendITest(ICallHandler callHandler) + { + _callHandler = callHandler; + } + + public void Method(string arg) + { + object?[] __arguments__ = [arg]; + var __callInfo__ = new CallInfo(Method_MethodInfo, __arguments__); + _callHandler.Handle(__callInfo__); + } + } + + file static class SetupInterceptors + { + [InterceptsLocation(@"MyTest.cs", 22, 17)] + internal static IPretendSetup Setup0(this Pretend pretend, Action setupExpression) + { + return pretend.GetOrCreateSetup(0, static (pretend, expr) => + { + Matcher matchCall = (callInfo, setup) => + { + var singleUseCallHandler = new SingleUseCallHandler(); + var fake = new PretendITest(singleUseCallHandler); + + var listener = MatcherListener.StartListening(); + try + { + setup.Method.Invoke(setup.Target, [fake]); + } + finally + { + listener.Dispose(); + } + + var capturedArguments = singleUseCallHandler.Arguments; + + var arg_arg = (string)callInfo.Arguments[0]; + var arg_capturedArg = (string)capturedArguments[0]; + if (arg_arg != arg_capturedArg) + { + return false; + } + return true; + }; + return new VoidCompiledSetup(); + }, setupExpression); + } + } + + file static class VerifyInterceptors + { + } + + file static class CreateInterceptors + { + } +} \ No newline at end of file diff --git a/test/SourceGeneratorTests/Baselines/MainTests/TaskOfTMethod/Pretender_g.cs b/test/SourceGeneratorTests/Baselines/MainTests/TaskOfTMethod/Pretender_g.cs index aef1da2..da50e76 100644 --- a/test/SourceGeneratorTests/Baselines/MainTests/TaskOfTMethod/Pretender_g.cs +++ b/test/SourceGeneratorTests/Baselines/MainTests/TaskOfTMethod/Pretender_g.cs @@ -63,8 +63,14 @@ file static class SetupInterceptors var fake = new PretendIMyInterface(singleUseCallHandler); var listener = MatcherListener.StartListening(); - setup.Method.Invoke(setup.Target, [fake]); - listener.Dispose(); + try + { + setup.Method.Invoke(setup.Target, [fake]); + } + finally + { + listener.Dispose(); + } var capturedArguments = singleUseCallHandler.Arguments; diff --git a/test/SourceGeneratorTests/MainTests.cs b/test/SourceGeneratorTests/MainTests.cs index 79434aa..6a232fa 100644 --- a/test/SourceGeneratorTests/MainTests.cs +++ b/test/SourceGeneratorTests/MainTests.cs @@ -1,3 +1,5 @@ +using System.Security.Cryptography.X509Certificates; + namespace SourceGeneratorTests; public partial class MainTests : TestBase @@ -46,6 +48,37 @@ public TestClass() """); } + [Fact] + public async Task FieldReference() + { + await RunAndCompareAsync($$""" + using System; + using Pretender; + + namespace FieldReference; + + public static class Constants + { + public const string MyConstant = "my_string"; + } + + public interface ITest + { + void Method(string arg); + } + + public class TestClass + { + public TestClass() + { + var pretend = Pretend.That(); + + pretend.Setup(i => i.Method(Constants.MyConstant)); + } + } + """); + } + //[Fact] //public async Task AbstractClass() diff --git a/test/SourceGeneratorTests/Pretender.SourceGenerator.SpecTests.csproj b/test/SourceGeneratorTests/Pretender.SourceGenerator.SpecTests.csproj index 2f1cf40..a17a87e 100644 --- a/test/SourceGeneratorTests/Pretender.SourceGenerator.SpecTests.csproj +++ b/test/SourceGeneratorTests/Pretender.SourceGenerator.SpecTests.csproj @@ -9,20 +9,20 @@ - - - - + + + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all