Blog ·
Vert parce qu’il ne pouvait pas y parvenir
Notre test de contrat répondait « non pris en charge » à la commande la plus ordinaire. Chaque test qui en dépendait était vert pour la mauvaise raison.
Par Provibr
Nous entretenons un agent factice. Il parle le même protocole que le vrai, il tourne à côté de la passerelle, et il existe pour que le côté plateforme de chaque commande puisse être testé sans toucher à l’hyperviseur d’un client. C’est un test de contrat avec un processus autour.
Jusqu’à cette semaine, cet agent factice répondait à la commande la plus ordinaire qui soit, éteindre cette machine, par COMMAND_STATUS_UNSUPPORTED.
Personne ne l’avait remarqué, et la raison pour laquelle personne ne l’avait remarqué est tout le propos de cet article. Chaque test qui envoyait une commande d’alimentation à l’agent factice était vert. Ils étaient verts parce que la commande était refusée, parce que la plateforme traitait ce refus correctement, et parce que l’assertion finale tenait. Les tests ne mentaient pas. Ils décrivaient un chemin que personne n’avait voulu emprunter.
Ce qu’une branche manquante fait aux tests qui l’entourent
Le moteur de flux a un nœud d’action : exécuter une commande contre un serveur, puis suivre le fil ok ou le fil error selon la manière dont cela s’est passé. Ce nœud était dans le produit depuis la ronde qui l’a introduit, et il avait des tests.
Ces tests ne pouvaient atteindre que le fil error. Non pas à cause d’un défaut en eux, mais parce que le seul agent dont ils disposaient refusait chaque commande qu’ils pouvaient envoyer. Un test qui affirme « la branche d’erreur a été prise » passe parfaitement bien quand l’erreur a été fabriquée par le harnais de test plutôt que par la chose testée.
C’est la forme contre laquelle nos propres notes mettent en garde, dans une phrase qui figure depuis des mois dans le manuel du projet : un test peut être vert parce qu’il ne peut pas atteindre le bon chemin. La variante qui nous a piégés ici est pire que l’ordinaire, car il ne s’agit pas d’une branche qui n’aurait jamais été écrite. La branche était écrite, elle fonctionne, et elle avait pendant tout ce temps un test vert à côté d’elle.
L’agent factice devait tenir un état, pas seulement répondre
Ajouter les cinq commandes d’alimentation n’était pas une affaire de renvoyer une réussite. La plateforme décide de ce qu’une commande a le droit de faire en lisant le statut courant du serveur : on ne peut pas reprendre une machine qui tourne, on ne peut pas démarrer une machine déjà allumée. Cette décision vit dans la plateforme, et elle lit une colonne.
Un agent qui répond « terminé » sans rien changer laisse cette colonne là où elle était. Un test peut donc envoyer exactement une commande, puis il est bloqué, car la seconde commande est refusée par la machine à états de la plateforme elle-même, correctement, sur un statut qui n’a jamais bougé.
L’agent factice tient donc un état d’alimentation par machine et renvoie l’observation d’état sur la session, exactement comme le fait un vrai agent. C’est cela qui manquait réellement. Non pas une réponse, une conséquence.
Une fois cela en place, un flux qui boucle sur une liste et émet deux commandes par passage a pu être exécuté contre l’agent factice pour la première fois. Six commandes, toutes arrivées à l’état terminé, et une boucle qui a atteint son second passage parce que le premier passage avait effectivement changé quelque chose.
Deux choses que l’agent factice ne fait délibérément pas
Il ne refuse pas les transitions impossibles avec un code d’erreur inventé. La frontière entre autorisé et non autorisé vit dans la plateforme, et les codes de motif de l’agent sont une liste fixe compilée dans les sources du vrai agent. Inventer un code que le vrai agent ne possède pas reviendrait à tester un contrat qui n’existe pas. Un agent factice plus strict que la chose qu’il remplace vous apprend des choses qui ne sont pas vraies.
Il ne simule pas non plus la durée. Un arrêt sur un vrai invité attend que le système d’exploitation réponde et se rabat sur un arrêt brutal après un délai. Dans l’agent factice, tout se produit à l’intérieur du battement. Mais le délai et le drapeau de repli sont transportés jusque dans la sortie, si bien qu’un test peut voir qu’ils sont arrivés même si rien ne les a attendus. Transmettre un paramètre et l’honorer sont deux affirmations différentes, et une seule des deux est faite ici.
Un second réveil rend une mesure aléatoire
L’autre réparation de la même ronde porte sur la mesure, pas sur le produit.
Les exécutions en pause sont réveillées par un ouvrier de la passerelle qui interroge une file d’attente toutes les dix secondes. Notre script de bout en bout fait le réveil lui-même, dans son propre processus, afin de pouvoir affirmer exactement ce qui s’est passé. Ces deux mécanismes vont bien séparément et sont catastrophiques ensemble : si une passerelle dont le réveilleur est actif tourne contre la même base de données de développement, elle mange les lignes dès que leur heure de réveil est passée, et le script mesure celui des deux qui est arrivé le premier.
C’est arrivé. Une branche du script a échoué une fois sans que rien ne cloche en elle, ce qui est la sorte d’échec la plus coûteuse, car la réaction naturelle est de partir à la recherche d’un défaut.
Le correctif n’est pas un commentaire disant « assurez-vous qu’aucun ouvrier ne tourne ». C’est une fonction qui le mesure. Avant toute chose, le script crée une attente jetable dont l’heure de réveil est déjà passée, l’observe pendant huit secondes, et vérifie si quelqu’un d’autre y a touché. Si c’est le cas, le script refuse de s’exécuter et dit pourquoi.
C’est peu de code pour beaucoup de confiance. Une hypothèse écrite dans un commentaire est une hypothèse ; la même hypothèse écrite comme une vérification qui s’exécute en premier est une précondition.
Les tentatives ont besoin d’un plafond et d’une temporisation croissante
Pendant que les bons chemins devenaient enfin atteignables, les chemins d’échec ont eux aussi été mesurés correctement.
Une attente qui lève une erreur à la reprise, parce qu’un modèle a été supprimé ou qu’un serveur n’existe plus, serait sinon réessayée à chaque battement pour toujours, dans une table de file d’attente qui ne se vide jamais. Il y a un plafond de cinq tentatives avec une temporisation qui double : soixante, cent vingt, deux cent quarante, quatre cent quatre-vingts secondes. Ces quatre intervalles ont été lus dans une exécution réelle plutôt que dans la fonction qui les calcule. À la cinquième tentative, l’attente est annulée et l’exécution est marquée en échec.
La contre-épreuve est la partie qui compte. Avec le garde-fou d’épuisement désactivé, la même exécution reste indéfiniment dans l’état d’attente. Un garde-fou n’est prouvé que lorsque la situation qu’il empêche a réellement été produite.
Il y a un second mode de défaillance en dessous. Un ouvrier revendique une attente en la sortant de l’état d’attente avant de commencer à parcourir, car les effets de bord d’un flux se produisent pendant le parcours et non après. Si le processus qui l’a revendiquée meurt en plein parcours, la ligne reste pour toujours dans l’état de revendication, et la requête qui cherche les attentes échues ne la voit pas : elle demande des lignes en attente. Un balayage remet en place les revendications de plus de quinze minutes, et un seul battement contre une passerelle réellement en marche en a libéré une, réveillé une et repris une, sans aucun échec.
De petites choses que l’on ne trouve qu’en les exécutant
Un délai de trois secondes ne produit pas un nœud invalide. Il rend le graphe entier invalide, car le minimum est de cinq, et l’exécuteur ne renvoie rien du tout plutôt qu’une exécution avec un nœud cassé. C’est correct et c’est aussi déroutant la première fois, car on attendait une erreur portant sur un nœud et on a obtenu un silence portant sur un graphe.
Les ports de la passerelle portent le nom de la passerelle. Définir les variables sans ce préfixe la laisse écouter sur ses valeurs par défaut, en silence, ce qui ressemble exactement à un problème de réseau.
Un arrêt écrit dans le registre les nœuds qu’il n’a jamais atteints, marqués comme ignorés. C’est une différence délibérée avec le fait de les omettre : une exécution qui s’est terminée tôt et une exécution qui a pris un autre chemin se ressemblent exactement si les nœuds non atteints sont simplement absents.
Et un nœud d’approbation laissait des lignes orphelines dans la table des notifications, seize sur la base de données de développement. Le nettoyage correspondant se fait par exécution et s’appuie sur la clé de déduplication, jamais sur une correspondance de motif sur un préfixe, car une correspondance de motif emporterait des lignes appartenant à la ronde de quelqu’un d’autre.
Ce que nous noterions
- Un test qui ne peut pas atteindre le bon chemin est vert pour la mauvaise raison. Demandez-vous ce que ferait ce test si la fonctionnalité était supprimée.
- Un agent factice qui refuse tout n’est pas un agent factice, c’est un mur. Chaque test derrière lui mesure le refus.
- Un objet factice doit tenir un état, pas seulement répondre. Si la plateforme décide d’après une colonne, quelque chose doit faire bouger cette colonne.
- Ne rendez pas un agent factice plus strict que la chose réelle. Un code d’erreur inventé teste un contrat que personne n’implémente.
- Transmettez un paramètre même quand vous ne pouvez pas l’honorer, afin qu’un test puisse prouver qu’il est arrivé.
- Deux réveilleurs sur une même base de données rendent la mesure aléatoire. L’un des deux gagne et vous ne pouvez pas savoir lequel.
- Transformez la précondition du commentaire en une vérification qui s’exécute en premier. C’est alors un fait plutôt qu’un espoir.
- Prouvez un garde-fou en le désactivant. Un plafond de tentatives que personne n’a atteint n’est qu’une affirmation.
- Une ligne revendiquée est invisible pour la requête qui cherche le travail échu. Quelque chose doit remettre en place les revendications périmées.
- Les nœuds non atteints ont leur place dans le registre, marqués comme ignorés, sans quoi une sortie anticipée est indiscernable d’un autre itinéraire.
Pour être clair : sept branches jusque-là non prouvées ont été prises de bout en bout, chacune avec une contre-épreuve rouge ; la vérification de fusion a exécuté 16 étapes sur 16 sans une seule ligne d’échec, la suite web a donné 4583 verts, les suites des paquets, de la passerelle et des outils 7287 verts, la suite propre de l’agent factice 481 verts, le script de bout en bout 89 verts, la branche du réveilleur 13 verts, la branche du courriel d’approbation 22 verts et la boucle avec de vraies actions 18 verts. Non prouvé : rien n’a été exécuté contre un vrai Proxmox, un graphe illisible sur la ligne d’attente elle-même, un lot plus grand que la taille de lot, et une remise de courriel réelle par SMTP.
Issu du travail de plateforme sur Provibr. La manière dont le parcours a appris à s’arrêter et à continuer demain est dans Chaque appel récursif était en position terminale, et l’endroit où passe la frontière entre une pause qu’une requête peut absorber et une pause qui suspend l’exécution est dans Une pause qui maintenait la commande ouverte.