TD05 : Tests

Ce TD va vous apprendre à programmer une batterie de tests pour tester le bon fonctionnement de la bibliothèque game.

Exercice 1 : activité préliminaire

Considérez l'exemple d'une structure de données "file" (ou queue en anglais), tel que les éléments les premiers entrés sont aussi les premiers sortis (First In, First Out) : https://github.com/orel33/queue.

  • Faites un clone de ce projet Git.

Ce petit projet se compose de plusieurs fichiers, dont voici une brève description :

  • le module queue (queue.c + queue.h)
  • un exemple d'utilisation de la queue (sample.c)
  • un fichier de tests du module queue (test_queue.c)
  • le fichier CMakeLists.txt pour compiler ce projet

La compilation du projet et l'exécution des tests se fait de la manière suivante :

$ mkdir build ; cd build
$ cmake .. ; make         # compilation
$ make test               # lancement des tests
  • Analysez en particulier le code des fichiers queue.h, test_queue.c.
  • Dans le fichier CMakeLists.txt, comprenez le rôle des commandes add_test() ainsi que de la commande enable_testing() en préambule.

La commande make test va lancer l'exécution de tous les tests définis dans CMakeLists.txt et qui sont implémentés dans le fichier test_queue.c. La fonction main() prend en paramètre le nom du test à exécuter (init_free, push, pop, length, empty) et appelle la fonction de test associée.

Ainsi, l'exécution de la commande ci-dessous exécute un test, qui a pour objectif de vérifier le code de la fonction queue_length() de notre module queue :

$ ./test_queue length        # execution du test "length"
$ echo $?                    # afficher le code de retour de la commande précédente
0                            # 0 => success!

On considère qu'un test est réussi (success) si et seulement si le code de retour du programme de test est égal à 0, c'est-à-dire que la fonction main() renvoie la valeur EXIT_SUCCESS (ou 0). Toutes les autres valeurs de retour correspondent à des cas d'erreur !

En résumé :

  • 0 ou EXIT_SUCCESS, l'exécution du test s'est terminé normalement en ne détectant aucun problème.
  • 1 ou EXIT_FAILURE, l'exécution du test s'est terminé normalement en détectant un problème.
  • Des valeurs supérieures à 128 correspondent à des terminaisons anormales (programme de test tué par un signal), liés à une erreur grave comme un accès illégal à la mémoire (Segmentation Fault, 139), une division par zéro (Floating Point Exception, 136), ...

Plus l'écriture d'un test est "précise", plus il doit être facile de conclure qu'il y a un bug dans la fonction testée (et non pas dans une autre fonction). L'ensemble des tests doit couvrir toutes les fonctions de notre module et être le plus exhaustif possible !

Notez que la commande make test va appeler le framework CTest (intégré à CMake) qui affiche un petit rapport d'exécution de tous les tests...

$ make test
Running tests...
Test project /home/orel/Documents/pt2/queue/build
    Start 1: test_queue_init_free
1/5 Test #1: test_queue_init_free .............   Passed    0.00 sec
    Start 2: test_queue_push
2/5 Test #2: test_queue_push ..................   Passed    0.00 sec
    Start 3: test_queue_pop
3/5 Test #3: test_queue_pop ...................   Passed    0.00 sec
    Start 4: test_queue_length
4/5 Test #4: test_queue_length ................   Passed    0.00 sec
    Start 5: test_queue_empty
5/5 Test #5: test_queue_empty .................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 5

Total Test time (real) =   0.01 sec

En principe, les messages d'erreurs dans vos tests sont écrits sur la sortie d'erreur avec la commande fprintf(stderr, ...) contrairement à printf() qui écrit sur la sortie standard.

  • Pour terminer, nous allons essayer de détecter un petit bug dans le code de la queue. Pour ce faire, il faut basculer dans le dépôt Git sur la branche bugtofix.
$ git switch bugtofix     # on change de branche
$ git branch              # affichage des branches
 * bugtofix               # <--- la branche courante
   master
  • Recompilez le code (qui a changé) et relancez les tests...
  • A l'aide de git diff master, identifiez le bug en comparant le contenu de la branche courante à la branche master.
  • Corrigez ce bug, en décommentant la ligne incriminée... Et relancez les tests ! Ouf, c'est corrigé :-)

