Spaces:
Running
Running
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"] | |