Passe de performance guidée par la mesure, pas par l'intuition. Porte de qualité (`npm run check`) — la commande qui tourne des dizaines de fois par jour : - Les 5 vérifications sont indépendantes : exécutées en parallèle via scripts/check.mjs au lieu d'un enchaînement `&&` qui imposait la somme des durées ET 7 démarrages npm imbriqués. 14 s → 6 s - Cache ESLint (--cache) et compilation incrémentale tsc. 16 s → 14 s - Chemin critique restant : vitest 4,9 s, dont ~2 s de mise en place jsdom. Tests de mutation : - Config Vitest dédiée en pool `threads` sans ré-isolation. Mesuré sur le dry-run Stryker : overhead d'amorçage 12 844 ms → 1 592 ms, soit ~90 % du cycle par mutant. 5 min 02 → 3 min 40 - Le mode incrémental ramène le profil complet à 12 s après une édition. Pistes mesurées puis REJETÉES (documentées dans README-CI.md pour éviter qu'on les retente) : - Découper la mutation en 5 processus Stryker parallèles : 262 s contre 235 s. Chaque processus repaie son bac à sable et son dry-run. - Monter `concurrency` de 6 à 16 : aucun effet (~8,5 mutants/s partout), le débit est borné par l'orchestration mono-processus de Stryker. - Séparer vitest API/frontend en deux processus : aucun gain, jsdom domine. - Pool `threads` sur la suite complète : 18,8 s contre 3,5 s. Le bon réglage dépend du jeu de fichiers (jsdom ou non) — d'où deux configs distinctes. Le périmètre muté est inchangé (1511 mutants) : un score obtenu en retirant les mutants gênants vaudrait moins que pas de score. Corrections au passage : - scripts/*.mjs n'était couvert par aucune section ESLint - Les deux suites pytest ont chacune un paquet `src` : réunies dans un seul processus, le premier importé masquait l'autre. Séparées (et parallèles). Co-Authored-By: Claude <noreply@anthropic.com>
128 lines
6.6 KiB
Markdown
128 lines
6.6 KiB
Markdown
# Qualité, tests et CI/CD
|
|
|
|
## Le modèle en une phrase
|
|
|
|
Coolify redéploie sur webhook à **chaque** push (`dev` → dev.metalfrom.eu,
|
|
`main` → metalfrom.eu). Rien ne s'interpose. La porte de qualité est donc
|
|
**locale**, dans le hook `pre-push`.
|
|
|
|
```
|
|
git push ──▶ [hook pre-push : npm run check] ──▶ Forgejo ──▶ webhook ──▶ Coolify redeploy
|
|
↑ 6 s, bloquant
|
|
```
|
|
|
|
## Installation (une fois par clone)
|
|
|
|
```bash
|
|
npm install
|
|
npm run hooks:install # active .githooks/pre-push
|
|
pip install -r requirements-dev.txt
|
|
```
|
|
|
|
## Commandes et temps mesurés
|
|
|
|
| Commande | Ce que ça fait | Froid | Incrémental |
|
|
|---|---|---|---|
|
|
| `npm run check` | **La porte** : ESLint + tsc + Ruff + 444 tests | **6 s** | — |
|
|
| `npm run check:sequential` | Idem, en série (pour isoler un échec) | 14 s | — |
|
|
| `npm run test:mutation` | Mutation, logique pure (352 mutants) | 33 s | ~5 s |
|
|
| `npm run test:mutation:full` | Mutation, toute l'API (1511 mutants) | 3 min 40 | **12 s** |
|
|
| `npm run check:full` | `check` + audits de dépendances + mutation complète | — | ~40 s |
|
|
| `npm run check:clean` | Purge les caches ESLint / tsc | — | — |
|
|
|
|
### Où passe le temps
|
|
|
|
La commande de la boucle de développement, c'est `npm run check` : **6 s**,
|
|
tests compris. C'est elle qui tourne cent fois par jour, et elle est passée de
|
|
17 s à 6 s par trois mesures successives :
|
|
|
|
| Levier | Gain |
|
|
|---|---|
|
|
| Les 5 vérifications en parallèle au lieu de `&&` (elles sont indépendantes, et l'enchaînement npm coûtait 7 démarrages) | 14 s → 6 s |
|
|
| Cache ESLint (`--cache`) et compilation incrémentale tsc | 16 s → 14 s |
|
|
| Chemin critique restant : vitest 4,9 s, dont ~2 s de mise en place jsdom | — |
|
|
|
|
La mutation n'est pas une commande de boucle courte :
|
|
|
|
- `test:mutation` (352 mutants) : 33 s à froid, ~5 s ensuite.
|
|
- `test:mutation:full` (1511 mutants) : 3 min 40 **une seule fois**, puis
|
|
**12 s** après une édition. Le fichier incrémental vit dans `reports/`, qui
|
|
est gitignoré : un clone neuf paie donc le run complet une fois.
|
|
|
|
**Ce qui a été essayé et rejeté sur mesure** (les intuitions se sont trompées
|
|
plus d'une fois — tout est re-mesurable) :
|
|
|
|
| Piste | Résultat |
|
|
|---|---|
|
|
| Découper la mutation en 5 processus Stryker parallèles | **262 s contre 235 s** : chaque processus repaie son bac à sable et son dry-run. Retiré. |
|
|
| Monter `concurrency` de 6 à 16 | Aucun effet : ~8,5 mutants/s dans tous les cas. Le débit est limité par l'orchestration mono-processus de Stryker, pas par le CPU. |
|
|
| Séparer les tests API / frontend en deux processus vitest | Aucun gain : 4,9 s dans les deux cas, jsdom domine. |
|
|
| Pool vitest `threads` sur la **suite complète** | 18,8 s contre 3,5 s (jsdom pénalisé). Défaut conservé. |
|
|
| Pool vitest `threads` sur la **config de mutation** (sans jsdom) | Overhead d'amorçage 12 844 ms → **1 592 ms**. Adopté. |
|
|
|
|
**Ce qui n'a délibérément pas été fait** : réduire le périmètre muté. Descendre
|
|
sous ~1500 mutants sur l'API reviendrait à ne plus mesurer grand-chose, et un
|
|
score flatteur obtenu en retirant les mutants gênants est pire que pas de score.
|
|
Le plancher de ~3 min à froid est celui de Stryker sur 1511 mutants, assumé.
|
|
|
|
## Ce que couvrent les tests
|
|
|
|
| Batterie | Où | Approche |
|
|
|---|---|---|
|
|
| **Unitaires** | `apps/api/test/validate.test.js` | Validation pure : bbox, limit/offset, coordonnées, années, recherche |
|
|
| **Routes / intégration** | `publicRoutes*.test.js`, `adminRoutes*.test.js` | `fastify.inject()` + faux pool. Gating d'auth, allowlists de colonnes, forme du SQL, transactions |
|
|
| **Sécurité** | `adminAuth.test.js`, `rateLimit.test.js` | JWT forgé, `alg:none`, expiration, anti-énumération, verrouillage de compte, plafonds de débit par IP et par jeton |
|
|
| **Annulation** | `cancellation.test.js` + `apps/crawler/tests/test_cancellation.py` | Les deux moitiés du protocole coopératif |
|
|
| **Accessibilité** | `apps/web/test/a11y.test.js` | axe-core + jsdom sur le HTML statique |
|
|
| **Frontend** | `apps/web/test/pure.test.js` | Filtrage, tri, échappement HTML |
|
|
| **Méta** | `apps/api/test/harness.test.js` | Vérifie le faux pool lui-même |
|
|
| **Python** | `apps/geocoder/tests/`, `apps/crawler/tests/` | Parseur de localisations, annulation |
|
|
| **Mutation** | `stryker*.config.json` | 82 % (logique pure) / 65 % (API complète) |
|
|
|
|
## Principe des tests
|
|
|
|
**Aucun test ne monte de conteneur, ne compile, ni ne touche le réseau.**
|
|
|
|
Deux choix structurants côté API :
|
|
|
|
1. **Une app Fastify par fichier de test, pas par cas.** `buildServer()` coûte
|
|
14 ms ; à raison d'une construction par `it()`, ces 14 ms étaient multipliés
|
|
par des milliers d'exécutions en mutation. Les tests partagent une app et
|
|
reprogramment un faux pool via `pool.reset()` (`test/helpers/testApp.js`).
|
|
|
|
2. **Plafonds de débit paramétrables.** Conséquence du point 1 : une app
|
|
partagée accumule l'état du limiteur. Les valeurs réelles de production sont
|
|
donc vérifiées séparément dans `rateLimit.test.js`, pour que cette
|
|
paramétrisation ne crée pas d'angle mort sur une protection de sécurité.
|
|
|
|
## Pourquoi deux profils de mutation
|
|
|
|
La mutation prouve qu'un test **échoue** quand le code casse — la couverture ne
|
|
dit que « cette ligne a été exécutée ».
|
|
|
|
- `test:mutation` (validate.js, adminAuth.js) : **82 %**. Logique pure, score
|
|
interprétable, assez rapide pour être lancé souvent.
|
|
- `test:mutation:full` (+ adminRoutes.js, app.js) : **65 %**. Les handlers de
|
|
route plafonnent structurellement plus bas — beaucoup de mutants portent sur
|
|
des messages de log ou des branches `catch` dont l'observable exact n'a pas
|
|
d'importance. Utile en audit, pas en boucle courte.
|
|
|
|
Les seuils d'échec (70 % et 55 %) sont des cliquets anti-régression, pas des
|
|
objectifs.
|
|
|
|
### Ce qui n'est pas couvert
|
|
|
|
- Règles a11y exigeant un moteur de rendu (contraste, cibles tactiles) : jsdom
|
|
ne calcule pas de styles. À compléter par un audit Lighthouse manuel.
|
|
- `migrate.js` : s'exécute au démarrage du conteneur et appelle `process.exit`.
|
|
- Crawler et workers de géocodage : seuls le parseur et l'annulation sont
|
|
couverts, le reste est de l'I/O réseau et base.
|
|
- `apps/web/site/pure.js` est testé mais **exclu de la mutation** : il est
|
|
chargé via `fs` + `vm` (comme le fait le navigateur), donc l'instrumentation
|
|
Stryker ne l'atteindrait pas et afficherait un score faussement parfait.
|
|
|
|
## Forgejo Actions
|
|
|
|
`.forgejo/workflows/ci.yml` existe mais **ne tourne pas** : aucun runner
|
|
`act_runner` n'est enregistré sur le VPS. Le fichier est prêt si tu en ajoutes
|
|
un. Il est volontairement informatif — il ne bloque pas le déploiement.
|