Skip to content

Commit

Permalink
feat: add incremental generator tests (#1829)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
  • Loading branch information
TimothyMakkison and ChrisPulman authored Sep 24, 2024
1 parent 1869ca6 commit f2ab216
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 2 deletions.
6 changes: 6 additions & 0 deletions InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -875,4 +875,10 @@ syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax

#endif
}

internal static class RefitGeneratorStepName
{
public const string ReportDiagnostics = "ReportDiagnostics";
public const string BuildRefit = "BuildRefit";
}
}
9 changes: 7 additions & 2 deletions Refit.GeneratorTests/Fixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataReference>();
var assemblies = AssemblyReferencesForCodegen;
Expand All @@ -112,14 +112,19 @@ 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)
);

return compilation;
}

private static CSharpCompilation CreateLibrary(params string[] source)
{
return CreateLibrary(source.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray());
}

private static Task<VerifyResult> VerifyGenerator(string source, bool ignoreNonInterfaces = true)
{
var compilation = CreateLibrary(source);
Expand Down
31 changes: 31 additions & 0 deletions Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs
Original file line number Diff line number Diff line change
@@ -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,
};
}
211 changes: 211 additions & 0 deletions Refit.GeneratorTests/Incremental/IncrementalTest.cs
Original file line number Diff line number Diff line change
@@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<int> 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<string?> GetUser(string user);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}
}
103 changes: 103 additions & 0 deletions Refit.GeneratorTests/Incremental/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -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<TypeDeclarationSyntax>()
.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<LocalDeclarationStatementSyntax>()
.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";
}

0 comments on commit f2ab216

Please sign in to comment.