Exercice 2 : un premier test pour son projet

En vous inspirant de l'activé préliminaire, chaque étudiant doit maintenant ajouter un premier test dummy dans son projet. Cet exercice fait l'objet d'un rendu individuel sur Moodle, mais il est néanmoins demandé d'effectuer ce travail dans le dépôt Git du projet de son équipe.

  • Chaque étudiant doit ajouter le programme de test game_test_<login>.c, qui produit un exécutable game_test_<login>, prenant en argument le nom d'un test <testname>, dummy dans cet exercice. (On remplacera <login> par son identifiant académique.)
  • Ajoutez dans son fichier game_test_<login>.c une fonction test_<testname>() qui code le test dummy. Ce test ne fait rien de particulier et renvoie juste EXIT_SUCCESS (ou 0) quand on appelle le programme game_test_<login>, en lui passant dummy en argument.
  • Attention, votre programme game_test_<login> doit obligatoirement renvoyer EXIT_FAILURE (ou 1), si on ne lui passe aucun argument ou si on lui passe un nom de test inconnu en argument !

Par exemple :

$ ./game_test_<login> dummy   # lancement explicite du test dummy
$ echo $?                     # afficher la valeur de retour
0
  • Chaque étudiant doit ensuite intégrer son test dans CMakeLists.txt à l'aide de la commande add_test(...), en lui donnant précisément le nom test_<login>_<testname> dans ce fichier.
  • Notez qu'il faut ajouter au préalable dans le fichier CMakeLists.txt la commande enable_testing() pour activer la gestion des tests dans CMake.
  • On prendra soin de régénérer le fichier Makefile avec la commande cmake à chaque modification du fichier CMakeLists.txt.
