Qualité et fiabilité du code

App : Python Course · Date : 1er septembre 2026

Périmètre : l'interpréteur Python intégré, le contenu du cours (2194+ exercices, 48 leçons, 17 modules, une nouvelle section « Astuces » — 15 cartes, 6 langues), l'algorithme de répétition espacée et — nouveau dans ce rapport — si la suite de tests elle-même détecte vraiment les régressions (tests de mutation) sur les 5 modules de logique les plus critiques

En bref : deux questions distinctes, deux réponses. Le code est-il correct ? 9710 candidats vérifiés face à CPython et à d'autres oracles — 0 écart. Les tests le protègent-ils vraiment ? 3694 défauts injectés délibérément (tests de mutation — la couverture a grandi avec le code) — les 3694 détectés. La seconde question est nouvelle dans ce rapport — c'est elle qui rend la première digne de confiance, et pas seulement affirmée.

0 sur 9710
écarts face à CPython/aux oracles
3694 sur 3694
défauts injectés détectés par les tests (mutation score 100%)

Tests de mutation — les tests détectent-ils vraiment les bugs

Un pourcentage de couverture répond seulement à « cette ligne s'est-elle exécutée au moins une fois ? », pas à « un test remarquerait-il si cette ligne était cassée ? ». Les tests de mutation répondent directement à la seconde question : un outil injecte délibérément des milliers de petits défauts individuels dans le code (change < en <=, && en ||, supprime une vérification de borne, etc.) et vérifie si au moins un test échoue ensuite. Si aucun test ne détecte le défaut injecté, c'est une véritable lacune — même si la couverture de lignes affiche 100 % à elle seule.

Module Mutants Détectés Score
Logique de déverrouillage des leçons (gate-engine) 208 208 100.00%
Succès, XP, série, cœurs (gamification-engine) 267 267 100.00%
Algorithme de répétition espacée (srs-engine) 47 47 100.00%
Interpréteur Python (tokeniseur, parseur, évaluateur — y compris le code import/fichiers du module 17) 3123 3123 100.00%
Section « Astuces » (lifehacks-engine + manage-lifehacks) 49 49 100.00%
Total, 5 modules les plus critiques 3694 3694 100.00%

« Détecté » signifie ici : le mutant a fait échouer au moins un test, ou a provoqué une boucle infinie/un blocage (également comptabilisé — le défaut a tout de même été clairement signalé, juste par un autre moyen ; la ligne de l'interpréteur Python ci-dessus compte exactement un cas de ce type). Chaque cas individuel où un mutant survivait initialement a été examiné à la main : soit un nouveau test a été écrit pour documenter le comportement réel, soit il a été démontré mathématiquement que ce changement de code précis ne peut jamais affecter le résultat (et dans ce cas, la ligne elle-même porte un commentaire l'expliquant, pas un silence), soit — quand cela s'est révélé être la réponse la plus juste — le code lui-même a été simplifié de sorte que la branche de logique non prise en charge a tout simplement cessé d'exister. L'un de ces cas est détaillé ci-dessous.

Méthodologie — correction du code (comparaison avec CPython)

Trois volets de validation indépendants, sans code partagé et sans dépendance entre les résultats. Interpréteur : l'app exécute le code via son propre interpréteur Python, écrit entièrement de zéro (ni CPython, ni une bibliothèque tierce) — chaque candidat (une solution de référence, une instance générée à partir d'un modèle, ou un programme généré aléatoirement) a été exécuté simultanément dans l'interpréteur de l'app et dans un CPython 3.12.3 installé sur le système, puis les résultats ont été comparés (résultat exact ou cohérence des erreurs). Structure du cours : un passage complet, sans échantillonnage, sur tous les fichiers de contenu. Algorithme de répétition espacée : sa sortie a été comparée à une implémentation écrite séparément (non copiée du planificateur lui-même) de la même formule mathématique. La validation s'est déroulée via un outil temporaire et isolé, en dehors du code propre de l'app, en plusieurs rondes indépendantes menées sans s'appuyer sur les résultats des rondes précédentes.

Résultats face à un oracle

