Claude commited on
Commit
0d00572
·
unverified ·
1 Parent(s): 76c090b

test(audit): éliminer tous les pytest.raises(Exception) résiduels

Browse files

Ratchet ``BROAD_RAISES_BASELINE`` Phase 8 : **24 → 0**.

Les 24 sites résiduels acceptés par la baseline temporaire ont
tous été remplacés par leur classe d'exception précise :

**13 sites Pydantic ``ValidationError``** (frozen ConfigDict +
validateurs métier) :

- ``tests/pipeline/test_sprint_a14_s6_spec.py:116`` — PipelineSpec.frozen
- ``tests/domain/test_sprint_a14_s4_artifacts.py:118,146,150``
- ``tests/domain/test_sprint_a14_s4_corpus.py:48,55``
- ``tests/domain/test_sprint_a14_s4_documents.py:85``
- ``tests/domain/test_sprint_a14_s4_provenance_errors.py:43``
- ``tests/domain/test_sprint_a14_s5_evaluation_specs.py:53,66,115,207,259``
- ``tests/app/schemas/test_sprint_a14_s39_run_spec_extended.py`` :
7 sites ``pytest.raises(Exception, match=...)`` — les
``ValueError`` levés dans les validateurs Pydantic sont
re-emballés en ``ValidationError``.

**2 sites ``FrozenInstanceError``** (dataclass frozen) :

- ``tests/adapters/storage/test_sprint_a14_s29_artifact_store.py:108,551``
— ArtifactKey et ArtifactRecord (dataclass frozen=True).

**2 sites ``PicaronesError``** dans pipeline :

- ``tests/pipeline/test_sprint_a14_s28_planner.py:560`` —
``PipelineExecutor.run_plan`` rejette un argument qui n'est pas
un ``ExecutionPlan``.
- ``tests/pipeline/test_sprint_a14_s28_planner.py:626`` —
``PipelineExecutor.__init__`` rejette un ``planner`` qui n'est
pas un ``PipelinePlanner``.

**Test ratchet abaissé à 0**

``BROAD_RAISES_BASELINE = 24 → 0``. Tout nouveau
``pytest.raises(Exception)`` introduit dans une PR future fait
échouer ``test_broad_pytest_raises_below_baseline`` —
**verrouillage strict** désormais en place.

**Bilan**

Suite : 4 789 passed, 16 skipped, 8 deselected, 2 xfailed. Ruff
propre. Chaque test attrape désormais exactement la classe
d'exception attendue — les régressions où une exception
différente serait levée font échouer le test.

tests/adapters/storage/test_sprint_a14_s29_artifact_store.py CHANGED
@@ -35,6 +35,7 @@ from __future__ import annotations
35
 
36
  import json
37
  import threading
 
38
  from pathlib import Path
39
 
40
  import pytest
@@ -105,7 +106,7 @@ class TestArtifactKeyDataclass:
105
 
106
  def test_frozen(self) -> None:
107
  k = _basic_key()
108
- with pytest.raises(Exception): # FrozenInstanceError
109
  k.adapter_name = "different" # type: ignore[misc]
110
 
111
 
@@ -548,5 +549,5 @@ class TestStoredArtifactDataclass:
548
  sa = StoredArtifact(
549
  key="k", artifact=_make_artifact(), payload=b"x",
550
  )
551
- with pytest.raises(Exception): # FrozenInstanceError
552
  sa.payload = b"y" # type: ignore[misc]
 
35
 
36
  import json
37
  import threading
38
+ from dataclasses import FrozenInstanceError
39
  from pathlib import Path
40
 
41
  import pytest
 
106
 
107
  def test_frozen(self) -> None:
108
  k = _basic_key()
109
+ with pytest.raises(FrozenInstanceError):
110
  k.adapter_name = "different" # type: ignore[misc]
111
 
112
 
 
549
  sa = StoredArtifact(
550
  key="k", artifact=_make_artifact(), payload=b"x",
551
  )
552
+ with pytest.raises(FrozenInstanceError):
553
  sa.payload = b"y" # type: ignore[misc]
tests/app/schemas/test_sprint_a14_s39_run_spec_extended.py CHANGED
@@ -14,6 +14,7 @@ purement additive.
14
  from __future__ import annotations
15
 
16
  import pytest
 
17
 
18
  from picarones.app.schemas.run_spec import (
19
  PipelineSpecYaml,
@@ -94,7 +95,7 @@ class TestPreferredTextOutput:
94
  assert pipe.preferred_text_output == "corrector.corrected_text"
95
 
96
  def test_rejects_missing_dot(self) -> None:
97
- with pytest.raises(Exception, match="format"):
98
  PipelineSpecYaml(
99
  name="bad",
100
  initial_inputs=(ArtifactType.IMAGE,),
@@ -108,7 +109,7 @@ class TestPreferredTextOutput:
108
  )
109
 
110
  def test_rejects_unknown_step(self) -> None:
111
- with pytest.raises(Exception, match="introuvable"):
112
  PipelineSpecYaml(
113
  name="bad",
114
  initial_inputs=(ArtifactType.IMAGE,),
@@ -122,7 +123,7 @@ class TestPreferredTextOutput:
122
  )
123
 
124
  def test_rejects_step_not_producing_type(self) -> None:
125
- with pytest.raises(Exception, match="ne produit pas"):
126
  PipelineSpecYaml(
127
  name="bad",
128
  initial_inputs=(ArtifactType.IMAGE,),
@@ -137,7 +138,7 @@ class TestPreferredTextOutput:
137
  )
138
 
139
  def test_rejects_unknown_artifact_type(self) -> None:
140
- with pytest.raises(Exception, match="output_type"):
141
  PipelineSpecYaml(
142
  name="bad",
143
  initial_inputs=(ArtifactType.IMAGE,),
@@ -173,7 +174,7 @@ class TestInputsFromValidation:
173
 
174
  def test_initial_step_id_rejects_unknown_initial_input(self) -> None:
175
  # `__initial__` mais le type n'est pas dans initial_inputs → erreur.
176
- with pytest.raises(Exception, match="initial_inputs"):
177
  PipelineSpecYaml(
178
  name="bad",
179
  initial_inputs=(ArtifactType.IMAGE,),
@@ -218,7 +219,7 @@ class TestInputsFromValidation:
218
 
219
  def test_rejects_forward_reference(self) -> None:
220
  # Un step ne peut pas référencer un step en aval de lui.
221
- with pytest.raises(Exception, match="antérieure"):
222
  PipelineSpecYaml(
223
  name="bad",
224
  initial_inputs=(ArtifactType.IMAGE,),
@@ -241,7 +242,7 @@ class TestInputsFromValidation:
241
  )
242
 
243
  def test_rejects_step_not_producing_referenced_type(self) -> None:
244
- with pytest.raises(Exception, match="ne produit pas"):
245
  PipelineSpecYaml(
246
  name="bad",
247
  initial_inputs=(ArtifactType.IMAGE,),
 
14
  from __future__ import annotations
15
 
16
  import pytest
17
+ from pydantic import ValidationError
18
 
19
  from picarones.app.schemas.run_spec import (
20
  PipelineSpecYaml,
 
95
  assert pipe.preferred_text_output == "corrector.corrected_text"
96
 
97
  def test_rejects_missing_dot(self) -> None:
98
+ with pytest.raises(ValidationError, match="format"):
99
  PipelineSpecYaml(
100
  name="bad",
101
  initial_inputs=(ArtifactType.IMAGE,),
 
109
  )
110
 
111
  def test_rejects_unknown_step(self) -> None:
112
+ with pytest.raises(ValidationError, match="introuvable"):
113
  PipelineSpecYaml(
114
  name="bad",
115
  initial_inputs=(ArtifactType.IMAGE,),
 
123
  )
124
 
125
  def test_rejects_step_not_producing_type(self) -> None:
126
+ with pytest.raises(ValidationError, match="ne produit pas"):
127
  PipelineSpecYaml(
128
  name="bad",
129
  initial_inputs=(ArtifactType.IMAGE,),
 
138
  )
139
 
140
  def test_rejects_unknown_artifact_type(self) -> None:
141
+ with pytest.raises(ValidationError, match="output_type"):
142
  PipelineSpecYaml(
143
  name="bad",
144
  initial_inputs=(ArtifactType.IMAGE,),
 
174
 
175
  def test_initial_step_id_rejects_unknown_initial_input(self) -> None:
176
  # `__initial__` mais le type n'est pas dans initial_inputs → erreur.
177
+ with pytest.raises(ValidationError, match="initial_inputs"):
178
  PipelineSpecYaml(
179
  name="bad",
180
  initial_inputs=(ArtifactType.IMAGE,),
 
219
 
220
  def test_rejects_forward_reference(self) -> None:
221
  # Un step ne peut pas référencer un step en aval de lui.
222
+ with pytest.raises(ValidationError, match="antérieure"):
223
  PipelineSpecYaml(
224
  name="bad",
225
  initial_inputs=(ArtifactType.IMAGE,),
 
242
  )
243
 
244
  def test_rejects_step_not_producing_referenced_type(self) -> None:
245
+ with pytest.raises(ValidationError, match="ne produit pas"):
246
  PipelineSpecYaml(
247
  name="bad",
248
  initial_inputs=(ArtifactType.IMAGE,),
tests/architecture/test_no_broad_pytest_raises.py CHANGED
@@ -72,7 +72,7 @@ def _scan_broad_raises() -> list[tuple[Path, int]]:
72
  #:
73
  #: 1. Remplacer ``pytest.raises(Exception)`` par la classe précise.
74
  #: 2. Baisser :data:`BROAD_RAISES_BASELINE` du même montant.
75
- BROAD_RAISES_BASELINE = 24
76
 
77
 
78
  def test_broad_pytest_raises_below_baseline() -> None:
 
72
  #:
73
  #: 1. Remplacer ``pytest.raises(Exception)`` par la classe précise.
74
  #: 2. Baisser :data:`BROAD_RAISES_BASELINE` du même montant.
75
+ BROAD_RAISES_BASELINE = 0
76
 
77
 
78
  def test_broad_pytest_raises_below_baseline() -> None:
tests/domain/test_sprint_a14_s4_artifacts.py CHANGED
@@ -13,6 +13,7 @@ from __future__ import annotations
13
  import hashlib
14
 
15
  import pytest
 
16
 
17
  from picarones.domain import (
18
  Artifact,
@@ -115,7 +116,7 @@ class TestArtifactCreation:
115
 
116
  def test_content_hash_must_be_64_hex(self) -> None:
117
  # Trop court
118
- with pytest.raises(Exception): # pydantic ValidationError
119
  Artifact(
120
  id="x", document_id="d1", type=ArtifactType.RAW_TEXT,
121
  content_hash="abc",
@@ -143,11 +144,11 @@ class TestArtifactCreation:
143
  class TestArtifactImmutability:
144
  def test_frozen_blocks_attribute_mutation(self) -> None:
145
  a = Artifact(id="x", document_id="d1", type=ArtifactType.RAW_TEXT)
146
- with pytest.raises(Exception): # pydantic ValidationError
147
  a.id = "y" # type: ignore[misc]
148
 
149
  def test_extra_fields_rejected(self) -> None:
150
- with pytest.raises(Exception): # pydantic ValidationError
151
  Artifact( # type: ignore[call-arg]
152
  id="x", document_id="d1", type=ArtifactType.RAW_TEXT,
153
  bogus_field="oops",
 
13
  import hashlib
14
 
15
  import pytest
16
+ from pydantic import ValidationError
17
 
18
  from picarones.domain import (
19
  Artifact,
 
116
 
117
  def test_content_hash_must_be_64_hex(self) -> None:
118
  # Trop court
119
+ with pytest.raises(ValidationError):
120
  Artifact(
121
  id="x", document_id="d1", type=ArtifactType.RAW_TEXT,
122
  content_hash="abc",
 
144
  class TestArtifactImmutability:
145
  def test_frozen_blocks_attribute_mutation(self) -> None:
146
  a = Artifact(id="x", document_id="d1", type=ArtifactType.RAW_TEXT)
147
+ with pytest.raises(ValidationError):
148
  a.id = "y" # type: ignore[misc]
149
 
150
  def test_extra_fields_rejected(self) -> None:
151
+ with pytest.raises(ValidationError):
152
  Artifact( # type: ignore[call-arg]
153
  id="x", document_id="d1", type=ArtifactType.RAW_TEXT,
154
  bogus_field="oops",
tests/domain/test_sprint_a14_s4_corpus.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import pytest
 
6
 
7
  from picarones.domain import ArtifactType, CorpusSpec, CorpusSpecError, DocumentRef, GroundTruthRef
8
 
@@ -45,14 +46,14 @@ class TestCorpusSpec:
45
  assert c.metadata["language"] == "fr"
46
 
47
  def test_name_validation(self) -> None:
48
- with pytest.raises(Exception): # pydantic ValidationError
49
  CorpusSpec(name="") # min_length=1
50
 
51
 
52
  class TestCorpusSpecImmutability:
53
  def test_frozen_blocks_mutation(self) -> None:
54
  c = CorpusSpec(name="x")
55
- with pytest.raises(Exception):
56
  c.name = "y" # type: ignore[misc]
57
 
58
  def test_json_roundtrip_with_multilevel_gt(self) -> None:
 
3
  from __future__ import annotations
4
 
5
  import pytest
6
+ from pydantic import ValidationError
7
 
8
  from picarones.domain import ArtifactType, CorpusSpec, CorpusSpecError, DocumentRef, GroundTruthRef
9
 
 
46
  assert c.metadata["language"] == "fr"
47
 
48
  def test_name_validation(self) -> None:
49
+ with pytest.raises(ValidationError):
50
  CorpusSpec(name="") # min_length=1
51
 
52
 
53
  class TestCorpusSpecImmutability:
54
  def test_frozen_blocks_mutation(self) -> None:
55
  c = CorpusSpec(name="x")
56
+ with pytest.raises(ValidationError):
57
  c.name = "y" # type: ignore[misc]
58
 
59
  def test_json_roundtrip_with_multilevel_gt(self) -> None:
tests/domain/test_sprint_a14_s4_documents.py CHANGED
@@ -81,8 +81,10 @@ class TestMultiLevelGT:
81
 
82
  class TestDocumentRefImmutability:
83
  def test_frozen_blocks_mutation(self) -> None:
 
 
84
  d = DocumentRef(id="x")
85
- with pytest.raises(Exception):
86
  d.id = "y" # type: ignore[misc]
87
 
88
  def test_json_roundtrip(self) -> None:
 
81
 
82
  class TestDocumentRefImmutability:
83
  def test_frozen_blocks_mutation(self) -> None:
84
+ from pydantic import ValidationError
85
+
86
  d = DocumentRef(id="x")
87
+ with pytest.raises(ValidationError):
88
  d.id = "y" # type: ignore[misc]
89
 
90
  def test_json_roundtrip(self) -> None:
tests/domain/test_sprint_a14_s4_provenance_errors.py CHANGED
@@ -39,8 +39,10 @@ class TestProvenanceRecord:
39
  assert not p1.is_compatible_with(p4) # parameters_hash diffère
40
 
41
  def test_frozen(self) -> None:
 
 
42
  p = ProvenanceRecord(code_version="1.0.0")
43
- with pytest.raises(Exception):
44
  p.code_version = "1.0.1" # type: ignore[misc]
45
 
46
  def test_json_roundtrip(self) -> None:
 
39
  assert not p1.is_compatible_with(p4) # parameters_hash diffère
40
 
41
  def test_frozen(self) -> None:
42
+ from pydantic import ValidationError
43
+
44
  p = ProvenanceRecord(code_version="1.0.0")
45
+ with pytest.raises(ValidationError):
46
  p.code_version = "1.0.1" # type: ignore[misc]
47
 
48
  def test_json_roundtrip(self) -> None:
tests/domain/test_sprint_a14_s5_evaluation_specs.py CHANGED
@@ -8,6 +8,7 @@ des dataclasses pydantic.
8
  from __future__ import annotations
9
 
10
  import pytest
 
11
 
12
  from picarones.domain import (
13
  ArtifactType,
@@ -50,7 +51,7 @@ class TestMetricSpec:
50
  name="cer",
51
  input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
52
  )
53
- with pytest.raises(Exception): # pydantic ValidationError
54
  spec.name = "wer" # type: ignore[misc]
55
 
56
  def test_no_callable_field(self) -> None:
@@ -63,7 +64,7 @@ class TestMetricSpec:
63
  assert not hasattr(spec, "func")
64
 
65
  def test_extra_field_rejected(self) -> None:
66
- with pytest.raises(Exception):
67
  MetricSpec( # type: ignore[call-arg]
68
  name="cer",
69
  input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
@@ -112,7 +113,7 @@ class TestProjectionSpec:
112
  target_type=ArtifactType.RAW_TEXT,
113
  projector_name="alto_to_text",
114
  )
115
- with pytest.raises(Exception):
116
  p.projector_name = "other" # type: ignore[misc]
117
 
118
  def test_json_roundtrip(self) -> None:
@@ -204,7 +205,7 @@ class TestEvaluationView:
204
  name="x",
205
  candidate_types=frozenset({ArtifactType.RAW_TEXT}),
206
  )
207
- with pytest.raises(Exception):
208
  view.name = "y" # type: ignore[misc]
209
 
210
  def test_json_roundtrip(self) -> None:
@@ -256,5 +257,5 @@ class TestEvaluationSpec:
256
 
257
  def test_frozen(self) -> None:
258
  s = EvaluationSpec()
259
- with pytest.raises(Exception):
260
  s.views = () # type: ignore[misc]
 
8
  from __future__ import annotations
9
 
10
  import pytest
11
+ from pydantic import ValidationError
12
 
13
  from picarones.domain import (
14
  ArtifactType,
 
51
  name="cer",
52
  input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
53
  )
54
+ with pytest.raises(ValidationError):
55
  spec.name = "wer" # type: ignore[misc]
56
 
57
  def test_no_callable_field(self) -> None:
 
64
  assert not hasattr(spec, "func")
65
 
66
  def test_extra_field_rejected(self) -> None:
67
+ with pytest.raises(ValidationError):
68
  MetricSpec( # type: ignore[call-arg]
69
  name="cer",
70
  input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
 
113
  target_type=ArtifactType.RAW_TEXT,
114
  projector_name="alto_to_text",
115
  )
116
+ with pytest.raises(ValidationError):
117
  p.projector_name = "other" # type: ignore[misc]
118
 
119
  def test_json_roundtrip(self) -> None:
 
205
  name="x",
206
  candidate_types=frozenset({ArtifactType.RAW_TEXT}),
207
  )
208
+ with pytest.raises(ValidationError):
209
  view.name = "y" # type: ignore[misc]
210
 
211
  def test_json_roundtrip(self) -> None:
 
257
 
258
  def test_frozen(self) -> None:
259
  s = EvaluationSpec()
260
+ with pytest.raises(ValidationError):
261
  s.views = () # type: ignore[misc]
tests/pipeline/test_sprint_a14_s28_planner.py CHANGED
@@ -557,7 +557,10 @@ class TestPipelineExecutorWithPlanner:
557
  executor = PipelineExecutor(
558
  adapter_resolver=lambda n: _OCRStub(),
559
  )
560
- with pytest.raises(Exception, match="ExecutionPlan"):
 
 
 
561
  executor.run_plan(
562
  plan="not a plan", # type: ignore[arg-type]
563
  document=DocumentRef(id="d1"),
@@ -623,7 +626,8 @@ class TestPipelineExecutorWithPlanner:
623
  assert plan.metric_junctions # non vide grâce au registry injecté
624
 
625
  def test_planner_must_be_pipeline_planner(self) -> None:
626
- with pytest.raises(Exception, match="PipelinePlanner"):
 
627
  PipelineExecutor(
628
  adapter_resolver=lambda n: _OCRStub(),
629
  planner="not a planner", # type: ignore[arg-type]
 
557
  executor = PipelineExecutor(
558
  adapter_resolver=lambda n: _OCRStub(),
559
  )
560
+ # ``PipelineExecutor.run_plan`` lève ``PicaronesError`` quand
561
+ # l'argument ``plan`` n'est pas un ``ExecutionPlan``.
562
+ from picarones.domain.errors import PicaronesError
563
+ with pytest.raises(PicaronesError, match="ExecutionPlan"):
564
  executor.run_plan(
565
  plan="not a plan", # type: ignore[arg-type]
566
  document=DocumentRef(id="d1"),
 
626
  assert plan.metric_junctions # non vide grâce au registry injecté
627
 
628
  def test_planner_must_be_pipeline_planner(self) -> None:
629
+ from picarones.domain.errors import PicaronesError
630
+ with pytest.raises(PicaronesError, match="PipelinePlanner"):
631
  PipelineExecutor(
632
  adapter_resolver=lambda n: _OCRStub(),
633
  planner="not a planner", # type: ignore[arg-type]
tests/pipeline/test_sprint_a14_s6_spec.py CHANGED
@@ -112,6 +112,8 @@ class TestPipelineSpec:
112
  assert s.step_by_id("missing") is None
113
 
114
  def test_frozen(self) -> None:
 
 
115
  s = PipelineSpec(name="x")
116
- with pytest.raises(Exception):
117
  s.name = "y" # type: ignore[misc]
 
112
  assert s.step_by_id("missing") is None
113
 
114
  def test_frozen(self) -> None:
115
+ from pydantic import ValidationError
116
+
117
  s = PipelineSpec(name="x")
118
+ with pytest.raises(ValidationError):
119
  s.name = "y" # type: ignore[misc]