Spaces:
Running
Running
| """Sprint A14-S5 β protocoles ``Projector`` et ``EvaluationViewExecutor``. | |
| VΓ©rifie qu'on peut implΓ©menter une classe satisfaisant chaque | |
| protocole sans erreur de typage runtime, et que ``ViewResult`` / | |
| ``ProjectionReport`` sont sΓ©rialisables JSON. | |
| Pas de test sur l'exΓ©cuteur rΓ©el β c'est S13. Ici on valide | |
| seulement les contrats. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from picarones.domain import Artifact, ArtifactType, EvaluationView | |
| from picarones.evaluation.projectors import ProjectionReport, Projector | |
| from picarones.evaluation.views import EvaluationViewExecutor, ViewResult | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ProjectionReport | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestProjectionReport: | |
| def test_minimal_report(self) -> None: | |
| r = ProjectionReport( | |
| source_artifact_id="a:b:c", | |
| source_type=ArtifactType.ALTO_XML, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="alto_to_text", | |
| ) | |
| assert r.lossy is True # dΓ©faut | |
| assert r.ignored_dimensions == () | |
| def test_with_ignored_dimensions(self) -> None: | |
| r = ProjectionReport( | |
| source_artifact_id="x", | |
| source_type=ArtifactType.ALTO_XML, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="alto_to_text", | |
| lossy=True, | |
| ignored_dimensions=("geometry", "block_structure"), | |
| warnings=("ordre de lecture devinΓ©",), | |
| ) | |
| assert "geometry" in r.ignored_dimensions | |
| def test_identity_projection_not_lossy(self) -> None: | |
| r = ProjectionReport( | |
| source_artifact_id="x", | |
| source_type=ArtifactType.RAW_TEXT, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="identity", | |
| lossy=False, | |
| ) | |
| assert r.lossy is False | |
| def test_frozen(self) -> None: | |
| r = ProjectionReport( | |
| source_artifact_id="x", | |
| source_type=ArtifactType.RAW_TEXT, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="identity", | |
| ) | |
| with pytest.raises(Exception): | |
| r.lossy = False # type: ignore[misc] | |
| def test_json_roundtrip(self) -> None: | |
| r = ProjectionReport( | |
| source_artifact_id="x", | |
| source_type=ArtifactType.ALTO_XML, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="alto_to_text", | |
| ignored_dimensions=("geometry",), | |
| warnings=("w",), | |
| ) | |
| r2 = ProjectionReport.model_validate_json(r.model_dump_json()) | |
| assert r == r2 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Projector β protocole satisfait par une classe minimale | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class _StubProjector: | |
| """Minimum pour satisfaire ``Projector``.""" | |
| name = "stub_alto_to_text" | |
| source_type = ArtifactType.ALTO_XML | |
| target_type = ArtifactType.RAW_TEXT | |
| def project( | |
| self, | |
| artifact: Artifact, | |
| params: dict[str, str | int | float | bool], | |
| ) -> tuple[Artifact, str, ProjectionReport]: | |
| target = Artifact( | |
| id=artifact.id + ":projected", | |
| document_id=artifact.document_id, | |
| type=self.target_type, | |
| ) | |
| report = ProjectionReport( | |
| source_artifact_id=artifact.id, | |
| source_type=self.source_type, | |
| target_type=self.target_type, | |
| projector_name=self.name, | |
| ) | |
| # Sprint S25 β le projecteur retourne aussi le payload calculΓ©. | |
| return target, "stub_projected_text", report | |
| class TestProjectorProtocol: | |
| def test_stub_satisfies_protocol(self) -> None: | |
| p = _StubProjector() | |
| assert isinstance(p, Projector) | |
| def test_stub_can_project(self) -> None: | |
| src = Artifact( | |
| id="d1:ocr:alto", | |
| document_id="d1", | |
| type=ArtifactType.ALTO_XML, | |
| ) | |
| tgt, payload, report = _StubProjector().project(src, {}) | |
| assert tgt.type == ArtifactType.RAW_TEXT | |
| assert payload == "stub_projected_text" | |
| assert report.source_artifact_id == "d1:ocr:alto" | |
| def test_non_conforming_object_does_not_satisfy(self) -> None: | |
| class _NotAProjector: | |
| pass | |
| assert not isinstance(_NotAProjector(), Projector) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ViewResult | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestViewResult: | |
| def test_minimal_result(self) -> None: | |
| r = ViewResult( | |
| view_name="text_final", | |
| pipeline_name="ocr", | |
| candidate_artifact_id="d1:ocr:raw_text", | |
| ground_truth_artifact_id="d1:gt:raw_text", | |
| ) | |
| assert r.metric_values == {} | |
| assert r.failed_metrics == {} | |
| assert r.projection_report is None | |
| def test_with_metrics_and_failures(self) -> None: | |
| r = ViewResult( | |
| view_name="text_final", | |
| pipeline_name="ocr", | |
| candidate_artifact_id="x", | |
| ground_truth_artifact_id="y", | |
| metric_values={"cer": 0.05, "wer": 0.12}, | |
| failed_metrics={"mufi_coverage": "GT vide, mΓ©trique inapplicable"}, | |
| warnings=("normalisation diplomatique appliquΓ©e",), | |
| ) | |
| assert r.metric_values["cer"] == 0.05 | |
| assert "mufi_coverage" in r.failed_metrics | |
| def test_with_projection_report(self) -> None: | |
| report = ProjectionReport( | |
| source_artifact_id="src", | |
| source_type=ArtifactType.ALTO_XML, | |
| target_type=ArtifactType.RAW_TEXT, | |
| projector_name="alto_to_text", | |
| ) | |
| r = ViewResult( | |
| view_name="text_final", | |
| pipeline_name="ocr", | |
| candidate_artifact_id="src", | |
| ground_truth_artifact_id="gt", | |
| projection_report=report, | |
| ignored_dimensions=("geometry",), | |
| ) | |
| assert r.projection_report is not None | |
| assert r.projection_report.projector_name == "alto_to_text" | |
| def test_frozen(self) -> None: | |
| r = ViewResult( | |
| view_name="x", | |
| pipeline_name="ocr", | |
| candidate_artifact_id="a", | |
| ground_truth_artifact_id="b", | |
| ) | |
| with pytest.raises(Exception): | |
| r.view_name = "y" # type: ignore[misc] | |
| def test_json_roundtrip(self) -> None: | |
| r = ViewResult( | |
| view_name="text_final", | |
| pipeline_name="ocr", | |
| candidate_artifact_id="x", | |
| ground_truth_artifact_id="y", | |
| metric_values={"cer": 0.05}, | |
| failed_metrics={"wer": "boom"}, | |
| warnings=("w",), | |
| ignored_dimensions=("geometry",), | |
| ) | |
| r2 = ViewResult.model_validate_json(r.model_dump_json()) | |
| assert r == r2 | |
| def test_pipeline_name_required(self) -> None: | |
| """``pipeline_name`` est un champ structurel, pas optionnel. | |
| Garde-fou : ce champ doit rester explicitement passΓ© par le | |
| ``EvaluationViewExecutor`` au lieu d'Γͺtre infΓ©rΓ© par les | |
| renderers via parsing de string. | |
| """ | |
| with pytest.raises(Exception): | |
| ViewResult( | |
| view_name="text_final", | |
| # pipeline_name=... manquant | |
| candidate_artifact_id="x", | |
| ground_truth_artifact_id="y", | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # EvaluationViewExecutor β protocole satisfait par un stub minimal | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class _StubExecutor: | |
| """ImplΓ©mentation triviale de ``EvaluationViewExecutor``. | |
| Ne fait aucun calcul rΓ©el β sert Γ vΓ©rifier qu'on peut Γ©crire | |
| une classe satisfaisant le protocole. Le vrai exΓ©cuteur arrive | |
| au S13. | |
| """ | |
| def evaluate( | |
| self, | |
| view: EvaluationView, | |
| candidate: Artifact, | |
| ground_truth: Artifact, | |
| *, | |
| pipeline_name: str, | |
| ) -> ViewResult: | |
| return ViewResult( | |
| view_name=view.name, | |
| pipeline_name=pipeline_name, | |
| candidate_artifact_id=candidate.id, | |
| ground_truth_artifact_id=ground_truth.id, | |
| ) | |
| class TestEvaluationViewExecutorProtocol: | |
| def test_stub_satisfies_protocol(self) -> None: | |
| ex = _StubExecutor() | |
| assert isinstance(ex, EvaluationViewExecutor) | |
| def test_stub_evaluate_returns_view_result(self) -> None: | |
| view = EvaluationView( | |
| name="text_final", | |
| candidate_types=frozenset({ArtifactType.RAW_TEXT}), | |
| ) | |
| cand = Artifact(id="c", document_id="d", type=ArtifactType.RAW_TEXT) | |
| gt = Artifact(id="g", document_id="d", type=ArtifactType.RAW_TEXT) | |
| result = _StubExecutor().evaluate(view, cand, gt, pipeline_name="ocr") | |
| assert result.view_name == "text_final" | |
| assert result.pipeline_name == "ocr" | |
| assert result.candidate_artifact_id == "c" | |