LEÇON 11 / 55 minutes

Erreurs, tests et qualité

Un programme fiable explique ses hypothèses, détecte les entrées invalides et vérifie les cas importants. Les diagnostics, contrats et tests sont des outils d’apprentissage autant que de qualité.

À retenir·Une erreur est une situation à modéliser ; un test est une expérience qui rend une propriété vérifiable.

Lire les diagnostics comme des indices

Un diagnostic pointe un problème observé par une étape du compilateur. Il peut concerner la syntaxe, un nom inconnu, un type incohérent, une visibilité invalide ou une contrainte. Lisez-le dans cet ordre : le code et le titre, le fichier et la ligne, puis le fragment souligné et les notes associées.

Corrigez d’abord l’erreur la plus proche de la cause. Si l’analyseur n’a pas compris une déclaration, les erreurs suivantes peuvent être des conséquences. Évitez de modifier plusieurs choses à la fois : vous ne saurez plus quelle correction a résolu le problème.

Diagnostic vérifié : type d’opérande incorrect

examples/tutorials/erreur-type.vit combine and avec un entier. Pour reproduire le diagnostic :

build/bin/vitte check examples/tutorials/erreur-type.vit --quiet --no-color --diagnostic=json

Résultat confirmé : code de sortie 1, diagnostic VITTE_TYPE_E_INVALID_OPERAND (l’opérateur logique attend un booléen).

Assertions et contrats

Une assertion exprime une propriété qui doit être vraie à un point précis. La forme assert(expression); est utile dans un test ou pour vérifier un invariant établi par le programme. Ne l’utilise pas pour remplacer une validation d’entrée que l’utilisateur peut raisonnablement faire échouer ; une entrée invalide doit recevoir un traitement prévu.

proc verifier_taille(taille: i64) {
    assert(taille >= 0);
}

Pour une procédure publique, les clauses requires et ensures décrivent des règles attachées à son interface. Elles sont particulièrement utiles quand un calcul a une précondition non évidente. Une assertion explique un état à un endroit ; un contrat décrit les attentes entre l’appelant et la procédure.

Exemple vérifié : contrat exécuté

examples/tutorials/contrats.vit double 21 et vérifie la postcondition décrite par le programme. Le CLI route cette construction vers le chemin bootstrap/compatibilité.

build/bin/vitte run examples/tutorials/contrats.vit --quiet --no-color

Résultat confirmé : code de sortie 0.

Écrire un test qui raconte un cas

La grammaire prévoit des blocs test, nommés ou non. L’exemple suivant illustre leur forme, mais la suite du dépôt ne confirme pas qu’ils soient exécutés comme des tests Vitte : un cas vérifié montre seulement que l’émetteur de compatibilité accepte la déclaration.

test "addition des prix" {
    assert(additionner(20, 22) == 42);
}

test "addition avec zéro" {
    assert(additionner(9, 0) == 9);
}

Le nom du test doit décrire le comportement qu’il vérifie, pas seulement le nom de la procédure testée. Un test nommé « addition avec zéro » indique l’hypothèse importante ; « test additionner » est beaucoup moins informatif.

Choisir les bons cas

Pour une procédure qui travaille sur une plage, les cas utiles comprennent une valeur ordinaire et les deux frontières. Pour une collection, les tailles zéro, un et plusieurs révèlent des comportements distincts. Pour un calcul entier, les valeurs négatives, zéro, les valeurs positives et les risques de dépassement couvrent les grandes catégories. Pour une condition, chaque branche devrait apparaître dans au moins un cas.

Un test prouve seulement qu’un exemple précis se comporte comme attendu. Plusieurs tests bien choisis rendent une régression moins probable, mais ne remplacent ni une spécification ni une revue des hypothèses. Les grands tests du dépôt documentent des contrats précis et sont de bons modèles pour nommer une suite de cas.

Représenter un échec comme une donnée

