File size: 7,148 Bytes
478e60e
5112943
1ef330c
 
 
 
5112943
478e60e
 
 
 
 
 
 
 
 
1ef330c
 
 
ebddecf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5112943
 
 
 
478e60e
 
 
 
 
 
 
 
 
 
 
 
 
50b07b8
478e60e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50b07b8
478e60e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5112943
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""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"]