Tests unitaires embarqués (par spécification)
Génère une suite de tests unitaires pour un ensemble de fichiers .c/.h remis par l'utilisateur, en utilisant les 7 étapes des tests par spécification de Maurício Aniche (Effective Software Testing, ch. 2). Le jugement qui fait de ceci un skill : bien partitionner le domaine de chaque fonction (l'étape la plus précieuse), dériver les limites, concevoir la plus petite suite qui détecte encore les bugs, l'automatiser dans le bon framework du projet et itérer jusqu'à ce que la couverture atteigne la cible.
Lisez references/specification-based-testing.md avant la Phase 3 — c'est l'étalonnage pour savoir comment penser les partitions et les limites, et les pragmatiques qui évitent une suite gonflée ou fragile.
Invariants de périmètre (à lire en premier)
- Sortie en anglais. Tout ce que ce skill produit est en anglais, même lorsque l'utilisateur écrit dans une autre langue : code de test, noms de test, commentaires, catalogue, annotations
Covers:, messages de saut/ignoré, la déclaration de chemin et le résumé final. - Tests unitaires uniquement. Ne jamais générer de tests d'intégration, e2e, système, HIL ou Robot Framework. La granularité est la fonction/module isolé, avec les dépendances simulées/moquées.
- Seulement les fichiers donnés. La suite concerne l'ensemble
.c/.hpassé par l'utilisateur. Ne pas scanner le projet pour d'autres cibles ni tester les modules voisins de votre propre chef. - Ne jamais modifier les fichiers cibles — pas temporairement, pas "pour tester et revenir en arrière". La suite compile les fichiers
.coriginaux, sans copie dans le répertoire de test. Pour atteindre quelque chose de difficile, utilisez l'API publique, les fausses dépendances ou un binaire de test séparé. - Autonome. Générer, exécuter et rapporter sans demander de confirmation pour exécuter. Il y a deux exceptions où vous vous arrêtez et demandez : (a) Chemin C (ni Zephyr ni ESP-IDF) où vous ne pouvez identifier aucun framework de test unitaire (Phase 0) ; (b) toutes les 2 itérations de couverture sans atteindre la cible (Phase 8).
- Aucun rapport rédigé par vous. Vous n'écrivez pas de rapport de résultats. Exécutez les outils et pointez vers les artefacts qu'ils génèrent (HTML de couverture, sortie de l'exécuteur). Le "résultat" est ce que l'outil émet, pas votre prose.
- Objectif de couverture : fonction ≥ 95 %, ligne ≥ 95 %, branche ≥ 90 % (95/95/90) par défaut. Une fois la cible atteinte, arrêtez d'itérer et listez les lignes/branches non couvertes restantes. Allez vers 100/100/100 seulement si l'utilisateur le demande. En dessous de la cible seulement lorsque ce qui manque est prouvablement inaccessible (bruit de log/assert, limitation de testabilité du module), avec chaque ligne/branche restante justifiée.
Le flux (7 étapes d'Aniche → 9 phases)
Les 7 étapes ne sont pas un pipeline rigide — elles sont itératives (la Phase 3 révèle souvent quelque chose qui vous renvoie à la Phase 1). Les phases ci-dessous sont la colonne vertébrale.
| Phase | Étape d'Aniche | Ce que vous faites |
|---|---|---|
| 0 | — | Détecter l'environnement → Chemin A (Zephyr), B (ESP-IDF) ou C (autre) |
| 1 | 1 Comprendre | Besoin, entrées, sorties de chaque fonction donnée |
| 2 | 2 Explorer le code | Cartographier les dépendances à isoler avec des fausses/maquettes |
| 3 | 3 Partitions | Catalogue numéroté des partitions (l'étape la plus importante) |
| 4 | 4 Limites | Catalogue numéroté des limites (point on/off) |
| 5 | 5 Concevoir les cas | Combiner partitions/limites, réduire les combinaisons |
| 6 | 6 Automatiser | Écrire la suite dans le framework du chemin, avec traçabilité |
| 7 | — | Exécuter les outils : exécution + couverture |
| 8 | 7 Augmenter | Itérer sur les écarts (couverture + éléments du catalogue sans test) |
Phase 0 — Détection de l'environnement
Identifiez l'environnement du projet contenant les fichiers donnés, dans cet ordre, et choisissez le chemin. Robot Framework, HIL, pytest d'intégration ne sont PAS des frameworks de test unitaire — ils ne comptent jamais dans aucun chemin.
-
Zephyr ? → Chemin A. Indices :
west.yml/west.yaml;CMakeLists.txtavecfind_package(Zephyr ...);prj.conf;testcase.yaml/sample.yaml;tests/;ztest/ZTEST_SUITE. Outillage : twister + ztest + FFF + couverture. Lisezreferences/zephyr-path.md. -
ESP-IDF ? → Chemin B. Indices :
CMakeLists.txtavecidf_component_register(;sdkconfig/sdkconfig.defaults; utilisation de$ENV{IDF_PATH}/idf.py; un composantmain/. Outillage : Unity natif d'ESP-IDF (construction hôte, ciblelinux) + gcov/gcovr. Utilisez Unity directement — ne vous arrêtez pas pour demander. Exception : si le projet utilise déjà systématiquement un autre framework de test unitaire (Ceedling, GoogleTest…), suivez celui-là. Lisezreferences/espidf-path.md. -
Autre (ni Zephyr ni ESP-IDF) → Chemin C. Inspectez la structure de test unitaire existante (Unity, CMock, Ceedling, GoogleTest, personnalisé) et suivez le motif du projet fidèlement. Si aucun framework de test unitaire n'est trouvé, ARRÊTEZ et demandez à l'utilisateur lequel utiliser — ne supposez jamais une valeur par défaut silencieuse dans le Chemin C. Lisez
references/non-zephyr-path.md.
Indiquez dans le texte de réponse (pas seulement dans votre raisonnement), avant de continuer, le chemin choisi et les indices concrets qui le justifient, avec le fichier et ce que vous avez trouvé — par exemple "Chemin A (Zephyr) : zephyr/west.yml, find_package(Zephyr) dans tests/.../CMakeLists.txt, suites ZTEST_SUITE dans tests/lib/."
Phase 1 — Comprendre le besoin, les entrées et les sorties
Pour chaque fonction que la suite couvrira, extrayez de la lecture du .c/.h :
- la règle métier : ce qu'elle fait et ce qu'elle ne doit pas faire (coins inclus) ;
- les entrées : types, plage/domaine, contraintes entre variables ;
- les sorties : comment les entrées deviennent le résultat (et les effets secondaires observables : sortie écrite, erreur retournée, mouvement émis/non émis).
Une partie est explicite ; une partie est des règles de domaine implicites (saturation, enroulement, unités, signe, jeu) que vous inférez. Ces règles implicites sont là où les bugs se cachent.
Phase 2 — Explorer le code
Lisez l'ensemble du .c/.h et construisez la carte des dépendances à isoler : HAL, pilotes, noyau/RTOS, E/S, temps, appels vers d'autres modules. Chaque dépendance devient une fausse/maquette dans la Phase 6. Identifiez également comment la logique réelle est atteinte (API publique directe, ou fonctions static via handle/vtable — appelez-les via l'API réelle, n'inventez jamais une signature).
Phase 3 — Identifier les partitions (l'étape la plus importante)
Appliquez les 3 lentilles de references/specification-based-testing.md à chaque fonction : chaque entrée isolément, les entrées en combinaison, et les classes de sortie. Considérez NULL/vide et les règles de domaine implicites. Si la fonction a un état, partitionnez également par l'état du module/contexte.
Produisez, par fonction, un catalogue nommé et numéroté avec des ID stables dans la suite :
- Partitions :
<func>/P1, P2, … - Limites (remplies dans la Phase 4) :
<func>/B1, B2, …
Chaque élément obtient une courte description. Ce catalogue est le contrat de traçabilité de la suite (matérialisé dans la Phase 6) et la source de vérité pour les écarts (Phase 8). Une Phase 3 bien faite devrait couvrir presque tout dès le premier passage de couverture.
Phase 4 — Analyser les limites
Pour chaque limite entre partitions voisines, ajoutez au catalogue le point on (la valeur dans la condition) et le point off (le voisin le plus proche dans la partition opposée) ; les égalités donnent un point on et deux points off. Supprimez une limite déjà couverte par un autre cas — ne dupliquez pas. "Partition ou limite ? Peu importe" : ce qui compte, c'est que le cas existe.
Phase 5 — Concevoir les cas de test
Combinez les partitions entre catégories d'entrée, mais réduisez l'explosion combinatoire : testez l'exceptionnel (NULL/vide) une fois et ne le combinez pas avec tout ; utilisez le domaine pour couper. Utilisez une seule entrée de base, en faisant varier le minimum par cas (facilite le débogage). En cas de doute, l'entrée la plus simple. Pour les entrées qui ne participent pas au comportement, des valeurs raisonnables/réalistes. Si un cas a la même structure que plusieurs autres, planifiez-le comme paramétré. Listez les cas et, pour chacun, les ID du catalogue qu'il exerce.
Phase 6 — Automatiser (avec traçabilité obligatoire)
Écrivez la suite dans le framework du chemin détecté (syntaxe, isolation et détails d'échafaudage dans references/zephyr-path.md, references/espidf-path.md ou references/non-zephyr-path.md).
Le catalogue des Phases 3/4 devient des commentaires dans la suite, à deux niveaux :
- En-tête par fonction — un bloc au début de la section de test de cette fonction, listant l'ensemble du catalogue (chaque partition/limite avec son ID et sa courte description).
- Annotation par test — chaque test référence quel(s) élément(s) il exerce, par exemple
/* Covers: P3 + B1 */.
Règles obligatoires (c'est ce qui rend la traçabilité précieuse) :
- Pas de test orphelin : chaque test référence ≥ 1 élément du catalogue.
- Pas d'élément orphelin : chaque partition/limite est exercée par ≥ 1 test. Un élément sans test est signalé comme un écart (alimente la Phase 8).
- ID stables et identiques entre l'en-tête et les annotations — pour qu'un
grep "P4"liste l'en-tête et tous les tests touchant cette partition. - Un test couvrant une combinaison liste tous les ID (
/* Covers: P2 + P5 */). Covers:dit la vérité : chaque ID annoté correspond à ce que le cas exerce réellement (entrée, état et assert), pas seulement à un élément qui existe dans le catalogue. Avant la Phase 7, relisez les annotations par rapport à chaque corps de test et corrigez celles qui ne correspondent pas.- Dans les Chemins B et C, le même schéma, adapté à la syntaxe du framework (commentaire au-dessus de chaque
TEST(...)/TEST_CASE(...)/TEST_F(...)).
Forme canonique (Chemin A, ztest) :
/*
* Catalogue — motor_ctrl_transform (Phase 3 · tests par spécification)
* Partitions :
* P1 p_axis_values == NULL -> rejeté, sortie non écrite
* P2 axis_count valide (1..N) -> transforme
* P3 valeur positive -> pas positifs
* P4 valeur négative -> signe conservé
* P5 valeur nulle -> pas nuls
* Limites :
* B1 axis_count == N (max) -> accepté
* B2 axis_count == N+1 -> rejeté
*/
/* Covers: P2 + P3 */
ZTEST(motor_ctrl_suite, test_transform_positive_gives_positive_steps) { /* ... */ }
/* Covers: P1 */
ZTEST(motor_ctrl_suite, test_transform_null_input_is_rejected) { /* ... */ }
Remarque : dans Zephyr récent, l'exécuteur exige que les noms de test commencent par
test_(ZTEST(suite, test_...)) — sans ce préfixe, twister ne collecte pas le cas.
Défaut trouvé : le code diverge de la documentation
Lorsque le code contredit l'en-tête, le commentaire de fonction ou le document de contrat API (par exemple, retourne un code d'erreur différent de celui documenté, ne restaure pas l'état en cas d'échec), ne laissez pas la suite en rouge et n'utilisez pas ZTEST_EXPECT_FAIL. Écrivez deux tests :
- Comportement documenté, ignoré avec une raison — l'assertion de ce que la documentation promet, sautée avec la raison :
ztest_test_skip()(Chemin A) ouTEST_IGNORE_MESSAGE("BUG: ...")(Chemin B). Commentaire au-dessus :/* Covers: Pn — BUG: <les docs disent X ; le code fait Y> */. - Caractérisation — épingle le comportement actuel, afin que tout changement (y compris la correction) apparaisse. Commentaire :
/* Covers: Pn — caractérisation du BUG ci-dessus */.
La suite se termine en vert, le bug apparaît à chaque exécution (en saut/ignoré) et le binaire entre dans les mesures. Syntaxe dans les fichiers de chemin. Listez les défauts dans le résumé final.
Phase 7 — Exécution + couverture
Avant d'exécuter, activez l'environnement du projet. L'outil de test est rarement sur le PATH global — il vit généralement dans un virtualenv du dépôt. Ne concluez pas "impossible d'exécuter" avant d'avoir cherché et activé l'environnement (cela s'applique à chaque chemin) :
- Cherchez un venv à la racine du firmware (
.venv/,venv/,env/) et utilisez son binaire (.venv/bin/west,.venv/bin/python) ousource .venv/bin/activate. Le venv est souvent un lien symbolique : vérifiez avectest -e .venv/bin/activateouls -laL(qui suit le lien), jamais seulement avecls -d/find -type d. Dites ce que vous avez trouvé : le chemin du venv utilisé, ou "pas de venv dans<répertoires vérifiés>". - Zephyr : définissez
ZEPHYR_BASE(par exempledeps/zephyr, ouwest topdir/.west/config) et, pour l'hôte,ZEPHYR_TOOLCHAIN_VARIANT=host. Siwestn'est pas sur lePATHmais que l'arbre Zephyr existe, exécutez twister directement :python3 $ZEPHYR_BASE/scripts/twister .... - ESP-IDF : assurez-vous que l'environnement IDF est défini (
. $IDF_PATH/export.shlorsqu'il existe) pour la construction hôte ; sinon compilez avecgccdirectement (voirespidf-path.md).
Ce n'est qu'après avoir épuisé cette recherche qu'il est honnête de signaler que l'exécution n'a pas pu avoir lieu ici.
Économie de contexte (les constructions de firmware sont verbeuses et chaque tour relit tout) :
- Redirigez la construction, twister et gcovr vers un fichier (
> build.log 2>&1) et montrez seulement le résumé (tail -n 30 build.log,grep -E 'error|FAIL|PASS|lines|branches' build.log). Ouvrez l'ensemble du journal seulement lorsque le résumé n'explique pas l'échec. - Écrivez chaque fichier de test en entier en une fois ; corrigez avec des modifications ciblées.
- Regroupez les lectures et vérifications indépendantes dans le même tour ; ne relisez pas les fichiers que vous avez déjà lus.
Exécutez l'outil du chemin et pointez vers les artefacts qu'il génère (n'écrivez pas de rapport) :
- Chemin A (Zephyr) :
west twister --coverage -p native_sim -T <chemin_suite>. Artefacts :twister-out/coverage/index.html,twister-out/coverage.json,twister-out/twister.xml,twister-out/twister.json. - Chemin B (ESP-IDF) : construction hôte avec Unity +
gcov/gcovr(voirespidf-path.md) ; pointez vers le rapport de couverture (coverage.html/.xml) et la sortie de test. - Chemin C (autre) : le propre exécuteur du projet + couverture (par exemple
ceedling gcov, ougcov/gcovrsur la construction de test). Conservez et pointez vers les artefacts émis.
Détails et dépannage dans le fichier de chaque chemin.
Phase 8 — Itération de couverture
Comparez la couverture rapportée par les outils avec la cible 95/95/90 (ou celle demandée par l'utilisateur). En dessous de la cible, chaque ligne/branche atteignable sans test est un écart ; une fois la cible atteinte, arrêtez et listez ce qui reste non couvert. Pour chaque écart, ajoutez des cas en utilisant les deux sources : la
couverture numérique (lignes/branches rouges) et …
[…] (tronqué pour la traduction)