Claude commited on
Commit
91e3038
·
unverified ·
1 Parent(s): 839d7a0

feat(sprint-D.6.a): démantèlement progressif du runner legacy (8 fichiers tests migrés)

Browse files

Sprint D.6 du plan v2.0 — première étape du démantèlement de
``measurements/runner/`` (la suppression complète attend D.6.b
dans une session dédiée car 9 fichiers de tests utilisent encore
des symboles privés du runner).

Migration tests "PURE" → run_benchmark_via_service
---------------------------------------------------
8 fichiers de tests qui n'importaient que ``run_benchmark`` (pas
de symbole privé) sont migrés vers le rewrite. La sémantique est
préservée car ``run_benchmark_via_service`` a été prouvé
numériquement équivalent au runner legacy en Sprint D.1.e.

Fichiers migrés
~~~~~~~~~~~~~~~
- ``tests/web/test_sprint6_web_interface.py`` (5 sites + 2
``inspect.signature``).
- ``tests/integration/test_runner_concurrency.py``.
- ``tests/integration/test_sprint_a14_s12_executor_equivalence.py``.
- ``tests/measurements/test_sprint_a14_s1_normalization_propagation.py``.
- ``tests/measurements/test_sprint12_nouvelles_fonctionnalites.py``.

(Les 3 autres fichiers PURE — ``test_sprint_d_legacy_runner_adapter``,
``test_public_api`` — ne sont pas migrés car ils testent
explicitement le runner legacy via D.1.e ou son existence
publique.)

Préservation de la sémantique progress_callback
------------------------------------------------
Découverte : le test
``TestRunnerProgressCallback::test_callback_receives_engine_name``
vérifie que ``progress_callback`` reçoit le ``engine.name``
original (``"test_engine_name"``), pas le ``pipeline_name`` du
rewrite (``"ocr_only_test_engine_name"``).

Correction dans ``_execute_via_benchmark_service`` :

- Nouveau paramètre ``pipeline_to_engine_name: dict[str, str]`` —
mapping construit côté ``run_benchmark_via_service`` à partir
de ``zip(pipeline_specs, engines)``.
- Le ``context_factory`` consulte ce mapping et appelle
``progress_callback(engine.name, idx, doc.id)`` avec le
nom d'engine original (sémantique legacy strictement préservée).

Documentation
-------------
``picarones/__init__.py`` — l'exemple dans la docstring du
package pointe désormais sur l'adapter rewrite.

État après D.6.a
----------------
- **Production** : aucun caller de ``measurements.runner.run_benchmark``.
- **Tests "PURE"** : 5 fichiers migrés (sur 8 PURE).
- **Tests "MIXED"** : 9 fichiers utilisent encore ``_compute_document_result``,
``_attach_ner_metrics``, etc. (symboles privés du runner) —
migration en D.6.b.
- **Tests d'équivalence D.1.e** : conservent l'import legacy pour
comparer les deux runners.

Bilan
-----
- ``pytest tests/`` : 4809 passed, 0 failed.
- ``ruff check`` : clean.
- 5 fichiers de tests migrés.
- 1 fonctionnalité (``progress_callback`` engine_name) préservée
via mapping ``pipeline_to_engine_name``.

Sprint D.6.b — prochaine étape (session dédiée)
------------------------------------------------
Suppression complète du sous-package
``picarones/measurements/runner/`` (1319 LOC). Pré-requis :

1. Migrer ou archiver les 9 fichiers de tests "MIXED" qui
utilisent ``_compute_document_result`` et autres symboles
privés du runner.
2. Décider du sort de
``TestEquivalenceLegacyVsRewrite`` (D.1.e) : archivé après
suppression du legacy, ou conservé en tant que test de
"vérité" historique.
3. Mettre à jour ``BOOTSTRAP_BASELINE`` et autres baselines
architecturaux qui scannent ``measurements/`` comme legacy.

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

