╔══════════════════════════════════════════════════════════════════════╗
║ git commit ║
║ ───────────────────────────────────────────────────────────────── ║
║ pre-commit (automatic, ~10s) ║
║ ║
║ ✦ ruff check lint — style errors, common bugs ║
║ ✦ ruff format formatting — code consistency ║
║ ✦ mypy typing — strict annotations, no hidden Any ║
║ ✦ bandit static security — injections, hardcoded secrets ║
║ ✦ trailing-whitespace / end-of-file / check-yaml / check-toml ║
╚══════════════════════════════════════════════════════════════════════╝
│
▼
╔══════════════════════════════════════════════════════════════════════╗
║ git push ║
║ ───────────────────────────────────────────────────────────────── ║
║ pre-push (automatic, ~5–15s) ║
║ ║
║ ✦ pytest tests/examples tests/shared ║
║ → pure tests, no I/O, fast regardless of project size ║
║ → infrastructure and E2E tests reserved for CI ║
╚══════════════════════════════════════════════════════════════════════╝
│
▼
╔══════════════════════════════════════════════════════════════════════╗
║ Pull Request → CI GitHub Actions ║
║ ───────────────────────────────────────────────────────────────── ║
║ Job: quality Job: security ║
║ ✦ ruff check ✦ bandit (src/) ║
║ ✦ ruff format --check ✦ pip-audit ║
║ ✦ mypy src ✦ xenon (complexity ≤ C) ║
║ ║
║ Job: tests Job: mutation (PR → main) ║
║ ✦ pytest (full suite) ✦ mutmut (examples/ only) ║
║ ✦ coverage ≥ 80% (blocking) → restricted for performance ║
║ ✦ upload to Codecov ║
╚══════════════════════════════════════════════════════════════════════╝
│
▼
╔══════════════════════════════════════════════════════════════════════╗
║ Merge ║
║ ───────────────────────────────────────────────────────────────── ║
║ ✦ PR checklist (manual — see CONTRIBUTING.md) ║
║ ✦ E2E tests on main (dedicated CI job, non-blocking on PR) ║
╚══════════════════════════════════════════════════════════════════════╝
| Tool | When | Blocking | Scope |
|---|---|---|---|
| ruff | commit + CI | yes | src/ + tests/ |
| mypy | commit + CI | yes | src/ |
| bandit | commit + CI | yes | src/ |
| pip-audit | CI | yes | all dependencies |
| xenon | CI | yes | src/ — avg ≤ A, module ≤ B, function ≤ C |
| pytest (examples + shared) | push | yes | tests/examples + tests/shared |
| pytest (full suite) | CI | yes | tests/ |
| coverage ≥ 80% | CI | yes | src/ |
| mutmut | CI (PR → main) | no | src/my_project/examples/ only |
| pytest-benchmark | manual (make benchmark) |
no | tests/examples/test_benchmarks.py |
| E2E tests | CI (main) | no | tests/e2e/ |
Coverage is a proxy, not a goal. A test has value only if it can fail for the right reason.
- Assert behaviour, not implementation — test what the code does, not how it does it
- No pass-through tests — a test that always passes regardless of the code is worse than no test
- One clear intent per test — if a test breaks, it must be obvious why
- Coverage ≥ 80% is a floor, not a target — chasing 100% produces noise; mutmut reveals gaps that coverage misses
make check # mirrors the CI "quality" job (lint + format + types + security + complexity)
make test # mirrors the CI "tests" job (full suite + coverage)
make ci # runs the full CI pipeline locally
make benchmark # performance benchmarks (excluded from normal run)
make mutation # mutation testing on the domain slice (slow)
make complexity # complexity check only (xenon — average ≤ A, module ≤ B, function ≤ C)Xenon bloque le build si les seuils suivants sont dépassés :
| Seuil | Grade max autorisé | Signification |
|---|---|---|
--max-average |
A (1–5) | Complexité moyenne du projet |
--max-modules |
B (6–10) | Pire module du projet |
--max-absolute |
C (11–15) | Pire fonction individuelle |
Les grades vont de A (simple) à F (> 25 chemins, impossible à tester). Une fonction qui dépasse C est un signal de refactoring.