Главная » Хабрахабр » [Из песочницы] Об устройстве встроенной функциональности тестирования в Rust (перевод)

[Из песочницы] Об устройстве встроенной функциональности тестирования в Rust (перевод)

Привет, Хабр! Представляю вашему вниманию перевод записи "#[test] в 2018" в блоге Джона Реннера (John Renner), которую можно найти здесь.

Изучая кодовую базу компилятора, я изучил внутренности тестирования в Rust и понял, что было бы интересно этим поделиться.
В последнее время я работал над реализацией eRFC для пользовательских тестовых фреймворков для Rust.

Атрибут #[test]

На сегодняшний день программисты на Rust полагаются на встроенный атрибут #[test]. Все, что вам нужно сделать, это отметить функцию как тест и включить некоторые проверки:

#[test] fn my_test() { assert!(2+2 == 4);
}

Когда эта программа будет скомпилирована при помощи команд rustc --test или cargo test, она создаст исполняемый файл, который может запустить эту и любую другую тестовую функцию. Этот метод тестирования позволяет органично держать тесты рядом с кодом. Вы можете даже поместить тесты внутри приватных модулей:

mod my_priv_mod #[test] fn test_priv_func() { assert!(my_priv_func()); }
}

Таким образом, приватные сущности могут быть легко протестированы без использования каких-либо внешних инструментов тестирования. Это ключ к эргономике тестов в Rust. Семантически, однако, это довольно странно. Каким образом функция main вызывает эти тесты, если они не видны (прим. переводчика: напоминаю, приватные — объявленные без использования ключевого слова pub — модули защищены инкапсуляцией от доступа извне)? Что именно делает rustc --test?

По сути, это причудливый макрос, который переписывает наш крэйт в 3 этапа: #[test] реализован как синтаксическое преобразование внутри компиляторного крэйта libsyntax.

Шаг 1: Повторный экспорт

Как упоминалось ранее, тесты могут существовать внутри приватных модулей, поэтому нам нужен способ экспонировать их в функцию main без нарушения существующего кода. С этой целью libsyntax создаёт локальные модули, называемые __test_reexports, которые рекурсивно переэкспортируют тесты. Это раскрытие переводит приведенный выше пример в:

mod my_priv_mod { fn my_priv_func() -> bool {} fn test_priv_func() { assert!(my_priv_func()); } pub mod __test_reexports { pub use super::test_priv_func; }
}

Теперь наш тест доступен как my_priv_mod::__test_reexports::test_priv_func. Для вложенных модулей __test_reexports будет переэкспортировать модули, содержащие тесты, поэтому тест a::b::my_test становится a::__test_reexports::b::__test_reexports::my_test. Пока что этот процесс кажется довольно безопасным, но что произойдет, если есть существующий модуль __test_reexports? Ответ: ничего.

Имя каждой функции, переменной, модуля и т.д. Чтобы объяснить, нам нужно понять, как AST представляет идентификаторы. Компилятор хранит отдельную хеш-таблицу, которая позволяет нам восстанавливать удобочитаемое имя Символа при необходимости (например, при печати синтаксической ошибки). сохраняется не как строка, а скорее как непрозрачный Символ, который по существу является идентификационным номером для каждого идентификатора. Эта техника предотвращает коллизию имен во время генерации кода и является основой гигиены макросистемы Rust. Когда компилятор создает модуль __test_reexports, он генерирует новый Символ для идентификатора, поэтому, хотя генерируемый компилятором __test_reexports может быть одноименным с вашим самописным модулем, он не будет использовать его Символ.

Шаг 2: Генерация обвязки

Теперь, когда наши тесты доступны из корня нашего крэйта, нам нужно что-то сделать с ними. libsyntax генерирует такой модуль:

pub mod __test { extern crate test; const TESTS: &'static [self::test::TestDescAndFn] = &[/*...*/]; #[main] pub fn main() { self::test::test_static_main(TESTS); }
}

