Sepro's docs Open documentation

Qt et Gitlab, perfect config

Dans le cadre de mon enseignement de la programmation, je cherche à m’inspirer autant que possible aux outils utilisés actuellement en développement. De plus, je souhaite que les étudiants soignent leur code et s’intègrent dans une démarche de projet réaliste et professionnelle.

Cela se traduit notamment par la vérification de leur code par des tests unitaires et de la couverture (coverage) de celui-ci.

Sur les postes clients Linux, nous utilisons Qt6 pour développer en C++. Nous avons une instance de serveur gitlab pour héberger nos projets et exécuter le pipeline d’intégration continue. L’an passé, j’utilisais Gogs et Jenkins, mais Gitlab propose une solution toute intégrée, et j’ai donc décidé de migrer projets et chaîne CI/CD dessus.

Le post explique mes tâtonnements et surtout la meilleure solution trouvée pour arriver à mes fins. Si vous avez des commentaires pour mieux faire, n’hésitez pas je suis preneur. ;O

L’image ci-dessous résume un workflow basique pour Gitlab. Le post ne couvre pas l’étape de déploiement et pourrait aller plus loin, comme par exemple avec des outils comme cppcheck. Peut-être dans un prochain post ?

Gitlab CI.CD

Outils utilisés

  • Debian : l’OS
  • Qt6 : l’IDE
  • C++ : Le langage, version C11
  • Cmake : Le gestionnaire de configuration de build
  • Ninja : L’outil de construction d’exécutable axé sur la rapidité.
  • Conan : Le gestionnaire de dépendances
  • Catch2 : Le framework de tests unitaire
  • Gitlab : Le serveur Git et d’intégration CI/CD

Projet modèle.

Le but est de créer un projet modèle pour les étudiants qu’ils puissent ensuite l’importer sur leur dépôt pour commencer à travailler. De leur point de vue, tout doit se dérouler automatiquement, avec un minimum de configuration. Le projet modèle utilisé dans ce post est accessible publiquement sur notre Gitlab.

Sitographie

Gitlab est formidable. Par contre, ce n’est pas évident de trouver les informations nécessaires à la création d’applications C++ avec un pipeline d’intégration continue. Heureusement que j’ai pu m’appuyer sur les posts ci-dessous !

Cmake et ninja

Creating C applications with Gitlab CI- SLideshare

Creating C applications with Gitlab CI-Gitlab

Fichiers du projet

Voici l’architecture des fichiers du projet.

├── CMakeLists.txt
├── cmake-modules
│   └── CodeCoverage.cmake
├── conanfile.txt
├── main.cpp
├── readme.md
└── test
    ├── CMakeLists.txt
    └── testProjet.cpp

Qt6

Dans Qt, nous utiliserons la chaîne de compilation cmake (et non pas qmake) pour générer l’exécutable.

qmake est l’outil maison de Qt et est plus facile d’utilisation, notamment lorsque l’on définit de nouvelles classes.

Par contre, impossible d’utiliser qmake dans une chaîne d’intégration continue. Et puis cmake est le standard pour compiler le même code sur des IDE et OS variés.

Fichiers du projet

Je vais décrire les fichiers du projet (dans le désordre !)

main.cpp

Un simple fichier .cpp avec un cout !

#include <iostream>

using namespace std;

int main()
{
    cout << "Modèle de projet CMAKE" << endl;
    return 0;
}

testProjet.cpp

Ce fichier contient les tests unitaires de notre projet. Dans cet exemple (tiré de la documentation de Catch2), la fonction Factorial() est écrite dans le fichier de tests ainsi que les tests unitaires. Bien sur, il est possible d’écrire les tests de fonctions et méthodes dans des modules distincts, il faudra juste les déclarer dans le fichier CMakelist.txt du dossier test.

J’ai choisi d’utiliser le framework Catch2 pour l’écriture des tests unitaires. Le framework s’installe facilement sous Linux, repose sur un seul fichier d’entête et en plus, comme nous le verrons plus loin peut être directement géré par conan

#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file

#include <catch2/catch.hpp>

//Dans le cas d'un projet C, déclarer les librairies avec extern "C"
//extern "C" {

//}

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(4) == 24);
    REQUIRE( Factorial(5) == 120);
    REQUIRE( Factorial(10) == 3628800 );
}

conanfile.txt

Conan est un gestionnaire de packages pour le C++. Mais il est aussi très utile pour gérer les dépendances d’un projet. Le conanfile ci-après décrit que l’on a besoin du paquet catch2 dans notre projet, et que le projet utilise le generator cmake.

L’une des commandes qui sera utilisée pour compiler sera :

conan install -s build_type=Debug -if conan-dependencies ..

Je passe quelques options pour utiliser conan.

  • -s build_type=Debug : Pour générer un exécutable Debug (pour des tests unitaires au top !)
  • -if conan-dependencies : Les fichiers générés par conan seront dans le répertoire conan-dependencies. C’est pour coller à la configuration de Qt6.
[requires]
catch2/2.13.7

[generators]
cmake

cmake-modules/CodeCoverage.cmake

Pour utiliser le code coverage, nous avons besoin de ce module additionnel. Il est fourni par Ryan Pavlik sur son GitHub.

CMakeList.txt, répertoire racine

cmake_minimum_required(VERSION 3.5)

set (CMAKE_BUILD_TYPE "Debug" CACHE
      STRING "Choose the type of build.")

# DEBUT - Remplacer CMAKE_mod par le nom de votre projet
project(CMAKE_mod LANGUAGES CXX)
# FIN

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

#Ajouter les fichiers sources de votre projet
set(SRCS
#   Dossier/classe.cpp
    )

#Ajouter les fichiers headers de votre projet
set(HEADERS
#   Dossier/classe.h
    )

#Ajouter les noms des dossiers de vos modules
#include_directories(Dossier)

# DEBUT
#- Remplacer CMAKE_mod par le nom de votre projet
add_executable(CMAKE_mod main.cpp ${SRCS} ${HEADERS})
# FIN

# Options ######################################################################
option(BUILD_TESTS "Enable to build unit tests" TRUE)

# Testing ######################################################################
if (BUILD_TESTS)

    # Coverage #################################################################
    set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake-modules)
    include(CodeCoverage)
    append_coverage_compiler_flags()
    target_link_libraries(${CMAKE_PROJECT_NAME} gcov)
    setup_target_for_coverage_gcovr_html(
        NAME coverage
        EXECUTABLE ${CMAKE_BINARY_DIR}/test/bin/testProjet
        DEPENDENCIES ${CMAKE_PROJECT_NAME}
        EXCLUDE "build/CMakeFiles/*")

    #enable_testing ()
    #add_subdirectory(test)
endif(BUILD_TESTS)

enable_testing()
include(CTest)
add_subdirectory(test)

Quelques explications à propos du fichier CMakelist.txt

  • Version de cmake minimum

      cmake_minimum_required(VERSION 3.5)
    
  • Pour pouvoir demander la compilation Debug à conan

      set (CMAKE_BUILD_TYPE "Debug" CACHE
          STRING "Choose the type of build.")
    
  • Nom du projet

      # DEBUT - Remplacer CMAKE_mod par le nom de votre projet
      project(CMAKE_mod LANGUAGES CXX)
      # FIN
    
  • la version de C++ du projet

      set(CMAKE_CXX_STANDARD 11)
      set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
  • Définition de deux variables qui contiendront le chemin vers nos bibliothèques ainsi que l’inclusion automatique des bibliothèques aux chemins.

      set(SRCS
      #   Dossier/classe.cpp
          )
    
      #Ajouter les fichiers headers de votre projet
      set(HEADERS
      #   Dossier/classe.h
          )
    
      #Ajouter les noms des dossiers de vos modules
      #include_directories(Dossier)    
    
  • Déclaration de l’exécutable à partir des fichiers sources.

      # DEBUT
      #- Remplacer CMAKE_mod par le nom de votre projet
      add_executable(CMAKE_mod main.cpp ${SRCS} ${HEADERS})
      # FIN
    
  • Déclaration des tests unitaires et du coverage

      # Options ######################################################################
      option(BUILD_TESTS "Enable to build unit tests" TRUE)
    
      # Testing ######################################################################
      if (BUILD_TESTS)
    
          # Coverage #################################################################
          set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake-modules)
          include(CodeCoverage)
          append_coverage_compiler_flags()
          target_link_libraries(${CMAKE_PROJECT_NAME} gcov)
          setup_target_for_coverage_gcovr_html(
              NAME coverage
              EXECUTABLE ${CMAKE_BINARY_DIR}/test/bin/testProjet
              DEPENDENCIES ${CMAKE_PROJECT_NAME}
              EXCLUDE "build/CMakeFiles/*")
    
      endif(BUILD_TESTS)
    
      enable_testing()
      include(CTest)
      add_subdirectory(test)
    

On peut noter que la partie coverage ne s’exécute que si la variable BUILD_TESTS est à TRUE C’est dans cette partie que l’on fait référence au sous-module CodeCoverage de Ryan Pavlik. La configuration est tiré de son readme. J’ai juste ajouté un EXCLUDE pour ne pas avoir le coverage d’un fichier Makefile dans la sortie html

La génération du rapport de code coverage se fait avec la commande

ninja coverage

Pour les tests unitaires, il faut inclure CTest ainsi que le sous répertoire de test qui a son propre CMakelist.txt

CMakelist.txt, dossier test

cmake_minimum_required(VERSION 3.0.2)
project(testProjet)

#Ajouter les dossiers/fichiers.cpp du projet
set(SRCS2
#    ${PROJECT_SOURCE_DIR}/Dossier/file.cpp
    )
#Ajouter les dossiers/fichiers.h du projet
set(HEADERS2
#    ${PROJECT_SOURCE_DIR}/Client_parking/client_parking.h
    )

#Ajouter les dossiers/fichiers.cpp des tests unitaires
set(TST_SRCS
#    TestVoitureE1.cpp
#    TestClientParkingE1.cpp
    testProjet.cpp
    )

include(CTest)

include(${CMAKE_BINARY_DIR}/conan-dependencies/conanbuildinfo.cmake)

conan_basic_setup(TARGETS)


add_executable(${PROJECT_NAME} ${TST_SRCS} ${SRCS2} ${HEADERS2})
#target_link_libraries(${PROJECT_NAME} ${CMAKE_PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} CONAN_PKG::catch2)

#find_package(Catch2 REQUIRED)

#target_link_libraries(testProjet PRIVATE Catch2::Catch2)

#target_include_directories(testProjet PRIVATE ${CMAKE_SOURCE_DIR})

#enable_testing()

#include(Catch)
#catch_discover_tests(testProjet)

add_test(NAME testPro COMMAND ${PROJECT_NAME})
  • De même que pour le CMakelist principal, je définis quelques variables qui permettront aux étudiants d’inclure facilement leur bibliothèques et les tests unitaires associés.

      cmake_minimum_required(VERSION 3.0.2)
      project(testProjet)
    
      #Ajouter les dossiers/fichiers.cpp du projet
      set(SRCS2
      #    ${PROJECT_SOURCE_DIR}/Dossier/file.cpp
          )
    
      #Ajouter les dossiers/fichiers.h du projet
      set(HEADERS2
          )
    
      #Ajouter les dossiers/fichiers.cpp des tests unitaires
      set(TST_SRCS
          testProjet.cpp
          )
    
      add_executable(${PROJECT_NAME} ${TST_SRCS} ${SRCS2} ${HEADERS2})
    
  • Ici, on indique que les dépendances seront gérées avec conan, dans le fameux répertoire conan-dependencies et que Catch2 est une dépendance du projet. J’ai laissé en commentaire les instructions pour ceux qui préfère installer Catch2 sur le PC sans passer par conan.

      include(CTest)
    
      include(${CMAKE_BINARY_DIR}/conan-dependencies/conanbuildinfo.cmake)
    
      conan_basic_setup(TARGETS)
      target_link_libraries(${PROJECT_NAME} CONAN_PKG::catch2)
    
    
  • Ajouter les tests au projet.

      add_test(NAME testPro COMMAND ${PROJECT_NAME})
    

Ma première compilation

Ca y est, le projet est fonctionnel.

Qt

Il peut, out of the box être exécuté sur Qt !

Attention, il y a deux exécutables. Un pour le programme, l’autre pour les tests unitaires.

  • Par défaut, c’est le programme principal qui s’exécute.
  • Pour exécuter les tests unitaires, il faut faire un clic droit sur testProjet -> Exécuter
  • Il reste à exécuter la commande pour la génération du code coverage dans le répertoire de build
ninja coverage

Puis à ouvrir le rapport html généré dans coverage/index.html Cela doit pouvoir s’automatiser d’ailleurs…,O

Code cover

En ligne de commande

En ligne de commande, on retrouve les différents protagonistes de notre chaîne de compilation.

  • conan va gérer les dépendance
  • Cmake va générer les fichiers de compilation pour ninja
  • ninja va compiler, d’abord les deux exécutables, puis ensuite le rapport de code coverage.
mkdir build && cd build
conan install -s build_type=Debug -if conan-dependencies ..
cmake -GNinja ..
ninja
ninja coverage

Voilà ! Déjà un bon bout du chemin est parcouru. Il ne reste plus qu’à pousser le code sur Gitlab et créer un fichier pour le pipeline d’intégration continue.

L’avantage, c’est qu’en se basant sur les outils précédents, l’écriture du fichier de pipeline est presque identique.

Fichier .gitlab-ci.yml

Toutes les options du fichier .gitlab-ci.yml ne sont pas activées. Elles peuvent être utiles pour d’autres besoins.

Quelques explications après le contenu du fichier.

image: sidimage

#pour l'inclusion des sous modules Git
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  
stages:
  - build
  - test
  - coverage
  - check

#Job de compilation
job:build:
#  only:
#    - master
#    - tags
  stage: build
  #Si il y a des actions à lancer avant le script
  before_script:
     - apt update && apt -y install ninja-build python3-pip gcovr
     - python3 -m pip install conan
  script:
    #execution de cmake
    - mkdir build && cd build
    - conan install -s build_type=Debug -if conan-dependencies ..
    - cmake -GNinja ..
    - ninja
 #   - ctest test/testProjet --no-compress-output --output-on-failure --output-junit Test.xml
  #création de l'archive contenant le build téléchargeable
  artifacts:
  #  when: always
    paths:
      - build

#Job pour les tests unitaires   
job:test:
  stage: test
  script:
    - cd build/test/bin
    - ./testProjet --reporter junit --out catch_results.xml
  #  - ctest test/testProjet --no-compress-output --output-on-failure --output-junit Test.xml
  artifacts:
    when: always
    reports:
#      junit: build/Test.xml
      junit: build/test/bin/catch_results.xml
          
# Job pour le coverage de l'application    
run tests:
  stage: coverage
  before_script:
    - apt update && apt -y install ninja-build python3-pip gcovr
    - python3 -m pip install conan
  script:
    - cd build
    - conan install -s build_type=Debug -if conan-dependencies ..
    - cmake -GNinja ..
    - ninja
    - ninja coverage
    - gcovr --xml-pretty --exclude-unreachable-branches --exclude CMakeFiles --print-summary -o coverage.xml --root ${CI_PROJECT_DIR}
  coverage: /^\s*lines:\s*\d+.\d+\%/
  artifacts:
    name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
    expire_in: 2 days
    reports:
      cobertura: build/coverage.xml

Pour arriver à ce fichier, il m’a fallu pas mal de tâtonnements malgré tout !
Tout d’abord, mon gitlab-runner est basé sur une image de sid/debian testing avec quelques paquets pré installés.
Après coup, il va falloir que je la modifie et je vais sûrement pouvoir repasser en debian Stable.
En effet, j’avais choisi une debian testing juste pour avoir la dernière version de cmake qui exportait les rapports en xml, mais finalement les reporters de catch2 le font mieux !

Bref !

  • Dans cette partie, utilisation des git submodules. Ici, cela n’a aucun intérêt, mais c’est toujours pratique de le préciser pour les projets qui ont des sous modules d’autres sources git.

  • Puis 3 stages pour mon pipelines, built, test et coverage.

      #pour l'inclusion des sous modules Git
      variables:
      GIT_SUBMODULE_STRATEGY: recursive
        
      stages:
      - build
      - test
      - coverage
    

