Débogage et Tests#

Prélude#

  • Exercices PLaTon: directement dans Jupyter!

Résumé des épisodes précédents …#

Pour le moment nous avons vu les concepts suivants :

  • Contrôle du flot d’exécution : instructions conditionnelles et itératives, fonctions

  • Gestion des données : variables, tableaux, collections, entrées et sorties, fichiers

  • Méthodologie de développement : fonctions, documentation, test

Pourquoi aller plus loin?

Passage à l’échelle!

Écrire des programmes corrects

Corriger les erreurs : le débogage#

Exemple#

Le bout de code suivant contient plusieurs erreurs. Comment pourrait-on s’y prendre pour les trouver?

using namespace std;
#include <iostream>
/** Teste si mot est un palindrome
 * @param mot une chaîne de charactères
 * @result un booléen
 **/
bool estPalindrome(string mot) {
    int n = mot.size()
    bool resultat = true;
    for ( int i = 0; i < n/2; i++ ) {
        if ( mot[i] != mot[n-i] ) {
            resultat = false;
        } else {
        resultat = true; }
    }
    return true;
}
CHECK( estPalindrome("kayak") )
CHECK( not estPalindrome("bonjour") )
CHECK( estPalindrome("aa") )
CHECK( estPalindrome("aaaaa") )
CHECK( estPalindrome("") )
CHECK( estPalindrome("abba") )
CHECK( estPalindrome("aba") );
CHECK( not estPalindrome("abby"));

Erreurs trouvées

  • Erreur de syntaxe

  • Erreurs de sémantique

Stratégies mises en œuvre

  • Compiler le code

  • Exécuter le code et regarder les messages d’erreurs

  • Faire des tests

  • Lire en détail le code

  • Tracer l’exécution du code

Débogage, selon le type d’erreur#

Erreurs de syntaxe#

Symptômes :

Détectées par l’interpréteur ou le compilateur

Débogage :

  1. Bien lire les messages d’erreurs

  2. ⚠️ Le compilateur pointe vers là où il détecte l’erreur⚠️
    Pas forcément là où est l’erreur

Erreurs à l’exécution#

Exemple : exemple-segmentation-fault.cpp

Symptômes :

  • Segmentation fault!

  • Exceptions : division par zéro, …

Débogage :

  • Analyser l’état du programme au moment de l’erreur

  • En Python, Java, … : regarder la pile d’appel («strack trace»)
    En C++ aussi avec la bonne option de compilation :

    g++ -g -fsanitize=address exemple-segmentation-fault.cpp -o exemple-segmentation-fault  
    
  • En C++ : Utilisation du débogeur!
    Voir plus loin.

Erreurs sémantiques#

Symptômes :

  • Le programme s’exécute «normalement»
    mais le résultat est incorrect

  • Le programme ne fait pas ce que le programmeur souhaitait

  • Rappel : le programme fait ce que le programmeur lui a demandé!

Danger

Difficulté

Isoler une erreur glissée dans :

  • Un programme de millions de lignes

  • De grosses données

  • Des milliards d’instructions exécutées

Un travail de détective!

  • Peut être très frustrant, surtout sous stress

  • Peut être une très belle source de satisfaction

  • Requiert de gérer ses émotions, et celle de son équipe …

Stratégies de débogage#

Stratégie : tracer l’exécution du programme#

Étapes :

  1. Instrumenter le programme
    À notre niveau : insérer des affichages avec cout ou cerr

  2. Exécuter le programme

  3. Analyser le journal d’exécution (log)
    À notre niveau : regarder ce qui est affiché

Exemple :

using namespace std;
#include <iostream>
bool estPalindrome(string mot) {
    int n = mot.size();
    bool result = true;
    for ( int i = 0; i < n/2; i++ ) {
        // Instrumentation du code
        cout << i << " ";
        cout << result << " ";
        cout << mot[i] << " ";
        cout << mot[n-1-i] << endl;
        //
        if ( mot[i] != mot[n-1-i] ) {
            result = false;
        } else {
    result = true;}
    }
    return result;
}
estPalindrome("abcddcbz")

Indication

Avantages

  • Simple à mettre en œuvre

Attention

Inconvénients

  • Modification du programme, recompilation et réexécution chaque fois que l’on souhaite changer ce que l’on observe

