L'année dernière, une mise à jour .Net a apporté une fonctionnalité: les générateurs de code source. Je me suis demandé ce que c'était et j'ai décidé d'écrire un générateur de simulation pour qu'il prenne une interface ou une classe abstraite comme entrée et produise des simulations qui peuvent être utilisées dans les tests avec des compilateurs aot. Presque immédiatement, la question s'est posée: comment tester le générateur lui-même? À cette époque, le livre de cuisine officiel ne contenait pas de recette pour bien faire les choses. Plus tard, ce problème a été résolu, mais vous pourriez être intéressé de voir comment les tests fonctionnent dans mon projet.
Le livre de cuisine contient une recette simple expliquant exactement comment démarrer le générateur. Vous pouvez le jouer contre un morceau de code source et vous assurer que la génération se termine sans erreur. Et puis la question se pose: comment s'assurer que le code est créé correctement et fonctionne correctement? Vous pouvez bien sûr prendre du code de référence, l'analyser à l'aide de CSharpSyntaxTree.ParseText puis le comparer à l'aide de IsEquivalentTo . Cependant, le code a tendance à changer, et la comparaison avec le code fonctionnellement identique, mais différant par les commentaires et les espaces, m'a donné un résultat négatif. Allons le long du chemin:
Créons une compilation;
Créons et exécutons un générateur;
Construisons la bibliothèque et chargeons-la dans le processus actuel;
Trouvons là le code résultant et exécutons-le.
Compilation
Le compilateur est lancé à l'aide de la fonction CSharpCompilation.Create . Ici, vous pouvez ajouter du code et inclure des liens vers des bibliothèques. Le code source est préparé à l'aide de CSharpSyntaxTree.ParseText et des bibliothèques MetadataReference.CreateFromFile (il existe des options pour les flux et les tableaux). Comment obtenir le chemin? Dans la plupart des cas, tout est simple:
typeof(UnresolvedType).Assembly.Location
Cependant, dans certains cas, le type est dans l'assemblage de référence, cela fonctionne:
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
À quoi pourrait ressembler la création d'une compilation
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));
Démarrage du générateur et création de l'assemblage
: 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 - . , , . , , .
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. .
Le générateur simulé est ici . Ceci est une version bêta et n'est pas recommandé pour une utilisation en production.