mercredi 16 septembre 2020

C++ : Code coverage avec CMake, Gcov et Lcov

La couverture de code ou Code coverage en anglais, est une technique visant à mesurer le taux de code source qui est couvert par les différents tests d'un logiciel.

Si par exemple, j'ai un fichier source contenant une fonction qui a 10 lignes de code et que mes tests unitaires passeront sur 8 d'entre elles alors j'aurai une couverture de code de 80%.

La mise en place d'un système de couverture de code permet au développeur de prendre connaissance des parties de codes qui sont couvertes par les tests et de celles qui ne le sont pas.

Les outils qui seront utilisés pour la démo seront Gcov, Lcov et CMake.
GCov est un utilitaire qui fait parti de la suite GNU Compiler Collection (GCC) et qui permettant de générer le nombre exact de fois que les instructions d'un programme ont été exécutées.
LCov est un outil qui permet de rendre graphiquement les résultats acquis via l'application GCov.
CMake quant à lui n'a plus besoin de présentation surtout dans le monde de l'open source, alors ce sera l'outil de build system que nous nous servirons.

Le projet ci-dessous nous servira d'exemple. C'est un projet tout simple qui a pour but d'afficher le nom et prénom d'un employé à l'écran.

Nous avons notre application principale dans le dossier app, une bibliothèque partagée de nos modèles de données dans le dossier models ainsi que nos tests unitaires, sous format Google Test, dans le dossier test.

L'application principale (app) créer tout simplement un employé puis l'affiche à l'écran.

app/src/main.cpp

#include "employee.h"
#include <iostream>

using namespace std;

int main(int argc, char *argv[])
{
    Employee myEmployee(1, "Joe", "Blow");
    cout << myEmployee << "\n";
    return 0;
}


Allons voir le contenu de la bibliothèque partagée de plus près et plus précisément la classe Employee  :

  • Trois champs privés: id, firstname et lastName. 
  • 1 constructeur pour initialiser nos trois champs
  • Trois méthodes getter afin de pouvoir récupérer les valeurs de nos champs
  • Surcharge de l'opérateur << dans le but d'afficher facilement un employé (prénom et nom).

models/include/employee.h

#pragma once

#include <iostream>
#include <string>

class Employee
{
public:
    Employee(unsigned int id, 
             const std::string & firstName,
             const std::string &lastName);
    unsigned int getId() const;
    const std::string &getFirstName() const;
    const std::string &getLastName() const;
    friend std::ostream& operator<<(std::ostream &os, const Employee &emp);
private:
    unsigned int id;
    std::string firstName;
    std::string lastName;
};


models/src/employee.cpp

#include "employee.h"
#include <stdexcept>

using namespace std;

Employee::Employee(unsigned int id, 
             const std::string & firstName,
             const std::string &lastName) 
    : id {id},
      firstName {firstName},
      lastName {lastName}
{
    if (id == 0) {
        throw invalid_argument("id must be greater than zero.");
    }
}

unsigned int Employee::getId() const
{
    return id;
}

const std::string& Employee::getFirstName() const
{
    return firstName;
}

const std::string& Employee::getLastName() const
{
    return lastName;
}

std::ostream& operator<<(std::ostream &os, const Employee &emp)
{
    os << emp.firstName << " " << emp.lastName;
    return os;
}


Si l'on compile et que l'on roule l'application, on obtient le résultat suivant : 

jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo$ mkdir build
jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo$ cd build/
jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo/build$ conan install ..
Configuration:
[settings]
arch=x86_64
arch_build=x86_64
build_type=Release
compiler=gcc
compiler.libcxx=libstdc++11
compiler.version=7
os=Linux
os_build=Linux
[options]
[build_requires]
[env]

conanfile.txt: Installing package
Requirements
    gtest/1.10.0 from 'conan-center' - Cache
Packages
    gtest/1.10.0:a4062ec0208a59375ac653551e662b6cc469fe58 - Cache

