
f()
Gerson Sunyé gerson.sunye@univ-nantes.fr
Introduction
Un exemple
Techniques de débogage
Réactives
Préventives
Bonnes pratiques
Conclusion
Le débogage (ou debugging en anglais) est le processus de localisation et de suppression des erreurs existantes et potentielles de bogues dans le code source des logiciels.
Erreurs de construction et de compilation
Erreurs de syntaxe, souvent détectés par l’éditeur
Erreurs d’exécution
Erreurs de mémoire, d’entrées, externes au logiciel (système)
Erreurs de logique (conditionnels, boucles, etc.)
Résultats incorrects
f()
Il peut n’y avoir aucune relation évidente entre la ou les manifestations externes d’une erreur et sa ou ses causes internes.
Le symptôme et la cause peuvent se trouver dans des parties éloignées du programme.
Une erreur peut dépendre de l’état d’autres logiciels ou de bibliothèques tiers.
Elle peut dépendre aussi des modifications d’autres développeurs:
Ces modification (nouvelles fonctionnalités, corrections) peuvent introduire de nouvelles erreurs.
L’erreur peut être difficile à observer, parce qu’elle dépend d’une séquence d’événements difficile à réproduire:
l’ordre d’exécution de processus concurrents, le temps de réponse d’un serveur externe, l’occupation de mémoire, etc.
Certaines erreurs disparessent lors que l’on essaye de les observer (Heisenbugs).
There has never been an unexpectedly short debugging period in the history of computers.
Comme nous avons vu pendant les séances de TD, la fonction dayOfWeek()
calcule le jour de la semaine d’une date donnée, grâce à l’algorithme de Zeller.
Cette fonction en appelle une autre, isLeapYear()
.
function dayOfWeek(day : number, month : number, year : number) : string {
let weekDayNames: Array<string> = [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi',
];
let leapMonthOffsets : Array<number> = [0, 3, 6, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2];
let unleapMonthOffsets : Array<number> = [0, 4, 0, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2];
let century : number = Math.trunc(year/100);
let centuryYear : number = year % 100;
let monthOffset : number = isLeapYear(year) ? leapMonthOffsets[month] : unleapMonthOffsets[month];
let weekDay : number;
weekDay =
(Math.trunc(centuryYear / 4) +
Math.trunc(century / 4) +
centuryYear +
monthOffset +
5 * century +
day +
2) %
7;
return weekDayNames[weekDay];
}
@TestFixture("DayOfWeek Unit Tests")
export class DayOfWeekTest {
@Test('January 1st 1970 is a Thursday')
public january_1st_70_is_Thursday() {
Expect(dayOfWeek(1,1,1970)).toBe('Jeudi');
}
}
Ce test ne passe pas, il indique une erreur. Comment la localiser ? |
Les tests prouvent l’échec du développeur. Le débogage est sa revanche.
Après l’apparition d’une erreur.
Appels à print()
Ajout de commentaires
Utilisation d’un débogueur
Ajout de code qui n’a pas d’impact sur la fonctionnalité, pour simplifier le débogage.
Assertions
Journalisation
En programmation, une assertion est un prédicat [1] à l’intérieur du code source, qui doit toujours avoir la valeur vrai à ce moment de l’exécution.
Si le prédicat d’une assertion est évalué à faux, l’exécution du programme s’arrête.
int div(int dividend, int divisor) {
assert (divisor != 0);
return dividend / divisor;
}
Les assertions aident la lecture du code source et renforcent les certitudes des développeurs
function dayOfWeek(day : number, month : number, year : number) : string {
assert(month >= 1 && month <= 12);
// ...
century
est comprise entre 0 et 99let century : number = Math.trunc(year/100);
assert(century >= 0 && century <= 99);
La journalisation désigne l’enregistrement séquentiel dans un fichier, une base de données ou console, de tous les événements affectant l’exécution d’une application.
La journalisation ressemble à des appels de print()
, avec quelques petites différences :
Elle peut être désactivée
Elle peut avoir plusieurs niveaux de détail
Plusieurs bibliothèques sont disponibles, dont tslog
Différentes fonctions disponibles : silly()
, trace()
, debug()
, info()
, warn()
, error()
, fatal()
import { Logger, ILogObj } from "tslog";
const log: Logger<ILogObj> = new Logger();
function isLeapYear(year : number) : boolean {
log.trace('Entering isLeapYer() with value ', year);
return (((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0);
}
La fonction trace()
sera appelée chaque fois que l’exécution rentre dans la fonction isLeapYear()
.
Cependant, elle n’affichera un message que si:
La journalisation est activée
Le niveau de journalisation est supérieur ou égal à celui de trace
Hiérarchie des niveaux: silly
> trace
> debug
> info
> warn
> error
> fatal
La forme la plus simple de débogage, mais souvent utile.
Consiste à ajouter des console.log()
(en TypeScript) pour afficher l’état (le contenu) des variables suspectes
function dayOfWeek(day : number, month : number, year : number) : string {
console.log('Appel de dayOfWeek avec day=', day, ' month=', month, ' year=', year);
// ...
}
// ...
let century : number = Math.trunc(year/100);
let centuryYear : number = year % 100;
console.log('century=', century, ' centuryYear=', centuryYear);
// ...
Forme simple de débogage, qui consiste à commenter une partie du code pour réduire l’espace de recherche
//let monthOffset : number = isLeapYear(year) ? leapMonthOffsets[month] : unleapMonthOffsets[month];
let monthOffset = unleapMonthOffsets; // Je sais que 1970 n'est pas bissextile
Un débogueur est un outil utilisé pour détecter la source des erreurs de programme ou de script, en effectuant une exécution étape par étape du code de l’application et en visualisant l’état des variables du code.
Un débogueur n’est pas un IDE
Bien que les deux puissent être intégrés, comme dans VS Code, ce sont des entités distinctes.
Un débogueur charge un programme (exécutable compilé ou code source interprété) et permet à l’utilisateur de suivre son exécution.
Les débogueurs sont généralement capables de désassembler, de suivre la pile, de surveiller les expressions, etc.
C’est simple: il n’est pas nécessaire de prévoir ce que pourrait être l’erreur.
C’est flexible: il permet de vérifier les erreurs "en direct" - il n’est pas nécessaire de réécrire et de recompiler lorsque vous vous rendez compte qu’un certain type d’erreur peut se produire.
C’est dynamique: il permet de visualiser l’ensemble de la portée pertinente
Dans le cas d’erreurs simples:
il n’est pas nécessaire de lancer l’environnement de débogage.
simple à vérifier à l’aide de prints/asserts
Environnement de débogage difficile à utiliser
L’erreur se produit dans un code optimisé
Le débogueur modifie l’exécution du programme (l’erreur ne se produit pas pendant le débogage).
Suit le programme tout au long de son exécution. Les utilisateurs peuvent avancer ligne par ligne ou utiliser des points d’arrêt.
Permet généralement de "surveiller" les registres, les emplacements mémoire et les symboles.
Permet de remonter la pile des erreurs d’exécution (back traces).
Permet à l’utilisateur de trouver les causes d’un comportement inattendu et de les corriger.
ligne sur laquelle le débogueur doit faire une pause
reprend l’exécution jusqu’au prochain breakpoint
exécute une ligne. Si elle contient une fonction, ne rentre pas dans la fonction
exécute une ligne. Si elle contient une fonction, rentre dans celle-ci
sort d’une fonction
Identifier le(s) cas de test qui montre(nt) de manière fiable l’existence de l’erreur.
Réduire l’espace de recherche à un ou plusieurs petits fragments du programme.
Corréler le comportement incorrect avec la logique du programme ou l’erreur de code.
Modifier le programme (et vérifier s’il y a d’autres parties du programme où la même logique ou une logique similaire peut également se produire).
Exécuter à nouveau tous les tests (test de non-régression) pour vérifier que l’erreur a réellement été supprimée, sans l’ajout de nouvelles erreurs.
Mettre à jour la documentation
Il est toujours plus simple de déboguer un code lisible.
Nettoyez le code :
Renommez des identifiants (variables, fonctions), simplifiez le flot de contrôle.
Ajoutez des commentaires.
Appliquez les principes vus dans le cours "Qualité de code".
Faites attention de ne pas introduire de nouvelles erreurs pendant la simplification ! |
Le débogage est deux fois plus difficile que le codage. Par conséquent, si vous écrivez le code aussi intelligemment que possible, vous n’êtes, par définition, pas assez intelligent pour le déboguer.
Essayez de comprendre autant que possible ce qui se passe.
Où sont déclarées les variables ?
Où sont-elles modifiées ?
Ne faites confiance à rien d’autre que le code!
La documentation et les commentaires n’évoluent pas forcément avec le code
Ils peuvent être dépassés
Ne modifiez pas plusieurs parties du code en même temps
Si l’erreur est apparue sur un code qui marchait avant, cherchez les dernières modifications
Le Gestionnaire de Versions est votre ami ! |
Tenez un journal des événements et des hypothèses
Notez-y les modifications et leur impact de chaque modification
Expliquez à votre animal (, , ) ce qui devrait se passer
La verbalisation (ainsi que l’écriture) clarifie souvent les pensées confuses.
Ne changez pas les choses au hasard, toute modification doit avoir un but.
Si vous n’êtes pas prêt à l’enregistrer dans le Gestionnaire de Versions () avec un commentaire que votre chef pourra lire, alors vous n’êtes pas prêt à apporter cette modification au code.
Modifiez le code pour rendre l’erreur visible :
Utilisez des variables explicatives pour simplifier les expressions complexes
Anticipez le placement des futurs breakpoints.
Ajoutez des commentaires :
Annoncez les pièges aux prochains chasseurs d’erreurs
Il y a plusieurs façons de déboguer: trouvez la plus appropriée à votre contexte.
Quand une erreur se manifeste, écrivez le test unitaire minimal capable de reproduire l’erreur: il vous aidera à réduire l’espace de recherche et à savoir quand vous l’avez résolue.