Menguji generator kode sumber

Tahun lalu, pembaruan .Net menghadirkan fitur: generator kode sumber. Saya bertanya-tanya apa itu dan saya memutuskan untuk menulis generator tiruan sehingga dibutuhkan antarmuka atau kelas abstrak sebagai masukan dan menghasilkan tiruan yang dapat digunakan dalam pengujian dengan kompiler aot. Hampir segera muncul pertanyaan: bagaimana cara menguji generator itu sendiri? Saat itu, buku resep resmi tidak memuat resep bagaimana cara melakukannya dengan benar. Kemudian masalah ini diperbaiki, tetapi Anda mungkin tertarik untuk melihat bagaimana tes bekerja dalam proyek saya.





Buku masak memiliki resep sederhana tentang bagaimana cara menyalakan generator. Anda dapat memainkannya melawan sepotong kode sumber dan memastikan pembuatannya selesai tanpa kesalahan. Dan kemudian muncul pertanyaan: bagaimana cara memastikan bahwa kode dibuat dengan benar dan berfungsi dengan benar? Anda tentu saja dapat mengambil beberapa kode referensi, menguraikannya menggunakan CSharpSyntaxTree.ParseText , lalu membandingkannya menggunakan IsEquivalentTo . Namun, kodenya cenderung berubah, dan perbandingan dengan kode secara fungsional identik, tetapi berbeda dalam komentar dan karakter spasi, memberi saya hasil negatif. Ayo pergi jauh:





  • Mari buat kompilasi;





  • Mari membuat dan menjalankan generator;





  • Mari membangun perpustakaan dan memuatnya ke dalam proses saat ini;





  • Mari temukan kode yang dihasilkan di sana dan jalankan.





Kompilasi

Kompilator diluncurkan menggunakan fungsi CSharpCompilation.Create . Di sini Anda dapat menambahkan kode dan menyertakan tautan ke perpustakaan. Kode sumber disiapkan menggunakan CSharpSyntaxTree.ParseText , dan pustaka MetadataReference.CreateFromFile (ada opsi untuk aliran dan larik). Bagaimana cara mendapatkan jalannya? Dalam kebanyakan kasus, semuanya sederhana:





typeof(UnresolvedType).Assembly.Location
      
      



Namun, dalam beberapa kasus jenisnya ada di rakitan referensi, maka ini berfungsi:





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



Seperti apa pembuatan kompilasi itu
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));
      
      



Tautan ke kode





Memulai generator dan membuat rakitan

: 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. .





Generator tiruan ada di sini . Ini adalah versi beta dan tidak disarankan untuk digunakan dalam produksi.








All Articles