Хотя это преобразование простое, оно дает нам много информации о том, как тесты фактически выполняются. Тесты собираются в массив и передаются в запускатель тестов, называемый test_static_main. Мы вернемся к тому, что такое TestDescAndFn, но на данный момент ключевым выводом является то, что есть крэйт, называемый test, который является частью ядра Rust и реализует весь рантайм для тестирования. Интерфейс test нестабилен, поэтому единственным стабильным способом взаимодействия с ним является макрос #[test].

Шаг 3: Генерация тестового объекта

Если вы ранее писали тесты в Rust, вы можете быть знакомы с некоторыми необязательными атрибутами, доступными для тестовых функциях. Например, тест можно аннотировать с помощью #[should_panic], если мы ожидаем, что тест вызовет панику. Это выглядит примерно так:

#[test] #[should_panic] fn foo() { panic!("intentional");
}

Это означает, что наши тесты больше, чем простые функции, и имеют информацию о конфигурации. test кодирует эти данные конфигурации в структуру, называемую TestDesc. Для каждой тестовой функции в крэйте libsyntax будет анализировать её атрибуты и генерировать экземпляр TestDesc. Затем он объединяет TestDesc и тестовую функцию в логичную структуру TestDescAndFn, с которой работает test_static_main. Для данного теста сгенерированный экземпляр TestDescAndFn выглядит так:

self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("foo"), ignore: false, should_panic: self::test::ShouldPanic::Yes, allow_fail: false, }, testfn: self::test::StaticTestFn(|| self::test::assert_test_result(::crate::__test_reexports::foo())),
}

Как только мы построили массив этих тестовых объектов, они передаются запускателю тестов через обвязку, сгенерированную на шаге 2. Хотя этот шаг можно считать частью второго шага, я хочу обратить внимание на него как на отдельную концепцию, потому что это будет ключом к реализации пользовательских тестовых фреймворков, но это будет еще одно сообщение в блоге.

Послесловие: Методы исследования

Хотя я почерпнул много информации непосредственно из исходников компилятора, мне удалось выяснить, что есть очень простой способ увидеть, что делает компилятор. У ночной сборки компилятора есть нестабильный флаг, который называется unpretty, который вы можете использовать для распечатки исходного кода модуля после раскрытия макросов:

$ rustc my_mod.rs -Z unpretty=hir

Примечание переводчика

Интереса ради, проиллюстрирую код тестового примера после макрораскрытия:

Пользовательский исходный код:

#[test] fn my_test() { assert!(2+2 == 4);
} fn main() {}

Код после раскрытия макросов:

#[prelude_import] use std::prelude::v1::*;
#[macro_use] extern crate std as std;
#[test] pub fn my_test() { if !(2 + 2 == 4) { { ::rt::begin_panic("assertion failed: 2 + 2 == 4", &("test_test.rs", 3u32, 3u32)) } }; } #[allow(dead_code)] fn main() { } pub mod __test_reexports { pub use super::my_test; } pub mod __test { extern crate test; #[main] pub fn main() -> () { test::test_main_static(TESTS) } const TESTS: &'static [self::test::TestDescAndFn] = &[self::test::TestDescAndFn { desc: self::test::TestDesc { name: self::test::StaticTestName("my_test"), ignore: false, should_panic: self::test::ShouldPanic::No, allow_fail: false, }, testfn: self::test::StaticTestFn(::__test_reexports::my_test), }]; }


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Бесплатная трансляция DotNext 2018 Moscow

Меньше недели осталось до конференции DotNext 2018 Moscow: она пройдет в конгресс-парке гостиницы «Рэдиссон Ройал Москва» 22-23 ноября. Между докладами будут вестись интервью с ключевыми спикерами конференции. По традиции, прямо на YouTube будет открыта бесплатная онлайн-трансляция первого зала (ссылка спрятана ...

Прерывания от внешних устройств в системе x86. Эволюция контроллеров прерываний

В данной статье хотелось бы рассмотреть механизмы доставки прерываний от внешних устройств в системе x86 и попытаться ответить на вопросы: что такое PIC и для чего он нужен? что такое APIC и для чего он нужен? Для чего нужны LAPIC ...