Подружить QtTest с GCOV/LCOV для покрытия кода используя сборочную систему QBS

от автора

Здравствуйте, товарищи программисты и все кто им сочувствует. Я хотел бы предложить обзор возможностей сборочной системы QBS для интеграции покрытия кода в Qt автотесты QtTest с использованием утилит gcov/lcov. Кому эта тема интересна, добро пожаловать по кат.

Итак, давайте разберемся, что означает термин «покрытие кода». Вкратце, это когда используется некая утилита (или комплекс ПО) которая позволяет отследить и отобразить в той или иной форме результаты того, насколько тот или иной автотест покрывает исходный код чего-либо.

Обычно, эта утилита встраивает часть некоего своего кода (например, в виде библиотеки, слинкованной с автотестом) для генерации дополнительной информации в процессе работы автотеста. При этом, полученную информацию можно отобразить в любой удобной форме (обычно это HTML страницы).

Содержимое этой информации включает процентное соотношение количества строк и ветвей кода, обработанных автотестом, а также полную информацию о состоянии всех строк и ветвей.

Что мы будем использовать

В нашем текущем примере мы будем идти по пути «наименьшего сопротивления» и использовать общедоступное и свободное ПО (т.к. мы все в душе лентяи):

Компонент

Что это такое

Причина выбора

GNU/Linux

Операционная система.

Доступность, бесплатность, простота.

GCC

Набор компиляторов.

Доступность, бесплатность, простота.

GCOV

Утилита, собирающая предварительную информацию о покрытии.

Уже содержится в наборе компиляторов GCC.

LCOV

Утилита конвертирующая результаты покрытия в удобной форме.

Доступность, бесплатность, простота.

GENHTML

Утилита конвертирующая результаты покрытия в HTML формат (входит в состав lcov).

Доступность, бесплатность, простота.

Qt

Кросс-платформенный С++ фреймворк.

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

QtCreator

Кросс-платформенное и универсальное IDE.

Доступность, бесплатность, простота. Поддерживает автотесты QtTest и сборочную систему QBS «из коробки».

QBS

Кросс-платформенная система сборки.

Доступность, бесплатность, простота, модерн и удобство (да и вообще, няшка).

Примечание: Я здесь не буду расписывать как устанавливать те или иные пакеты, т.к. считаем, что пользователь разберется сам (например, погуглит).

Как работает GCOV/LCOV

Вкратце, чтобы включить поддержку покрытия кода в автотестах, используя компилятор GCC (или CLang), достаточно собрать приложение с отключенной оптимизацией (чтобы компилятор не выкидывал секции, ветвления и т.д., а оставлял «как есть») и передать ему флаг —coverage как в опции компилятора так и в опции линковщика.

Примечание: Именно так и никак иначе, т.к.приложение автотеста просто не скомпилируется.

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

Все это хозяйство заработает только после реального запуска приложения автотеста. По завершению работы приложения автотеста будет автоматически сгенерирован бинарный файл с результатами покрытия в специальном формате.

Далее, этот выходной файл результатов покрытия можно передать, например, утилите lcov для генерации «дружелюбной» информации для пользователя. Например, как набор красивых HTML страниц с подробной информацией о покрытии.

Какие нужны шаги

Ниже приведу минимальный список шагов что и как мы будем делать для достижения результата:

  1. Пишем некий код, который надо проверить.

  2. Пишем некий автотест который линкуется с кодом и проверяет его.

  3. Запускаем автотест и проверяем что он работает.

  4. Добавляем опцию --coverage при сборке автотеста.

  5. Запускаем автотест и проверяем что он сгенерировал выходную информацию о покрытии в выходной файл.

  6. Передаем сгенерированный выходной файл в утилиту lcov для генерации красивых результатов в HTML формате.

  7. Открываем 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 для реализации самых разнообразных задач.

И, конечно, ссылки:

  • Исходный код примера на github.

  • Что такое QBS.


ссылка на оригинал статьи https://habr.com/ru/articles/577686/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *