Опыт создания инструмента мутационного тестирования для Erlang


Несколько недель назад, я услышал о мутационном тестировании для Clojure, это способ проверки качества тестов, при котором в исходный код вносятся небольшие изменения и тесты либо замечают это, либо нет. К примеру, если в программе использовалось условие «a > 1», а замена на «a < 1» никак не меняет результатов тестирования значит тесты могли бы быть лучше.

Я подумал, что такой инструмент будет несложно написать для 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];

Парсер спотыкался на всех конструкциях, которые я в нём не описал, то есть на всех конструкциях, которые я не помнил или не знал, что оказалось довольно интересным опытом.

После того как парсер был готов описание преобразований (мутаций) уже было совсем простым делом.

Результат


  1. Куча веселья.
  2. Парсер Erlang на Python под лицензией BSD.
  3. Библиотека мутаций, которая меняет код без изменения форматирования и удаления комментариев, как следствие она порождает диф, который можно наложить на исходный код и получить мутированный.

Код библиотеки:
github.com/parsifal-47/muterl

Комментарии (0)

© Habrahabr.ru