Stratégie : utiliser le débogueur#

Exemple : analyse post-mortem d’une erreur à l’exécution ♣#

Dans un terminal, on compile le programme (noter l’option -g) :

$  clang++ -g exemple-segmentation-fault.cpp -o exemple-segmentation-fault

Quand on l’exécute, le programme plante :

$  ./exemple-segmentation-fault
segmentation fault (core dumped)  ./exemple-segmentation-fault

«core dumped» : une copie de la mémoire du programme au moment du plantage (core) a été enregistrée dans un fichier (dumped).

Attention

Complication technique

Selon la configuration du système, le fichier peut s’appeler :

  • core

  • core.<numéro du processus>

  • /var/lib/apport/coredump/core...exemple-segmentation-fault...

  • /var/lib/systemd/coredump/core.exemple-segmentation-fault...

  • … ou ne pas être enregistré du tout du fait de limites système :

$  ulimit -c
$  ulimit -c unlimited

Utilisons le débogueur pour analyser cet état post-mortem (après plantage) :

$  gdb --tui --silent exemple-segmentation-fault

(gdb) core core
...
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055ff8b3aa1b3 in main () at exemple-segmentation-fault.cpp:7
7       v[i] = 1;
(gdb) p i
$1 = 10

Exemple : exécution pas à pas#

Dans un terminal, on compile le programme (noter l’option -g) :

$  clang++ -g est-palindrome.cpp -o est-palindrome

Quand on l’exécute, le résultat est incorrect :

$  ./est-palindrome
abcddcbz est un palindrome

Lançons le débogueur pour observer l’exécution du programme :

$ gdb --tui --silent est-palidrome
GNU gdb (Ubuntu 8.3-0ubuntu1) 8.3
(gdb) b 13
Breakpoint 1 at 0x1250: file est-palindrome.cpp, line 13.

(gdb) start
Temporary breakpoint 2, main () at est-palindrome.cpp:25
25      string s = "abcddcbz";

(gdb) c
Continuing.
Breakpoint 1, estPalindrome (mot=...) at est-palindrome.cpp:13
13      int n = mot.size();

(gdb) n
14      bool result = true;

