Picarones / tests /_migration_helpers.py
Claude
chore(versioning): S0-ter — fix broken narrative across changelog timeline
ebddecf unverified
"""Helpers tests — pattern ``RunOrchestrator`` pour les tests B4.
Phase B3-final (mai 2026) — implémente directement le pattern 3
étapes ``prepare_preset_args`` → ``execute_preset`` →
``run_result_to_benchmark_result`` pour servir les 6 fichiers de
tests catégorie A migrés en Phase B4.
Pourquoi un helper test dédié plutôt qu'inline dans chaque test ?
-----------------------------------------------------------------
Les tests B4 (~30+ cas) consomment ce helper avec la même signature
que l'ancien ``run_benchmark_via_service``. Le mettre inline dans
chaque test ajouterait ~10 lignes de boilerplate par cas, noyant
l'intention du test.
Ce helper ``run_via_orchestrator`` est un **outil de test**
(préfixe ``_`` du module + dossier ``tests/``). Son existence ne
constitue pas de la dette technique en production : il n'y a pas
de shim équivalent dans ``picarones/`` (les call sites CLI/Web
font le pattern 3 étapes explicitement).
Convention « ``code_version`` dans les tests »
==============================================
De nombreux tests (~50+ fichiers) instancient des ``RunContext``,
``ProvenanceRecord``, ``ArtifactKey``, ``RunSpec`` avec
``code_version="1.0.0"`` en littéral. **Cette valeur est un
placeholder de fixture**, pas une assertion sur la version réelle
de Picarones au moment du test.
Picarones suit SemVer pré-1.0 et sa version courante est résolue
dynamiquement via ``picarones.__version__`` (voir
``docs/explanation/versioning.md``). Les tests utilisent ``"1.0.0"``
comme valeur arbitraire stable parce que :
- la sémantique testée ne dépend pas de la valeur réelle (cache,
manifeste, provenance — ce qui compte est l'**égalité** ou la
**non-égalité** entre deux ``code_version``, pas la valeur) ;
- une string ``"X.Y.Z"`` cohérente PEP 440 facilite les assertions
sur la structure ;
- la stabilité historique évite de devoir réécrire 50+ tests à
chaque bump de version.
Le garde-fou ``tests/architecture/test_no_hardcoded_version.py``
neutralise spécifiquement les patterns ``code_version="X.Y.Z"`` via
``PLACEHOLDER_PATTERNS`` pour ne pas les confondre avec de vraies
mentions de version Picarones.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from picarones.evaluation.benchmark_result import BenchmarkResult
from picarones.evaluation.corpus import Corpus
def run_via_orchestrator(
corpus: "Corpus",
engines: list[Any],
*,
views: tuple[str, ...] = ("text_final",),
char_exclude: Any | None = None,
normalization_profile: Any | None = None,
output_json: str | Path | None = None,
code_version: str | None = None,
show_progress: bool = True, # noqa: ARG001 — absorbé pour compat tests
progress_callback: Callable[[str, int, str], None] | None = None,
timeout_seconds: float = 60.0,
cancel_event: Any | None = None,
partial_dir: str | Path | None = None,
entity_extractor: Callable[[str], list[dict]] | str | None = None,
profile: str = "standard",
) -> "BenchmarkResult":
"""Helper test : invoque ``RunOrchestrator`` et retourne un
``BenchmarkResult`` legacy.
Reproduit le pattern 3 étapes utilisé en production (CLI/Web)
pour que les tests B4 valident le même chemin que les utilisateurs.
Signature alignée sur l'ancien ``run_benchmark_via_service`` pour
minimiser le boilerplate dans les tests.
NER attach
----------
Si ``entity_extractor`` est un callable direct (pattern legacy),
le helper invoque ``attach_ner_metrics_to_benchmark`` en post-
process. Si c'est un dotted path string, ``execute_preset`` le
résout lui-même via ``RunSpec.entity_extractor``.
"""
from picarones.app.services import (
RunOrchestrator,
prepare_preset_args,
run_result_to_benchmark_result,
)
# Séparation callable vs dotted path (cf. shim historique).
entity_extractor_dotted: str | None = None
entity_extractor_callable: Callable | None = None
if entity_extractor is not None:
if isinstance(entity_extractor, str):
entity_extractor_dotted = entity_extractor
elif callable(entity_extractor):
entity_extractor_callable = entity_extractor
pipeline_to_engine_name = {
# Construit après pipeline_specs ci-dessous (closure-friendly).
}
wrapped_callback = None
if progress_callback is not None:
def wrapped_callback(
pipeline_name: str, doc_idx: int, doc_id: str,
) -> None:
engine_name = pipeline_to_engine_name.get(
pipeline_name, pipeline_name,
)
progress_callback(engine_name, doc_idx, doc_id)
with tempfile.TemporaryDirectory(prefix="picarones_test_") as ws:
ws_path = Path(ws)
run_dir = ws_path / "run"
args = prepare_preset_args(
corpus, engines,
workspace_dir=ws_path / "gt",
output_dir=run_dir,
views=views,
char_exclude=char_exclude,
normalization_profile=normalization_profile,
partial_dir=partial_dir,
entity_extractor=entity_extractor_dotted,
profile=profile,
output_json=output_json,
timeout_seconds_per_doc=timeout_seconds,
code_version=code_version,
)
# Map pipeline_name → engine.name pour le callback wrapper.
pipeline_to_engine_name.update({
spec.name: engine.name
for spec, engine in zip(args.pipeline_specs, engines)
})
orch_result = RunOrchestrator(run_dir).execute_preset(
spec=args.spec,
corpus_spec=args.corpus_spec,
extracted_dir=args.extracted_dir,
pipeline_specs=args.pipeline_specs,
adapter_resolver=args.adapter_resolver,
adapter_kwargs=args.adapter_kwargs,
progress_callback=wrapped_callback,
cancel_event=cancel_event,
)
benchmark_result = run_result_to_benchmark_result(
orch_result.run_result,
corpus=corpus, engines=engines,
char_exclude=char_exclude,
normalization_profile=normalization_profile,
profile=profile,
)
# NER attach post-process si callable direct fourni.
if entity_extractor_callable is not None:
from picarones.app.services._benchmark_ner import (
attach_ner_metrics_to_benchmark,
)
attach_ner_metrics_to_benchmark(
benchmark_result, corpus, entity_extractor_callable,
)
# Sérialisation output_json (legacy comportement).
if output_json is not None:
from picarones.app.services._benchmark_persistence import (
persist_benchmark_result_json,
)
persist_benchmark_result_json(
benchmark_result, Path(output_json),
)
return benchmark_result
__all__ = ["run_via_orchestrator"]