enable_testing()
# ...
add_test(test_<login>_<testname> ./game_test_<login> <testname>)
  • Vérifiez alors que votre test s'exécute correctement à l'appel de la commande make test, ou plus spécifiquement à l'appel de la commande ctest -R test_<login>_dummy qui exécute uniquement ce test-là.
  • Une fois que vous avez finalisé ce travail, vous devez soumettre dans le fichier rendu.txt sous Moodle votre REPOSITORY et le COMMIT intégrant votre test dummy fonctionnel (pas forcément celui de tous les membres de l'équipe).

Exercice 3 : mise en œuvre des tests sur votre projet

Chaque équipe doit maintenant se répartir équitablement l'ensemble des tests pour la bibliothèque game, c'est-à-dire les tests unitaires des fonctions définies dans game.h et game_aux.h. On se limitera à tester chaque fonction de la bibliothèque game pour des paramètres valides.

  • Chaque étudiant doit intégrer ses propres tests dans son fichier game_test_<login>.c (en plus du test dummy) et mettre à jour le fichier CMakeLists.txt tel que les commandes suivantes réalisent les objectifs suivants dans le répertoire build :
$ make                   # compilation de tout votre projet, tests y compris
$ make test              # exécution de tous les tests
$ ctest                  # même chose
$ ctest -R <login>       # exécution des tests de l'étudiant <login>

Voici les consignes générales :

  • Dans le GitLab de votre projet, sélectionnez le menu Issues, et commencez par ajouter un jalon (ou Milestones) Tests, puis ajoutez des tickets (ou Issues) associés à ce jalon pour l'ensemble des tests à implémenter correspondant aux fonctions de game.h et de game_aux.h. Effectuez ensuite la répartition de ces tickets entre chaque étudiant. Une fois un test implémenté, fermez ce ticket et continuez... Lire l'annexe ci-dessous avant de commencer.
  • Implémentez ces tests, conformément aux consignes de l'exercice précédent. Vous pouvez vous inspirer de l'exemple donné en Annexe.
  • Ajoutez dans votre CMakeLists.txt la ligne include(CTest) en plus de enable_testing()... Cela permet d'ajouter des cibles supplémentaires dans votre Makefile, dont la cible make ExperimentalMemCheck, qui permet de contrôler la gestion de la mémoire avec l'outil valgrind. Cela permet d'exécuter vos tests tout en analysant les accès mémoire et ainsi de détecter des bugs supplémentaires liés à des fuites mémoires ou à des accès invalides.
  • Rendez sur Moodle votre travail (rendu collectif).

Remarques :

  • Vos tests sont évalués sur leur capacité à trouver un maximum de bugs dans les fonctions de la bibliothèque game. Pour ce faire, nous avons produit plusieurs versions du code de cette bibliothèque, dans lesquelles nous avons introduit volontairement des bugs plus ou moins discrets. Plus vos tests sont en mesure de détecter ces bugs, meilleure sera votre note. Ces tests vous permettront par la suite de chasser les bugs dans votre propre implémentation de la bibliothèque game.
  • Attention, à bien configurer Git afin que les logs (git log) affichent correctement votre nom et votre email académique. Ce dernier point est particulièrement important, car les corrections automatiques VPL utilisent cette information pour vous évaluer !
  • Attention, certaines fonctions sont faciles à tester et d'autres sont plus difficiles, et encore pour certaines fonctions, le test n'est pas vraiment réalisable sans outils externes, par exemple game_print() ou game_delete(). Dans ce cas, on se contentera juste d'appeler la fonction pour vérifier qu'elle ne provoque pas d'erreur grave (comme segmentation fault).

⚠️ Pour le bon fonctionnement de la correction automatique avec VPL, le fichier libgame.a doit obligatoirement se trouver à la racine de votre dépôt Git.

Annexes

Un exemple de fonction test

Pour préciser nos attentes, voici un exemple de test (à compléter) pour la fonction game_is_empty() du jeu Takuzu, utilisé en 2022-2023 :

bool test_is_empty(void)
{
  game g = game_default();
  bool test1 = game_is_empty(g, 0, 0);
  bool test2 = !game_is_empty(g, 5, 5);
  game_delete(g);
  return test1 && test2;
}

Utilisation des tickets GitLab

GitLab dispose d'un gestionnaire de tickets très performant, accessible via le menu Issues de l'interface web de votre projet.

Pour l'ensemble des tâches à réaliser dans votre projet cette année, il est demandé d'ouvrir systématiquement des tickets (ou Issues) dans GitLab, et de les associer à un jalon (ou Milestone). Ces jalons représentent les grandes étapes du développement de votre projet (Tests, IO, Solve, ...), ou encore de nouvelles versions (V1, V2). De plus, il est possible de saisir une date limite pour les tickets ou les jalons.

Chaque ticket peut être affecté (Assignement) à un membre du projet, qui sera responsable de documenter cette tâche, de la réaliser, puis de clôturer le ticket quand la tâche sera accomplie.

En outre, il est demandé d'associer à chaque ticket tous les commits reliés. Cela peut se faire simplement en mentionnant le numéro du ticket dans le message du commit précédé d'un mot-clé comme close ou fix. Par exemple, pour le ticket #3 :

# associer un commit à un ticket sans le fermer
$ git commit -m "bla bla bla bla (related to #3)"
# associer un commit à un ticket, puis le fermer automatiquement
$ git commit -m "bla bla bla bla (close #3)"
$ git commit -m "bla bla bla bla (fix #3)"
$ git commit -m "bla bla bla bla (implement #3)"

Plus d'info sur les mots-clés que vous pouvez utiliser pour la fermeture automatique des tickets : default pattern closing.

Afin de vous organiser, le Board dans GitLab offre une vision d'ensemble des tickets ouverts et fermés, et permet de suivre l'avancement du projet. Afin de faciliter la lecture du Board, il est possible de créer des Labels colorés et de les associer aux tickets, comme par exemple dev en bleu et bug en rouge. Ainsi, apprenez à utiliser correctement cet outil de gestion de projets pour collaborer efficacement avec votre équipe !

Documentation : https://docs.gitlab.com/ee/user/project/issues/managing_issues.html

Bonus : Inspection des tests et débogage

  • Créez dans votre projet un sous-répertoire .vscode et copiez-y ce fichier launch.json. Grâce à lui vous allez pouvoir lancer en mode débug les exécutables ainsi que vos tests :

Demo debug d'un projet cmake

NB: Vérifiez que vous avez installé les plugins "CMake" et "CMake Tools" dans vscode.