Blog

Lecture

Étude de la qualité du code C++ chez Green Systèmes

Cet article s'adresse principalement aux devs, mais aussi à toute personne à laquelle le titre n'évoque rien, si ce n'est de la curiosité.

Pourquoi et comment évaluer la qualité du code

Lors du développement d'un logiciel desktop (installé sur votre ordinateur) ou SAAS (Software As A Service), on effectue une phase appelée l'intégration continue (CI = Continuous Integration). L'objectif est de préparer au mieux un logiciel pour la livraison aux clients.

Cette phase est la plupart du temps automatisée et intégrée à la solution de versionnage du code (VCS = Version Control System). Les VCS les plus répandus sont Github et Gitlab, reposant sur le système Git. Chez Green Systèmes, nous utilisons Gitlab avec des builders (machines pour le CI) Raspberry Pi.

Les tests sont composés de trois étapes :

  • Les tests unitaires (Unit tests): Le code est testé par petite portion pour vérifier que le résultat de cette portion sera toujours bon,
  • La couverture du code (Code coverage): Indique quel pourcentage de code a été testé. On sait alors quelle proportion reste à tester pour atteindre les 100%,
  • La compilation release: Le code source (lisible par les humains) est transformé en code machine (lisible par la machine) pour être livré aux clients.

Ce sont les deux premières étapes qui permettent d'évaluer la qualité du code. C'est-à-dire que le logiciel est quasi sans bug. Cependant, il y a toujours une possibilité de bugs externe au programme, par une bibliothèque utilisée dans le programme par exemple. Une bibliothèque est un ensemble de fonctionnalité qu'il est possible d'intégrer à un programme pour éviter de tout redévelopper.

Il existe aussi pour les langages de programmation dits compilés, comme le C++, une phase d'analyse de la mémoire. Celle-ci vérifie que la gestion de la mémoire est complète et n'entraine pas de pertes d'emplacements mémoires. Des logiciels tels que Valgrind peuvent être utilisés. Cette partie n'est pas détaillée dans cet article.

Dans notre cas, nous effectuons des tests et de la couverture de code aussi bien sur la Green Solution (SAAS), que sur les programmes de la Green Box.

Cas d'exemple d'un programme en C++ sous Linux

Cette partie d'illustration s'adresse plus aux devs, mais elle reste lisible par tous. Il s'agit d'une version résumé de notre configuration pour éxécuter les tests et la couverture de code de l'un des programmes fonctionnant dans la Green Box.

Les devs devront avoir un peu de connaissances en CMake, Docker, Gitlab CI et Bash. Cependant, vous pourrez aussi facilement faire la même chose avec d'autres environnements et "d'autres langages". Si vous utilisez Windows, cette partie sera sûrement différente.

Faire une compilation adaptée aux tests

Afin de rendre un programme C++ compilé et adapté à subir des tests et du code coverage, vous devez ajouter à g++ ou c++ les options : -fprofile-arcs -ftest-coverage. Ces options permettent de générer des fichiers utiles aux phases de tests et de code coverage. Ces fichiers permettent de lier le code C++ aux adresses du programme compilé.

Nous avons aussi fait le choix d'ajouter l'option -DBUILD_TEST, ajoutant certaines fonctionnalités pour faciliter les tests. Par exemple, nous avons des méthodes permettant des accès particuliers sur des listes d'objets, inutiles dans l'éxécution normale du programme.

Ainsi, il suffit de mettre dans le CMakeLists.txt :

if( BUILD_TEST )
    set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -DBUILD_TEST")
endif()

Attention, si vous avez préalablement défini des options pour la variable CMAKE_CXX_FLAGS, il faudra faire set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "-fpro[...]").

Ensuite, il faut définir l'inclusion de fichiers de tests :

set(SOURCE_FILES
        src/entities/ClassA.cpp
        src/entities/ClassA.h
        src/entities/ClassB.cpp
        src/entities/ClassB.h
        src/Manager.cpp
        src/Manager.h)

if( BUILD_TEST )
    set( SOURCE_FILES ${SOURCE_FILES}
        src/tests/testClassA.cpp
        src/tests/testClassB.cpp
    )
else()
    set( SOURCE_FILES ${SOURCE_FILES} main.cpp )
endif()

Puis, définir la bonne liaison des bibliothèques, la bibliothèque Google Tests dans notre cas :

if( BUILD_TEST )
    target_link_libraries(${PROJECT_NAME} libgtest.a libgtest_main.a libA.a libB)
else()
    target_link_libraries(${PROJECT_NAME} libA.a libB)
endif()

Vous avez remarqué ? Nous utilisons à chaque fois : if( BUILD_TEST ). Cette condition permet de customiser un peu la compilation. Pour activer cette condition, il suffit de l'ajouter à CMake : cmake -DBUILD_TEST=ON.

Tester le code et sa couverture

Là, notre logiciel se compile sans problème dans les deux cas : test et release.

Pour la couverture, nous utilisons gcov et lcov, des outils inclus dans la GNU Compiler Collection.

gcov prépare les fichiers utiles à la couverture.

lcov calcule la couverture.

Les commandes à appeler sont :

    # On prépare les fichiers de compilation
cmake -DBUILD_TEST=ON ..
    # On compile (1 job/cœur de CPU)
make -j `nproc`
    # On prépare pour la couverture
gcov CMakeFiles/VOTRE_PROGRAMME.dir/src/*
    # On lance les tests, car le programme est compilé ainsi
./VOTRE_PROGRAMME
    # On génère la couverture
lcov -t "Result" -o coverage.info -c -d CMakeFiles/VOTRE_PROGRAMME.dir/src
    # On l'affiche
lcov --list coverage.info

 

À ce point, on sait si les tests fonctionnent correctement et quelles parties du code sont réellement testées.

Cependant, le pourcentage de couverture indiqué n'est pas bon. Par défaut, il inclut tous les headers et les fichiers de tests. Pour remedier à ce problème, il suffit de supprimer les fichiers non utiles au calcul de la couverture (à mettre à la suite des fonctions précédentes) :

    # On extrait les fichiers de la couverture
lcov --list-full-path -l coverage.info > output
    # On liste les fichiers header système
listExternal=`cat output |grep '^/usr/' | cut -d' ' -f1 | cut -d'|' -f1`
    # On enlève les fichiers header système
lcov --remove coverage.info $listExternal -o coverage.info
    # On liste les fichiers de tests
listTests=`cat output |grep 'tests/' | cut -d' ' -f1 | cut -d'|' -f1`
    # On enlève les fichiers de tests
lcov --remove coverage.info $listTests -o coverage.info
    # On génère un rendu HTML des résultats du code couvert
genhtml -o html coverage.info

 

Intégration à Gitlab CI

Maintenant que nous avons tous les éléments en main, il suffit d'intégrer ça dans le CI. Dans notre cas, nous utilisons un container Docker pour compiler nos logiciels.

Pour ceux qui n'utiliserait pas Docker, il suffit juste d'appeler les commandes vues précédemment dans la section script associée au code coverage.

stages:
  - build

variables:
  CONTAINER_VERSION_PROD_IMAGE: image/docker:prod-$CI_BUILD_REF_NAME
  CONTAINER_VERSION_DEV_IMAGE: image/docker:dev-$CI_BUILD_REF_NAME
  CONTAINER_VERSION_TEST_IMAGE: image/docker:test

buildContainerDev:
  image: docker:latest
  tags:
    - arm
    - docker-builder
  stage: build
  script:
    - docker build -t $CONTAINER_VERSION_DEV_IMAGE .
    - docker push $CONTAINER_VERSION_DEV_IMAGE
  except:
    - tags


buildContainerProd:
  image: docker:latest
  tags:
    - arm
    - docker-builder
  stage: build
  script:
    - docker build -t $CONTAINER_VERSION_PROD_IMAGE .
    - docker push $CONTAINER_VERSION_PROD_IMAGE
  only:
    - tags

unitTest:
  image: docker:latest
  tags:
    - arm
    - docker-builder
  stage: build
  script:
    - docker build -f test.Dockerfile -t $CONTAINER_VERSION_TEST_IMAGE .
    - docker run -i -v `pwd`:/home/builder --rm $CONTAINER_VERSION_TEST_IMAGE
    - docker rmi $CONTAINER_VERSION_TEST_IMAGE
  artifacts:
    expire_in: 3 week
    paths:
      - build/coverage.zip

Ici, on éxécute une compilation soit de release, soit de debug. La release ne s'effectue que lorsque nous taggons le code, sinon c'est la debug qui s'éxécute. Dans tous les cas, nous effectuons les tests ensuite.

Comme vous pouvez le voir, nous mettons de côté la sortie de la couverture de code dans le fichier coverage.zip grâce à l'artifact. Nous avons juste à ajouter les commandes suivantes :

genhtml -o html coverage.info
zip -r coverage.zip html/

On obtient ainsi le Dockerfile :

FROM tracesoftware/gitlab-builder:arm-cpp

VOLUME /home/builder
ADD . /home/builder

RUN apt-get update && \
    apt-get install -y ca-certificates libc6-dev zlib1g-dev libssl-dev

CMD rm -fr build && \
    mkdir build && \
    cd build && \
    cmake -DBUILD_TEST=ON .. && \
    make -j `nproc`&& \
    gcov CMakeFiles/VOTRE_PROGRAMME.dir/src/* && \
    ./GreenBox_EMS_Plugin && \
    lcov -t "Result" -o coverage.info -c -d CMakeFiles/VOTRE_PROGRAMME.dir/src && \
    lcov --list-full-path -l coverage.info > output && \
    listExternal=`cat output |grep '^/usr/' | cut -d' ' -f1 | cut -d'|' -f1` && \
    lcov --remove coverage.info $listExternal -o coverage.info && \
    listTests=`cat output |grep 'tests/' | cut -d' ' -f1 | cut -d'|' -f1` && \
    lcov --remove coverage.info $listTests -o coverage.info && \
    genhtml -o html coverage.info && \
    zip -r coverage.zip html/ && \
    lcov --list coverage.info

Dans Gitlab, il restera juste à mettre la regex Total:\|(\d+.\d+\%) dans la configuration de votre projet pour actualiser les badges. Plus d'informations dans la documentation.

Voilà :)

À ce stade, votre programme est testé à XX%. L'objectif est d'atteindre au moins 85%, voire 100% de couverture.

Même si l'écriture des tests semble être une perte de temps (elle est en effet au moins aussi longue que l'écriture du programme en lui-même), elle permet de rendre votre code fiable. Au tout début, notre partie SAAS aboutissait souvent sur des erreurs bloquantes. L'ajout des tests et de la couverture de code fût un gain énorme pour la fiabilité et le confort d'utilisation.

Se dire que l'on pourra écrire les tests plus tard EST UNE ERREUR. Cela entrainera forcément une pause totale du développement pour rattraper le retard. Vous vous poserez alors la question : "Combien de temps ça va me prendre ?". Là, il est déjà bien trop tard.

M. Colboc, directeur de Green Systèmes :

La phase d'écriture des tests n'est pas à prendre à la légère. Elle diminue grandement les coûts du service après vente, et évite les vagues de mécontentement liées à l'utilisation d'un logiciel non stable. Ainsi, les clients sont satisfaits et nous pouvons proposer sereinement notre solution.

Partager cet article sur :