November 27, 2021

Измерение уровня покрытия кода тестами в Raku | Code coverage level measurement in Raku

Легко заметить одну особенность в культуре написания модулей на Raku и Perl — почти всегда присутствует папка t с авто тестами. Даже в маленьких проектах. Конечно, количество и качество тестов может варьироваться, но чаще всего они есть.

Когда мы говорим об авто тестировании, возникает желание как-то оценить «качество» написанных тестов. Одним из критериев может являться процент исполненных строк (веток) кода во время успешного прохождения тестов. На данный момент для Raku это можно сделать двумя способами.

Первый — с помощью CommaIDE (https://commaide.com), IDE для разработки на языке Raku. CommaIDE это отличная вещь, особенно, после последних релизов. Я настоятельно рекомендую её попробовать. В платной версии IDE есть возможность запустить тесты, и посчитать процент покрытия кода. Кроме того, для наглядности, код раскрасится в соответствующие цвета.

Второй — использовать модуль App::RaCoCo (Raku Code Coverage). Он поставляет консольное приложение racoco, которое запускает тесты, и подсчитывает процент покрытия. Сегодня хочется рассказать об этом модуле подробнее.

Как это работает в теории

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

NB: существуют несколько бекендов для компилятора Raku (например, jvm, js, moarvm). На данный момент предоставить всю необходимую информацию может только MoarVM. Кроме того, более информативно считать не выполненные строки кода, а ветви. Например, в одной строке
say 'second' if $first; две ветви. К сожалению, сейчас даже MoarVM не предоставляет информации о выполненных ветвях. По-этому, далее будет говориться только о выполненных строках.

Как это работает на практике

Чтобы получить список потенциально исполняемых строк кода некоторого файла, нужно:

  1. получить байт-код этого файла. Его можно или найти в папке lib/.precomp, или скомпилировать самостоятельно. Например,
    raku -Ilib --target=mbc --output=result lib/source-file.rakumod;
  2. снять дамп с этого файла командой moar --dump result. Вхождения, начинающиеся со слова annotation, заканчиваются номером строки, которая может быть исполнена в соответствующем модуле. Например:
[...]
00013      bindlex            lex_Frame_9_self_obj, loc_19_obj
00014      param_sn           loc_4_obj
     annotation: lib/source-file.rakumod (MyModuleName):9
00015      getcode            loc_1_obj, Frame_10
00016      getcode            loc_2_obj, Frame_11
[...]

Чтобы получить данные о том, какие строки выполнились, нужно:

  1. положить в переменные окружения специальный флаг MVM_COVERAGE_LOG. Его значением должен являться путь к файлу с будущими логами исполнения кода;
  2. запустить тесты;
  3. разобрать файл с логами. Нас интересуют только строки с файлами из тестируемой библиотеки. Например:
HIT  src/vm/moar/ModuleLoader.nqp  133
HIT  src/vm/moar/ModuleLoader.nqp  5
HIT  lib/source-file.rakumod (MyModuleName)  1
HIT  lib/source-file.rakumod (MyModuleName)  9
HIT  gen/moar/World.nqp  5240
HIT  gen/moar/World.nqp  5240

Имея эти данные и немного знаний об арифметике можно посчитать результат.

Как это использовать

Рассмотрим несколько типичных вариантов использования racoco.

MyModyle> racoco
t/00-simple.t ................... ok 
t/01-difficult.t ................ ok  
All tests successful.
Files=2, Tests=54,  3 wallclock secs
Result: PASS                        # prove6 result 
Coverage: 70.6%                     # code coverage percentage

MyModule> racoco --exec='prove --exec=raku'
t/00-simple.t ................... ok 
t/01-difficult.t ................ ok
All tests successful.
Files=2, Tests=54, 3 wallclock secs
Result: PASS                        # prove result
Coverage: 70.8%

MyModule> racoco --exec='prove6 -l' # this is for who do not write
                                    # 「use lib 'lib';」 in tests
[...]

MyModule> racoco --silent
Coverage: 70.8%          # just what it was all about

MyModule> racoco --html  # HTML with statistics and colored code
Visualisation: file://.racoco/report.html
Coverage: 70.8%
Список файлов модуля со статистикой
MyModule> racoco --/exec --html --color-blind
            # don't run tests, use the latest data
            # and color code for colorblind people
Раскрашеный код для дальтоников
MyModule> racoco --fail-level=93   # the launch should fail
[...]                              # in case of low coverage
Coverage: 70.8%      # exit code: 23 (93 - 70 = 23)

MyModule> racoco --silent
Coverage: 70.8%
MyModule> racoco --silent --append --exec='prove6 xt'
Coverage: 95.2%  # combined coverage of both launches
                 # similarly --exec='prove6 t xt'

/root/  > racoco --lib='/home/user/raku/MyModule/lib' \
                 --exec='prove6 /home/user/raku/MyModule/t'
                 # launch not from the module folder

MyModule> racoco --raku-bin-dir='/home/user/hack-rakudo/install/bin'
               # explicit indication of the path to raku and moar

Неочевидный момент

racoco может выбросить сообщение об ошибке про неочевидное содержимое папки .precomp. Это происходит из-за того, что папка содержит подпапки для каждой использованной версии компилятора. В общем случае, пользователь может выбрать любую версию компилятора для запуска тестов. При этом, если нет необходимости, скомпилированные ранее файлы могут быть не обновлены. Таким образом, racoco не может угадать какую папку анализировать. Для подобных случаев существует флаг --fix-compunit, который удалит всё содержимое папки .precomp самостоятельно (если вам дорого время компиляции, то можно уничтожить неактуальные версии скомпилированных файлов вручную).

Немного про CI

Невозможно говорить про авто тесты и покрытие кода и не заговорить про CI. В этом контексте измерение покрытия кода может использоваться для нескольких вещей:

  • Падение сборки в случае снижения покрытия ниже определённого уровня;
  • Падение сборки в случае снижения покрытия относительно предыдущих значений;
  • Простое измерение покрытия кода как характеристики.

Для примера, рассмотрим то, как можно добавить шилдик об уровне покрытия кода в README файл на GitHub.

Есть замечательная статья о том, как настроить автоматическую проверку кода тестами с помощью GitHub Actions для проектов на Raku. Первый YAML из этой статьи отлично работает. Нужно заменить последний шаг (Run Tests), на следующую последовательность:

  1. Установить App::RaCoCo. Этот модуль намеренно не содержит никаких зависимостей, чтобы его установка занимала как можно меньше времени;
    - name: Install App::RaCoCO run: zef install --/test App::RaCoCo
  2. Запустить racoco;
    - name: Run App::RaCoCo run: racoco
  3. Вытащить результат работы racoco из файла .racoco/report.txt. Нам нужна только первая строчка — процент покрытия. Убирается дробную часть и знак процента. Результат складываем во временную переменную окружения сборки COVERAGE;
    - name: Discover Code Coverage Level run: | coverage=head -1 .racoco/report.txt | sed 's/\.*//' echo "COVERAGE=$coverage%" >> $GITHUB_ENV shell: bash
  4. Для построения шилдика используем плагин schneegans/dynamic-badges-action;
    - name: Create Code Coverage Badge uses: schneegans/dynamic-badges-action@v1.1.0 with: auth: $\{\{ secrets.<YOUR_TOCKEN> }} gistID: <gist-ID> filename: <you-gist-file>.json label: Coverage message: $\{\{ env.COVERAGE }} 5. Добавляем строчку со ссылкой на шилдик в README.md.
    ![badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/<user>/<gist-id>/raw/<you-gist-file>.json)

Пример почти такого YAML и итоговый шилдик можно посмотреть в репозитории App::RaCoCo.

Наглядный уровень покрытия
English version