Comment déboguer un petit programme


Note

Ceci est une traduction de l'excellent billet de blog How to debug small programs d'Eric Lippert.

La plupart des mauvaises questions que je vois sur StackOverflow sont de la forme :

J'ai écrit ce programme pour mon TP, et il ne marche pas. (20 lignes de code)

Et... c'est tout.

Si vous lisez ceci, il y a de fortes chances que ce soit parce que moi ou quelqu'un d'autre vous a redirigé⋅e ici depuis votre question sur StackOverflow, juste avant qu'elle soit fermée ou supprimée. (Si vous lisez ceci et que vous n'êtes pas dans cette situation, n'hésitez pas à laisser dans les commentaires vos conseils favoris pour déboguer de petits programmes).

StackOverflow est un site de questions-réponses pour des questions spécifiques sur du code. "J'ai écrit du code bogué que je n'arrive pas à réparer" n'est pas une question, c'est une histoire, pas très intéressante de surcroît. "Pourquoi, lorsque je soustrais un à zéro, obtiens-je un nombre plus grand que zéro, ce qui rend vrai, à tort, la condition ligne 12 ?" est une question spécifique à propos d'un code réel.

Donc vous demandez à Internet de déboguer un programme erroné que vous avez écrit ? Vous n'avez probablement jamais appris à déboguer un petit programme, parce que laissez moi vous dire, ce que vous êtes en train de faire n'est pas une manière efficace de résoudre ce problème. Il est temps d'apprendre à déboguer par vous même, parce que StackOverflow ne va pas déboguer votre programme pour vous.

Je vais supposer que votre programme compile mais que son comportement n'est pas correct, et qui plus est, que vous avez écrit un test qui montre qu'il n'est pas correct. Voici comment trouver le bug.

Tout d'abord, activez tous les avertissements (warnings) du compilateur. Il n'y a pas de raison qu'un programme de 20 lignes produise ne serait-ce qu'un seul avertissement. Avec un avertissement, le compilateur vous dit "ce programme compile mais ne fait probablement pas ce que vous croyez qu'il fait", et puisque c'est précisément la situation dans laquelle vous vous trouvez, vous vous devez d'être attentif⋅ve aux avertissements.

Lisez les attentivement. Si vous ne comprenez pas pourquoi un avertissement est affiché, c'est une bonne question pour StackOverflow parce que c'est une question spécifique à propos d'un code réel. Assurez-vous de poster le texte exact de l'avertissement, le code exact qui le produit, et la version exacte du compilateur que vous utilisez.

Si le programme a toujours un bug, trouvez un canard en plastique. Ou si vous n'en trouvez pas, trouvez un⋅e autre étudiant⋅e en informatique, c'est à peu près la même chose (NdT: ce n'est pas moi qui le dit, c'est Eric Lippert...). Expliquez au canard, en utilisant des mots simples, pourquoi chaque ligne de chaque fonction de votre programme est manifestement correcte. À un certain point, vous serez incapable de le faire, soit parce que vous ne comprenez pas la fonction que vous avez écrite, ou parce qu'elle est erronée, ou les deux. Concentrez vos efforts sur cette fonction ; c'est probablement là qu'est le bug. Sérieusement, la méthode du canard en plastique marche. Et comme le programmeur légendaire Raymond Chen le fait remarque dans son commentaire à ce billet, si vous n'arrivez pas à expliquer au canard pourquoi vous exécutez une instruction particulière, peut-être est-ce que vous avez commencé à programmer avant d'avoir un plan d'attaque.

Une fois que votre programme compile proprement et que le canard n'a plus d'objection, s'il y a toujours un bug, alors décomposez votre code en fonctions plus petites, chacune faisant exactement une opération logique. Une erreur rependue chez les programmeurs, et pas seulement les débutants, est d'écrire des fonctions qui essayent de faire plusieurs choses, et les font mal. De petites fonctions sont plus facile à comprendre et donc il sera plus facile pour vous et pour le canard d'y trouver les bugs.

Lorsque vous ré-écrivez vos fonctions en fonctions plus petites, prenez une minute pour écrire la spécification de chaque fonction. Même si ce n'est qu'une phrase ou deux, une spécification est une aide utile. La spécification décrit ce que fait votre fonction, quelles sont les entrées acceptables, quelles sont les sorties, quels sont les cas d'erreur, etc. Souvent, en écrivant une spécification, vous vous rendrez compte que vous avez oublié de gérer un cas particulier dans une fonction, est c'est là qu'est le bug.