Quand une opération peut échouer normalement, représente cette possibilité dans son résultat au lieu de cacher l’échec derrière une valeur ambiguë. Un résultat peut porter un booléen de réussite, une valeur et une catégorie d’erreur ; un choix pick peut aussi nommer les issues possibles. La bibliothèque fondamentale fournit des formes de résultat concrètes, même si l’intégration de certains types génériques continue d’évoluer.

Par exemple, un parseur devrait distinguer « l’entrée représente zéro » de « l’entrée est invalide ». Renvoyer simplement zéro pour les deux cas fait perdre de l’information à l’appelant.

Un test durable décrit le contrat

Un test utile décrit ce que le code doit garantir depuis l’extérieur. Il reste pertinent même si vous réécris le corps de la procédure.

Construire une matrice de cas

Un ensemble de tests devient plus complet quand vous choisissez ses cas de manière systématique. Pour une procédure qui reçoit deux nombres, examinez leurs plages et leurs rôles avant d’écrire des valeurs arbitraires. Une petite matrice pour un prix et une quantité peut comporter :

FamilleExemple de question
Cas ordinaireDeux valeurs positives donnent-elles le sous-total attendu ?
ZéroUn prix nul ou une quantité nulle donne-t-il zéro ?
Limite autoriséeLa plus grande valeur permise est-elle traitée correctement ?
Entrée invalideUne valeur négative est-elle refusée ou représentée comme erreur ?
InteractionDeux limites présentes en même temps déclenchent-elles un résultat défini ?

vous n’avez pas besoin d’un test pour chaque nombre possible. Choisissez des valeurs qui appartiennent à des catégories de comportement distinctes, puis ajoutez des cas lorsqu’un défaut est découvert.

Séparer diagnostic, test et validation

Un diagnostic du compilateur répond à la question « cette source peut-elle être acceptée à cette étape ? ». Un test du programme répond à la question « pour cette entrée, le résultat observé respecte-t-il l’attente ? ». Une validation dans l’application répond à la question « cette entrée externe appartient-elle au domaine autorisé ? ».

Ces trois protections se complètent mais ne se remplacent pas. Un programme peut compiler tout en calculant une mauvaise valeur. Un test peut passer pour un cas particulier alors qu’une autre frontière est incorrecte. Une assertion interne ne doit pas remplacer un message d’erreur pour une saisie utilisateur prévisible.

Réduire un défaut à une reproduction lisible

Quand un résultat surprend, conservez les entrées et la commande, puis supprimez le code qui n’intervient pas dans le problème. Une bonne reproduction tient dans un fichier court et permet à quelqu’un d’autre de constater la même chose. Notez le frontend, la phase et le diagnostic. Cette information évite d’attribuer à « Vitte » un comportement qui vient en réalité d’un backend particulier ou d’un chemin de compatibilité.

Pour un diagnostic négatif attendu comme VITTE_TYPE_E_INVALID_OPERAND, le test réussi consiste justement à observer le code de sortie non nul et le code de diagnostic prévu. Dans un test positif, un code de sortie 0 ne suffit pas toujours : examine aussi la sortie ou une valeur contrôlée par le programme.

Quand un échec devient une valeur

Un échec d’entrée qui se produit normalement doit apparaître dans l’interface. Un choix pick peut nommer la réussite et différents échecs ; chaque variante peut porter les informations dont l’appelant a besoin. Cette modélisation facilite les tests, car chaque état a un nom et un résultat à vérifier.

La forme générique de résultat présente dans les bibliothèques du projet n’est pas automatiquement disponible à chaque étape de compilation. Le modèle peut être décrit indépendamment, puis rapproché de la forme concrètement utilisable d’après la matrice de support et les exemples runtime.

Règle d’évaluation

Un cas de test doit préciser une entrée, une action, une attente et une manière d’observer le résultat. Si l’une de ces quatre pièces manque, il est difficile de savoir ce qu’un échec signifie.

Un diagnostic décrit une phase et un périmètre

Un diagnostic Vitte est plus qu’un texte d’erreur : son code et son emplacement situent la construction refusée. Une erreur lexicale ou syntaxique signifie généralement que la structure attendue n’a pas été reconnue. Une erreur sémantique peut indiquer un nom inaccessible ou un type incompatible après le parsing. Un diagnostic backend peut apparaître plus tard, lorsque la représentation est comprise mais ne peut pas être traduite en code final. La même ligne de source peut donc être acceptée par une commande et refusée par une autre si elles ne parcourent pas les mêmes étapes.

Le cas vérifié VITTE_TYPE_E_INVALID_OPERAND montre un rejet de typage : and reçoit un entier à la place d’une condition booléenne. Cette précision mène à la cause — le type de l’opérande — et permet de distinguer ce problème d’une faute de ponctuation. Lorsqu’un programme produit plusieurs messages, les premiers peuvent expliquer les suivants ; l’ordre des corrections doit suivre le premier endroit où le compilateur ne peut plus interpréter la source comme prévu.

Test, contrat et validation couvrent des risques différents

Un test exécute un cas et compare le résultat observé à une attente. Un contrat déclare les préconditions nécessaires et les propriétés promises par une procédure. Une validation décide si une entrée provenant du monde extérieur appartient au domaine autorisé. Ces mécanismes ne répondent pas à la même question : le test observe un exemple, le contrat formalise une relation, la validation choisit une réaction compréhensible face à une donnée incertaine.

Les valeurs de frontière sont particulièrement informatives. Pour un indice, zéro et la dernière position valide se distinguent de l’indice égal à la longueur. Pour une plage, les nombres juste sous, sur et juste au-dessus de la limite révèlent des erreurs de comparaison. Pour un calcul numérique, le type et les dépassements sont des propriétés autant que la formule. Une suite de tests utile couvre les comportements différents plutôt qu’une grande quantité de valeurs semblables.

Modéliser l’échec sans perdre l’information

Une valeur spéciale comme -1 peut entrer en conflit avec une valeur métier qui serait autrement valide. Un résultat explicite — booléen avec sortie séparée, type pick à variantes ou résultat documenté — permet à l’appelant de distinguer la réussite de l’échec. Les données associées à une erreur peuvent préciser la raison, la position ou la valeur refusée.

La forme générique Resultat<T, E> illustre ce modèle conceptuel, mais les types génériques du dépôt ne sont pas tous exécutables avec chaque backend. Lorsque ce support manque, une forme concrète peut transmettre les mêmes cas dans le sous-ensemble disponible. La documentation d’un test ou d’une API gagne à préciser cette limite au lieu de laisser croire que la représentation conceptuelle est nécessairement disponible dans tous les chemins de compilation.

Reproduire un défaut avec un contexte suffisant

Une reproduction minimale contient la construction en cause et les dépendances indispensables à son diagnostic. Supprimer une partie du contexte peut faire disparaître le défaut ; conserver trop de code rend la cause difficile à isoler. La bonne reproduction reste assez courte pour que le parseur, l’analyseur ou le backend concernés soient clairement identifiables et que la commande puisse être reprise par une autre personne.

Le contexte de version est essentiel pour un compilateur en évolution. Le chemin bootstrap, le frontend natif, les options de diagnostic et les modules recherchés peuvent produire des résultats différents. Une reproduction fiable conserve ces paramètres et le code complet du diagnostic, y compris les notes utiles. Une différence de backend ne doit pas être présentée comme une règle générale du langage.

Le rôle distinct des fichiers de test

Une fixture peut vérifier que le programme finit avec le statut prévu, que la sortie contient une valeur particulière ou que le compilateur produit un code de diagnostic donné. Ces assertions ne mesurent pas toutes la même chose. Une fixture runtime observe un programme généré ; une fixture négative observe le refus du compilateur ; un test de bibliothèque peut contrôler une propriété d’API sans être un exemple directement exécutable par l’utilisateur.

Les déclarations test de la grammaire sont décrites dans la documentation, mais l’audit ne confirme pas leur exécution complète comme suite de tests Vitte. Les tests du dépôt peuvent aussi être pilotés par des outils externes. La manière dont un fichier est nommé n’établit donc pas à elle seule quel mécanisme le lance.