user3797758
user3797758

Reputation: 1073

Unit test C# source generator to emulate a user changing source code?

I'm writing a source generator (IIncrementalGenerator to be specific) and I want to make sure that the caching is correctly responding to changes in the source code.

  1. Run the source generator
  2. Assert that certain internal stages are outputting the correct data
  3. Change the Compilation to modify the code that the source generator is listening for
  4. Assert that certain stages have a Modified or New state for the run reason (and that the data itself is correct)

This would show that the generator is responsive to changes in the source code, otherwise I'm limited to just testing that the generator creates correct source code in the initial compile, not follow up changes.

Here is the core code that I've got for running the generator over multiple iterations:

public static IReadOnlyList<GeneratorDriverRunResult> RunGeneratorIterations<T>(T generator, params Compilation[] codeIterations)
    where T : IIncrementalGenerator
{
    GeneratorDriverOptions driverOptions = new(IncrementalGeneratorOutputKind.None, true);
    ISourceGenerator[] generators = { generator.AsSourceGenerator() };
    GeneratorDriver driver = CSharpGeneratorDriver.Create(generators, driverOptions: driverOptions);

    List<GeneratorDriverRunResult> runResults = new();

    // Run the generation pass
    driver = driver.RunGeneratorsAndUpdateCompilation(codeIterations[0], out _, out _);
    runResults.Add(driver.GetRunResult());
        
    for (int i = 1; i < codeIterations.Length; i++)
    {
        driver = driver.RunGenerators(codeIterations[i], CancellationToken.None);
        runResults.Add(driver.GetRunResult());
    }
        
    Assert.AreEqual(codeIterations.Length, runResults.Count);
    return runResults;
}

The issue with this is that when I run tests using the above code itt throws an exception in the second driver.RunGenerators call:

Unexpected value '(Modified, Added)' of type 'System.ValueTuple'2[[Microsoft.CodeAnalysis.EntryState, Microsoft.CodeAnalysis.EntryState]]

This tells me that I'm not setting up the next compile pass correctly, How do I run a source generator with incrementally changing source code in unit tests?

Small update on this:

It seems that if I tell driverOptions not to track the generator stages it doesn't crash... but I then can't inspect the actual data in my unit test. I'm beginning to think I'm fighting a compiler bug.

Upvotes: 4

Views: 357

Answers (1)

PEK
PEK

Reputation: 4338

Are you combining with CompilationProvider? Then you might run into this issue.

Here is a workaround from https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md:

Consider the following (incorrect) combine where the basic inputs are combined, then used to generate some source:

public void Initialize(IncrementalGeneratorInitializationContext context)
{
    var compilation = context.CompilationProvider;
    var texts = context.AdditionalTextsProvider;

    // Don't do this!
    var combined = texts.Combine(compilation);

    context.RegisterSourceOutput(combined, static (spc, pair) =>
    {
        var assemblyName = pair.Right.AssemblyName;
        // produce source ...
    });

Any time the compilation changes, which it will frequently as the user is typing in the IDE, then RegisterSourceOutput will get re-run. Instead, look up the compilation dependant information first, then combine that with the additional files:

public void Initialize(IncrementalGeneratorInitializationContext context)
{
    var assemblyName = context.CompilationProvider.Select(static (c, _) => c.AssemblyName);
    var texts = context.AdditionalTextsProvider;

    var combined = texts.Combine(assemblyName);

    context.RegisterSourceOutput(combined, (spc, pair) =>
    {
        var assemblyName = pair.Right;
        // produce source ...
    });
}

Upvotes: 0

Related Questions