diff --git a/pretender.sln b/pretender.sln index dc57272..0bba829 100644 --- a/pretender.sln +++ b/pretender.sln @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{DA78B66F-E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Comparison", "perf\Comparison\Comparison.csproj", "{96C653E3-D10B-47A0-8E42-0B93119AE145}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pretender.Tests", "test\Pretender.Tests\Pretender.Tests.csproj", "{17552274-CC28-438A-80BB-4A161F95AB11}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {96C653E3-D10B-47A0-8E42-0B93119AE145}.Release|x64.Build.0 = Release|Any CPU {96C653E3-D10B-47A0-8E42-0B93119AE145}.Release|x86.ActiveCfg = Release|Any CPU {96C653E3-D10B-47A0-8E42-0B93119AE145}.Release|x86.Build.0 = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|x64.ActiveCfg = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|x64.Build.0 = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|x86.ActiveCfg = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Debug|x86.Build.0 = Debug|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|Any CPU.Build.0 = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|x64.ActiveCfg = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|x64.Build.0 = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|x86.ActiveCfg = Release|Any CPU + {17552274-CC28-438A-80BB-4A161F95AB11}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +118,7 @@ Global {90CC0802-39BB-4390-8A06-C7FD0C14D5C7} = {DFB5E6EE-B017-40FD-BC67-CE0471060A68} {09EB76D6-C82D-48A9-A0F7-B9BCC10B7621} = {2667A3D7-30CA-4DF5-B2F4-A7554C6D3ADD} {96C653E3-D10B-47A0-8E42-0B93119AE145} = {DA78B66F-EE75-46B5-8EC8-6F498A8AFC64} + {17552274-CC28-438A-80BB-4A161F95AB11} = {2667A3D7-30CA-4DF5-B2F4-A7554C6D3ADD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FD826CBD-9A31-44A4-B352-E637B9FABD6B} diff --git a/src/Pretender.SourceGenerator/Parser/PretendParser.cs b/src/Pretender.SourceGenerator/Parser/PretendParser.cs new file mode 100644 index 0000000..4fd1444 --- /dev/null +++ b/src/Pretender.SourceGenerator/Parser/PretendParser.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using static Pretender.SourceGenerator.PretenderSourceGenerator; + +namespace Pretender.SourceGenerator.Parser +{ + internal class PretendParser + { + + public PretendParser(PretendInvocation pretendInvocation, CompilationData compilationData) + { + PretendInvocation = pretendInvocation; + } + + public PretendInvocation PretendInvocation { get; } + + public (object? Emitter, ImmutableArray? Diagnostics) GetEmitter(CancellationToken cancellationToken) + { + + return (null, null); + } + } +} diff --git a/src/Pretender.SourceGenerator/PretendInvocation.cs b/src/Pretender.SourceGenerator/PretendInvocation.cs new file mode 100644 index 0000000..5c6e022 --- /dev/null +++ b/src/Pretender.SourceGenerator/PretendInvocation.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Pretender.SourceGenerator.Parser; + +namespace Pretender.SourceGenerator +{ + internal class PretendInvocation + { + public PretendInvocation(ITypeSymbol pretendType, Location location, bool fillExisting) + { + PretendType = pretendType; + Location = location; + FillExisting = fillExisting; + } + + public ITypeSymbol PretendType { get; } + public Location Location { get; } + public bool FillExisting { get; } + + public static bool IsCandidateSyntaxNode(SyntaxNode node) + { + // Pretend.That(); + if (node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + // TODO: Will this work with a using static Pretender.Pretend + // ... + // That(); + Expression: IdentifierNameSyntax { Identifier.ValueText: "Pretend" }, + Name: GenericNameSyntax { Identifier.ValueText: "That", TypeArgumentList.Arguments.Count: 1 }, + } + }) + { + return true; + } + + // TODO: Also do Attribute + + return false; + } + + public static PretendInvocation? Create(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + Debug.Assert(IsCandidateSyntaxNode(context.Node)); + var operation = context.SemanticModel.GetOperation(context.Node, cancellationToken); + if (operation is IInvocationOperation invocation) + { + cancellationToken.ThrowIfCancellationRequested(); + return CreateFromGeneric(invocation); + } + // TODO: Support attribute + + return null; + } + + private static PretendInvocation? CreateFromGeneric(IInvocationOperation operation) + { + if (operation.TargetMethod is not IMethodSymbol + { + Name: "That", + ContainingType: INamedTypeSymbol namedTypeSymbol, + TypeArguments.Length: 1, + } || !KnownTypeSymbols.IsPretend(namedTypeSymbol)) + { + return null; + } + + return new PretendInvocation(operation.TargetMethod.TypeArguments[0], operation.Syntax.GetLocation(), false); + } + } +} diff --git a/src/Pretender.SourceGenerator/PretenderSourceGenerator.cs b/src/Pretender.SourceGenerator/PretenderSourceGenerator.cs index 90fcac2..50020f5 100644 --- a/src/Pretender.SourceGenerator/PretenderSourceGenerator.cs +++ b/src/Pretender.SourceGenerator/PretenderSourceGenerator.cs @@ -24,27 +24,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) #region Pretend IncrementalValuesProvider pretendsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( - predicate: static (node, token) => - { - // Pretend.That(); - if (node is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax - { - // TODO: Will this work with a using static Pretender.Pretend - // ... - // That(); - Expression: IdentifierNameSyntax { Identifier.ValueText: "Pretend" }, - Name: GenericNameSyntax { Identifier.ValueText: "That", TypeArgumentList.Arguments.Count: 1 }, - }, - }) - { - return true; - } - - // TODO: Allow constructor and shortcut Pretend.Of(); - return false; - }, + predicate: (node, _) => PretendInvocation.IsCandidateSyntaxNode(node), transform: static (context, token) => { var operation = context.SemanticModel.GetOperation(context.Node, token); diff --git a/src/Pretender.SourceGenerator/SetupCreationSpec.cs b/src/Pretender.SourceGenerator/SetupCreationSpec.cs index 1538d78..4ab3778 100644 --- a/src/Pretender.SourceGenerator/SetupCreationSpec.cs +++ b/src/Pretender.SourceGenerator/SetupCreationSpec.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/Pretender/Behaviors/CallbackBehavior.cs b/src/Pretender/Behaviors/CallbackBehavior.cs index dfd4af0..4da4f4e 100644 --- a/src/Pretender/Behaviors/CallbackBehavior.cs +++ b/src/Pretender/Behaviors/CallbackBehavior.cs @@ -1,19 +1,17 @@ namespace Pretender.Behaviors { - public delegate void Callback(ref CallInfo callInfo); - internal class CallbackBehavior : Behavior { - private readonly Callback _action; + private readonly Action _action; - public CallbackBehavior(Callback action) + public CallbackBehavior(Action action) { _action = action; } public override void Execute(CallInfo callInfo) { - _action(ref callInfo); + _action(callInfo); } } } diff --git a/src/Pretender/Called.cs b/src/Pretender/Called.cs index 1707368..1472a7b 100644 --- a/src/Pretender/Called.cs +++ b/src/Pretender/Called.cs @@ -1,4 +1,6 @@ -namespace Pretender +using System.Diagnostics; + +namespace Pretender { public readonly struct Called { @@ -15,15 +17,31 @@ private Called(int from, int to, CalledKind calledKind) enum CalledKind { - Exact + Exact, + AtLeast, + Range, } public static Called Exactly(int expectedCalls) => new(expectedCalls, expectedCalls, CalledKind.Exact); + public static Called AtLeastOnce() + => new(1, int.MaxValue, CalledKind.AtLeast); + + public static implicit operator Called(Range range) + { + if (range.Start.IsFromEnd || range.End.IsFromEnd) + { + throw new ArgumentException(); + } + + return new(range.Start.Value, range.End.Value, CalledKind.Range); + } + public static implicit operator Called(int expectedCalls) => new(expectedCalls, expectedCalls, CalledKind.Exact); + [StackTraceHidden] public void Validate(int callCount) { switch (_calledKind) @@ -35,10 +53,26 @@ public void Validate(int callCount) throw new Exception("It was not called exactly that many times."); } break; + case CalledKind.AtLeast: + if (callCount < _from) + { + throw new Exception($"It was not called at least {_from} time(s)"); + } + break; + case CalledKind.Range: + if (callCount < _from || callCount >= _to) + { + throw new Exception($"It was not between the range {_from}..{_to}"); + } + break; default: throw new Exception("Invalid call kind."); } + } + public override string ToString() + { + return $"From = {_from}, To = {_to}, Kind = {_calledKind}"; } } } diff --git a/src/Pretender/It.cs b/src/Pretender/It.cs index ea75ec9..f98d80b 100644 --- a/src/Pretender/It.cs +++ b/src/Pretender/It.cs @@ -7,7 +7,7 @@ public static class It [Matcher] public static T IsAny() { - // This method is never normally invoked during its normal usage inside an expression + // This method is never normally invoked during its normal usage return default!; } diff --git a/src/Pretender/Matchers/AnyMatcher.cs b/src/Pretender/Matchers/AnyMatcher.cs index 3f85ae7..c658bf6 100644 --- a/src/Pretender/Matchers/AnyMatcher.cs +++ b/src/Pretender/Matchers/AnyMatcher.cs @@ -2,8 +2,6 @@ { public sealed class AnyMatcher : IMatcher { - public static AnyMatcher Instance = new(); - public bool Matches(object? argument) { return true; diff --git a/src/Pretender/Matchers/IMatcher.cs b/src/Pretender/Matchers/IMatcher.cs index f4eacd5..0f9d51e 100644 --- a/src/Pretender/Matchers/IMatcher.cs +++ b/src/Pretender/Matchers/IMatcher.cs @@ -1,5 +1,12 @@ namespace Pretender.Matchers { + /// + /// + /// + /// + /// Matchers don't actually need to implement this interface, matchers are used by duck-typing. + /// So as long as they implement a `Matches` method taking one argument and returning a bool it will be used. + /// public interface IMatcher { bool Matches(object? argument); diff --git a/src/Pretender/Pretend.cs b/src/Pretender/Pretend.cs index 3bf41bd..5052100 100644 --- a/src/Pretender/Pretend.cs +++ b/src/Pretender/Pretend.cs @@ -1,12 +1,10 @@ using System.ComponentModel; using System.Diagnostics; -using System.Globalization; -using Pretender.Internals; namespace Pretender; [DebuggerDisplay("{DebuggerToString(),nq}")] -public partial class Pretend +public sealed partial class Pretend { // TODO: Should we minimize allocations for rarely called mocks? private List? _calls; @@ -44,6 +42,7 @@ public void Verify(Func verifyExpression, Called called) [EditorBrowsable(EditorBrowsableState.Never)] // TODO: Make this obsolete + [StackTraceHidden] public void Verify(IPretendSetup pretendSetup, Called called) { // Right now we can't trust that this setup was created before, loop over all the calls and check it diff --git a/src/Pretender/PretendSetupExtensions.cs b/src/Pretender/PretendSetupExtensions.cs index a55248c..54f51c4 100644 --- a/src/Pretender/PretendSetupExtensions.cs +++ b/src/Pretender/PretendSetupExtensions.cs @@ -25,10 +25,20 @@ public static Pretend Throws(this IPretendSetup pretendSetu return pretendSetup.Pretend; } - public static Pretend Callback(this IPretendSetup pretendSetup, Callback callback) + public static Pretend Callback(this IPretendSetup pretendSetup, Action callback) { pretendSetup.SetBehavior(new CallbackBehavior(callback)); return pretendSetup.Pretend; } + + public static Pretend Does(this IPretendSetup pretendSetup, Action callback) + { + pretendSetup.SetBehavior(new CallbackBehavior(callInfo => + { + var firstArg = (T1)callInfo.Arguments[0]!; + callback(firstArg); + })); + return pretendSetup.Pretend; + } } } diff --git a/src/Pretender/Pretender.csproj b/src/Pretender/Pretender.csproj index 9e971aa..2072f78 100644 --- a/src/Pretender/Pretender.csproj +++ b/src/Pretender/Pretender.csproj @@ -1,7 +1,7 @@  - 0.1.2 + 0.1.3 prerelease A mocking framework that makes use of source generators an interceptors to be fast and give you control. Pretender diff --git a/test/Pretender.Tests/CalledTests.cs b/test/Pretender.Tests/CalledTests.cs new file mode 100644 index 0000000..42f5893 --- /dev/null +++ b/test/Pretender.Tests/CalledTests.cs @@ -0,0 +1,61 @@ +namespace Pretender.Tests +{ + public class CalledTests + { + public static IEnumerable Validate_DoesNotThrowData() + { + // Exactly + yield return Data(1, 1); + yield return Data(20, 20); + + // AtLeast + yield return Data(Called.AtLeastOnce(), 1); + yield return Data(Called.AtLeastOnce(), 2); + + // Range + yield return Data(1..4, 1); + yield return Data(1..4, 2); + yield return Data(1..4, 3); + + static object[] Data(Called called, int calls) + { + return [called, calls]; + } + } + + [Theory] + [MemberData(nameof(Validate_DoesNotThrowData))] + public void Validate_DoesNotThrow(Called called, int calls) + { + called.Validate(calls); + } + + public static IEnumerable Validate_ThrowsData() + { + // Exactly + yield return Data(1, 2); + yield return Data(1, 0); + + // AtLeast + yield return Data(Called.AtLeastOnce(), 0); + + // Range + yield return Data(2..5, 0); + yield return Data(2..5, 1); + yield return Data(2..5, 5); + yield return Data(2..5, 6); + + static object[] Data(Called called, int calls) + { + return [called, calls]; + } + } + + [Theory] + [MemberData(nameof(Validate_ThrowsData))] + public void Validate_Throws(Called called, int calls) + { + Assert.Throws(() => called.Validate(calls)); + } + } +} \ No newline at end of file diff --git a/test/Pretender.Tests/GlobalUsings.cs b/test/Pretender.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/test/Pretender.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Pretender.Tests/Pretender.Tests.csproj b/test/Pretender.Tests/Pretender.Tests.csproj new file mode 100644 index 0000000..7fe5319 --- /dev/null +++ b/test/Pretender.Tests/Pretender.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/Pretenter.Tests/GlobalUsings.cs b/test/Pretenter.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/test/Pretenter.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Pretenter.Tests/Pretender.Tests.csproj b/test/Pretenter.Tests/Pretender.Tests.csproj new file mode 100644 index 0000000..9e0c306 --- /dev/null +++ b/test/Pretenter.Tests/Pretender.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/test/Pretenter.Tests/UnitTest1.cs b/test/Pretenter.Tests/UnitTest1.cs new file mode 100644 index 0000000..6ac834d --- /dev/null +++ b/test/Pretenter.Tests/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace Pretenter.Tests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} \ No newline at end of file