Débogage et Tests

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

Pour le moment nous avons vu les concepts suivants:

  • Lecture, écriture

  • Instructions conditionnelles et itératives

  • Fonctions

  • Variables, tableaux, collections

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();     
    for ( int i = 0; i < n/2; i++ ) {
        if ( mot[i] != mot[n-1 -i] ) {
            return false;
        }
    }
    return true;
}
CHECK( estPalindrome("a") );
CHECK( estPalindrome("1") );
CHECK( not estPalindrome("am") );
CHECK( not estPalindrome("yo") );
CHECK( estPalindrome("ressasser") );
CHECK( not estPalindrome("jaune") );
CHECK( estPalindrome("aa") );
CHECK( estPalindrome("non") );
CHECK( estPalindrome("ici") );
CHECK( estPalindrome("ala") );
CHECK( estPalindrome("kayak") );
estPalindrome("ressasser")

Erreurs trouvées:

  • Erreur de syntaxe

  • Erreurs de sémantique

Stratégies mises en œuvre:

  • [x] compiler le code

  • [x] exécuter le code et regarder les messages d’erreurs

  • [x] faire des tests

  • [x] lire en détail le code

  • [x] tracé 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:

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!

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é!

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

  • 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")

Avantages:

  • Simple à mettre en œuvre

Inconvénients:

  • Modification du programme, recompilation, et reexé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):

$  g++ -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»: le programme a laissé un fichier, appelé core, décrivant son état au moment du plantage.

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

$ gdb --tui 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):

$  g++ -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 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 essentiel: 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.

Inconvénients:

  • Un peu lourd à mettre en œuvre

  • Pas encore disponible dans un notebook Jupyter

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

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:

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

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

    start        // lance le programme en pas à pas
    step         // instruction suivante; rentre dans les fonctions
    continue     // continuer jusqu'au point d'arrêt suivant
    next         // instruction suivante
    break l      // ajoute un point d'arrêt ligne `l` 
    print expr   // affiche la valeur de l'expression
    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?

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

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

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

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

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

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

  • Réduire le problème

  • Dévelopement incrémental:
    ne jamais trop s’éloigner d’une version qui marche

  • Analyser les plantages

    • débogueur

  • Observer l’exécution:

    • traçage

    • débogueur pas à pas

Tests

  • Pour du code robuste

  • 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