Testing the source code generator

Last year, a .Net update brought a feature: source code generators. I wondered what it was and I decided to write a mock generator so that it took an interface or an abstract class as input and produced mocks that can be used in testing with aot compilers. Almost immediately the question arose: how to test the generator itself? At that time, the official cookbook did not contain a recipe for how to do it right. Later this problem was fixed, but you might be interested to see how tests work in my project.





The cookbook has a simple recipe for exactly how to start the generator. You can play it against a piece of source code and make sure the generation completes without errors. And then the question arises: how to make sure that the code is created correctly and works correctly? You can of course take some reference code, parse it using CSharpSyntaxTree.ParseText and then compare it using IsEquivalentTo . However, the code tends to change, and a comparison with the code functionally identical, but different in comments and whitespace characters, gave me a negative result. Let's go the long way:





  • Let's create a compilation;





  • Let's create and run a generator;





  • Let's build the library and load it into the current process;





  • Let's find the resulting code there and execute it.





Compilation

The compiler is launched using the CSharpCompilation.Create function . Here you can add code and include links to libraries. The source code is prepared using CSharpSyntaxTree.ParseText , and the MetadataReference.CreateFromFile libraries (there are options for streams and arrays). How to get the path? In most cases, everything is simple:





typeof(UnresolvedType).Assembly.Location
      
      



However, in some cases the type is in the reference assembly, then this works:





Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
      
      



What compilation creation might look like
protected static CSharpCompilation CreateCompilation(string source, string compilationName)
    => CSharpCompilation.Create(compilationName,
        syntaxTrees: new[]
        {
            CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview))
        },
        references: new[]
        {
            MetadataReference.CreateFromFile(Assembly.GetCallingAssembly().Location),
            MetadataReference.CreateFromFile(typeof(string).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(LightMock.InvocationInfo).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(IMock<>).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Xunit.Assert).Assembly.Location),
            MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location),
            MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Runtime")).Location),
            MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("netstandard")).Location),
        },
        options: new CSharpCompilationOptions(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary));
      
      



Link to code





Starting the generator and creating the assembly

: CSharpGeneratorDriver.Create, , (aka AdditionalFiles csproj). CSharpGeneratorDriver.RunGeneratorsAndUpdateCompilation , . , ITestOutputHelper Xunit . , Output .





protected (ImmutableArray<Diagnostic> diagnostics, bool success, byte[] assembly) DoCompile(string source, string compilationName)
{
    var compilation = CreateCompilation(source, compilationName);
    var driver = CSharpGeneratorDriver.Create(
        ImmutableArray.Create(new LightMockGenerator()),
        Enumerable.Empty<AdditionalText>(),
        (CSharpParseOptions)compilation.SyntaxTrees.First().Options);

    driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var diagnostics);
    var ms = new MemoryStream();
    var result = updatedCompilation.Emit(ms);
    foreach (var i in result.Diagnostics)
        testOutputHelper.WriteLine(i.ToString());
    return (diagnostics, result.Success, ms.ToArray());
}
      
      







.Net Core AssemblyLoadContext. . Assembly, . : . . dynamic - . , , . , , .





public interface ITestScript<T>
    where T : class
{
    IMock<T> Context { get; } //    
    T MockObject { get; } //    

    int DoRun(); //    ,
    //    
}

      
      







using System;
using Xunit;

namespace LightMock.Generator.Tests.Mock
{
    public class AbstractClassWithBasicMethods : ITestScript<AAbstractClassWithBasicMethods>
    {
    		//   Mock<T>  
        private readonly Mock<AAbstractClassWithBasicMethods> mock;

        public AbstractClassWithBasicMethods()
            => mock = new Mock<AAbstractClassWithBasicMethods>();

        public IMock<AAbstractClassWithBasicMethods> Context => mock;

        public AAbstractClassWithBasicMethods MockObject => mock.Object;

        public int DoRun()
        {
        		//  Protected()  
            mock.Protected().Arrange(f => f.ProtectedGetSomething()).Returns(1234);
            Assert.Equal(expected: 1234, mock.Object.InvokeProtectedGetSomething());

            mock.Object.InvokeProtectedDoSomething(5678);
            mock.Protected().Assert(f => f.ProtectedDoSomething(5678));

            return 42;
        }
    }
}

      
      







, , : AnalyzerConfigOptionsProvider AnalyzerConfigOptions.





sealed class MockAnalyzerConfigOptions : AnalyzerConfigOptions
{
    public static MockAnalyzerConfigOptions Empty { get; }
        = new MockAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty);

    private readonly ImmutableDictionary<string, string> backing;

    public MockAnalyzerConfigOptions(ImmutableDictionary<string, string> backing)
        => this.backing = backing;

    public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
        => backing.TryGetValue(key, out value);
}

sealed class MockAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
    private readonly ImmutableDictionary<object, AnalyzerConfigOptions> otherOptions;

    public MockAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions)
        : this(globalOptions, ImmutableDictionary<object, AnalyzerConfigOptions>.Empty)
    { }

    public MockAnalyzerConfigOptionsProvider(AnalyzerConfigOptions globalOptions,
        ImmutableDictionary<object, AnalyzerConfigOptions> otherOptions)
    {
        GlobalOptions = globalOptions;
        this.otherOptions = otherOptions;
    }

    public static MockAnalyzerConfigOptionsProvider Empty { get; }
        = new MockAnalyzerConfigOptionsProvider(
            MockAnalyzerConfigOptions.Empty,
            ImmutableDictionary<object, AnalyzerConfigOptions>.Empty);

    public override AnalyzerConfigOptions GlobalOptions { get; }

    public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
        => GetOptionsPrivate(tree);

    public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
        => GetOptionsPrivate(textFile);

    AnalyzerConfigOptions GetOptionsPrivate(object o)
        => otherOptions.TryGetValue(o, out var options) ? options : MockAnalyzerConfigOptions.Empty;
}

      
      



: , .





CSharpGeneratorDriver.Create optionsProvider, . , . , .





.





- . , , . . .





, . .





, . , , , , ITestOutputHelper Xunit.





, CancellationToken. .





The mock generator is here . This is a beta version and is not recommended for use in production.








All Articles