Spaces:
Running
Running
File size: 8,319 Bytes
8542799 46bb905 8542799 9011070 8542799 d109222 9011070 8542799 | 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 186 187 188 189 190 191 192 | """Tests Sprint 76 β A.I.4 chantier 2 : Γ©volution intra-document.
Couvre :
1. ``compute_taxonomy_position_heatmap`` :
- GT/hyp identiques β total_errors = 0, per_class entiΓ¨rement zΓ©ro
- GT vide β ``None``
- Erreur en dΓ©but de doc β bin[0] non nul, autres bins nuls
- Erreur en fin de doc β bin[n_bins-1] non nul
- Erreurs uniformΓ©ment distribuΓ©es β tous bins β 1
- Cas dΓ©gΓ©nΓ©rΓ© ``n_bins=0`` β ``ValueError``
- Doc avec moins de mots que n_bins β distribution sparse correcte
2. Rendu HTML :
- Bien formΓ© (SVG)
- ``""`` si data None
- ``""`` si total_errors=0
- ``""`` si toutes les classes ont 0 erreur
3. Anti-injection : labels i18n contenant ``<script>``.
4. ComplΓ©tude i18n FR/EN.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from picarones.evaluation.metrics.taxonomy_intra_doc import (
compute_taxonomy_position_heatmap,
)
from picarones.reports.html.renderers.taxonomy_intra_doc import (
build_taxonomy_intra_doc_html,
)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 1. Couche de calcul
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestCompute:
def test_identical_no_errors(self) -> None:
result = compute_taxonomy_position_heatmap(
"alpha beta gamma delta epsilon",
"alpha beta gamma delta epsilon",
)
assert result is not None
assert result["total_errors"] == 0
for cls, counts in result["per_class"].items():
assert all(c == 0 for c in counts)
def test_empty_gt_returns_none(self) -> None:
assert compute_taxonomy_position_heatmap("", "anything") is None
assert compute_taxonomy_position_heatmap(None, None) is None
def test_error_at_start(self) -> None:
# 10 mots GT, erreur sur le premier seulement
gt = "alphA beta gamma delta epsilon zeta eta theta iota kappa"
# Avec 10 bins et 10 mots β 1 mot par bin
# Erreur de casse en position 0 β bin 0
hyp = "Alpha beta gamma delta epsilon zeta eta theta iota kappa"
result = compute_taxonomy_position_heatmap(gt, hyp, n_bins=10)
assert result is not None
assert result["total_errors"] == 1
assert result["totals_per_bin"][0] == 1
for i in range(1, 10):
assert result["totals_per_bin"][i] == 0
def test_error_at_end(self) -> None:
gt = "alpha beta gamma delta epsilon zeta eta theta iota kappA"
hyp = "alpha beta gamma delta epsilon zeta eta theta iota Kappa"
result = compute_taxonomy_position_heatmap(gt, hyp, n_bins=10)
assert result is not None
assert result["total_errors"] == 1
# Position 9 sur 10 β bin 9
assert result["totals_per_bin"][9] == 1
def test_uniform_distribution(self) -> None:
# 10 mots, 1 erreur de casse sur chacun β 1 erreur par bin
gt = "Alpha Beta Gamma Delta Epsilon Zeta Eta Theta Iota Kappa"
hyp = "alpha beta gamma delta epsilon zeta eta theta iota kappa"
result = compute_taxonomy_position_heatmap(gt, hyp, n_bins=10)
assert result is not None
assert result["total_errors"] == 10
# Tous les bins β 1
assert all(c == 1 for c in result["totals_per_bin"])
def test_invalid_n_bins(self) -> None:
with pytest.raises(ValueError):
compute_taxonomy_position_heatmap("a b", "a b", n_bins=0)
with pytest.raises(ValueError):
compute_taxonomy_position_heatmap("a b", "a b", n_bins=-1)
def test_per_class_breakdown(self) -> None:
# 1 erreur de casse + 1 lacune
gt = "Alpha beta gamma"
hyp = "alpha beta" # alphaβAlpha (case) ; gamma manque (lacuna)
result = compute_taxonomy_position_heatmap(gt, hyp, n_bins=3)
assert result is not None
assert sum(result["per_class"]["case_error"]) == 1
assert sum(result["per_class"]["lacuna"]) == 1
def test_more_bins_than_words(self) -> None:
# 3 mots et 10 bins β certains bins resteront vides
result = compute_taxonomy_position_heatmap(
"Alpha Beta Gamma", "alpha beta gamma", n_bins=10,
)
assert result is not None
assert sum(result["totals_per_bin"]) == 3
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 2. Rendu HTML
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestRender:
def test_returns_empty_when_none(self) -> None:
assert build_taxonomy_intra_doc_html(None) == ""
def test_returns_empty_when_no_errors(self) -> None:
data = compute_taxonomy_position_heatmap("a b c", "a b c")
# total_errors=0 β ""
assert build_taxonomy_intra_doc_html(data) == ""
def test_renders_svg(self) -> None:
data = compute_taxonomy_position_heatmap(
"Alpha beta gamma delta",
"alpha Beta gamma DELTA",
n_bins=4,
)
html = build_taxonomy_intra_doc_html(data)
assert "<svg" in html
assert "</svg>" in html
def test_class_labels_present(self) -> None:
data = compute_taxonomy_position_heatmap(
"Alpha", "alpha", n_bins=5,
)
html = build_taxonomy_intra_doc_html(data)
assert "case_error" in html
def test_n_words_displayed(self) -> None:
data = compute_taxonomy_position_heatmap(
"Alpha beta", "alpha BETA", n_bins=5,
)
html = build_taxonomy_intra_doc_html(data)
assert "2" in html # n_words_gt = 2
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 3. Anti-injection
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestAntiInjection:
def test_label_via_i18n_escaped(self) -> None:
data = compute_taxonomy_position_heatmap(
"Alpha", "alpha", n_bins=5,
)
labels = {"intradoc_title": "<b>Hack</b>"}
html = build_taxonomy_intra_doc_html(data, labels=labels)
assert "<b>Hack</b>" not in html
assert "<b>Hack</b>" in html
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 4. ComplΓ©tude i18n
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestI18nCompleteness:
def _load(self, lang: str) -> dict:
path = (
Path(__file__).parent.parent.parent
/ "picarones" / "reports" / "i18n" / f"{lang}.json"
)
return json.loads(path.read_text(encoding="utf-8"))
def test_all_keys_fr(self) -> None:
d = self._load("fr")
for key in ("intradoc_title", "intradoc_note", "intradoc_n_words"):
assert key in d, f"manque clΓ© FR : {key}"
def test_all_keys_en(self) -> None:
d_fr = self._load("fr")
d_en = self._load("en")
for key in d_fr:
if key.startswith("intradoc_"):
assert key in d_en, f"manque clΓ© EN : {key}"
|