El a帽o pasado, una actualizaci贸n de .Net trajo una caracter铆stica: generadores de c贸digo fuente. Me pregunt茅 qu茅 era y decid铆 escribir un generador simulado para que tomara una interfaz o una clase abstracta como entrada y produjera simulacros que se pueden usar en pruebas con compiladores aot. Casi de inmediato surgi贸 la pregunta: 驴c贸mo probar el generador en s铆? En ese momento, el libro de cocina oficial no conten铆a una receta sobre c贸mo hacerlo bien. M谩s tarde, este problema se solucion贸, pero es posible que le interese ver c贸mo funcionan las pruebas en mi proyecto.
El libro de cocina tiene una receta simple sobre c贸mo encender exactamente el generador. Puede compararlo con un fragmento de c贸digo fuente y asegurarse de que la generaci贸n se complete sin errores. Y luego surge la pregunta: 驴c贸mo asegurarse de que el c贸digo se crea correctamente y funciona correctamente? Por supuesto, puede tomar alg煤n c贸digo de referencia, analizarlo usando CSharpSyntaxTree.ParseText y luego compararlo usando IsEquivalentTo . Sin embargo, el c贸digo tiende a cambiar, y la comparaci贸n con el c贸digo funcionalmente id茅ntico, pero diferente en los comentarios y los espacios en blanco, me dio un resultado negativo. Vayamos por el camino m谩s largo:
Creemos una compilaci贸n;
Creemos y ejecutemos un generador;
Construyamos la biblioteca y carg谩mosla en el proceso actual;
Busquemos el c贸digo resultante all铆 y ejec煤telo.
Compilacion
El compilador se inicia mediante la funci贸n CSharpCompilation.Create . Aqu铆 puede agregar c贸digo e incluir enlaces a bibliotecas. El c贸digo fuente se prepara utilizando CSharpSyntaxTree.ParseText y las bibliotecas MetadataReference.CreateFromFile (hay opciones para secuencias y matrices). 驴C贸mo llegar al camino? En la mayor铆a de los casos, todo es sencillo:
typeof(UnresolvedType).Assembly.Location
Sin embargo, en algunos casos, el tipo est谩 en el ensamblado de referencia, entonces esto funciona:
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
C贸mo se ver铆a la creaci贸n de la compilaci贸n
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));
Arrancando el generador y creando el ensamblaje
: 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. .
El generador simulado est谩 aqu铆 . Esta es una versi贸n beta y no se recomienda su uso en producci贸n.