Здравствуйте, товарищи программисты и все кто им сочувствует. Я хотел бы предложить обзор возможностей сборочной системы 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 для реализации самых разнообразных задач.
И, конечно, ссылки: