Qt et Gitlab, perfect config
20 Dec 2021Dans 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 ?
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 !
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épertoireconan-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)
CMakelist.txt
Quelques explications à propos du fichier -
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
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.
.gitlab-ci.yml
Fichier 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
- Les tests reussis
- Le message d’information si un test échoue
- Le pourcentage de couverture du code
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.
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