From b957675687447a1e76bc7511d58854d7b19b1b5a Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Thu, 19 Sep 2024 12:15:42 +0100 Subject: [PATCH] feat: add incremental generator tests --- .../InterfaceStubGenerator.cs | 6 + Refit.GeneratorTests/Fixture.cs | 9 +- .../IncrementalGeneratorRunReasons.cs | 31 +++ .../Incremental/IncrementalTest.cs | 211 ++++++++++++++++++ .../Incremental/TestHelper.cs | 103 +++++++++ 5 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs create mode 100644 Refit.GeneratorTests/Incremental/IncrementalTest.cs create mode 100644 Refit.GeneratorTests/Incremental/TestHelper.cs diff --git a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs index 6b0a236c9..7e535c7bc 100644 --- a/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs +++ b/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs @@ -897,4 +897,10 @@ syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax #endif } + + internal static class RefitGeneratorStepName + { + public const string ReportDiagnostics = "ReportDiagnostics"; + public const string BuildRefit = "BuildRefit"; + } } diff --git a/Refit.GeneratorTests/Fixture.cs b/Refit.GeneratorTests/Fixture.cs index 3b779981f..574583f89 100644 --- a/Refit.GeneratorTests/Fixture.cs +++ b/Refit.GeneratorTests/Fixture.cs @@ -97,7 +97,7 @@ public static Task VerifyForDeclaration(string declarations) return VerifyGenerator(source); } - private static CSharpCompilation CreateLibrary(params string[] source) + public static CSharpCompilation CreateLibrary(params SyntaxTree[] source) { var references = new List(); var assemblies = AssemblyReferencesForCodegen; @@ -112,7 +112,7 @@ private static CSharpCompilation CreateLibrary(params string[] source) references.Add(RefitAssembly); var compilation = CSharpCompilation.Create( "compilation", - source.Select(s => CSharpSyntaxTree.ParseText(s)), + source, references, new CSharpCompilationOptions(OutputKind.ConsoleApplication) ); @@ -120,6 +120,11 @@ private static CSharpCompilation CreateLibrary(params string[] source) return compilation; } + private static CSharpCompilation CreateLibrary(params string[] source) + { + return CreateLibrary(source.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray()); + } + private static Task VerifyGenerator(string source, bool ignoreNonInterfaces = true) { var compilation = CreateLibrary(source); diff --git a/Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs b/Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs new file mode 100644 index 000000000..646ede0e7 --- /dev/null +++ b/Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; + +namespace Refit.GeneratorTests.Incremental; + +internal record IncrementalGeneratorRunReasons( + IncrementalStepRunReason BuildMediatorStep, + IncrementalStepRunReason ReportDiagnosticsStep +) +{ + public static readonly IncrementalGeneratorRunReasons New = + new(IncrementalStepRunReason.New, IncrementalStepRunReason.New); + + public static readonly IncrementalGeneratorRunReasons Cached = + new( + // compilation step should always be modified as each time a new compilation is passed + IncrementalStepRunReason.Cached, + IncrementalStepRunReason.Cached + ); + + public static readonly IncrementalGeneratorRunReasons Modified = Cached with + { + ReportDiagnosticsStep = IncrementalStepRunReason.Modified, + BuildMediatorStep = IncrementalStepRunReason.Modified, + }; + + public static readonly IncrementalGeneratorRunReasons ModifiedSource = Cached with + { + ReportDiagnosticsStep = IncrementalStepRunReason.Unchanged, + BuildMediatorStep = IncrementalStepRunReason.Modified, + }; +} diff --git a/Refit.GeneratorTests/Incremental/IncrementalTest.cs b/Refit.GeneratorTests/Incremental/IncrementalTest.cs new file mode 100644 index 000000000..642fee757 --- /dev/null +++ b/Refit.GeneratorTests/Incremental/IncrementalTest.cs @@ -0,0 +1,211 @@ +using Microsoft.CodeAnalysis.CSharp; + +namespace Refit.GeneratorTests.Incremental; + +public class IncrementalTest +{ + private const string Default = + """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Refit; + + namespace RefitGeneratorTest; + + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string user); + } + """; + + // [Fact] + public void AddUnrelatedTypeDoesntRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}")); + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + } + + // [Fact] + public void SmallChangeDoesntRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // only change body, don't change the method + var compilation2 = TestHelper.ReplaceMemberDeclaration( + compilation1, + "IGitHubApi", + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string user); + + private record Temp(); + } + """ + ); + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); + } + + // [Fact] + public void ModifyParameterNameDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // change parameter name + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{myUser}")] + Task GetUser(string myUser); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } + + // [Fact] + public void ModifyParameterTypeDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // change parameter type + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(int user); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } + + // [Fact] + public void ModifyParameterNullabilityDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // change parameter nullability + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string? user); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } + + // [Fact] + public void AddParameterDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // add parameter + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string user, [Query] int myParam); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } + + // [Fact] + public void ModifyReturnTypeDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // change return type + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string user); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } + + // [Fact] + public void ModifyReturnNullabilityDoesRegenerate() + { + var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default); + var compilation1 = Fixture.CreateLibrary(syntaxTree); + + var driver1 = TestHelper.GenerateTracked(compilation1); + TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); + + // change return nullability + var newInterface = + """ + public interface IGitHubApi + { + [Get("/users/{user}")] + Task GetUser(string user); + } + """; + var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface); + + var driver2 = driver1.RunGenerators(compilation2); + TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); + } +} diff --git a/Refit.GeneratorTests/Incremental/TestHelper.cs b/Refit.GeneratorTests/Incremental/TestHelper.cs new file mode 100644 index 000000000..2b803f2aa --- /dev/null +++ b/Refit.GeneratorTests/Incremental/TestHelper.cs @@ -0,0 +1,103 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using Refit.Generator; + +namespace Refit.GeneratorTests.Incremental; + +internal static class TestHelper +{ + private static readonly GeneratorDriverOptions EnableIncrementalTrackingDriverOptions = + new(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true); + + internal static GeneratorDriver GenerateTracked(Compilation compilation) + { + var generator = new InterfaceStubGeneratorV2(); + + var driver = CSharpGeneratorDriver.Create( + new[] { generator.AsSourceGenerator() }, + driverOptions: EnableIncrementalTrackingDriverOptions + ); + return driver.RunGenerators(compilation); + } + + internal static CSharpCompilation ReplaceMemberDeclaration( + CSharpCompilation compilation, + string memberName, + string newMember + ) + { + var syntaxTree = compilation.SyntaxTrees.Single(); + var memberDeclaration = syntaxTree + .GetCompilationUnitRoot() + .DescendantNodes() + .OfType() + .Single(x => x.Identifier.Text == memberName); + var updatedMemberDeclaration = SyntaxFactory.ParseMemberDeclaration(newMember)!; + + var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration); + var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); + + return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree); + } + + internal static CSharpCompilation ReplaceLocalDeclaration( + CSharpCompilation compilation, + string variableName, + string newDeclaration + ) + { + var syntaxTree = compilation.SyntaxTrees.Single(); + + var memberDeclaration = syntaxTree + .GetCompilationUnitRoot() + .DescendantNodes() + .OfType() + .Single(x => x.Declaration.Variables.Any(x => x.Identifier.ToString() == variableName)); + var updatedMemberDeclaration = SyntaxFactory.ParseStatement(newDeclaration)!; + + var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration); + var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); + + return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree); + } + + internal static void AssertRunReasons( + GeneratorDriver driver, + IncrementalGeneratorRunReasons reasons, + int outputIndex = 0 + ) + { + var runResult = driver.GetRunResult().Results[0]; + + AssertRunReason( + runResult, + RefitGeneratorStepName.ReportDiagnostics, + reasons.ReportDiagnosticsStep, + outputIndex + ); + AssertRunReason(runResult, RefitGeneratorStepName.BuildRefit, reasons.BuildMediatorStep, outputIndex); + } + + private static void AssertRunReason( + GeneratorRunResult runResult, + string stepName, + IncrementalStepRunReason expectedStepReason, + int outputIndex + ) + { + var actualStepReason = runResult + .TrackedSteps[stepName] + .SelectMany(x => x.Outputs) + .ElementAt(outputIndex) + .Reason; + Assert.Equal(actualStepReason, expectedStepReason); + } +} + +internal static class RefitGeneratorStepName +{ + public const string ReportDiagnostics = "ReportDiagnostics"; + public const string BuildRefit = "BuildRefit"; +}