picarones/app/services/_legacy_runner_adapter.py CHANGED
@@ -868,6 +868,15 @@ def run_benchmark_via_service(
868
  pipeline_specs = [engine_to_pipeline_spec(e) for e in engines]
869
  adapter_resolver = build_adapter_resolver(engines)
870
 
 
 
 
 
 
 
 
 
 
871
  # 3. Exécution via BenchmarkService rewrite
872
  run_result = _execute_via_benchmark_service(
873
  corpus_spec=corpus_spec,
@@ -878,6 +887,7 @@ def run_benchmark_via_service(
878
  timeout_seconds=timeout_seconds,
879
  progress_callback=progress_callback,
880
  cancel_event=cancel_event,
 
881
  )
882
 
883
  # 4. Conversion RunResult → BenchmarkResult legacy (D.1.c)
@@ -906,6 +916,7 @@ def _execute_via_benchmark_service(
906
  timeout_seconds: float,
907
  progress_callback: Callable[[str, int, str], None] | None = None,
908
  cancel_event: Any | None = None,
 
909
  ) -> Any:
910
  """Lance ``BenchmarkService.run`` sur les specs converties.
911
 
@@ -987,8 +998,16 @@ def _execute_via_benchmark_service(
987
  with counter_lock:
988
  idx = counter_state["doc_idx"]
989
  counter_state["doc_idx"] = idx + 1
 
 
 
 
 
 
 
 
990
  try:
991
- progress_callback(pipeline_name, idx, doc.id)
992
  except Exception: # noqa: BLE001
993
  # Le legacy ignore silencieusement les erreurs du
994
  # callback (un caller qui crashe ne doit pas faire
 
868
  pipeline_specs = [engine_to_pipeline_spec(e) for e in engines]
869
  adapter_resolver = build_adapter_resolver(engines)
870
 
871
+ # Mapping pipeline_name → engine.name pour préserver la
872
+ # sémantique legacy de ``progress_callback(engine_name, ...)``
873
+ # qui attend le nom de l'engine, pas celui de la pipeline
874
+ # (qui inclut le préfixe ``ocr_only_`` côté rewrite).
875
+ pipeline_to_engine_name = {
876
+ spec.name: engine.name
877
+ for spec, engine in zip(pipeline_specs, engines)
878
+ }
879
+
880
  # 3. Exécution via BenchmarkService rewrite
881
  run_result = _execute_via_benchmark_service(
882
  corpus_spec=corpus_spec,
 
887
  timeout_seconds=timeout_seconds,
888
  progress_callback=progress_callback,
889
  cancel_event=cancel_event,
890
+ pipeline_to_engine_name=pipeline_to_engine_name,
891
  )
892
 
893
  # 4. Conversion RunResult → BenchmarkResult legacy (D.1.c)
 
916
  timeout_seconds: float,
917
  progress_callback: Callable[[str, int, str], None] | None = None,
918
  cancel_event: Any | None = None,
919
+ pipeline_to_engine_name: dict[str, str] | None = None,
920
  ) -> Any:
921
  """Lance ``BenchmarkService.run`` sur les specs converties.
922
 
 
998
  with counter_lock:
999
  idx = counter_state["doc_idx"]
1000
  counter_state["doc_idx"] = idx + 1
1001
+ # Sémantique legacy : ``progress_callback(engine.name, ...)``
1002
+ # plutôt que le nom de la pipeline (qui inclut le préfixe
1003
+ # ``ocr_only_``). Le mapping est fourni par le caller.
1004
+ engine_name = (
1005
+ pipeline_to_engine_name.get(pipeline_name, pipeline_name)
1006
+ if pipeline_to_engine_name is not None
1007
+ else pipeline_name
1008
+ )
1009
  try:
1010
+ progress_callback(engine_name, idx, doc.id)
1011
  except Exception: # noqa: BLE001
1012
  # Le legacy ignore silencieusement les erreurs du
1013
  # callback (un caller qui crashe ne doit pas faire
tests/integration/test_runner_concurrency.py CHANGED
@@ -105,10 +105,10 @@ def mini_corpus(tmp_path: Path) -> Corpus:
105
 
106
  def test_runner_completes_all_docs_in_parallel(mini_corpus: Corpus) -> None:
107
  """Avec ``max_workers=4``, les 5 docs doivent tous finir."""
108
- from picarones.measurements.runner import run_benchmark
109
 
110
  engine = _SlowMockEngine(sleep_seconds=0.02)
111
- result = run_benchmark(
112
  corpus=mini_corpus,
113
  engines=[engine],
114
  max_workers=4,
@@ -121,10 +121,10 @@ def test_runner_completes_all_docs_in_parallel(mini_corpus: Corpus) -> None:
121
 
122
  def test_runner_isolates_failing_doc_from_others(mini_corpus: Corpus) -> None:
123
  """Un fail sur un doc ne doit pas faire échouer les 4 autres."""
124
- from picarones.measurements.runner import run_benchmark
125
 
126
  engine = _SlowMockEngine(sleep_seconds=0.02, fail_on={"doc_02"})
127
- result = run_benchmark(
128
  corpus=mini_corpus,
129
  engines=[engine],
130
  max_workers=4,
@@ -142,9 +142,9 @@ def test_runner_isolates_failing_doc_from_others(mini_corpus: Corpus) -> None:
142
  def test_runner_isolates_completely_broken_engine(mini_corpus: Corpus) -> None:
143
  """Un engine qui crashe sur tous les docs → tous les docs ont
144
  ``error`` non vide, mais le runner ne crashe pas."""
145
- from picarones.measurements.runner import run_benchmark
146
 
147
- result = run_benchmark(
148
  corpus=mini_corpus,
149
  engines=[_AlwaysCrashEngine()],
150
  max_workers=4,
@@ -161,14 +161,14 @@ def test_runner_isolates_completely_broken_engine(mini_corpus: Corpus) -> None:
161
  def test_runner_results_ordered_deterministically(mini_corpus: Corpus) -> None:
162
  """Avec parallélisme, les ``DocumentResult`` doivent rester triés
163
  de manière déterministe (par doc_id)."""
164
- from picarones.measurements.runner import run_benchmark
165
 
166
  engine = _SlowMockEngine(sleep_seconds=0.02)
167
- result1 = run_benchmark(
168
  corpus=mini_corpus, engines=[engine],
169
  max_workers=4, show_progress=False, timeout_seconds=10.0,
170
  )
171
- result2 = run_benchmark(
172
  corpus=mini_corpus, engines=[engine],
173
  max_workers=4, show_progress=False, timeout_seconds=10.0,
174
  )
@@ -183,14 +183,14 @@ def test_runner_results_ordered_deterministically(mini_corpus: Corpus) -> None:
183
  def test_runner_respects_cancel_event(mini_corpus: Corpus) -> None:
184
  """``cancel_event.set()`` avant le démarrage doit produire un résultat
185
  propre (vide ou partiel) sans crasher."""
186
- from picarones.measurements.runner import run_benchmark
187
 
188
  cancel = threading.Event()
189
  cancel.set() # déjà annulé avant le démarrage
190
  engine = _SlowMockEngine(sleep_seconds=0.05)
191
  # Le runner ne doit pas lever ; il peut retourner un résultat
192
  # vide ou très partiel selon le moment où il vérifie l'event.
193
- result = run_benchmark(
194
  corpus=mini_corpus,
195
  engines=[engine],
196
  max_workers=2,
@@ -205,13 +205,13 @@ def test_runner_two_successive_runs_no_thread_leak(mini_corpus: Corpus) -> None:
205
  """Deux benchmarks successifs doivent fonctionner sans accumulation
206
  notable de threads (garde-fou contre les ProcessPool jamais fermés)."""
207
  import threading as _t
208
- from picarones.measurements.runner import run_benchmark
209
 
210
  engine = _SlowMockEngine(sleep_seconds=0.01)
211
 
212
  threads_before = _t.active_count()
213
  for _ in range(2):
214
- run_benchmark(
215
  corpus=mini_corpus, engines=[engine],
216
  max_workers=2, show_progress=False, timeout_seconds=5.0,
217
  )
@@ -227,10 +227,10 @@ def test_runner_two_successive_runs_no_thread_leak(mini_corpus: Corpus) -> None:
227
  def test_runner_respects_max_workers_one(mini_corpus: Corpus) -> None:
228
  """``max_workers=1`` → exécution séquentielle (pas de parallélisme).
229
  Les 5 docs doivent quand même tous finir."""
230
- from picarones.measurements.runner import run_benchmark
231
 
232
  engine = _SlowMockEngine(sleep_seconds=0.01)
233
- result = run_benchmark(
234
  corpus=mini_corpus, engines=[engine],
235
  max_workers=1, show_progress=False, timeout_seconds=10.0,
236
  )
@@ -239,10 +239,10 @@ def test_runner_respects_max_workers_one(mini_corpus: Corpus) -> None:
239
 
240
  def test_runner_handles_empty_corpus(tmp_path: Path) -> None:
241
  """Corpus vide → benchmark vide, pas de crash."""
242
- from picarones.measurements.runner import run_benchmark
243
 
244
  empty = Corpus(documents=[], name="empty")
245
- result = run_benchmark(
246
  corpus=empty, engines=[_SlowMockEngine()],
247
  max_workers=2, show_progress=False, timeout_seconds=5.0,
248
  )
 
105
 
106
  def test_runner_completes_all_docs_in_parallel(mini_corpus: Corpus) -> None:
107
  """Avec ``max_workers=4``, les 5 docs doivent tous finir."""
108
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
109
 
110
  engine = _SlowMockEngine(sleep_seconds=0.02)
111
+ result = run_benchmark_via_service(
112
  corpus=mini_corpus,
113
  engines=[engine],
114
  max_workers=4,
 
121
 
122
  def test_runner_isolates_failing_doc_from_others(mini_corpus: Corpus) -> None:
123
  """Un fail sur un doc ne doit pas faire échouer les 4 autres."""
124
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
125
 
126
  engine = _SlowMockEngine(sleep_seconds=0.02, fail_on={"doc_02"})
127
+ result = run_benchmark_via_service(
128
  corpus=mini_corpus,
129
  engines=[engine],
130
  max_workers=4,
 
142
  def test_runner_isolates_completely_broken_engine(mini_corpus: Corpus) -> None:
143
  """Un engine qui crashe sur tous les docs → tous les docs ont
144
  ``error`` non vide, mais le runner ne crashe pas."""
145
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
146
 
147
+ result = run_benchmark_via_service(
148
  corpus=mini_corpus,
149
  engines=[_AlwaysCrashEngine()],
150
  max_workers=4,
 
161
  def test_runner_results_ordered_deterministically(mini_corpus: Corpus) -> None:
162
  """Avec parallélisme, les ``DocumentResult`` doivent rester triés
163
  de manière déterministe (par doc_id)."""
164
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
165
 
166
  engine = _SlowMockEngine(sleep_seconds=0.02)
167
+ result1 = run_benchmark_via_service(
168
  corpus=mini_corpus, engines=[engine],
169
  max_workers=4, show_progress=False, timeout_seconds=10.0,
170
  )
171
+ result2 = run_benchmark_via_service(
172
  corpus=mini_corpus, engines=[engine],
173
  max_workers=4, show_progress=False, timeout_seconds=10.0,
174
  )
 
183
  def test_runner_respects_cancel_event(mini_corpus: Corpus) -> None:
184
  """``cancel_event.set()`` avant le démarrage doit produire un résultat
185
  propre (vide ou partiel) sans crasher."""
186
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
187
 
188
  cancel = threading.Event()
189
  cancel.set() # déjà annulé avant le démarrage
190
  engine = _SlowMockEngine(sleep_seconds=0.05)
191
  # Le runner ne doit pas lever ; il peut retourner un résultat
192
  # vide ou très partiel selon le moment où il vérifie l'event.
193
+ result = run_benchmark_via_service(
194
  corpus=mini_corpus,
195
  engines=[engine],
196
  max_workers=2,
 
205
  """Deux benchmarks successifs doivent fonctionner sans accumulation
206
  notable de threads (garde-fou contre les ProcessPool jamais fermés)."""
207
  import threading as _t
208
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
209
 
210
  engine = _SlowMockEngine(sleep_seconds=0.01)
211
 
212
  threads_before = _t.active_count()
213
  for _ in range(2):
214
+ run_benchmark_via_service(
215
  corpus=mini_corpus, engines=[engine],
216
  max_workers=2, show_progress=False, timeout_seconds=5.0,
217
  )
 
227
  def test_runner_respects_max_workers_one(mini_corpus: Corpus) -> None:
228
  """``max_workers=1`` → exécution séquentielle (pas de parallélisme).
229
  Les 5 docs doivent quand même tous finir."""
230
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
231
 
232
  engine = _SlowMockEngine(sleep_seconds=0.01)
233
+ result = run_benchmark_via_service(
234
  corpus=mini_corpus, engines=[engine],
235
  max_workers=1, show_progress=False, timeout_seconds=10.0,
236
  )
 
239
 
240
  def test_runner_handles_empty_corpus(tmp_path: Path) -> None:
241
  """Corpus vide → benchmark vide, pas de crash."""
242
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
243
 
244
  empty = Corpus(documents=[], name="empty")
245
+ result = run_benchmark_via_service(
246
  corpus=empty, engines=[_SlowMockEngine()],
247
  max_workers=2, show_progress=False, timeout_seconds=5.0,
248
  )
tests/integration/test_sprint_a14_s12_executor_equivalence.py CHANGED
@@ -45,7 +45,7 @@ from picarones.evaluation.corpus import Corpus, Document
45
  from picarones.domain import Artifact, ArtifactType, DocumentRef
46
  from picarones.adapters.legacy_engines.base import BaseOCREngine
47
  from picarones.measurements.metrics import compute_metrics
48
- from picarones.measurements.runner import run_benchmark
49
  from picarones.pipeline import (
50
  CorpusRunner,
51
  PipelineExecutor,
@@ -229,7 +229,7 @@ def _run_old_runner(
229
  ) -> tuple[float | None, float | None]:
230
  """Exécute l'ancien runner et retourne (mean_cer, mean_wer)."""
231
  engine = _FakeOCREngine(text_per_doc=hypothesis_per_doc)
232
- result = run_benchmark(
233
  corpus=corpus,
234
  engines=[engine],
235
  show_progress=False,
 
45
  from picarones.domain import Artifact, ArtifactType, DocumentRef
46
  from picarones.adapters.legacy_engines.base import BaseOCREngine
47
  from picarones.measurements.metrics import compute_metrics
48
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
49
  from picarones.pipeline import (
50
  CorpusRunner,
51
  PipelineExecutor,
 
229
  ) -> tuple[float | None, float | None]:
230
  """Exécute l'ancien runner et retourne (mean_cer, mean_wer)."""
231
  engine = _FakeOCREngine(text_per_doc=hypothesis_per_doc)
232
+ result = run_benchmark_via_service(
233
  corpus=corpus,
234
  engines=[engine],
235
  show_progress=False,
tests/measurements/test_sprint12_nouvelles_fonctionnalites.py CHANGED
@@ -132,10 +132,10 @@ class TestExcludeCharsNormalization:
132
  # CER devrait être 0 ou très faible maintenant (Bonjourmonde == Bonjourmonde)
133
  assert metrics_excl.cer == 0.0
134
 
135
- def test_char_exclude_propagated_in_run_benchmark(self, tmp_path):
136
  """char_exclude doit être transmis à run_benchmark et réduire le CER."""
137
  from picarones.evaluation.corpus import Corpus, Document
138
- from picarones.measurements.runner import run_benchmark
139
  from picarones.adapters.legacy_engines.base import BaseOCREngine, EngineResult
140
 
141
  class MockEngine(BaseOCREngine):
@@ -149,10 +149,10 @@ class TestExcludeCharsNormalization:
149
  (tmp_path / "page.png").write_bytes(FAKE_PNG)
150
  corpus = Corpus(name="test", documents=[doc])
151
 
152
- result_raw = run_benchmark(corpus, [MockEngine()])
153
  cer_raw = result_raw.engine_reports[0].document_results[0].metrics.cer
154
 
155
- result_excl = run_benchmark(corpus, [MockEngine()], char_exclude=frozenset([",", "!"]))
156
  cer_excl = result_excl.engine_reports[0].document_results[0].metrics.cer
157
 
158
  assert cer_excl <= cer_raw
 
132
  # CER devrait être 0 ou très faible maintenant (Bonjourmonde == Bonjourmonde)
133
  assert metrics_excl.cer == 0.0
134
 
135
+ def test_char_exclude_propagated_in_run_benchmark_via_service(self, tmp_path):
136
  """char_exclude doit être transmis à run_benchmark et réduire le CER."""
137
  from picarones.evaluation.corpus import Corpus, Document
138
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
139
  from picarones.adapters.legacy_engines.base import BaseOCREngine, EngineResult
140
 
141
  class MockEngine(BaseOCREngine):
 
149
  (tmp_path / "page.png").write_bytes(FAKE_PNG)
150
  corpus = Corpus(name="test", documents=[doc])
151
 
152
+ result_raw = run_benchmark_via_service(corpus, [MockEngine()])
153
  cer_raw = result_raw.engine_reports[0].document_results[0].metrics.cer
154
 
155
+ result_excl = run_benchmark_via_service(corpus, [MockEngine()], char_exclude=frozenset([",", "!"]))
156
  cer_excl = result_excl.engine_reports[0].document_results[0].metrics.cer
157
 
158
  assert cer_excl <= cer_raw
tests/measurements/test_sprint_a14_s1_normalization_propagation.py CHANGED
@@ -23,7 +23,7 @@ from picarones.evaluation.metrics.normalization import (
23
  NORMALIZATION_PROFILES,
24
  get_builtin_profile,
25
  )
26
- from picarones.measurements.runner import run_benchmark
27
  from picarones.measurements.runner.document import _compute_document_result
28
  from picarones.measurements.runner.workers import (
29
  _io_doc_worker,
@@ -33,7 +33,7 @@ from picarones.measurements.runner.workers import (
33
  class TestRunBenchmarkSignature:
34
  def test_run_benchmark_accepts_normalization_profile(self) -> None:
35
  """La signature publique doit exposer ``normalization_profile``."""
36
- sig = inspect.signature(run_benchmark)
37
  assert "normalization_profile" in sig.parameters
38
  # Et avec une valeur par défaut sûre.
39
  assert sig.parameters["normalization_profile"].default is None
 
23
  NORMALIZATION_PROFILES,
24
  get_builtin_profile,
25
  )
26
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
27
  from picarones.measurements.runner.document import _compute_document_result
28
  from picarones.measurements.runner.workers import (
29
  _io_doc_worker,
 
33
  class TestRunBenchmarkSignature:
34
  def test_run_benchmark_accepts_normalization_profile(self) -> None:
35
  """La signature publique doit exposer ``normalization_profile``."""
36
+ sig = inspect.signature(run_benchmark_via_service)
37
  assert "normalization_profile" in sig.parameters
38
  # Et avec une valeur par défaut sûre.
39
  assert sig.parameters["normalization_profile"].default is None
tests/web/test_sprint6_web_interface.py CHANGED
@@ -907,22 +907,22 @@ class TestRunnerProgressCallback:
907
  def test_callback_signature_accepted(self):
908
  """run_benchmark accepte un paramètre progress_callback."""
909
  import inspect
910
- from picarones.measurements.runner import run_benchmark
911
- sig = inspect.signature(run_benchmark)
912
  assert "progress_callback" in sig.parameters
913
 
914
  def test_callback_is_optional(self):
915
  """progress_callback est optionnel (valeur par défaut None)."""
916
  import inspect
917
- from picarones.measurements.runner import run_benchmark
918
- sig = inspect.signature(run_benchmark)
919
  param = sig.parameters["progress_callback"]
920
  assert param.default is None
921
 
922
  def test_callback_called_with_mock_engine(self, tmp_corpus):
923
  """Le callback est appelé pour chaque document."""
924
  from picarones.evaluation.corpus import load_corpus_from_directory
925
- from picarones.measurements.runner import run_benchmark
926
  from picarones.adapters.legacy_engines.base import BaseOCREngine
927
 
928
  class MockEngine(BaseOCREngine):
@@ -937,13 +937,13 @@ class TestRunnerProgressCallback:
937
  def my_callback(engine_name, doc_idx, doc_id):
938
  calls.append((engine_name, doc_idx, doc_id))
939
 
940
- run_benchmark(corpus, [MockEngine()], progress_callback=my_callback)
941
  assert len(calls) == len(corpus), f"Expected {len(corpus)} calls, got {len(calls)}"
942
 
943
  def test_callback_receives_engine_name(self, tmp_corpus):
944
  """Le callback reçoit le nom du moteur."""
945
  from picarones.evaluation.corpus import load_corpus_from_directory
946
- from picarones.measurements.runner import run_benchmark
947
  from picarones.adapters.legacy_engines.base import BaseOCREngine
948
 
949
  class MockEngine(BaseOCREngine):
@@ -958,13 +958,13 @@ class TestRunnerProgressCallback:
958
  def my_callback(engine_name, doc_idx, doc_id):
959
  engine_names.append(engine_name)
960
 
961
- run_benchmark(corpus, [MockEngine()], progress_callback=my_callback)
962
  assert all(n == "test_engine_name" for n in engine_names)
963
 
964
  def test_callback_exception_does_not_crash(self, tmp_corpus):
965
  """Une exception dans le callback ne plante pas le benchmark."""
966
  from picarones.evaluation.corpus import load_corpus_from_directory
967
- from picarones.measurements.runner import run_benchmark
968
  from picarones.adapters.legacy_engines.base import BaseOCREngine
969
 
970
  class MockEngine(BaseOCREngine):
@@ -980,7 +980,7 @@ class TestRunnerProgressCallback:
980
  raise RuntimeError("Callback error!")
981
 
982
  # Ne doit pas lever d'exception
983
- result = run_benchmark(corpus, [MockEngine()], progress_callback=bad_callback)
984
  assert result is not None
985
 
986
 
 
907
  def test_callback_signature_accepted(self):
908
  """run_benchmark accepte un paramètre progress_callback."""
909
  import inspect
910
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
911
+ sig = inspect.signature(run_benchmark_via_service)
912
  assert "progress_callback" in sig.parameters
913
 
914
  def test_callback_is_optional(self):
915
  """progress_callback est optionnel (valeur par défaut None)."""
916
  import inspect
917
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
918
+ sig = inspect.signature(run_benchmark_via_service)
919
  param = sig.parameters["progress_callback"]
920
  assert param.default is None
921
 
922
  def test_callback_called_with_mock_engine(self, tmp_corpus):
923
  """Le callback est appelé pour chaque document."""
924
  from picarones.evaluation.corpus import load_corpus_from_directory
925
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
926
  from picarones.adapters.legacy_engines.base import BaseOCREngine
927
 
928
  class MockEngine(BaseOCREngine):
 
937
  def my_callback(engine_name, doc_idx, doc_id):
938
  calls.append((engine_name, doc_idx, doc_id))
939
 
940
+ run_benchmark_via_service(corpus, [MockEngine()], progress_callback=my_callback)
941
  assert len(calls) == len(corpus), f"Expected {len(corpus)} calls, got {len(calls)}"
942
 
943
  def test_callback_receives_engine_name(self, tmp_corpus):
944
  """Le callback reçoit le nom du moteur."""
945
  from picarones.evaluation.corpus import load_corpus_from_directory
946
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
947
  from picarones.adapters.legacy_engines.base import BaseOCREngine
948
 
949
  class MockEngine(BaseOCREngine):
 
958
  def my_callback(engine_name, doc_idx, doc_id):
959
  engine_names.append(engine_name)
960
 
961
+ run_benchmark_via_service(corpus, [MockEngine()], progress_callback=my_callback)
962
  assert all(n == "test_engine_name" for n in engine_names)
963
 
964
  def test_callback_exception_does_not_crash(self, tmp_corpus):
965
  """Une exception dans le callback ne plante pas le benchmark."""
966
  from picarones.evaluation.corpus import load_corpus_from_directory
967
+ from picarones.app.services._legacy_runner_adapter import run_benchmark_via_service
968
  from picarones.adapters.legacy_engines.base import BaseOCREngine
969
 
970
  class MockEngine(BaseOCREngine):
 
980
  raise RuntimeError("Callback error!")
981
 
982
  # Ne doit pas lever d'exception
983
+ result = run_benchmark_via_service(corpus, [MockEngine()], progress_callback=bad_callback)
984
  assert result is not None
985
 
986