D’après le rapport de l’équipe de test, le problème de comportement est reproductible sur tous les postes utilisés. Le problème de gestion mémoire, par contre, n’en concerne que deux sur les sept que cette équipe possède.
20h. Après un petit break d’une heure avec la journée « régulière », nous faisons le point. Le problème de gestion mémoire semble lié au matériel. Le problème de comportement, lui, semble plutôt résulter d’une erreur de conception ou de programmation : l’application ne plante pas, c’est sa logique de comportement qui est en cause. A moins que les deux choses soient liées, mais si on ne peut pas l’exclure, on ne peut pas non plus partir de ce postulat pour le moment.
Le chargé de projet technique nous distribue donc les tâches comme suit :
- Un développeur, que l’on appellera D1 par la suite, se chargera de vérifier les entrées utilisateurs, c'est-à-dire la bonne prise en compte de ses interactions avec l’application.
- Un développeur, D2, se chargera de vérifier les sorties, c'est-à-dire la conformité du comportement de l’application avec la réponse aux entrées générée par cette dernière.
- D3 se chargera de vérifier ce qu’il y a entre les deux, c’est à dire la génération de la réponse applicative aux entrées utilisateurs et qui servira de base aux sorties.
- D4, enfin, tentera de reproduire le problème de gestion mémoire remonté par les testeurs, en utilisant l’une des deux machines concernées. Il installera préalablement son environnement de développement sur cette machine. De cette façon, une fois l’anomalie reproduite, il pourra exécuter ligne à ligne le code source[5] concerné et pourra analyser le contenu de la mémoire au moment du problème.
- Le chargé technique (CT), enfin, coordonnera les opérations et prêtera assistance aux quatre développeurs, sur demande.
Et moi, je suis D3.
21h. Je suis en train de rédiger différents scénarios de test permettant de valider ou d’invalider le comportement du générateur de réponses applicatives. D1 et D2 sont peu ou prou occupés à faire de même. D4, aidé de CT, sont en train de terminer l’installation de l’environnement de développement sur la machine des testeurs. Le chef de projet, quant à lui, est occupé dans son bureau. Nous savons qu’il aurait pu ne pas rester ce soir, mais qu’il le fait par soutien à son équipe. Nous l’en remercions intérieurement.
22h. D1 a repéré un problème mineur, mais qui ne peut expliquer le problème remonté. Il le corrige. D2 cherche toujours, en reproduisant ses scénarios de test. J’ai un doute, pour ma part : un comportement non spécifié, mais non proscrit par la spécification. Un « vide fonctionnel », en quelque sorte… J’en parle à CT qui se trouve aussi perplexe que moi. Nous tâchons de déterminer les conséquences de ce comportement, afin d’en estimer la capacité de nuisance mais aussi afin de s’assurer que le comportement fautif remonté par les testeurs ne soit pas parmi ces conséquences. D4 tente, en vain, de reproduire le problème de gestion mémoire.
23h. D1 a « refactoré »[6], une partie du module de gestion des entrées. Cela signifie qu’il a réécrit une partie de ce code afin de le simplifier sans que cela ne modifie son comportement. On considère en effet qu’à comportement égal, un code complexe tend à provoquer plus d’anomalie qu’un code simple car il est sujet à plus de cas particuliers, et plus difficiles à déceler. D2 a également trouvé quelque chose dans les sorties. Rien qui puisse tout expliquer, mais une anomalie se produisant dans certains cas particuliers et qu’il commence à corriger. CT et moi travaillons toujours à déterminer les implications du comportement non spécifié observé une heure plus tôt. D4 n’a toujours pas reproduit le problème et commence à désepérer.
23h30, petite pause décidée à l’unanimité. Nous allons fumer
- D1 nous explique son refactoring des entrées. Pas de commentaires, mais une approbation générale. Il en a encore pour une bonne heure, d’autres points mineurs ayant été identifiés.
- D2 nous explique plus en détails l’anomalie trouvée dans les sorties. Celle-ci ne se produit que lorsque la réponse applicative servant de base aux sorties est dans un certain état.
- J’objecte : la réponse applicative ne peut pas contenir les valeurs provoquant cet état. Si le cas se produit, c’est la réponse applicative elle-même qui est fautive.
- D2 : « Ah ok, je ne pensais pas. Mais à supposer qu’elle le soit, il vaut mieux que la sortie ne plante pas ! »
- CT : « Oui, c’est clair. Autant blinder[7], d’autant que t’as déjà commencé le refactoring il me semble. »
- D2 : « Oui, j’ai presque fini en fait. »
- D3 (moi) : « Tu pourras me donner des infos pour reproduire le cas que tu as observé ? Il faut que je corrige le générateur ».
- D2 : « Ok, je te montre ça tout à l’heure. »
- CT et moi décidons de réécrire la partie de code engendrant le comportement non spécifié décelé une heure et demi plus tôt afin d’éviter qu’il se produise. Nous soupçonnons ce comportement ambigu de générer des incohérences qui pourraient expliquer le problème de gestion mémoire sur lequel travaille D4, voire celui que rencontre D2.
- D4 est fatigué de tester, tester, et tester encore sans parvenir à reproduire le problème mémoire remonté par les testeurs. Nous nous demandons si l’installation de l’environnement de développement sur la machine des testeurs ne prévient pas la reproduction de l’anomalie du fait de la modification de la représentation mémoire qu’elle engendre.
Minuit. C’est déjà demain. Nous nous y remettons.
1h30. D1 a terminé sa partie. Ses tests passent, tout va bien. La source des problèmes remontés n’était donc pas dans les entrées, tandis que des problèmes potentiels ont été fixés. Il rentre chez lui. Nous l’envions un peu, sauf D2 qui ne semble même pas s’être aperçu de son départ.
2h. D2 est en train de finaliser son refactoring. Il devrait en avoir terminé dans une demi-heure, ce après quoi il pourra me montrer le cas particulier dont on a parlé lors du point de 23h30. De mon côté, je viens de terminer réécriture de la partie du générateur qui provoque le comportement non spécifié : il ne se produit plus. Mais je n’ai toujours pas trouvé l’origine du comportement fautif remonté par les testeurs. D4 continue de tester en vain, mais nous annonce qu’il va bientôt abandonner.
2h15. Cri de joie de D4 : « Je l’ai ! La mémoire ne peut être ‘written’ ! ».
2h15’30’’. Cri de colère de D4. Son PC vient de rebooter. Le problème est reproduit, mais hélas dans sa version la plus radicale. « Et m****, c’est quoi ce ****** ? J’ai rien fait de spécial, j’ai rien eu le temps de noter, comment je vais le reproduire maintenant ? Je vais pas y repasser 3 heures ». En fait, non, cela en a plutôt pris six.
2h45. D2 m’explique rapidement le cas particulier évoqué plus tôt et qui fait planter la sortie. Il m’indique les valeurs d’entrée qu’il utilise pour le générer. Je les note pour tout à l’heure et lui demande s’il se prépare à rentrer. « Non, j’ai découvert un souci sur mon refactoring de tout à l’heure, il faut que je le corrige d’abord ». CT va lui prêter main forte.
3h. Je parviens à reproduire une génération de réponse applicative qui correspond au cas erroné remonté par les testeurs. Et ce malgré mon refactoring, donc. Les valeurs d’entrées ne correspondent pas à celles remontées par D2. Par contre, que se passe-t-il si j’essaie le cas de D2 ? Ai-je le même comportement ?
3h30. « P*****, je l’ai ! ». C’est D4 qui est parvenu à reproduire le problème, mais sans rebooter cette fois. Il note scrupuleusement l’état de sa mémoire à ce moment précis, ainsi que les paramètres utilisés.
4h. Le cas que m’a remonté D2 est différent de celui des testeurs. J’ai rapidement patché celui des testeurs par mise en place de blindages divers, c’était le plus simple à moins de tout refaire. En termes de programmation, ce n’est pas très beau, mais ça résous le problème. Par contre, le cas de D2 est plus complexe. J’ai de la ré-écriture à faire.
4h05. CT nous demande de terminer ce que l’on fait et de ne rien commencer d’autre. Il est temps d’aller se coucher. « Ok. J’ai presque fini ». Je ne sais plus trop, c’est peut-être moi qui ai dit ça.
4h30. Je m’emmêle les pinceaux dans mon code, et m’y reprends à plusieurs fois pour faire des choses pourtant simple. Je râle dans mon coin. D2 trace, trace et re-trace[8] son code, aidé par CT, son problème de refactoring n’étant pas réglé. D4, quant à lui, « blinde », à défaut de pouvoir régler la cause du problème. « J’y comprends rien, là. Ce cas là ne peut pas se produire, en théorie. Ecoute, je blinde en attendant de voir ça à tête reposée », explique-t-il à CT.
5h. CT : « Vous y êtes ?
- Oui oui, presque, deux secondes. », lui répond D4.
5h15. J’ai terminé. Fixer le cas soulevé par D2 a été difficile car impossible à reprendre sans modifier la conception, mais ça va, ce n’est pas trop mal fait au final. Je dis au revoir aux autres et je rentre.
Le lendemain, je me réveille vers 10h00. J’arrive au travail vers 11h00. CT est déjà là, ainsi que D1 et D4. J’ai un peu honte. « Vous êtes rentrés à quelle heure ?
- le temps de rentrer, vers 7h… On est parti vers 6h et demi, m’explique D4.
Plus tard, dans la journée, j’apprends que le cas soulevé par D2 et que j’ai fixé ne pouvait pas se produire : les entrées ne pouvaient fournir de telles valeurs. Comme D1 était déjà parti, nous ne nous en sommes pas aperçus et avons fait tout cela pour rien.
Le lendemain, nous apprenons par CT que le problème mémoire se produit toujours. D4 a bien fixé le problème, mais du fait de la fatigue, il n’a pas effectué de commit CVS[9], si bien que son correctif est resté sur son poste. Entre temps, les testeurs ont désinstallé l’environnement de dev de leur machine, si bien que tout son travail de cette nuit là a été perdu.
Quelques jours plus tard, les testeurs nous remontent de nouvelles anomalies. Plusieurs d’entre elles proviennent des refactorings effectués cette nuit là par D1, D2 et moi : des erreurs de programmation manifestement dues à l’inattention et la fatigue, mais aussi des erreurs de logique et d’appréciation de conséquences dues à la précipitation dans laquelle ces modifications ont été faites. Certaines corrections ont demandé plusieurs jours de travail. L’une d’entre elle a été corrigée par l’annulation du refactoring effectué et son remplacement par une version précédente contenue dans CVS.
Moralité : travailler « à l’arrache », de nuit, pour fixer des bugs ou terminer des travaux dans l’urgence, ça ne sert à rien d’autre que se donner bonne conscience. C’est même contre-productif, le nombre de problème générés pouvant dépasser le nombre de problèmes réglés. En outre, cela déstabilise l’équipe de développement, du fait de la fatigue et du décalage horaire engendrés.
Il s’agit donc d’une pratique à bannir.
Note : Cette histoire est basée sur des faits tout à fait réels, et n’a vraiment rien d’extraordinaire. J'ai du la vivre au moins dix ou quinze fois. Nombre de développeurs de votre entourage peuvent certainement vous raconter des histoires très similaires.
Notes
[1]Cette structure de projet comprenait une branche développement, dite « technique », et une branche IHM, composée de designers et de graphistes.
[2]Ce que l’on appelle ici « l’état de l’application », c’est la représentation mémoire que celle-ci à de l’utilisateur, de ses actions, et des réponses qu’elle génère en réaction à ces dernières.
[3]En presque-français dans le texte, la boîte indique « AppName.exe - Erreur d'application. L'instruction à "0x77F738a9" emploie l'adresse mémoire "0X0259200a". La mémoire ne peut pas être 'written' ». On aurait bien aimé qu’elle le puisse, pourtant…
[4]Nous sommes sous Windows 98 au moment où ceci se déroule. Aujourd’hui nous aurions un simple message d’erreur. Au pire un écran bleu, mais il faudrait se battre pour arriver à l’obtenir.
[5]Ce que l’on appelle code source, c’est l’ensemble des instructions qui constituent le programme, en langage compréhensible par le programmeur (ici, ce langage se nomme « C++ »). Le code exécutable, quant à lui, est une traduction de ce code source en langage compréhensible par la machine (et que l’on appelle parfois « langage machine »). L’application telle qu’installée et distribuée aux utilisateurs ne contient que le code exécutable. Faire le lien entre ce code et le code source nécessite une autre application, que l’on appelle ici « environnement de développement ».
[6]Et pardon pour ce barbarisme.
[7]« Blinder », en jargon, signifie prévoir que des cas aberrants puissent se produire pour les gérer de façon cohérente. Par exemple, si une probabilité se trouve à 1.5 tandis qu’elle ne peut normalement qu’être comprise entre 0 et 1, l’application la ramène à 1 avant de l’introduire dans ses calculs.
[8]« Tracer », c’est exécuter son programme non pas en le lançant normalement, mais manuellement, instruction par instruction. Ceci permet d’observer l’action exacte de chaque instruction, afin de solutionner un problème.
[9]CVS est un outil de gestion du code source. Le « commit » consiste à intégrer les modifications effectuées sur un poste de travail à la base de code source commune à tous les développeurs. C’est cette dernière qui est utilisée pour construire et livrer les versions de l’application. En l’absence de commit, la modification n’est visible que sur le poste de travail sur laquelle elle a été faite.