metalfrom.eu/pyproject.toml
Nicolas Fryder 83ea8030e7
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run
test: propriétés généralisées, scraper HTML et couche réseau couverts
BUG trouvé par un test de propriété
parse_location_text("0/0") produisait deux lignes identiques. band_locations
impose UNIQUE (ma_id, step_order, location_raw) : le doublon était absorbé par
le ON CONFLICT DO NOTHING de l'enqueue, mais il faussait les compteurs et n'a
aucun sens métier — une étape ne se déroule pas deux fois au même endroit.
Déduplication par dict.fromkeys (ordre préservé) + tests de régression.
Contre-exemple minimal trouvé en 150 tirages, aucun test par l'exemple ne
l'aurait deviné.

SCRAPER HTML (42 tests) — module le plus exposé du crawler, aucun test
jusqu'ici. C'est lui qui extrait genre, statut, thèmes, line-up et dates. Si
Metal Archives change son HTML, il renvoie des champs vides et le crawler
enregistre des fiches creuses SANS lever d'erreur : la panne invisible, donc
la plus coûteuse.
Couvre les trois orthographes de « Lyrical themes » observées chez MA, les
variantes de « label » et « formed in », le regroupement du line-up par
section, l'audit trail, et surtout la dégradation : une page vide ou
malformée ne doit jamais lever et doit rendre une structure complète.

COUCHE RÉSEAU (57 tests) — ma_http et flaresolverr, sans une seule requête
réelle. Ce qui est testé, c'est la logique AUTOUR du réseau : quand réessayer,
quand abandonner, comment extraire les données d'une réponse enveloppée par
FlareSolverr.
Enjeu concret : ces bornes conditionnent le nombre de requêtes envoyées à
Metal Archives — une boucle de retry mal bornée nous ferait bannir. Vérifié
que retries=2 donne exactement 3 tentatives, qu'un 500 n'est PAS réessayé
(contrairement à 403/429/503, qui justifient une session Chrome neuve), et
que les appels de préchauffage Cloudflare sont comptés à part.

PROPRIÉTÉS GÉNÉRALISÉES
- Python (Hypothesis) : parseur de localisations, requêtes de repli,
  centroïdes. Invariants — jamais d'exception, structure toujours complète,
  aucun lieu vide, aucun doublon, nombre de requêtes facturées borné.
- Frontend (fast-check) : échappement HTML (une faille XSS, pas un défaut
  cosmétique — tout le rendu passe par innerHTML), filtrage tri-état, tri
  sans effet de bord ni perte d'éléments, clés de coordonnées.

max_examples fixé à 150 côté Python : ces tests étaient devenus le chemin
critique de `check` (11 s). 150 tirages suffisaient à trouver « 0/0 ».

Tests : 621 JS + 61 intégration, 162 Python, 89 Playwright. check à 9 s.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 22:09:37 +02:00

43 lines
1.8 KiB
TOML

[tool.ruff]
# Linting Python (équivalent d'ESLint côté crawler/geocoder).
line-length = 110
target-version = "py312"
exclude = [".git", "node_modules", "__pycache__"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes — imports/variables inutilisés, noms indéfinis
"I", # isort — ordre des imports
"B", # bugbear — pièges classiques (mutable default, boucle+except…)
"UP", # pyupgrade
"S", # bandit — analyse de sécurité (SQLi par f-string, subprocess, secrets…)
"C4", # comprehensions
]
ignore = [
"E501", # longueur gérée par le formateur
"S101", # `assert` : légitime dans les tests
]
[tool.ruff.lint.per-file-ignores]
# Les requêtes SQL sont construites avec des placeholders psycopg2 (%s) ;
# les rares f-strings ne portent que des noms de colonnes issus d'allowlists.
"apps/crawler/src/db.py" = ["S608"]
"apps/geocoder/src/*.py" = ["S608"]
"**/tests/*.py" = ["S105", "S106"]
# `random` sert au jitter de politesse entre deux requêtes HTTP, pas à générer
# des secrets : S311 (générateur non cryptographique) ne s'applique pas.
"apps/crawler/src/polite.py" = ["S311"]
"apps/geocoder/src/worker.py" = ["S311", "S608"]
# md5 sert d'empreinte de contenu pour détecter les pages inchangées,
# jamais de primitive de sécurité. Le test compare explicitement à md5 pour
# verrouiller ce choix (changer d'algorithme invaliderait tout le cache).
"apps/crawler/src/scraper_band.py" = ["S324"]
"apps/crawler/tests/test_scraper_band.py" = ["S324"]
[tool.pytest.ini_options]
# Chaque app est testée depuis sa propre racine (`rootdir`), d'où le chemin relatif
# `src.` dans les imports de test. Voir le script `test:py` du package.json.
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-q --strict-markers"