Si le bug est toujours là, commencez par vérifier que toutes vos spécifications décrivent les pré-conditions et les post-conditions de chaque fonction. Une pré-condition est ce qui doit être vrai pour que la fonction puisse fonctionner correctement. Une post-condition est ce qui doit être vrai une fois que la fonction s'est exécutée. Par exemple, une pré-condition peut être "cet argument est un pointeur non nul", ou "la liste passée en entrée doit avoir au moins deux éléments", ou "cet argument est un entier positif". Une post-condition peut être "la liste contient exactement un élément de moins que ce qu'elle avait en entrée" ou "telle partie du tableau est maintenant triée". Si les pré-conditions d'une fonction sont violées, c'est qu'il y a un bug dans la fonction appelante. Si les post-conditions d'une fonction sont violées même lorsque ses pré-conditions sont satisfaites, c'est qu'il y a un bug dans la fonction elle-même. Là encore, en écrivant les pré-conditions et les post-conditions, vous trouverez souvent un cas que vous avez oublié de traiter dans cette fonction.

Si vous avez toujours un bug, apprenez à écrire des assertions qui vérifient les pré-conditions et les post-conditions. Une assertion est comme un commentaire qui vous prévient lorsqu'une condition est violée ; une condition violée est presque toujours un bug. En C# vous pouvez écrire using System.Diagnostics; au sommet de votre programme, puis Debug.Assert(value != null); ou n'importe quoi d'autre. Chaque langage a un mécanisme d'assertion ; trouver quelqu'un pour vous enseigner comment utiliser celui de votre langage. Ajoutez des assertions pour les pré-conditions au début du corps de votre fonction, et pour les post-conditions juste avant que la fonction ne retourne. (Notez que c'est plus facile à faire lorsque la fonction a un seul point de retour.) Désormais lorsque vous lancez votre programme, si une assertion échoue, vous serez informé⋅e de la nature du problème, et il sera plus facile à déboguer.

Ensuite, écrivez des tests pour chaque fonction, qui vérifient qu'elle se comporte correctement. Testez chaque partie indépendamment jusqu'à ce que vous soyez sûr⋅e de vous. Testez beaucoup de cas simples ; si votre fonction trie des listes, essayez la liste vide, une liste à un seul élément, deux éléments, trois éléments tous identiques, trois éléments en ordre inverse, et quelques listes plus longues. Il y a des chances que votre bug apparaisse dans un un cas simple, ce qui le rendra plus facile à analyser.

Enfin, si votre programme est toujours bogué, écrivez sur une feuille de papier la liste exacte des actions que votre programme est censé faire à chaque ligne, pour le cas qui pose problème. Votre programme ne fait qu'une vingtaine de lignes. Vous devriez pouvoir écrire tout ce qu'il fait. Exécutez alors le code dans dans un débogueur, en examinant chaque variable à chaque étape d'exécution, et comparez ligne après ligne ce que fait programme à ce que vous avez écrit sur votre feuille. Si le programme fait quelque chose qui n'est pas dans votre liste, alors soit votre liste est fausse, auquel cas vous n'avez pas compris ce que fait le programme, soit votre programme est faux, auquel cas vous l'avez mal codé. Corrigez ce qui est faux. Si vous ne savez pas comment le corriger, au moins avez vous une question spécifique que vous pouvez poser à StackOverflow ! Dans tous les cas, répétez ce processus jusqu'à ce que l'exécution du programme et la description que vous en avez faite coïncident.

Lorsque vous exécutez le code dans un débogueur, je vous encourage à prêter attention au moindre doute. La plupart des programmeurs ont un biais naturel qui les porte à croire que leur programme fonctionne comme prévu, mais si vous êtes en train de déboguer, c'est que cet a priori est faux ! Il m'arrive souvent, en déboguant un problème, d'apercevoir du coin de l'œil une notification de Visual Studio signifiant "un emplacement mémoire a été modifié", et de savoir que cet emplacement mémoire n'a rien à voir avec mon problème. Donc pourquoi est-il modifié ? N'ignorez pas ces points de gêne ; étudiez les comportements étranges jusqu'à comprendre s'ils sont corrects ou incorrects, et pourquoi.

Si tout cela vous semble être beaucoup de travail, c'est que ça l'est en effet. Si vous n'arrivez pas à appliquer ces techniques sur des programmes de vingt lignes écrits par vous même, vous n'y arriverez pas sur des programmes de deux millions de lignes écrits par d'autres, mais c'est pourtant ce qu'un développeur professionnel doit faire au quotidien. Commencez à vous entraîner !

Et la prochaine fois que vous faites un TP, écrivez les spécifications, les tests, pré-conditions, post-conditions et assertions dans vos fonctions avant même d'écrire le code à proprement parler ! Vous diminuerez grandement le risque d'avoir un bug, et si vous en avez un, vous augmentez grandement vos chances de le corriger rapidement.

Cette méthode ne permet pas de trouver tous les bugs dans tous les programmes, mais elle est très efficace pour le type de programmes courts que l'on donne en TP à des débutants. Et elle fonctionne également à plus grande échelle pour trouver des bugs dans des programmes non triviaux.