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>
43 lines
1.8 KiB
TOML
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"
|