ADR-035 — Taxonomie des exceptions de couverture
Statut : Accepté (2026-06-14). Companion d’ADR-031 (mesure de couverture en root) et de la politique de couverture couche 0 (Principe 1) ; n’amende ni l’un ni l’autre, il en grave la taxonomie d’exceptions.
Catégorie : Méthode (gouvernance technique / CI).
Contexte
La couche 0 vise 100 % de couverture (lignes + branches) hors exceptions
documentées (Principe 1). Le registre docs/COVERAGE-EXCEPTIONS.md énumère
chaque ligne/branche de production non couverte et la justifie. Au fil de
l’implémentation, six catégories d’exception sont apparues : trois
« canoniques » (STRUCTURAL, PRIVILEGE, FEATURE-KERNEL) et trois introduites en
cours de route (EFAULT-SAFE, CHILD-EXIT, TEST-HARNESS), plus une étiquette
VALUE-UNREACHABLE. L’audit 053 a montré que cette prolifération brouillait
l’invariant « couvrable VIDE » : certaines entrées « inatteignables » étaient
en réalité atteignables-mais-non-outillées (donc à couvrir, pas à
exempter — cf. le cas Sqpoll réglé au 046).
Décision
1. Quatre catégories d’exception, plus une étiquette de dette
Canoniques — branche/ligne qu’aucun test légitime ne peut atteindre :
- STRUCTURAL — inatteignable par construction : un invariant garantit que
le bras n’est jamais pris (longueur ≤ borne,
bid < count, free-list, sémantique kernel per-process, joker#[non_exhaustive]dont toutes les variantes réelles sont mappées, garde défensive permanente du Principe 5…). Absorbe les anciennes étiquettes :EFAULT-SAFE(brasEFAULT/EBADFexclu car buffers/sorties valides par construction) — cas particulier de STRUCTURAL ; le mot-clé reste autorisé dans la justification, pas comme catégorie ;VALUE-UNREACHABLEpermanente (joker exhaustif, garde défensive) — idem.
- PRIVILEGE — exige une capability / condition de privilège absente sur le
runner (
CAP_*,RLIMIT_*, Yamaptrace_scope, cpuset cgroup…). Le bras opposé est mort dans cet environnement. (Mesure root : cf. ADR-031.) - FEATURE-KERNEL — une feature kernel est présente sur les exécuteurs (≥ 6.12) ⇒ le bras de repli (kernel ancien / feature absente) est mort.
Limite d’instrumentation (le code s’exécute mais n’est pas mesurable) :
- CHILD-EXIT — code réellement exécuté dans un processus enfant forké
dont le profil LLVM ne peut pas être écrit : l’enfant a largué ses privilèges
(écriture
.profrawrefusée à un non-root), s’est confiné (seccomp-strict tue / Landlock refuse l’O_RDWR), ou s’estabort()é (bypass de l’atexit). Modèle fork+flush d’ADR-031. Preuve d’exécution : le code de sortie observé par le parent viawaitid. Distincte de STRUCTURAL (ce n’est pas inatteignable) et de PRIVILEGE (le privilège n’est pas le frein — l’instrumentation l’est).
Dette outillée (à résorber, jamais permanente) :
- DEFERRED-TOOLING — branche réellement atteignable mais non encore couverte faute d’outillage (un primitif manquant, un harnais à écrire). À couvrir, jamais exemption permanente. Chaque entrée nomme l’outillage requis et la condition de levée. Une exception « atteignable-mais-dure » doit être ici (jamais déguisée en STRUCTURAL).
2. Le code de test n’est pas une exception production
Les bras de code de test (court-circuit d’assert!, matches! de validation,
gardes de harnais) ne figurent pas au registre production. S’ils nuisent à
la lisibilité d’une mesure, ils sont consignés en annexe « résidus internes aux
tests », exclus du décompte. (L’ancienne étiquette TEST-HARNESS est
retirée du registre.)
3. Invariant « couvrable VIDE »
L’ensemble « couvrable » = les branches atteignables non couvertes hors
exception légitime. Il doit rester VIDE : toute branche atteignable est soit
couverte par un test, soit en DEFERRED-TOOLING avec un plan de levée. Une entrée
STRUCTURAL/PRIVILEGE/FEATURE-KERNEL/CHILD-EXIT atteste, elle, d’une
impossibilité (de construction, de privilège, de feature, d’instrumentation),
pas d’une dette.
Conséquences
- Registre simplifié : 4 catégories + 1 étiquette de dette, légende et récap
alignés.
EFAULT-SAFE/VALUE-UNREACHABLErepliées dans STRUCTURAL ;TEST-HARNESSen annexe. - Discipline anti-complaisance : une branche atteignable ne peut plus être
« rangée » en STRUCTURAL ; elle va en
DEFERRED-TOOLING(visible, à couvrir) ou est couverte. Application immédiate au055: les 3 candidates (fs::try_lockcontention,net::get_so_errorasync,process::pidfd_send_signalSome) sont couvertes par de vrais tests ⇒DEFERRED-TOOLINGvide. - Pas d’amendement à ADR-031 (mesure root) ni au Principe 1 (objectif 100 %) : ADR-035 ne fait que nommer les exceptions légitimes et bannir les fausses.
- Revue humaine du registre inchangée ; le plancher CI (
--fail-under-lines 96) reste l’anti-régression brut.
Alternatives écartées
- Garder les 6 catégories : prolifération sans valeur ;
EFAULT-SAFEetVALUE-UNREACHABLEpermanente sont indiscernables de STRUCTURAL à l’usage. - Supprimer toute catégorie non canonique : perdrait
CHILD-EXIT(limite d’instrumentation réelle, bien distincte) etDEFERRED-TOOLING(le filet anti-complaisance qui force la couverture des atteignables). - Exempter les branches atteignables-mais-dures en STRUCTURAL : exactement le
travers que cet ADR interdit (cf.
Sqpoll/046).
Références
- ADR-031 (mesure de couverture en root —
modèle fork+flush dont découle
CHILD-EXIT). - Principe 1 (couverture 100 % couches 0/1),
docs/COVERAGE-EXCEPTIONS.md. - Audit
053(~/Code/air-relay/053-consolidation-flaky-exceptions.out) et son application055.
Amendement — catégorie TARGET-ONLY (2026-06-29, RFC ADR-015)
Statut : Proposé (à ratifier BDFL). Companion d’ADR-052
(runtime air-runtime couche 1) et d’ADR-049. Étend la
taxonomie d’une cinquième catégorie ; n’amende ni ADR-031 ni le Principe 1.
Contexte
Le runtime userland (air-runtime, ADR-052) contient du code dont la sémantique n’est
correcte que sur la cible *-linux-air : programmer le registre TLS (set_fs/tpidr_el0),
créer un thread avec CLONE_SETTLS pointant un bloc TLS d’Air, appliquer les relocations
static-PIE, lire le self-pointer du TCB. Exécuter ces lignes sous le gate de couverture — qui
tourne sur *-linux-gnu (glibc) — est unsound : mêler le TLS d’Air et celui de glibc
corrompt l’état par-thread (c’est l’unsoundness #151, la raison d’être même du runtime). Ce code
ne peut donc pas tourner du tout sous le gate, et encore moins être mesuré.
Ce n’est ni STRUCTURAL (le code est atteignable et s’exécute — sur la bonne cible), ni CHILD-EXIT (là le code tourne dans le run du gate mais le profil n’est pas écrit ; ici il ne tourne pas dans le run du gate tout court), ni DEFERRED-TOOLING (ce n’est pas une dette d’outillage à résorber par un test hôte : aucun test hôte ne pourra jamais l’exécuter sainement).
Décision — TARGET-ONLY (limite d’environnement de gate)
-
TARGET-ONLY — code sound uniquement sur
*-linux-air, donc ni exécuté ni mesuré par le gate de couverture (*-linux-gnu). Preuve de correction : un selftest dédié sur*-linux-air(binairert/crates/airrt-selftest), validé par code de sortie sur les 2 arches (x86_64 sur carbon, aarch64 sur raspi/CI ARM) — même esprit de preuve-par-exit-code que CHILD-EXIT, mais le frein est l’environnement de cible du gate, pas l’instrumentation. -
Granularité = fichier. Le code TARGET-ONLY est isolé dans des fichiers dédiés, retirés de la mesure par
--ignore-filename-regex(précédent : le code généré_capnp.rs), jamais par une exception ligne-à-ligne. Où une ligne TARGET-ONLY isolée vit dans un fichier par ailleurs mesuré (p. ex. l’applier de relocation), elle est extraite dans un fichier dédié pour rester ignorable par fichier. Fichiers concernés (à date) :air-runtime/src/{thread_control_block, thread_local_storage,thread,start}.rs, le module de primitives target-only d’air-thread, l’applier dereloc. -
Le reste de la crate reste mesuré à 100 %. Retirer
--exclude air-runtime(exclusion en bloc du squelette) au profit de l’--ignore-filename-regexciblé : les modules host-testables (env/args/parser dereloc/machineriefork) retombent sous le 100 % lignes+branches. -
Discipline anti-complaisance. TARGET-ONLY n’est pas un refuge pour du code « dur à tester » (cela reste DEFERRED-TOOLING) : il est réservé au code structurellement insondable sur la cible du gate (unsoundness TLS Air/glibc). Chaque fichier TARGET-ONLY : commentaire in-code justifiant, nom du selftest qui le prouve, et condition de levée — réversible : la couverture sera réactivée le jour où une toolchain Air-native mesurera
cargo llvm-covdirectement sur*-linux-air(cf. note « cohabitation toolchain », PR #161).
Invariant « couvrable VIDE » (inchangé)
Une entrée TARGET-ONLY atteste une impossibilité (cible du gate unsound), au même titre que STRUCTURAL/PRIVILEGE/FEATURE-KERNEL/CHILD-EXIT — pas une dette. L’ensemble « couvrable » reste VIDE. Récap : 5 catégories (STRUCTURAL, PRIVILEGE, FEATURE-KERNEL, CHILD-EXIT, TARGET-ONLY)
- 1 étiquette de dette (DEFERRED-TOOLING).
Alternatives écartées
- Garder
--exclude air-runtimeen bloc : perd la mesure des modules host-testables de la crate (env/args/reloc-parser/fork) → moins rigoureux ; masque la frontière host/target. - Laisser le mécanisme dans
rt/(hors-arbre, déjà exclu) : contredit ADR-052 D4 (la logique de runtime est couche 1) ; le code couche 1 ne serait jamais ni porté ni mesuré. - Exception ligne-à-ligne plutôt que fichier : fragile et bruité ; la granularité fichier
(code target-only isolé) est nette et auditable (modèle
_capnp.rs).
Licence du document : MPL 2.0.