Tests
Les couches de test côté Rust, leurs commandes et les règles qui les tiennent (RFC 0015)
Cette page est la seule référence des commandes de test ; task --list donne leur
description courte. Le front a sa propre couche (RFC 0017), les suites heavy et de
performance la leur (RFC 0016).
Les couches
| Couche | Où | Commande | Où elle tourne |
|---|---|---|---|
| Unitaire | mod tests dans le fichier, ou un tests.rs voisin | task test:unit | chaque PR |
| En processus, sans Redis | libs/server/*/tests/*.rs sur le store en mémoire | task test:unit | chaque PR |
| En processus, avec Redis | les suites de sécurité (throttle, OAuth, audit) et le contrat du store | task test:all | chaque PR |
| CLI, binaire réel | apps/kubectl_proxyauth/tests/ : le binaire contre un ProxyAuthK8S mocké (wiremock), curl -L dans le rôle du navigateur SSO, trousseau keyutils (bus de session pointé nulle part, jamais celui du poste) | cargo nextest run -p cli -p cli_trace -p kubectl_proxyauth (curl dans le PATH) | chaque PR (back-cargo-test) |
| Garde-fous | authz_matrix, openapi_contract, variant_coverage | task test:unit | chaque PR |
| envtest | libs/server/api/tests/envtest*.rs contre un vrai kube-apiserver | task envtest:run | chaque PR |
| Intégration | tests/integration, banc k3d (Suite d'intégration) | task it:up puis task it:test | chaque PR, chaque nuit |
| Heavy | tests/integration/tests/heavy_*.rs : l'écho amont, le serveur de la version précédente à côté (RFC 0016) | task it:up puis task it:heavy | chaque nuit, à la main, avec le label heavy |
| Charge HTTP | tests/perf/k6/scenarios/, lancés par tests/integration/tests/perf_http.rs (RFC 0016) | task it:up puis task perf:http, perf:compare BASE=… | chaque nuit, à la main, avec le label perf |
| Fuzz | fuzz/fuzz_targets/ | task fuzz:check, task fuzz TARGET=… | compilation à chaque PR, 60 s par cible chaque nuit |
| Mutants | les crates de sécurité (.cargo/mutants.toml) | task mutants:run, mutants:diff BASE=…, mutants:list | chaque semaine sur main, ou avec le label mutants |
| Mutants, informatif | common, controller, l'agent (.cargo/mutants-wide.toml) | task mutants:wide | chaque semaine sur main, résumé du run sans issue |
| Couverture | tout le workspace (tiers rapides) | task test:coverage, task test:coverage:ci | chaque PR |
| Release | le graphe de features du build livré | task test:release-features | chaque PR (job release-features) |
task test:unit ne demande ni Redis ni réseau : les tests où Redis n'est qu'un stockage
tournent sur InMemoryKv. Les tests qui testent Redis lui-même sautent alors, et la tâche
les liste à la fin. task test:all, test:server, test:integration et test:coverage*
mettent REQUIRE_TEST_REDIS=1 : là, un Redis absent est un échec.
Mesuré le 2026-10-30 (RFC 0015 §17) : 88,14 % des lignes, COVERAGE_EXCLUDE appliqué,
pour un plancher COVERAGE_MIN de 85. Par crate, la plus basse est trace (48 %), puis
l'agent (80 %) et controller (85 %). Le périmètre de sécurité compte 350 mutants, le
rapport informatif 675.
Heavy et charge HTTP
La suite d'intégration voit ce que reçoit le client ; la suite heavy voit ce que reçoit
l'amont. Ses tests mettent un écho (.it/heavy/echo.py, un serveur HTTPS qui écrit
chaque requête reçue) à la place de l'apiserver derrière un cluster, envoient leur requête
par l'Ingress avec un marqueur dans la query, puis lisent la transcription
(heavy::relayed(ns, "marker=…")). Un test exige d'abord sa requête marquée, puis regarde
ses en-têtes : une requête qui n'arrive pas n'est jamais un succès, sauf là où c'est la
preuve, avec un témoin (le point 17 de la RFC 0014). heavy::Previous démarre l'image du
serveur v0.4.2 (server-previous de .it/images.lock) à côté de l'actuelle.
La charge lance k6 sur chaque scénario, à travers l'Ingress, pendant qu'un fil relève la
mémoire du back (kubectl top). Le rapport (PERF_OUT, par défaut .it/run/perf) :
un NN_*.json par scénario, resources.json, summary.md. Seuls le taux d'erreur (< 1 %)
et le refus du throttle bloquent ; les cibles de latence s'appliquent avec PERF_ENFORCE=1.
PERF_DURATION (défaut 2m) règle le palier : quelques heures en font un soak.
task perf:compare BASE=tests/perf/baselines/<version> compare les p95 (marge 25 %,
FAIL=1 pour sortir en erreur). Une base se commite depuis un rapport du runner CI, jamais
d'un poste.
Un point §6.P prouvé par l'une de ces deux couches a le statut heavy dans MATRIX.md.
Le Redis de test se règle avec TEST_REDIS_URL (défaut redis://127.0.0.1:6379). Ne le
pointez pas sur un Redis partagé : les tests y écrivent leurs clés (préfixées, effacées à la
fin, jamais de FLUSHDB). Un Redis jetable suffit :
podman run -d --rm --name test-redis -p 127.0.0.1:6390:6379 \
docker.io/library/redis:8-alpine redis-server --save '' --appendonly no
TEST_REDIS_URL=redis://127.0.0.1:6390 task test:allLe store en mémoire
common::kv_memory::InMemoryKv (feature test-util) remplace Redis derrière State. Il
copie les commandes et scripts Lua que le serveur utilise (GETDEL, l'arrondi de TTL,
l'INCR qui répare un compteur sans échéance, les scripts de présence) ;
libs/server/common/tests/kv_contract.rs joue les mêmes cas contre lui et contre un vrai
Redis. Il ne couvre pas le mode cluster de Redis (aucun job n'en démarre un).
Dans un test d'api :
let store = memory_store();
seed_proxy(&store, &proxy_fixture(&ns, &cluster, &upstream.uri())).await;
let app = harness::app(memory_state(&store, upstream.uri()));store.fail_on(KvOp::Exists) fait échouer une opération pendant que le store répond aux
autres : c'est ainsi que se testent les branches d'erreur (par exemple
THROTTLE_FAIL_CLOSED, throttle_store_failures.rs).
Les règles
- Un correctif apporte son test de régression, qui échoue sans le correctif. Exception : un correctif de CI, de dépendance ou de doc, dit dans la PR.
- Un refus a son test négatif, nommé par le refus :
<cas>_refused,_rejected,_denied(ou_returns_4xx). - Un saut n'est jamais silencieux :
common::test_skip::redis_unavailableécritSKIPPED: <test> — <raison>, et la tâche qui promet le test fait du saut un échec. - On exclut, on ne baisse pas : un fichier hors mesure entre dans
COVERAGE_EXCLUDE(.tasks/test.yaml) avec sa raison ;COVERAGE_MINne descend jamais. - L'heure entre par un argument là où le code décide d'une borne (
IssuerKeys::current(now),issuer::reusable) ; les caches lisenttokio::time::Instant, figé par#[tokio::test(start_paused = true)]. Pas d'entrée-sortie (wiremock, Redis) sous horloge figée : tokio avance l'heure dès que le runtime attend, et les délais réels du code partent. - Rangement :
mod testsdans le fichier par défaut ; untests.rsvoisin quand la suite couvre plusieurs fichiers ; une fabrique partagée par crate de tests (tests/harness/pourapi,tests/support/pourcontroller).
Les garde-fous
Ils échouent quand un cas est ajouté sans son test ; le message dit quoi faire.
- Une route (
authz_matrix.rs) : chaque route servie a une ligne dansMAINouLISTENER, avec sa classe.Public(aucune identité demandée) n'est admis que sousPUBLIC_PREFIXESet jamais pour une opération qui déclaresecuritydans son#[utoipa::path];Authenticated(jeton du fournisseur du service) ;ClusterScoped(jeton du cluster et ses règles) ;AgentEnrolled(listener des agents). Le test appelle chaque ligne : le refus de sa classe et un contrôle positif. - Une réponse (
openapi_contract.rs) : chaque réponse 2xx déclare sonbody, ou figure dansNO_BODYavec sa raison. - Une variante (
variant_coverage.rs) :UpstreamAuthMode,VerbetUpstreamTrustsont lus par desmatchsans_; une variante nouvelle ne compile pas tant que son bras ne nomme pas le test qui la couvre. - Un mutant (
.github/workflows/mutants.yaml) : un mutant qui survit sur l'exécution hebdomadaire ouvre l'issuemutants. On le tue par un test, ou on le liste dansexclude_rede.cargo/mutants.tomlavec la raison pour laquelle il est équivalent. - Le build livré (
task test:release-features) :common/test-util, qui porte le store en mémoire, ne doit pas atteindrecargo build --release --workspace.