stage build

  • Le job de build installe quelques paquets (que j’ajouterai par défaut à mon runner quand j’aurai le temps), et exécute les mêmes commandes de compilation qu’en local. J’ai commenté les lignes only qui peuvent servir si on ne veut exécuter cette tache que sur certaines branches/tag

  • Le job crée un artifact téléchargeable pour finir.

      job:build:
      #  only:
      #    - master
      #    - tags
      stage: build
      #Si il y a des actions à lancer avant le script
      before_script:
          - apt update && apt -y install ninja-build python3-pip gcovr
          - python3 -m pip install conan
      script:
          #execution de cmake
          - mkdir build && cd build
          - conan install -s build_type=Debug -if conan-dependencies ..
          - cmake -GNinja ..
          - ninja
      #   - ctest test/testProjet --no-compress-output --output-on-failure --output-junit Test.xml
      #création de l'archive contenant le build téléchargeable
      artifacts:
      #  when: always
          paths:
          - build
    

stage test

  • Ce stage permet d’exécuter les tests unitaires pour générer un xml exploitable par Gitlab. Ici, c’est directement le reporter de catch qui est utilisé, plus verbeux que ctest, surtout lorsque les tests échouent. (on a alors directement la raison dans l’interface web de Gitlab)

  • La ligne commentée utilisait ctest de la suite cmake. Il faut une version 3.22+ pour générer des rapports xml. Cependant, lorsque les tests échouent, on a pas la raison sans inspecter le pipeline, en raison d’un problème de formatage du xml.

      job:test:
      stage: test
      script:
          - cd build/test/bin
          - ./testProjet --reporter junit --out catch_results.xml
      #  - ctest test/testProjet --no-compress-output --output-on-failure --output-junit Test.xml
      artifacts:
          when: always
          reports:
      #      junit: build/Test.xml
          junit: build/test/bin/catch_results.xml
    

Stage coverage

  • L’exemple est tiré de la très bonne documentation de Gitlab, avec un soupçon de personnalisation ! J’ai juste exclude les fichiers du Makefiles.

  • Les lignes finales (artifact, reports, cobertura) nous permette d’avoir le % de couverture dans le rapport du pipeline.

      # Job pour le coverage de l'application    
      run tests:
      stage: coverage
      before_script:
          - apt update && apt -y install ninja-build python3-pip gcovr
          - python3 -m pip install conan
      script:
          - cd build
          - conan install -s build_type=Debug -if conan-dependencies ..
          - cmake -GNinja ..
          - ninja
          - ninja coverage
          - gcovr --xml-pretty --exclude-unreachable-branches --exclude CMakeFiles --print-summary -o coverage.xml --root ${CI_PROJECT_DIR}
      coverage: /^\s*lines:\s*\d+.\d+\%/
      artifacts:
          name: ${CI_JOB_NAME}-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
          expire_in: 2 days
          reports:
          cobertura: build/coverage.xml
    
    

Résultats sur Gitlab

  • Le pipeline global

Pipeline

  • Les tests reussis

Pipeline test

  • Le message d’information si un test échoue

Pipeline test failed

  • Le pourcentage de couverture du code

Pipeline cover

Les badges.

Pour finir, Gitlab permet d’associer des badges au projet. https://docs.gitlab.com/ee/user/project/badges.html

Ainsi, on peut même avoir sur la page du projet les informations concernant le pipeline et le taux de couverture.

Badges

Next ?

Maintenant, pour mon travail, ce post n’est que de la préparation. Il s’agit désormais de faire bosser les étudiants et les mettre en situation. L’apprentissage de la programmation est ardu, et même si l’enseignant donne des conseils méthodologique ou explique les notions fondamentales, seule la pratique permet de devenir développeur. Je compare souvent cela à l’apprentissage et la maitrise d’un instrument de musique. Vous avez un cours par semaine, mais si vous ne vous entrainez pas tous les jours, jamais vous ne deviendrez musicien !

Et donc, pour ce qui est de la méthode d’enseignement, ils ont un projet en équipe à réaliser en 5 SPRINTS de 5 semaines. Pour les premiers SPRINTS, je leur fournis la conception et les fichiers de tests unitaires. Charge à eux de réaliser les classes et méthodes pour que les tests soient fonctionnels.

Divers

En cas de memory leaks, utiliser valgrind

valgrind ./testProjet --reporter junit --out catch_results.xml