[.NET, C#] Тестирование генератора исходного кода
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В прошлом году обновление .Net принесло фичу: генераторы исходного кода. Мне стало интересно что это такое и я решил написать генератор моков, чтоб на вход брал интерфейс или абстрактный класс и выдавал моки, которые можно использовать в тестировании с aot компиляторами. Почти сразу встал вопрос: а как тестировать сам генератор? На тот момент официальная поваренная книга не содержала рецепт как это сделать правильно. Позже эту проблему исправили, но, возможно, вам будет интересно посмотреть как работают тесты в моём проекте.В поваренной книге есть простой рецепт как именно запускать генератор. Вы можете натравить его на кусок исходного кода и убедиться, что генерация завершается без ошибок. И тут возникает вопрос: как убедиться что код создан правильно и правильно работает? Можно конечно взять какой-то эталонный код, разобрать его с помощью CSharpSyntaxTree.ParseText и потом сравнить через IsEquivalentTo. Однако код имеет свойство меняться, да и сравнение с кодом функционально идентичным, но отличающийся комментариями и пробельными символами давало у меня отрицательный результат. Что-же пойдём длинным путём:
- Создадим компиляцию;
- Создадим и запустим генератор;
- Выполним сборку библиотеки и загрузим её в текущий процесс;
- Найдём там полученный код и выполним его.
КомпиляцияЗапуск компилятора производится с помощью функции CSharpCompilation.Create. Здесь можно добавить код и подключить ссылки на библиотеки. Исходный код подготавливается с помощью CSharpSyntaxTree.ParseText, а библиотеки MetadataReference.CreateFromFile (есть варианты для потоков и массивов). Как добыть путь? В большинстве случаев всё просто:
typeof(UnresolvedType).Assembly.Location
Однако в некоторых случаях тип находится в базовой (reference) сборке, тогда работает вот это:
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location
Assembly.Load(new AssemblyName("netstandard")).Location
Как может выглядеть создание компиляции
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));
Ссылка на кодЗапуск генератора и создание сборкиТут всё просто: дёргается 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. Так студия меньше фризит.Генератор моков тут. Это бета версия и к использованию в проде не рекомендуется.
===========
Источник:
habr.com
===========
Похожие новости:
- [.NET, C#, F#] Букварь по F# для любопытствующих C#-разработчиков (перевод)
- [Программирование, .NET, C#] Шпион под прикрытием: проверяем исходный код ILSpy с помощью PVS-Studio
- [Разработка игр, C#, Unity, Дизайн игр] Жидкий персонаж на Unity 3D
- [Программирование, .NET, C#] A Spy Undercover: PVS-Studio to Check ILSpy Source Code
- [.NET, C++] Разбор протокола World of Tanks
- [C#, VueJS] Оптимизация страницы с использованием RxJS и Expression Tree
- [.NET, Разработка игр, C#] Бэк-офис для игр или «результат борьбы с пенсионной скукой»
- [Java, C#, Учебный процесс в IT, Карьера в IT-индустрии] Как стать разработчиком Java и С#: открываем онлайн-практикум с поддержкой менторов
- [Программирование, .NET, C#] C# программист, испытай себя — найди ошибку
- [Программирование, .NET, C#] C# Programmer, It's Time to Test Yourself and Find Error
Теги для поиска: #_.net, #_c#, #_generator (генератор), #_ishodnyj_kod (исходный код), #_roslyn, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 07:07
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В прошлом году обновление .Net принесло фичу: генераторы исходного кода. Мне стало интересно что это такое и я решил написать генератор моков, чтоб на вход брал интерфейс или абстрактный класс и выдавал моки, которые можно использовать в тестировании с aot компиляторами. Почти сразу встал вопрос: а как тестировать сам генератор? На тот момент официальная поваренная книга не содержала рецепт как это сделать правильно. Позже эту проблему исправили, но, возможно, вам будет интересно посмотреть как работают тесты в моём проекте.В поваренной книге есть простой рецепт как именно запускать генератор. Вы можете натравить его на кусок исходного кода и убедиться, что генерация завершается без ошибок. И тут возникает вопрос: как убедиться что код создан правильно и правильно работает? Можно конечно взять какой-то эталонный код, разобрать его с помощью CSharpSyntaxTree.ParseText и потом сравнить через IsEquivalentTo. Однако код имеет свойство меняться, да и сравнение с кодом функционально идентичным, но отличающийся комментариями и пробельными символами давало у меня отрицательный результат. Что-же пойдём длинным путём:
typeof(UnresolvedType).Assembly.Location
Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location
Assembly.Load(new AssemblyName("System.Runtime")).Location Assembly.Load(new AssemblyName("netstandard")).Location 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)); 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()); } 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; } } } 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; } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 07:07
Часовой пояс: UTC + 5