Опыт создания инструмента мутационного тестирования для Erlang
Я подумал, что такой инструмент будет несложно написать для Erlang, потому что язык предоставляет богатый спектр функций обработки внутренних представлений, генерируемых компилятором. Но всё оказалось не так просто. Под катом я описал проблемы и решение, к которому я в итоге пришёл.
Главное, чего я пытался достичь это удобство использования инструмента. То есть, он не должен требовать сложной настройки или установки Emacs. Скачал, запустил и обрадовался.
Предполагался следующий сценарий: в код вносится мутация и запускаются тесты. Если тесты не распознали мутацию, то нужно понятным образом сохранить эту информацию для отчёта. Начались сложности.
Сложность №1: запуск тестов
Самый универсальный способ запуска тестов это shell команда, а в Erlang как назло есть сложности с получением кода возврата, эта проблема описана например здесь:
erlang.org/pipermail/erlang-questions/2008-October/039176.html
То есть можно, если подключить внешнюю библиотеку (https://github.com/saleyn/erlexec), которая требует gcc хитрой версии и не тестируется на osx, а на линукс в некоторых случаях требует магии, которая описана в трэвис-файле.
github.com/saleyn/erlexec/blob/master/.travis.yml#L23
Ладно, пусть будет внешняя библиотека, либо какая-то дополнительная утилита, которая запускает сначала мутатор, написанный на эрланг, потом сами тесты и затем генерирует отчёт, если это требуется.
Сложность №2: pretty printer
Я начал писать мутатор, для того чтобы не быть слишком абстрактным, я взял jsx, в этом пакете есть большое количество unit-тестов, вроде бы всё, что нужно.
Я написал следующий код на Erlang:
{ok, Forms} = epp:parse_file("src/jsx_decoder.erl", []),
io:format("~s", [erl_prettypr:format(erl_syntax:form_list(Forms))]).
меня немного смутило то что конструкции:
-compile({inline, [handle_event/3]}).
превратились в
-compile({inline, [{handle_event, 3}]}).
оказывается парсер понимает и так и эдак.
Но больше всего эстетической боли вызвало то что весь код был выведен заново, без комментариев и сопоставить его с начальной версией стало довольно сложно.
Я решил попробовать «поймать» разобранный код немного раньше, чтобы как-то сохранить комментарии, но оказалось что они пропадают сразу после токенизации:
1> erl_scan:string("a. % this is my atom").
{ok,[{atom,1,a},{dot,1}],1}
на сколько я понимаю, это довольно низкоуровневая функция, которая выполняется до макроподстановки:
erl_scan:string("-define(A).").
{ok,[{'-',1},
{atom,1,define},
{'(',1},
{var,1,'A'},
{')',1},
{dot,1}],
1}
Сложность №3: макросы
если посмотреть на выхлоп pretty-printer-а то можно заметить их отсутствие. В случае с jsx, при обработке файла съедались все его тесты, потому что они находились внутри »-ifdef (TEST).».
Это конечно решаемая проблема, достаточно было передать правильный набор определений в epp: parse_file, но раскрытие макросов делало код ещё менее узнаваемым чем просто после pretty print.
Решение
После пары дней колебаний я взялся и написал парсер Erlang-а на питоне. Для начала я открыл оригинальную грамматику эрланга:
github.com/erlang/otp/blob/master/lib/stdlib/src/erl_parse.yrl
и решил не переносить её один-в-один, поскольку это не показалось простым занятием, вместо этого я написал грамматику эрланга по памяти, а затем, прогнав парсер по нескольким известным проектам, отладил её. Идентичности построения AST не требовалось, это упрощало задачу. В процессе отладки, я нашёл несколько забавных мест в этих репозиториях, вот например в hackney:
github.com/benoitc/hackney/blob/master/src/hackney.erl#L1025
-define(METHOD_TPL(Method),
Method(URL) ->
hackney:request(Method, URL)).
-include("hackney_methods.hrl").
-define(METHOD_TPL(Method),
Method(URL, Headers) ->
hackney:request(Method, URL, Headers)).
-include("hackney_methods.hrl")
... и так ещё несколько раз
или вот например у Вирдинга в большинстве случаев бессмысленное использование begin и end:
github.com/rvirding/lfe/blob/develop/src/lfe_parse.erl#L238
reduce(0, [__1|__Vs]) -> [ begin __1 end | __Vs];
reduce(1, [__1|__Vs]) -> [ begin value (__1) end | __Vs];
reduce(2, [__1|__Vs]) -> [ begin value (__1) end | __Vs];
reduce(3, [__1|__Vs]) -> [ begin value (__1) end | __Vs];
reduce(4, [__1|__Vs]) -> [ begin value (__1) end | __Vs];
reduce(5, [__1|__Vs]) -> [ begin make_fun (value (__1)) end | __Vs];
Парсер спотыкался на всех конструкциях, которые я в нём не описал, то есть на всех конструкциях, которые я не помнил или не знал, что оказалось довольно интересным опытом.
После того как парсер был готов описание преобразований (мутаций) уже было совсем простым делом.
Результат
- Куча веселья.
- Парсер Erlang на Python под лицензией BSD.
- Библиотека мутаций, которая меняет код без изменения форматирования и удаления комментариев, как следствие она порождает диф, который можно наложить на исходный код и получить мутированный.
Код библиотеки:
github.com/parsifal-47/muterl