You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在单元测试中针对IIncrementalGenerator类型的源代码生成器测试源码变更的响应逻辑?

如何在单元测试中针对IIncrementalGenerator类型的源代码生成器测试源码变更的响应逻辑?

我之前在测试IIncrementalGenerator的增量缓存逻辑时,也踩过几乎一模一样的坑!你想验证源码变更后生成器内部阶段的状态(Modified/New)是否符合预期的思路完全没问题,问题出在多轮编译时的Driver调用方式和Compilation的传递逻辑上。

首先先明确你遇到的报错根源:

报错信息:Unexpected value '(Modified, Added)' of type 'System.ValueTuple'2[[Microsoft.CodeAnalysis.EntryState, Microsoft.CodeAnalysis.EntryState]]

这个异常本质是因为你启用了增量生成器的状态追踪(driverOptions的第二个参数trackIncrementalGeneratorSteps设为true)后,直接传入完全独立的全新Compilation实例给后续的RunGenerators调用,生成器的内部状态追踪逻辑无法正确映射前后编译的条目(比如语法树、符号)的关联关系,导致遇到了未预期的状态组合。

你提到关闭状态追踪就不报错但没法验证状态,这是正常的——关闭状态追踪后,生成器不会记录各个阶段的EntryState,自然不会触发状态组合的异常,但也失去了我们要测试的增量状态验证能力,这只是临时规避,不是解决办法。

修正后的测试方案

咱们换个思路:不要传入完全独立的Compilation实例,而是让每一轮的编译基于上一轮的结果修改而来,同时调整Driver的调用方式,让它能正确维护增量状态。

1. 重写RunGeneratorIterations方法

修正后的方法会强制每一轮编译都基于上一轮的上下文修改,并且全程用RunGeneratorsAndUpdateCompilation衔接,确保Driver能正确追踪状态:

public static IReadOnlyList<GeneratorDriverRunResult> RunGeneratorIterations<T>(T generator, params Func<Compilation, Compilation>[] compilationModifiers)
    where T : IIncrementalGenerator
{
    // 初始化Driver配置,保留状态追踪
    var driverOptions = new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, true);
    var wrappedGenerator = generator.AsSourceGenerator();
    var driver = CSharpGeneratorDriver.Create(new[] { wrappedGenerator }, driverOptions: driverOptions);
    
    var runResults = new List<GeneratorDriverRunResult>();
    Compilation currentCompilation = null;

    foreach (var modifier in compilationModifiers)
    {
        // 基于上一轮编译(或初始创建)得到当前轮的编译
        currentCompilation = modifier(currentCompilation);
        // 用RunGeneratorsAndUpdateCompilation更新编译并运行生成器,维护增量状态
        driver = driver.RunGeneratorsAndUpdateCompilation(currentCompilation, out var updatedCompilation, out _);
        runResults.Add(driver.GetRunResult());
        // 更新当前编译为最新版本
        currentCompilation = updatedCompilation;
    }

    Assert.AreEqual(compilationModifiers.Length, runResults.Count);
    return runResults;
}

2. 编写编译修改逻辑

现在你需要把原来的独立Compilation改成“基于上一轮修改”的逻辑,比如用两个辅助方法创建初始编译和替换语法树:

// 创建初始编译的辅助方法
private static Compilation CreateCompilation(string sourceCode)
{
    var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
    var references = new[]
    {
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location)
    };
    return CSharpCompilation.Create(
        "TestCompilation",
        new[] { syntaxTree },
        references,
        new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}

// 替换编译中指定语法树的辅助方法
private static Compilation UpdateCompilationSyntaxTree(Compilation prevCompilation, string oldSource, string newSource)
{
    var oldTree = prevCompilation.SyntaxTrees.First(t => t.GetText().ToString().Trim() == oldSource.Trim());
    var newTree = CSharpSyntaxTree.ParseText(newSource);
    return prevCompilation.ReplaceSyntaxTree(oldTree, newTree);
}

3. 实际测试调用

现在你可以这样编写测试用例,验证源码变更后的生成器状态:

// 定义初始和修改后的源码
var initialCode = @"public class TestClass { }";
var modifiedCode = @"public class TestClass { public int Id { get; set; } }";

// 构建编译迭代:先创建初始编译,再基于初始编译修改
var compilationIterations = new Func<Compilation, Compilation>[]
{
    // 第一轮:创建初始编译
    _ => CreateCompilation(initialCode),
    // 第二轮:替换初始编译的语法树
    prev => UpdateCompilationSyntaxTree(prev, initialCode, modifiedCode)
};

// 运行多轮生成器测试
var runResults = RunGeneratorIterations(yourIncrementalGenerator, compilationIterations);

// 验证第二轮的生成器阶段状态
var secondRun = runResults[1];
var generatorResult = secondRun.Results.First(r => r.Generator is YourGeneratorType);
// 找到你要验证的追踪阶段(需要你的生成器在定义IncrementalValuesProvider时设置了Name)
var trackedStage = generatorResult.TrackedOutputs.First(o => o.Name == "YourTrackedStageName");

// 断言状态为Modified,符合预期
Assert.AreEqual(EntryState.Modified, trackedStage.State);

关键注意事项

  • 确保你的增量生成器在定义IncrementalValuesProvider时,用WithTrackingName方法设置了阶段名称,这样才能在TrackedOutputs中找到对应的状态:
    // 示例:给你的增量阶段设置追踪名称
    provider = provider.WithTrackingName("YourTrackedStageName");
    
  • 不要创建完全独立的Compilation实例,必须基于上一轮的编译修改,这样生成器的增量逻辑才能正确识别哪些内容变更了。

备注:内容来源于stack exchange,提问作者user3797758

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.20 07:53:14