Введение
Покрытие исходного кода тестами (Code Coverage) является одним из показателей, позволяющих оценить полноту тестов, а также достижимость участков программного кода. Сами по себе проценты кодового покрытия не говорят о качестве тестов и/или программного обеспечения, однако являются полезной метрикой при разработке и сопровождении ПО, так как наглядно показывают участки кода, которые вообще не были затронуты.

На первый взгляд сбор покрытия выглядит достаточно простой задачей: собрать проект с поддержкой coverage, запустить тесты, получить артефакты и на их основе сгенерировать отчёт. Но что же делать если проект - это целая ОС?
Собираем ОС с покрытием
Стоит отметить, что ОС “Нейтрино” является пакетной, т.е. её компоненты являются отдельными “пакетами” с бинарными компонентами, конфигами, скриптами, которые устанавливаются с помощью пакетного менеджера. Пакетный подход позволяет устанавливать, обновлять и удалять отдельные компоненты независимо друг от друга, а пакетный менеджер при этом отслеживает их версии и зависимости. Это упрощает сопровождение системы и позволяет вносить изменения только в необходимые компоненты, тогда как в ОС, поставляемой в виде единого образа, изменение одного компонента обычно требует формирования и установки нового образа всей системы.
Таким образом, при сборе кодового покрытия основной сущностью для нас является именно пакет.
Для сборки компонентов используется собственный компилятор на основе GCC. Для получения данных о покрытии компилятор должен определённым образом инструментировать итоговые бинарные компоненты. Для этого используются флаги компилятора -fprofile-arcs и -ftest-coverage.
При таком варианте компиляции создаются так называемые buildtime-артефакты - файлы .gcno, содержащие информацию о структуре исходного кода. Для каждой единицы компиляции создаётся свой артефакт.
Итак, мы смогли собрать всю ОС, инструментировав каждый её бинарный компонент, но проводить тестирование инструментированных вариантов бинарных компонентов на постоянной основе не получится, ведь они не будут отражать реальное положение дел, так как поставляемые заказчикам компоненты не имеют инструментации, а значит в некоторых сценариях потенциально могут иметь отличное поведение.
Было принято решение итеративно проводить отдельную верификационную сборку, при которой создаются инструментированные “клоны” реальных компонентов, а их buildtime-артефакты сохраняются на внутренний сайт пакетной системы с сохранением исходной структуры каталогов:

Такая сборка производится раз в неделю для того, чтобы накапливать значительные изменения в дистрибутиве и оценивать динамику изменения кодового покрытия.
Флаги компилятора, о которых было сказано ранее, выставляются в рамках распределённой сборочной системы только для верификационных сборок. Пакеты собираются параллельно на нескольких узлах, их артефакты сохраняются также параллельно. По завершении сборки получается полный набор buildtime-артефактов на все пакеты.
Сбор данных о покрытии на целевых устройствах
В нашей автоматизированной тестовой системе реализовано обновление стендов (тестовых устройств с разнообразными процессорными архитектурами) на различные редакции ОС “Нейтрино”, а также их инструментированные варианты. Обновив стенд с использованием инструментированных компонентов можно наконец-то перейти к запуску тестов и сбору покрытия!
Во время запуска любого инструментированного бинарного компонента генерируются runtime-артефакты - файлы .gcda, содержащие статистику фактического выполнения исходного кода. Также как и с .gcno - для каждой единицы компиляции (при условии, если она была исполнена) получается свой .gcda-артефакт. Исходная сборочная структура директорий сохраняется:
x86 └── baseutils └── src └── base-utils ├── g │ └── getconf │ └── nto │ └── x86 │ └── o │ └── getconf.gcda └── m └── mount └── nto └── x86 └── o ├── mount.gcda └── vfslist.gcda
Генерация отчёта
Имея два набора артефактов уже можно получить отчёт о покрытии. Для этого скопируем все runtime-артефакты с целевой системы на инструментальную и расположим каждый runtime-артефакт рядом со своим buildtime-артефактом.
Для генерации отчёта используются утилиты lcov и genhtml. Первая генерирует файлы трассировки .info со специальной разметкой, содержащий информацию о кодовом покрытии:
cd <artifacts-directory> lcov -t "coverage" \ -c \ -o "coverage.info" \ -d . \ --rc lcov_branch_coverage=1 \ --config-file <path-to-lcovrc-file> \ --gcov-tool <gcov-tool-name>
Далее происходит генерация HTML-отчёта второй утилитой:
genhtml -o report \ coverage.info \ --rc lcov_branch_coverage=1 \ --config-file <path-to-lcovrc-file>
На выходе получаем директорию report с HTML-отчётом следующего вида:

Отлично, вот и первый отчёт! Но как можно автоматизировать его генерацию, при наличии множества компонентов ОС и тестов для них?
Немного о типах тестирования
В нашей ОС количество компонентов и, соответственно, тестов исчисляется тысячами. Кроме того, различные компоненты и подсистемы могут проверяться различными типами тестирования и собственными наборами тестов.
И тут мы сталкиваемся с проблемой - типов тестирования существует несколько, но какие из них целесообразно запускать и в каком объёме?
Для наглядности ниже приведена реализация разделения типов тестирования:
enum TestingType { Unit( "Модульное тестирование", "Отчёт о модульном тестировании", "/testing/test-system/units/" ), Daily( "Ежедневное тестирование", "Отчёт о ежедневном тестировании", "/testing/test-system/target/" ), Verification( "Верификационное тестирование", "Отчёт о верификационном тестировании", "/testing/test-system/target/" ), Performance( "Performance-тестирование", "Отчёт о тестировании производительности", "/testing/test-system/performance/" ), Benchmark( "Benchmark-тестирование", "Отчёт о тестировании для сравнения с эталонными значениями", "/testing/test-system/benchmark/" ) ... }
Для каждого типа тестирования определены: описание, название проекта и путь к реализующему CI/CD-конвейеру (в нашем случае Jenkins). Каждый из конвейеров является независимой линией сборки, исполнения тестов, а также генерации отчётов.
Далее пройдём по каждому типу и выберем нужные:
Unit - модульные тесты компонентов ОС. Подходят для нашей задачи, т.к. могут достигнуть практически любых участков исходного кода.
Daily - представляет из себя набор тестов, проверяющих только критические компоненты ОС. Их неполный набор не отразит реальный процент покрытия.
Verification - полный набор интеграционных тестов, существующий в нашей автоматизированной системе. Вот и второй кандидат на получение кодового покрытия.
Performance - в условиях инструментированных бинарных компонентов не является показательным типом тестирования, т.к. полученные статистические характеристики измеряемых метрик зачастую занижены.
Benchmark - аналогичная performance’ам ситуация.
От независимых тестов к единому отчёту
Из-за независимости конвейеров каждый после себя может оставлять разный набор runtime-артефактов и, соответственно, разные по процентному содержанию отчёты о покрытии. Такое положение нашу команду не устраивает, ведь несколько отчётов с разными процентами покрытия и разными затронутыми частями исходного кода просто неудобны для анализа.
В связи с этим было принято решение об организации нового CI/CD процесса для сбора покрытия и его объединения между различными типами тестирования:
После завершения тестирования каждый независимый конвейер должен формировать набор файлов трассировки - по одному на каждый пакет, чтобы имелась возможность анализировать кодовое покрытие как по каждому отдельному пакету, так и по их совокупности.
-
Было выделено несколько вариантов отчётов о кодовом покрытии, приведённых в таблице ниже.
Вид отчёта о кодовом покрытии
Состав
По поверхности атаки
Критические компоненты ОС, через которые злоумышленник может получить доступ к системе.
По базовому дистрибутиву
Базовые пакеты ОС - ядро, менеджер процессов, стандартные драйвера и утилиты, …
По расширенному дистрибутиву
Базовые пакеты ОС, а также дополнительные компоненты
По опциональным пакетам
Необязательные пакеты, предоставляемые по запросу заказчиков
По полному дистрибутиву
Абсолютно все существующие пакеты
Сами конвейеры тестирования не занимаются построением отчётов, а лишь исполняют тесты, подготавливают набор
.info-файлов и выгружают его на внутренний сайт пакетной системы. Объединение множества трассировочных файлов и генерация нескольких видов отчётов - задача ещё одного отдельного конвейера.
Таким образом, можно схематично изобразить архитектуру имеющегося CI/CD процесса:

Файлы трассировки хранятся следующим образом:

Объединение трассировочных файлов
После выполнения всех конвейеров тестирования необходимо получить единый результат, который учитывает покрытие, собранное разными типами тестов. Для этого головной конвейер собирает трассировочные файлы, сформированные отдельными конвейерами, и объединяет их перед генерацией итоговых отчётов. Конвейер генерации является параметризованным по типам тестирования, что позволяет получить отдельные отчёты от разных типов и их комбинаций.
На первый взгляд, задача сводится к простой операции: найти все трассировочные файлы по указанным типам тестирования и передать их в lcov для объединения. Однако на практике такой подход не работает без дополнительной логики.
Основная проблема заключается в том, что разные типы тестирования не обязательно работают с одинаковым набором пакетов. Например, один пакет может тестироваться только в рамках модульных тестов, другой - только интеграционными тестами, а третий может присутствовать сразу в нескольких типах. Поэтому заранее определить фиксированный набор трассировочных файлов для каждого пакета невозможно. При этом отсутствие трассировочного файла для конкретного типа тестирования не всегда означает ошибку.
Именно для этого конвейер генерации сначала определяет полный набор пакетов, для которых существуют результаты покрытия, а затем для каждого пакета объединяет все доступные трассировочные файлы по указанным типа тестирования. Такой подход позволяет оценивать полноту разных типов тестов для конкретного пакета.
После для каждого пакета формируется единый трассировочный файл, который становится входным для следующего этапа - формирования итоговых отчётов по покрытию.
Объединение трассировочных файлов выполняется следующим образом:
lcov -a "<testing-type-A>/<package-name>/tracefile.info" \ -a "<testing-type-B>/<package-name>/tracefile.info" \ -a "<testing-type-C>/<package-name>/tracefile.info" \ -o "<package-name>.info"
Конвейер генерации фактически выполняет две независимые задачи: определяет, какие результаты необходимо объединить, и, непосредственно, выполняет их объединение. Такое разделение позволяет не привязывать процесс формирования отчёта к конкретному набору тестов и корректно обрабатывать изменения состава пакетов и типов тестирования.
Формирование итоговых отчётов
Имея на руках набор объединённых трассировочных файлов по каждому пакету, остаётся лишь сгенерировать отчёты. Для каждого типа отчёта существует фиксированный набор пакетов, что позволяет уже без труда объединить трассировочные файлы и получить готовые HTML-отчёты.
Помимо lcov-отчётов формируются высокоуровневые HTML-отчёты по организациям в системе контроля версий. Такие отчёты имеют собственную вёрстку, но используют те же трассировочные файлы, что и genhtml.
Что дальше?
Следующим этапом развития системы планируется переход с lcov на gcovr. Основная цель такого перехода - упростить инфраструктуру сбора покрытия и сократить количество промежуточных операций, необходимых для получения итогового результата.
Одним из преимуществ gcovr является собственный формат хранения результатов покрытия на основе JSON. В отличие от традиционного .info-формата lcov, JSON-структура значительно лучше подходит для автоматической обработки: данные имеют явную структуру, а их содержимое проще анализировать и использовать в собственных инструментах.
Отдельное преимущество gcovr - значительно более широкий набор форматов итоговых отчётов. Помимо HTML, он поддерживает текстовый и CSV-вывод, JSON и Markdown, а также несколько XML-форматов, включая Cobertura, Clover, JaCoCo и SonarQube, полезные для систем непрерывной интеграции.
Ещё одна важная возможность - формирование полностью автономного HTML-отчёта, в котором необходимые ресурсы отчёта включены непосредственно в файл. Готовый отчёт можно сохранить как один артефакт и открыть независимо от исходного окружения.
Таким образом, переход на gcovr должен позволить не только заменить используемый инструмент, но и упростить саму инфраструктуру сбора покрытия.
Надеюсь, данная статья будет полезна тем, кто сталкивается с задачей организации сбора кодового покрытия в больших проектах и CI/CD-инфраструктуре.
Также делитесь своим опытом сбора кодового покрытия в комментариях!
Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей
