Русский
Русский
English
Статистика
Реклама

Тестирование генератора исходного кода

В прошлом году обновление .Net принесло фичу: генераторы исходного кода. Мне стало интересно что это такое и я решил написать генератор моков, чтоб на вход брал интерфейс или абстрактный класс и выдавал моки, которые можно использовать в тестировании с aot компиляторами. Почти сразу встал вопрос: а как тестировать сам генератор? На тот момент официальная поваренная книга не содержала рецепт как это сделать правильно. Позже эту проблему исправили, но, возможно, вам будет интересно посмотреть как работают тесты в моём проекте.

В поваренной книге есть простой рецепт как именно запускать генератор. Вы можете натравить его на кусок исходного кода и убедиться, что генерация завершается без ошибок. И тут возникает вопрос: как убедиться что код создан правильно и правильно работает? Можно конечно взять какой-то эталонный код, разобрать его с помощью CSharpSyntaxTree.ParseText и потом сравнить через IsEquivalentTo. Однако код имеет свойство меняться, да и сравнение с кодом функционально идентичным, но отличающийся комментариями и пробельными символами давало у меня отрицательный результат. Что-же пойдём длинным путём:

  • Создадим компиляцию;

  • Создадим и запустим генератор;

  • Выполним сборку библиотеки и загрузим её в текущий процесс;

  • Найдём там полученный код и выполним его.

Компиляция

Запуск компилятора производится с помощью функции CSharpCompilation.Create. Здесь можно добавить код и подключить ссылки на библиотеки. Исходный код подготавливается с помощью CSharpSyntaxTree.ParseText, а библиотеки MetadataReference.CreateFromFile (есть варианты для потоков и массивов). Как добыть путь? В большинстве случаев всё просто:

typeof(UnresolvedType).Assembly.Location

Однако в некоторых случаях тип находится в базовой (reference) сборке, тогда работает вот это:

Assembly.Load(new AssemblyName("System.Linq.Expressions")).LocationAssembly.Load(new AssemblyName("System.Runtime")).LocationAssembly.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
К списку статей
Опубликовано: 09.02.2021 00:17:30
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Net

C

Генератор

Исходный код

Roslyn

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru