Подружить 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

, где:

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 ' вместо 
    // '#include '.
    property string libIncludeBase: ".."
    cpp.includePaths: [libIncludeBase]

    // Экспорт свойств продукта библиотеки для всех других продуктов, которые будут
    // зависеть от этой библиотеки (экспортирует директорию с заголовками).
    Export {
        Depends { name: "cpp" }
        cpp.includePaths: [product.libIncludeBase]
    }
}

Библиотека foo пусть будет статической и пусть содержит всего лишь один класс Foo с одним методом encode():

#pragma once

#include 

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 

#include 

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("number");
    QTest::addColumn("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(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

, где:

В нашем случае она содержит только одну реализацию объекта нашего запускателя в файле 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 для реализации самых разнообразных задач.

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

© Habrahabr.ru