(gdb) n
15      for ( int i = 0; i < n/2; i++ ) {

(gdb) n
16          if ( mot[i] != mot[n-i-1] ) {

(gdb) p i
$1 = 0

(gdb) p mot[i]
$2 = ... 'a'

(gdb) p mot[n-i-1]
$3 = ... 'z'

(gdb) n
17              result = false;

(gdb) n
15      for ( int i = 0; i < n/2; i++ ) {

(gdb) n
16          if ( mot[i] != mot[n-i-1] ) {

(gdb) p mot[i]
$4 =  'b'

(gdb) p mot[n-i-1]
$5 =  'b'

(gdb) n
19      result = true;}

Résumé#

Un outil très puissant : le débogueur pas à pas

Usages

  • Analyse post-mortem après un plantage du programme

  • Observation du programme en cours de fonctionnement

Outils

  • En C++ : gdb

  • En Python : module pdb

  • Les Environnements de Développement Intégrés (Code::Blocks, …) fournissent des interfaces graphiques confortables pour utiliser le déboggueur.

Attention

Inconvénients

  • Un peu lourd à mettre en œuvre

  • Pas encore intégrée dans Jupyter pour C++

  • Seule l’analyse post-mortem est fonctionnelle sur le service JupyterHub

Indication

Avantages

  • Contrôle fin de l’exécution :

    • En passant à la ligne suivante (next)

    • En rentrant dans les sous fonctions (step)

    • Jusqu’au prochain points d’arrêts (conditionnels)

  • Contrôle fin de l’affichage

    • Suivi des variables

    • Analyse de la pile d’exécution

    • Analyse de l’état de la mémoire

  • Pas besoin de recompiler

Mode d’emploi#

  • Compiler avec l’option -g :

    clang++ -g monprogramme.cpp -o monprogramme
    
  • Lancer le débogueur avec :

    gdb --tui --silent monprogramme
    

    (--tui: text user interface; --silent: ne pas afficher copyright et message d’introduction au démarrage)

  • Commandes du débogueur (raccourcis : s, n, c, b l, p expr, q, …)

    start        // lance le programme en pas à pas
    next         // instruction suivante
    step         // instruction suivante; entre dans les fonctions
    continue     // continue jusqu'au point d'arrêt suivant
    break l      // ajoute un point d'arrêt ligne `l` 
    print expr   // affiche la valeur de l'expression
    refresh      // rafraîchit l'affichage
    quit         // quitte
    
  • Commandes plus avancées ♣

    display expr // affiche la valeur de l'expression (persistant)
    continue     // continue l'exécution du programme
    show locals  // affiche les variables locales
    where full   // affiche la pile d'appel
    help         // aide en ligne
    

Stratégie : réduire le problème par dichotomie#

  1. Caractériser le bogue : «lorsque j’appelle telle fonction avec tels paramètres, la réponse est incorrecte»
    En faire un test!

  2. Faire une expérience pour déterminer dans quelle «moitié» du programme est l’erreur.

  3. Trouver le plus petit exemple incorrect
    En faire un test!

  4. Exécuter pas à pas la fonction sur cet exemple

  5. Trouver l’erreur

  6. Corriger l’erreur

  7. Vérifier les tests (non régression)

  8. Rajouter des tests?

Indication

Astuce Être efficace dans la boucle essai-erreur!!!

Gagner du temps : développement piloté par les tests#

http://fr.wikipedia.org/wiki/Test_Driven_Development

Indication

Bonne pratique durant le développement Pour ajouter une nouvelle fonctionnalité :

  • Écrire les spécifications (typiquement sous forme de javadoc!)

  • Écrire le test correspondant

  • Attention aux cas particuliers!

  • Le développement est terminé lorsque les tests passent

Indication

Bonne pratique durant le débogage Pour corriger un bogue signalé :

  • Écrire un test qui met en évidence le bogue

  • Le débogage est terminé quand les tests passent

Tests : pour aller plus loin ♣#

Approche qualité traditionnelle ♣#

  • Équipes très hiérarchisées :

    • Analystes programmeurs

    • Équipe de test

    • Développeur

  • Procédures strictes (voire lourdes)

  • Évolution lente, planifiée longuement à l’avance

Méthodes agiles 2000- ♣#

Exemple : http://fr.wikipedia.org/wiki/Extreme_programming

image programmation agile

Objectif : remettre le développeur au coeur du processus

  • Créativité

  • Responsabilité

  • Auto assurance : Tests!

Les tests sont votre outil de libération!

Les objectifs et types de tests ♣#

  • Mesurer la qualité du code

  • Mesurer les progrès

  • Formaliser (et anticiper!) les demandes du client
    Tests fonctionnels
    Cahier de recette

  • Garantir la robustesse d’une brique avant d’empiler dessus
    Tests unitaires

  • Anticiper la mise en commun de plusieurs briques
    Tests d’intégration

  • Anticiper la mise en production
    Tests de charge

  • Alerte immédiate si l’on introduit un bogue
    Tests de non régression

Il faut aller plus loin! ♣#

Résumé#

Types d’erreur#

  • Erreurs de syntaxe

  • Erreurs à l’exécution

  • Erreurs sémantiques

Stratégies de déboggage#

  • Prévention : dévelopement incrémental

    • ne jamais trop s’éloigner d’une version qui marche

  • Réduire le problème par dichotomie

  • Analyser les plantages

    • pile d’appel

    • débogueur

  • Observer l’exécution :

    • traçage

    • débogueur pas à pas

Tests#

  • Pour du code robuste

  • Pour mesurer la qualité (suivre les progrès et régressions)

  • Pour éviter ou simplifier le débogage

  • Pour être serein

Tests automatique#

  • Pour être efficace

  • Au S1 : CHECK( f( ... ) == ... );

  • Au S2 : une bibliothèque de tests unitaires plus riche

#include <iostream>
using namespace std;

cout << "machin" << 1+3 << endl;
#include <strstream>
using namespace std;
ostringstream cout;
cout << "machin" << 1 + 3 <<  endl; 
cout.str()
int r = 1
1 + 1
1 + 1 ;
r = 2
CHECK( 1 == 1 )
CHECK( 2 == 2 )
1 + 1
2 + 2