Installing (downloading, building) binaries...
gtest/1.10.0: Already installed!
conanfile.txt: Generator cmake created conanbuildinfo.cmake
conanfile.txt: Generator txt created conanbuildinfo.txt
conanfile.txt: Generated conaninfo.txt
conanfile.txt: Generated graphinfo
jed@jed-MS-7593:~/Programming/test/CPP-CMake-CodeCoverage-Demo/build$ cmake -DCMAKE_BUILD_TYPE=Debug ..
-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Conan: Adjusting output directories
-- Conan: Using cmake global configuration
-- Conan: Adjusting default RPATHs Conan policies
-- Conan: Adjusting language standard
-- Current conanbuildinfo.cmake directory: /home/jed/Programming/test/CPP-CMake-CodeCoverage-Demo/build
-- Conan: Compiler GCC>=5, checking major version 7
-- Conan: Checking correct version: 7
-- Appending code coverage compiler flags: -g -fprofile-arcs -ftest-coverage
-- Configuring done
-- Generating done
-- Build files have been written to: /home/jed/Programming/test/CPP-CMake-CodeCoverage-Demo/build
jed@jed-MS-7593:~/Programming/test/CPP-CMake-CodeCoverage-Demo/build$ cmake --build . && ./bin/app
Scanning dependencies of target models
[ 14%] Building CXX object models/CMakeFiles/models.dir/src/employee.cpp.o
[ 28%] Linking CXX shared library ../lib/libmodels.so
[ 28%] Built target models
Scanning dependencies of target app
[ 42%] Building CXX object app/CMakeFiles/app.dir/src/main.cpp.o
[ 57%] Linking CXX executable ../bin/app
[ 57%] Built target app
Scanning dependencies of target codecoverageexample_unittests
[ 71%] Building CXX object test/CMakeFiles/codecoverageexample_unittests.dir/main.cpp.o
[ 85%] Building CXX object test/CMakeFiles/codecoverageexample_unittests.dir/employee_unittest.cpp.o
[100%] Linking CXX executable ../bin/codecoverageexample_unittests
[100%] Built target codecoverageexample_unittests
Joe Blow


Afin de prendre en charge la couverture de code, il faut l'indiquer à notre compilateur et à notre linker en configurant quelques options. Lars Bilke a créé un module CMake CodeCoverage.cmake et c'est celui que nous utiliserons. 

Vous pouvez toutefois configurer manuellement les options avec entre autres -g, -fprofile-arcs et -ftest-coverage.

Le fichier CMakeLists.txt à la racine du projet contient les instructions qui activeront les options de couverture de code.

cmake_minimum_required(VERSION 3.10)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake/modules)

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

include(CodeCoverage)
append_coverage_compiler_flags()

enable_testing()

include(GoogleTest)

add_subdirectory("models")
add_subdirectory("app")
add_subdirectory("test")

Allons ensuite écrire notre premier test unitaire. Ce test créer un nouvel employé qui par conséquent appelle le constructeur de la classe.

test/employee_unittest.cpp

#include "employee.h"
#include <gtest/gtest.h>

using namespace std;

TEST(Employee_Constructor, AllValidArgs_ReturnSuccess)
{
    Employee sample(1, "Joe", "Blow");
}

Pour lancer l'analyse de couverture de code, il suffit de compiler l'application, lancer les tests unitaires puis ensuite générer le résultat par exemple au format HTML.

jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo/build$ cmake --build . \
> && ctest --progress \
> && lcov -c -d . -o main_coverage.info \
> && genhtml main_coverage.info --output-directory out

Si l'on ouvre le fichier résultant index.html du dossier build\out, on voit que nos tests, ou devrais-je plutôt dire notre seul test, couvre 37.5% de notre code.


Allons voir la couverture en détail en cliquant sur le lien employee.cpp. Les lignes bleues indiquent qu'elles sont couvertes par les tests et celles en rouge indiquent qu'elles ne le sont pas.



On peut ainsi voir que nos tests ne couvrent pas le cas du constructeur ou nous passerions un id ayant la valeur zéro. Si on ajoute ce test et que relance le tout (compilation/liaison/tests/couverture de code) nous verrons que ce cas est maintenant couvert et que notre couverture code est passée de 37.5% à 43.8%.

TEST(Employee_Constructor, IdIsZero_ThrowInvalidArgument)
{
    try {
        Employee sample(0, "Joe", "Blow");
        FAIL();
    }
    catch(invalid_argument &err) {
        ASSERT_STREQ("id must be greater than zero.", err.what());
    }
}



On continue, ainsi de suite jusqu'à ce que l'on atteigne une couverture de code satisfaisante.

La démo présentée ci-dessus est disponible sur mon git : https://github.com/jeremydumais/CPP-CMake-CodeCoverage-Demo