Здравствуйте, товарищи программисты и все кто им сочувствует. Я хотел бы предложить обзор возможностей сборочной системы QBS для интеграции покрытия кода в Qt автотесты QtTest с использованием утилит gcov/lcov. Кому эта тема интересна, добро пожаловать по кат.
Итак, давайте разберемся, что означает термин "покрытие кода". Вкратце, это когда используется некая утилита (или комплекс ПО) которая позволяет отследить и отобразить в той или иной форме результаты того, насколько тот или иной автотест покрывает исходный код чего-либо.
Обычно, эта утилита встраивает часть некоего своего кода (например, в виде библиотеки, слинкованной с автотестом) для генерации дополнительной информации в процессе работы автотеста. При этом, полученную информацию можно отобразить в любой удобной форме (обычно это HTML страницы).
Содержимое этой информации включает процентное соотношение количества строк и ветвей кода, обработанных автотестом, а также полную информацию о состоянии всех строк и ветвей.
Что мы будем использовать
В нашем текущем примере мы будем идти по пути "наименьшего сопротивления" и использовать общедоступное и свободное ПО (т.к. мы все в душе лентяи):
Компонент |
Что это такое |
Причина выбора |
Операционная система. |
Доступность, бесплатность, простота. |
|
Набор компиляторов. |
Доступность, бесплатность, простота. |
|
Утилита, собирающая предварительную информацию о покрытии. |
Уже содержится в наборе компиляторов GCC. |
|
Утилита конвертирующая результаты покрытия в удобной форме. |
Доступность, бесплатность, простота. |
|
Утилита конвертирующая результаты покрытия в HTML формат (входит в состав lcov). |
Доступность, бесплатность, простота. |
|
Кросс-платформенный С++ фреймворк. |
Доступность, бесплатность, простота. Уже содержит свой собственный тестовый фреймворк QtTest. |
|
Кросс-платформенное и универсальное IDE. |
Доступность, бесплатность, простота. Поддерживает автотесты QtTest и сборочную систему QBS "из коробки". |
|
Кросс-платформенная система сборки. |
Доступность, бесплатность, простота, модерн и удобство (да и вообще, няшка). |
Примечание: Я здесь не буду расписывать как устанавливать те или иные пакеты, т.к. считаем, что пользователь разберется сам (например, погуглит).
Как работает GCOV/LCOV
Вкратце, чтобы включить поддержку покрытия кода в автотестах, используя компилятор GCC (или CLang), достаточно собрать приложение с отключенной оптимизацией (чтобы компилятор не выкидывал секции, ветвления и т.д., а оставлял "как есть") и передать ему флаг --coverage как в опции компилятора так и в опции линковщика.
Примечание: Именно так и никак иначе, т.к.приложение автотеста просто не скомпилируется.
При этом, линковщик автоматически слинкует с исполняемым приложением автотеста некий дополнительный код, который будет заниматься сбором информации об использованных строках и ветвях в коде (я так предполагаю).
Все это хозяйство заработает только после реального запуска приложения автотеста. По завершению работы приложения автотеста будет автоматически сгенерирован бинарный файл с результатами покрытия в специальном формате.
Далее, этот выходной файл результатов покрытия можно передать, например, утилите lcov для генерации "дружелюбной" информации для пользователя. Например, как набор красивых HTML страниц с подробной информацией о покрытии.
Какие нужны шаги
Ниже приведу минимальный список шагов что и как мы будем делать для достижения результата:
Пишем некий код, который надо проверить.
Пишем некий автотест который линкуется с кодом и проверяет его.
Запускаем автотест и проверяем что он работает.
Добавляем опцию
--coverage
при сборке автотеста.Запускаем автотест и проверяем что он сгенерировал выходную информацию о покрытии в выходной файл.
Передаем сгенерированный выходной файл в утилиту lcov для генерации красивых результатов в HTML формате.
Открываем HTML результаты и любуемся.
ШАГ1. Создаем проект и пишем код
Давайте для простоты создадим простейшее дерево проекта COVERAGE-EXAMPLE
со следующей структурой:
COVERAGE-EXAMPLE
│ .gitignore
│ coverage-example.qbs
│
├───src
│ │ src.qbs
│ │
│ └───libs
│ │ libs.qbs
│ │
│ └───foo
│ foo.cpp
│ foo.h
│ foo.qbs
, где:
coverage-example.qbs
- это корневой файл проекта:
Project {
name: "coverage-example" // Задаем уникальное имя корневого проекта.
references: [
"src/src.qbs", // Подключаем директорию с исходниками в проект.
]
}
src/
- директория содержащая исходные коды под-проекты главного проекта, которые перечислены в файлеsrc.qbs
(в нашем случае это будут только под-проекты библиотек):
Project {
name: "sources" // Задаем уникальное имя под-проекта общих исходников.
references: [
"libs/libs.qbs" // Подключаем директорию с исходниками библиотек в проект.
]
}
libs/
- директория содержащая исходные коды под-проектов библиотек, которые перечислены в файлеlibs.qbs
(в нашем случае это будет только один продукт библиотекиfoo
):
Project {
name: "libs" // Задаем уникальное имя для под-проектов библиотек.
references: [
"foo/foo.qbs" // Добавляем директорию с исходниками библиотеки 'foo' в проект.
]
}
libs/foo/
- директория содержащая исходные коды продукта нашей библиотеки, конфигурация которой описана в файлеfoo.qbs
:
StaticLibrary { // Задаем тип библиотеки как статическую.
name: "foo" // Задаем уникальное имя библиотеки (будет на выходе foo.a).
Depends { name: "cpp" } // Задаем зависимость от QBS-ного модуля CPP.
Depends { name: "Qt"; submodules: "core" } // Говорим линковать с модулем QtCore.
files: [ "foo.cpp", "foo.h" ] // Перечисляем исходники библиотеки.
// Это специальный финт для экспорта директории с заголовками библиотеки, так,
// чтобы ее можно было подключать как '#include <foo/foo.h>' вместо
// '#include <foo.h>'.
property string libIncludeBase: ".."
cpp.includePaths: [libIncludeBase]
// Экспорт свойств продукта библиотеки для всех других продуктов, которые будут
// зависеть от этой библиотеки (экспортирует директорию с заголовками).
Export {
Depends { name: "cpp" }
cpp.includePaths: [product.libIncludeBase]
}
}
Библиотека foo
пусть будет статической и пусть содержит всего лишь один класс Foo
с одним методом encode()
:
#pragma once
#include <QByteArray>
class Foo
{
public:
enum class Number { One, Two, Three };
QByteArray encode(Number number) const;
};
Этот метод принимает на вход какое-то значение из перечисления и преобразует его в некоторое имя:
#include "foo.h"
QByteArray Foo::encode(Number number) const
{
switch (number) {
case Number::One:
return "one";
case Number::Two:
return "two";
case Number::Three:
return "three";
default:
return "unknown";
}
}
ШАГ2. Создаем автотест
Чтобы проверить что целевой метод:
QByteArray Foo::encode(Number number) const
работает правильно, необходимо ему на вход подавать каждое значение из перечисления Number
и сравнивать результат, возвращаемый методом, с ожидаемой строкой.
Для простоты эксперимента возьмем тестовый фреймворк QtTest
из состава Qt
, для чего немного расширим структуру проекта, добавив директорию tests
:
COVERAGE-EXAMPLE
│ .gitignore
│ coverage-example.qbs
│
├───src
│ │ src.qbs
│ │
│ └───libs
│ │ libs.qbs
│ │
│ └───foo
│ foo.cpp
│ foo.h
│ foo.qbs
│
└───tests
│ tests.qbs
│
└───auto
│ auto.qbs
│
└───foo
foo.qbs
tst_foo.cpp
При этом, файл coverage-example.qbs
пополнится до такого содержимого:
Project {
name: "coverage-example"
references: [
"src/src.qbs",
"tests/tests.qbs" // Подключаем директорию с тестами в проект.
]
}
, где:
tests/tests.qbs
- директория содержащая исходные коды под-проектов тестов, которые перечислены в файлеtests.qbs
(в нашем случае это будут только под-проекты автотестов):
Project {
name: "tests" // Задаем уникальное имя для под-проекта тестов.
references: [
"auto/auto.qbs" // Подключаем директорию с автотестами в проект.
]
}
auto/
- директория содержащая только под-проекты автотестов, которые перечислены в файлеauto.qbs
(на данный момент содержит только один автотестfoo
для нашей библиотекиfoo
):
Project {
name: "autotests" // Задаем уникальное имя для под-проекта с автотестами.
references: [
"foo/foo.qbs" // Подключаем директорию с исходниками автотеста 'foo' в проект.
]
}
auto/foo/
- директория содержащая продукт приложения автотеста для библиотекиfoo
, конфигурация которого описана в файлеfoo.qbs
:
CppApplication { // Задаем тип автотеста как С++ приложение.
name: "tst_foo" // Задаем уникальное имя приложения автотеста.
Depends { name: "Qt"; submodules: ["test"] } // Говорим линковать с модулем QtTest.
Depends { name: "foo" } // Говорим линковать с нашей библиотекой 'foo'.
files: ["tst_foo.cpp"] // Перечисляем исходники автотеста.
}
Приложение автотеста tst_foo.cpp
содержит следующий код:
#include <foo/foo.h>
#include <QtTest>
Q_DECLARE_METATYPE(Foo::Number)
class tst_Foo final : public QObject
{
Q_OBJECT
private slots:
void encode_data();
void encode();
};
void tst_Foo::encode_data()
{
QTest::addColumn<Foo::Number>("number");
QTest::addColumn<QByteArray>("name");
QTest::newRow("one") << Foo::Number::One << QByteArray("one");
QTest::newRow("two") << Foo::Number::Two << QByteArray("two");
QTest::newRow("three") << Foo::Number::Three << QByteArray("three");
QTest::newRow("unknown") << static_cast<Foo::Number>(123) << QByteArray("unknown");
}
void tst_Foo::encode()
{
QFETCH(Foo::Number, number);
QFETCH(QByteArray, name);
Foo foo;
const auto encoded = foo.encode(number);
QCOMPARE(encoded, name);
}
QTEST_MAIN(tst_Foo)
#include "tst_foo.moc"
Здесь:
метод теста
tst_Foo:encode_data()
формирует набор датасетов, подаваемых в проверяемый методFoo::encode()
библиотеки.метод теста
tst_Foo::encode()
запускает по очереди целевую функцию из библиотеки с каджым из датасетов.
Примечание: Это все магия QtTest, с которой подробнее можно ознакомиться из оффициальной документации Qt.
ШАГ3. Запускаем автотест еще без покрытия кода
Для проверки того, что наш автотест работает как надо, нам просто необходимо его запустить (автотест - это просто обычное приложение). В процессе работы он выдаст результат, похожий на этот:
********* Start testing of tst_Foo *********
Config: Using QtTest library 5.14.2, Qt 5.14.2 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 10.2.0)
PASS : tst_Foo::initTestCase()
PASS : tst_Foo::encode(one)
PASS : tst_Foo::encode(two)
PASS : tst_Foo::encode(three)
PASS : tst_Foo::encode(unknown)
PASS : tst_Foo::cleanupTestCase()
Totals: 6 passed, 0 failed, 0 skipped, 0 blacklisted, 0ms
********* Finished testing of tst_Foo *********
ШАГ4. Добавляем опцию --coverage
Чтобы информация о покрытии генерировалась в процессе работы автотеста, необходимо добавить опцию --coverage
и компилятору и линковщику, а также собирать проект с отключенной оптимизацией (например, это дебаг режим).
В нашем случае при сборке статической библиотеки до линковки дело не доходит, т.к. здесь работает только компилятор и архиватор. А вот уже приложение автотеста и компилируется и линкуется. Поэтому опцию --coverage
нужно добавить в два разных продукта в разные шаги сборки:
и в библиотеку (только компилятору)
и в автотест (и компилятору и линковщику).
Для этого QBS предоставляет модуль cpp, который имеет сециально предназначенные для этого свойства. В нашем случае, свойство cpp.driverFlags
- то что нужно, оно передаст опцию --coverage
и компилятору и линковщику.
Простой и самый напрашивающийся вариант - это продублировать свойство cpp.driverFlags
и в продукт библиоеки и в продукт автотеста. Но это решение некрасивое, и есть более изящный подход, используя мощь QBS. ;)
Мы просто создадим дополнительный QBS модуль, и назовем его для примера как coverage
. Этот модуль будет подставлять нужные опции сам автоматически. Достаточно только его подключить как зависимость к продукту. Для этого придется немного расширить дерево проекта:
COVERAGE-EXAMPLE
│ .gitignore
│ coverage-example.qbs
│
├───qbs
│ └───modules
│ └───coverage
│ coverage.qbs
│
├───src
│ │ src.qbs
│ │
│ └───libs
│ │ libs.qbs
│ │
│ └───foo
│ foo.cpp
│ foo.h
│ foo.qbs
│
└───tests
│ tests.qbs
│
└───auto
│ auto.qbs
│
└───foo
foo.qbs
tst_foo.cpp
, где:
qbs/
- директория в которой находятся наши вспомогательные QBS модули или файлы импорта.qbs/modules/
- директория содержащая вспомогательные модули (должна иметь имяmodules
).qbs/modules/coverage/
- директория, содержащая исходный QBS код нашего вспомогательного модуляcoverage
описанного в файлеcoverage.qbs
:
Module {
// Задаем условие что этот модуль активен только для компилятора GCC
// и только если текущая конфигурация есть debug.
condition: qbs.debugInformation && qbs.toolchain.contains("gcc")
Depends { name: "cpp" } // Добавляем зависимость от QBS-ного модуля CPP.
cpp.driverFlags: ["--coverage"] // Задаем флаги и компилятору и линковщику.
}
Далее, добавляем этот модуль как зависимость к продукту библиотеки в файле foo.qbs
:
StaticLibrary {
name: "foo"
Depends { name: "cpp" }
Depends { name: "coverage" } // Добавляем зависимость от модуля coverage.
Depends { name: "Qt"; submodules: "core" }
files: [ "foo.cpp", "foo.h" ]
property string libIncludeBase: ".."
cpp.includePaths: [libIncludeBase]
Export {
Depends { name: "cpp" }
Depends { name: "coverage" } // Экспортируем зависимость от модуля coverage.
cpp.includePaths: [product.libIncludeBase]
}
}
Теперь при сборке библиотеки, будет добавлена опция --coverage
компилятору. А благодаря тому, что мы экспортировали зависимость от модуля coverage
из библиотеки foo
с помощью Export , то теперь любой продукт, подключивший библиотеку foo
также получит эту опцию --coverage
. Таким образом, мы убиваем сразу двух (и более зайцев).
И почти последгий штрих - надо указать QBS где искать наш новый модуль coverage
. Для этого нужно добавить в корневой файл проекта coverage-example.qbs
одну строчку:
Project {
name: "coverage-example"
qbsSearchPaths: "qbs" // Говорим QBS что искать наши модули в директории qbs
references: [
"src/src.qbs",
"tests/tests.qbs"
]
}
На этом, казалось бы, этот шаг должен был быть завершен - но нет. После включения опции --coverage
после сборки библиотеки и приложения автотеста, в директории с объектными файлами автоматически будут создаваться файлы с расширением *.gcno
:
user@ubuntu:~/Documents/coverage-git/build-coverage-example-Desktop-Debug/Debug_Desktop_038b678e9426a45b$ ls foo.0beec7b5/3a52ce780950d4d9/
foo.cpp.gcno foo.cpp.o
user@ubuntu:~/Documents/coverage-git/build-coverage-example-Desktop-Debug/Debug_Desktop_038b678e9426a45b$ ls tst-foo.e3f741e4/3a52ce780950d4d9/
tst_foo.cpp.gcda tst_foo.cpp.gcno tst_foo.cpp.o
Но мы не хотим держать эту помойку и пускать все на самотек. Мы хотим, чтобы при операции clean
очищалась вся директория сборки. Для этого нужно добавить в модуль coverage
правило, которое обрабатывало бы файлы *.gcno
как артефакты (т.е. включить их в граф сборки QBS).
import qbs.FileInfo // Импортирует QBS-ный сервис для работы с инфой о файлах.
import qbs.Utilities // Импортируем QBS-ные вспомогательные функции из утилит.
Module {
condition: qbs.debugInformation && qbs.toolchain.contains("gcc")
additionalProductTypes: ["gcno"] // Говорим что обязательно использовать тег gcno.
Depends { name: "cpp" }
cpp.driverFlags: ["--coverage"]
Rule { // Правило для фейковой генерации файлов *.gcno.
// Говорим что как будто мы будем генерировать файлы *.gcno из
// сорцов *.cpp или *.c.
inputs: ["cpp", "c"]
// Задаем имя тега для генерируемого артефакта (любое,
// пусть будет gcno).
outputFileTags: ["gcno"]
// Описываем свойства генерируемого артефакта:
// - то что файлам *.gcno присваивается тег gcno.
// - то что файлы *.gcno будут создаваться в определенном месте
// (рядом с объектниками).
outputArtifacts: {
return [{
fileTags: ["gcno"],
filePath: FileInfo.joinPaths(Utilities.getHash(input.baseDir),
input.fileName + ".gcno")
}];
}
// Описываем код правила, т.е. как мы будем из файлов *.c *.cpp
// делать файлы *.gcno. А никак - мы не контролируем этот процесс,
// т.к. сам компилятор этим занимается автоматически. Поэтому код
// этого правила - просто пуская команда, которая ничего не делает,
// кроме того что просто печатает в коноль сообщение:
// generating foo.gcno
prepare: {
var cmd = new JavaScriptCommand();
cmd.description = "generating " + output.fileName;
return [cmd];
}
}
}
Примечание:
Т.к. выходной артифакт
gcno
никому не нужен (он не включен ни в какую зависимость) то чтобы правило заработало, необходимо явно сказать QBS-у чтобы он всегда запускал это правило с помощью опции модуляadditionalProductTypes
.Т.к. наше правило ничего не делает (просто регистрирует файлы *.gcno как артефакты), то и его код должен ничего не делать тоже.
ШАГ5. Запускаем автотест уже с покрытием кода
Теперь запускаем снова наше приложение автотеста. И видим, что он отработал как обычно (вывел в консоль результаты):
********* Start testing of tst_Foo *********
Config: Using QtTest library 5.14.2, Qt 5.14.2 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 10.2.0)
PASS : tst_Foo::initTestCase()
PASS : tst_Foo::encode(one)
PASS : tst_Foo::encode(two)
PASS : tst_Foo::encode(three)
PASS : tst_Foo::encode(unknown)
PASS : tst_Foo::cleanupTestCase()
Totals: 6 passed, 0 failed, 0 skipped, 0 blacklisted, 0ms
********* Finished testing of tst_Foo *********
Но теперь рядом с объектными файлами библиотеки и приложения автотеста автоматически сгенерировались бинарные файлы отчетов *.gcda
:
user@ubuntu:~/Documents/coverage-git/build-coverage-example-Desktop-Debug/Debug_Desktop_038b678e9426a45b$ ls foo.0beec7b5/3a52ce780950d4d9/
foo.cpp.gcda foo.cpp.gcno foo.cpp.o
user@ubuntu:~/Documents/coverage-git/build-coverage-example-Desktop-Debug/Debug_Desktop_038b678e9426a45b$ ls tst-foo.e3f741e4/3a52ce780950d4d9/
tst_foo.cpp.gcda tst_foo.cpp.gcno tst_foo.cpp.o
Но мы лентяи не не хотим вручную запускать автотесты. Для этого у QBS уже имеется специальный объект AutotestRunner, который достаточно добавить в корневой проект coverage-example.qbs
:
Project {
name: "coverage-example"
qbsSearchPaths: "qbs"
AutotestRunner { }
references: [
"src/src.qbs",
"tests/tests.qbs"
]
}
И всем продуктам приложениям автотестов просто добавить тег autotest
(в нашем случае у нас один объект автотеста в tests/auto/foo/foo.qbs)
:
CppApplication {
name: "tst_foo"
type: base.concat("autotest") // Добавили новый тег - автотест.
Depends { name: "Qt"; submodules: ["test"] }
Depends { name: "foo" }
files: ["tst_foo.cpp"]
}
И тепрерь, при сборке нашего раннера, QBS автоматически отследит все его зависимости (они являются автотестами), соберет их (если еще не собраны), и запустит по очереди.
ШАГ6. Генерируем красивые результаты
Итак, чтобы сгенерировать красивые результаты покрытия в виде HTML страниц, необходимо обработать все бинарные файлы результатов утилитой lcov.
Чтобы автоматизировать этот процесс - напишем свой зарускатель автотестов, т.к. стандартный AutotestRunner
не годится для этой цели. Причина в том, что нам нужно не только запускать автотест, но и написать правила, которые будут отслеживать генерируемые файлы *.gcda
как выходные артефакты и подавать их на вход утилите lcov.
Назовем наш новый запускатель как CoverageRunner
и поместим его реализацию в директорию imports
:
COVERAGE-EXAMPLE
│ .gitignore
│ coverage-example.qbs
│
├───qbs
│ ├───imports
│ │ CoverageRunner.qbs
│ │
│ └───modules
│ └───coverage
│ coverage.qbs
│
├───src
│ │ src.qbs
│ │
│ └───libs
│ │ libs.qbs
│ │
│ └───foo
│ foo.cpp
│ foo.h
│ foo.qbs
│
└───tests
│ tests.qbs
│
└───auto
│ auto.qbs
│
└───foo
foo.qbs
tst_foo.cpp
, где:
qbs/imports/
- диретория, содержащая разные реализации вспомогательных объектов.
В нашем случае она содержит только одну реализацию объекта нашего запускателя в файле CoverageRunner.qbs
:
import qbs.File
import qbs.FileInfo
import qbs.ModUtils
import qbs.Probes
import qbs.Utilities
Product {
name: "coverage-runner" // Задаем уникальное имя нашему запускателю тестов.
// Задаем тег конечного выходного артефакта в цепочке всех промежуточных
// артефактов, генерируемым нашим запускателем. Конечный артефакт - это
// информация о покрытии в виде HTML страниц.
type: ["out_html"]
// Говорим QBS-у чтобы он собирал этот запускатель только при явном указании.
builtByDefault: false
// Задаем переменные окружения для запуска тестов и утилиты lcov.
property stringList environment: ModUtils.flattenDictionary(qbs.commonRunEnvironment)
// Задаем путь к утилите lcov, найденный пробником.
property path lcovPath: lcovProbe.filePath
// Задаем путь к утилите genhtml, найденный пробником.
property path genhtmlPath: genhtmlProbe.filePath
// Пробник, который ишет утилиту lcov.
Probes.BinaryProbe {
id: lcovProbe
names: "lcov"
}
// Пробник, который ищет утилиту genhtml.
Probes.BinaryProbe {
id: genhtmlProbe
names: "genhtml"
}
// Задаем зависимости в виде любых продуктов с тегом autotest.
Depends {
productTypes: "autotest"
limitToSubProject: true
}
// Реализуем первое правило в цепочке, которое будет запускать автотесты
// и добавляет генерируемые ими выходные файлы *.gcna в граф сборки.
Rule {
id: gcnoGenerator
inputsFromDependencies: ["application"]
// Задаем выходной тег артефактам, и полный путь к файлам генерируемым
// этим правилом.
outputFileTags: ["gcda"]
outputArtifacts: {
var artifacts = [];
function traverse(dep) {
var gcnos = dep.artifacts["gcno"] || [];
gcnos.forEach(function(gcno) {
artifacts.push({
fileTags: ["gcda"],
filePath: FileInfo.joinPaths(FileInfo.path(gcno.filePath), gcno.completeBaseName + ".gcda")
});
});
dep.dependencies.forEach(traverse);
}
product.dependencies.forEach(traverse);
return artifacts;
}
prepare: {
if (!input.product.type.contains("autotest")) {
var cmd = new JavaScriptCommand();
cmd.silent = true;
return cmd;
}
var commandFilePath;
var installed = input.moduleProperty("qbs", "install");
if (installed)
commandFilePath = ModUtils.artifactInstalledFilePath(input);
if (!commandFilePath || !File.exists(commandFilePath))
commandFilePath = input.filePath;
var arguments = (input.autotest && input.autotest.arguments && input.autotest.arguments.length > 0) ? input.autotest.arguments : [];
var workingDir = (input.autotest && input.autotest.workingDir) ? input.autotest.workingDir : FileInfo.path(commandFilePath);
var fullCommandLine = [].concat([commandFilePath]).concat(arguments);
var cmd = new Command(fullCommandLine[0], fullCommandLine.slice(1));
cmd.description = "running test " + input.fileName;
cmd.environment = product.environment;
cmd.workingDirectory = workingDir;
cmd.jobPool = "coverage-runner";
if (input.autotest && input.autotest.allowFailure)
cmd.maxExitCode = 32767;
return cmd;
}
}
// Реализуем второе правило в цепочке. Оно берет на вход все сгенерированные
// файлы *.gcda, подает их утилите lcov для генерации результатов покрытия
// в текстовой форме в виде файлов с расширением *.info.
Rule {
id: infoGenerator
inputs: ["gcda"] // Задаем брать все бинарные файлы *.gcda на вход правилу.
// Задаем выходной тег для артефактов, и полный путь файлов, генерируемых
// этим правилом.
outputFileTags: ["src_info"]
outputArtifacts: {
return [{
fileTags: ["src_info"],
filePath: FileInfo.joinPaths(Utilities.getHash(input.baseDir), input.fileName + ".info")
}];
}
// Этот код запускает утилиту lcov для каждого из входных файлов *.gcda и
// формирует выходные файлы с текстовой информацией *.info.
prepare: {
var args = ["--quiet", "--capture"];
args.push("--directory", FileInfo.path(input.filePath));
args.push("--output-file", output.filePath);
var cmd = new Command(product.lcovPath, args);
cmd.description = "generating " + output.fileName;
return cmd;
}
}
// Реализуем третье правило в цепочке. Оно берет на вход все файлы *.info и
// объединяет их в один общий файл с именем <имя продукта>.info, который помещает
// рядом с исполняемым файлом автотестов (я так захотел).
Rule {
id: infoMerger
multiplex: true
inputs: ["src_info"] // Задаем брать все текстовые файлы *.info.
// Задаем выходной тег артифакта, и полный путь к объединенному *.info
// файлу генерируемому этим правилом.
outputFileTags: ["out_info"]
outputArtifacts: {
return [{
fileTags: ["out_info"],
filePath: FileInfo.joinPaths(product.destinationDirectory, product.targetName + ".info")
}];
}
// Этот код запускает утилиту lcov для объелинения всех входных файлов
// *.info в один результирующий файл *.info.
prepare: {
var args = ["--quiet"];
inputs.src_info.forEach(function(info) {
args.push("--add-tracefile", info.filePath);
});
args.push("--output-file", output.filePath);
var cmd = new Command(product.lcovPath, args);
cmd.description = "generating " + output.fileName;
return cmd;
}
}
// Реализуем четвертое (и последнее) правилло в цепочке. Оно берет на вход
// результирующий текстовый файл *.info и подает его утилите genhtml для
// генерации результата в виде HTML страниц.
Rule {
id: htmlGenerator
inputs: ["out_info"] // Задаем брать результирующий текстовый файл.
// Задаем выходной тег артифакта и полный путь к директории с HTML
// страницами, генерируемыми этим правилом.
// Имя этого тега должно совпадать с именем пега продукта - запускателя.
outputFileTags: ["out_html"]
outputArtifacts: {
return [{
fileTags: ["out_html"],
filePath: FileInfo.joinPaths(product.destinationDirectory, "html")
}];
}
// Этот код запускает утилиту genhtml для генерации HTML станиц из
// результирующего файла *.info.
prepare: {
var args = ["--quiet", "--ignore-errors"];
args.push("source", input.filePath);
args.push("--output-directory", output.filePath);
var cmd = new Command(product.genhtmlPath, args);
cmd.description = "generating html";
return cmd;
}
}
}
Этот запускатель реализует набор правил, выстроенных в цепочку в графе сборки. При сборке запускателя будут выполненны следующие действия:
Запуск автотестов для генерации бинарных файлов
*.gcda
.Подача всех файлов
*.gcda
в утилиту lcov для генерации текстовых фалов*.info
.Подача всех текстовых файлов
*.info
в утилиту lcov для их объединения в один результирующий файл*.info
.Подача результирующего текстового файла
*.info
в утилиту genhtml для генерации HTML страниц о покрытии кода.
Далее, необходимо модифицировать корневой файл проекта coverage-example.qbs
, заменив в нем AutotestRunner
на наш CoverageRunner
:
import "qbs/imports/CoverageRunner.qbs" as CoverageRunner // Импортируем наш запускатель
Project {
name: "coverage-example"
qbsSearchPaths: "qbs"
CoverageRunner { } // Декларируем наш запускатель.
references: [
"src/src.qbs",
"tests/tests.qbs"
]
}
Теперь, для генерации результатов покрытия в виде HTML страниц достаточно просто выполнить сборку нашего продукта - запускателя тестов coverage-runner
.
ШАГ7. Просматриваем результаты покрытия
После сборки нашего запускателя, он сгенерирует результаты покрытия в виде HTML страниц в выходную директорию сборки запускателя, например:
user@ubuntu:~/Documents/coverage-git/build-coverage-example-Desktop-Debug/Debug_Desktop_038b678e9426a45b$ ls coverage-runner.90e4c3ec/html/
amber.png emerald.png gcov.css glass.png home index.html index-sort-f.html index-sort-l.html QtCore QtTest ruby.png snow.png updown.png usr
Теперь мы можем открыть файл index.html
и посмотреть что получилось:
Конечно, здесь добавляется и лишняя информация. Но это не проблема, т.к. ее можно исключить на этапе генерации файлов *.info
, но мы не будем касаться этого в данной статье.
Результаты покрытия по нашей библиотеки примерно следующие:
Теперь можете сами поиграться с содержимым автотеста, например, закомментировав некоторые строчки дата-сетов, перегенерировать HTML и посмотреть что поменяется. ;)
Заключение
В этой статье краттко рассмотрели всю мощь и гибкость QBS для реализации самых разнообразных задач.
И, конечно, ссылки:
Комментарии (3)
ABBAPOH
14.09.2021 00:19Хе, прикольно. Первую половину очень легко запихнуть в апстрим - в правило генерации реальных объективов добавить выхлоп gcno файлов.
С автотест раннером посложнее - он не зависит от языка и ничего не знает про cpp, надо подумать, как протащить инфу через него, чтобы потом собрать хтмлки красивые.
iroln
Оно ещё развивается?
Потому как ещё в 2018 официально решили отказаться от её поддержки внутри The Qt Company:
https://www.qt.io/blog/2018/10/29/deprecation-of-qbs
kuzulis Автор
Да, активно развивается, но не Qt Company, а сообществом. Вот пруф на последние коммиты: https://github.com/qbs/qbs/commits/master
Недавно зарелизили версию 1.20 (с тех пор как задепрекейтили) и я уже и не помню, какой это релиз по счету).
PS: На данный момент имеем желание полностью уйти от инфраструктуры Qt Company на GitHub. Также, пилится отдельный сайт (как я понял). Так что все хорошо.