Volet de validation Vérifiés Concordants Écarts
Interpréteur — contenu du cours et mode de pratique étendu (solutions de référence et alternatives, exercices à compléter en mode valeur, modèles statiques et paramétrés du mode de pratique étendu, exécution réelle d'import/open() du module 17) et section « Astuces » (extraits de code avant/après) 393 393 0
Interpréteur — tests aléatoires (fuzzing) (20 catégories de constructions) 400 400 0
Algorithme de répétition espacée (1020 scénarios, 8917 étapes de révision) 8917 8917 0
Total combiné 9710 9710 0

Vérifications structurelles et de contenu

Vérification Couverture Problèmes trouvés
Structure du cours (validité des solutions, intégrité du graphe de prérequis, unicité des id) 2194 fichiers d'exercices, 48 leçons, 17 modules 0
Structure du pool du mode de pratique étendu 64 entrées (53 statiques + 11 modèles) 0
Section « Astuces » (chaque carte référence un concept réel du cours ; le code avant/après produit une sortie identique sur l'interpréteur réel) 15 cartes, 30 extraits de code 0
Parité de localisation (clés de contenu, aucune valeur non traduite) 12 214 clés × EN, ES, FR, DE, PT (61 070 comparaisons) 0

Outils

Stryker Mutator — tests de mutation (génération et exécution de milliers de défauts délibérés face à la suite de tests existante).
Vitest — exécuteur de tests (1112 tests unitaires/d'intégration à la date de ce rapport).
TypeScript, mode strict — strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes.
CPython 3.12.3 — oracle externe pour vérifier le comportement de l'interpréteur.
Scripts isolés maison — génération de tests fuzz et comparaison de l'algorithme de répétition espacée, en dehors du code propre de l'app.

Comment ces chiffres ont vraiment été obtenus — sans enjolivure

Ces chiffres ne sont pas ceux de départ. Le premier passage de tests de mutation sur le module le plus gros et le plus difficile (l'interpréteur Python, ~2500 lignes) a donné 75,65 % — soit près d'un quart des défauts injectés délibérément passaient totalement inaperçus. Combler cet écart jusqu'à 100 % a nécessité sept passages consécutifs, chacun refermant une partie du reste tout en faisant apparaître de nouveaux cas limites — y compris des erreurs de méthodologie commises pendant le processus de vérification lui-même.

De vrais bugs de l'application trouvés et corrigés (pas seulement des lacunes de tests) — mis au jour précisément grâce à ce processus rigoureux : le score d'une leçon était indûment abaissé après une erreur corrigée (une seule erreur plafonnait le score maximal possible, même si l'apprenant se corrigeait immédiatement à la reprise) ; le test de niveau accordait à tort les succès « Sans Faute » et « Sans Pitié » pour des leçons sautées, non réellement accomplies ; et trois bugs ponctuels de l'interpréteur lui-même (division entière, arrondi de l'opérateur de reste, comparaison de bool avec int).

Une erreur de méthodologie a été détectée et corrigée en cours de route : une vérification intermédiaire (remplacer le texte d'une ligne entière au lieu de muter aux coordonnées exactes de Stryker) a surestimé le nombre de mutants réellement « équivalents » (c'est-à-dire impossibles à corriger) — sur 35 hypothèses de ce type, une seule s'est confirmée. Corrigé en passant à une vérification précise, au niveau des coordonnées, pour chaque cas individuel.

Les 5 derniers mutants (comparaison de chaînes utilisée par le tri des listes) n'ont cédé ni à une preuve d'équivalence, ni à un test écrit via l'interface publique habituelle — l'algorithme de tri natif de JavaScript effectue des comparaisons redondantes qui masquent un résultat erroné sur une seule paire. Résolu en testant directement la fonction de comparaison interne, en contournant entièrement le comportement imprévisible de l'algorithme de tri tiers.

En résumé : aucun chiffre de ce rapport n'est peint d'avance. Chacun est le résultat d'un travail itératif et vérifié — y compris le fait d'admettre et de corriger ses propres erreurs en chemin. C'est précisément pour cela que ces chiffres sont dignes de confiance : l'app ne se contente pas de bien se comporter sur les exemples vérifiés — le filet de sécurité des tests qui l'entoure détecte réellement les régressions futures, au lieu d'exister seulement sur le papier.

Pourquoi « 2797 sur 2797, 100 % » est brièvement devenu « 3373 sur 3374, 99,97 % » avant de revenir à 100 % : après avoir atteint 100 % une première fois (ci-dessus), l'interpréteur a reçu de nouveaux sujets de curriculum — itération directe (for x in iterable: sans range()), __str__/__eq__, @staticmethod/@classmethod, dictionnaires/ensembles/compréhensions/générateurs — des centaines de lignes nouvelles qui n'étaient jamais passées par des tests de mutation. Plutôt que de laisser cette croissance non vérifiée (et de continuer à afficher discrètement un « 100 % » périmé), elle a été soumise à Stryker avec la même rigueur que le reste : 32 nouveaux tests de régression, chacun écrit contre un mutant survivant précis (en lisant d'abord le code autour, en confirmant logiquement pourquoi les tests existants le manquaient, et seulement ensuite en écrivant le test) — pas du remplissage de couverture. Tout le reste ayant changé depuis le passage précédent de tests de mutation (17 août) a aussi été vérifié : le parseur (nouvelle grammaire pour ces mêmes nouveaux sujets) et le planificateur de répétition espacée (un nouveau seuil précédant l'entrelacement) — déjà tous deux à 100 % sans travail supplémentaire nécessaire. Il restait donc exactement un mutant non résolu, et c'est là que le rapport indiquait brièvement « 3373 sur 3374, 99,97 % », avec une explication de pourquoi c'est plus honnête qu'un « 100 % » fabriqué : une fonction de résolution de variable locale (Scope.tryGet) comportait une vérification qui, en théorie, pouvait compter dans deux scénarios distincts, mais l'un d'eux s'est révélé mathématiquement impossible compte tenu de tous les points d'appel réels de l'app — et l'autre, réellement atteignable, était déjà couvert par un test dédié. Marquer les deux mutants de cette ligne « équivalents » avec un seul commentaire aurait été le chemin le plus rapide vers « 100 % » — c'est précisément pour cela que ça n'a pas été fait à ce moment-là : l'outil ne peut désactiver la vérification de mutation que pour une ligne entière, pas pour un mutant précis, donc ce raccourci aurait aussi balayé la couverture du scénario qui compte vraiment.

Ce qui a changé ensuite — et pourquoi il s'agit d'un 100 % authentique, pas d'un contournement : plutôt que de se contenter de 99,97 % ou de supprimer discrètement le reste, la question suivante a été de savoir si cette ligne était, dès le départ, du code mort. Un traçage plus poussé de chaque appel à tryGet() dans l'app (il y en a exactement un) et de chaque endroit d'où la valeur qu'elle protégeait pouvait provenir a montré que oui : la ligne était réellement inatteignable dans n'importe quel état réel du programme, pas seulement difficile à tester. Elle a été supprimée purement et simplement. Ce n'est pas un raccourci : supprimer du code réellement mort ne fait perdre aucune couverture réelle (contrairement à la suppression du mutant par un commentaire, qui aurait aussi jeté la couverture réelle, déjà vérifiée, du mutant jumeau) — le mutant que cette ligne générait a simplement cessé d'exister avec elle. La suite de tests complète (1035 tests à l'époque) a confirmé que le comportement n'avait pas changé d'un iota. Le passage suivant de Stryker a donné un authentique 100,00 %. La leçon de ce petit détour dans le rapport lui-même : accepter de montrer un chiffre honnête juste en dessous de 100 % s'est révélé être le chemin le plus rapide vers un vrai 100 %, pas plus lent que de camoufler l'écart dès le départ.

D'où vient la croissance de 3371 à 3694 et de 9655 à 9710 (1er septembre 2026) : le cours s'est agrandi — un nouveau 17ᵉ module, « Import, modules et gestion de fichiers », a ajouté une exécution réelle (pas seulement une reconnaissance de syntaxe) d'import/open()/with face à un système de fichiers virtuel simulé, et une nouvelle section « Astuces » (15 cartes « avant → après » expliquant pourquoi la forme la plus courte fonctionne de la même manière). Les deux ajouts sont passés par la même rigueur que le reste de l'app : interpreter.ts et parser.ts ont été soumis de nouveau aux tests de mutation (respectivement +238 et +36 mutants, tous détectés), le nouveau lifehacks-engine et manage-lifehacks ont été amenés de zéro à 100 % (2 lacunes de tests réelles corrigées, pas masquées), les 15 cartes (30 extraits de code) ont été vérifiées face à CPython, tout comme les 2 nouveaux exercices du module 17 fondés sur des boucles. Le même jour, le dernier retard de traduction a aussi été comblé : la section « Astuces » et les 434 nouvelles clés du module 17 ont été traduites en espagnol, en français, en allemand et en portugais dès leur ajout, si bien que les 6 langues du cours sont maintenant à 100 % de parité de contenu (12 214 clés chacune) — c'est précisément pour cela que le nombre de comparaisons de localisation est passé de 58 635 à 61 070.

Remarques

Au cours de la validation itérative, trois bugs ponctuels de l'interpréteur ont été trouvés et corrigés — aucun n'a jamais affecté de contenu réel du cours, et les trois sont désormais verrouillés par des tests permanents qui veillent à ce qu'ils ne réapparaissent pas :

Le plafond d'intervalle maximal de l'algorithme de répétition espacée s'est correctement déclenché sur plusieurs centaines d'étapes conçues pour l'atteindre, sans aucune exception. Les résultats ci-dessus proviennent de la dernière ronde de validation, sans problème connu en suspens.