File size: 9,386 Bytes
8588daf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46bb905
8588daf
 
 
 
 
 
9011070
8588daf
 
 
 
 
 
d109222
9011070
8588daf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""Tests Sprint 89 β€” A.II.8b : spΓ©cialisation inter-moteurs.

Couvre :

1. ``compute_specialization_score`` : symΓ©trie, plage [0, 1].
2. ``classify_specialization`` : seuils par dΓ©faut + custom.
3. ``compute_specialization_matrix`` : structure, symΓ©trie, max_pair.
4. ``top_specialized_pairs`` : tri, n, min_score.
5. Vue HTML : adaptive, anti-injection, FR + EN.
6. ComplΓ©tude i18n FR/EN.
"""

from __future__ import annotations

import json
from pathlib import Path

from picarones.evaluation.metrics.specialization import (
    DEFAULT_THRESHOLDS,
    classify_specialization,
    compute_specialization_matrix,
    compute_specialization_score,
    top_specialized_pairs,
)
from picarones.reports.html.renderers.specialization import (
    build_specialization_html,
)


def _load_labels(lang: str) -> dict:
    p = (
        Path(__file__).parent.parent.parent
        / "picarones" / "reports" / "i18n" / f"{lang}.json"
    )
    return json.loads(p.read_text(encoding="utf-8"))


# ──────────────────────────────────────────────────────────────────────────
# 1. compute_specialization_score
# ──────────────────────────────────────────────────────────────────────────


class TestScore:
    def test_identical_profiles_zero(self) -> None:
        tax = {"a": 50, "b": 50}
        assert compute_specialization_score(tax, tax) < 0.001

    def test_disjoint_profiles_one(self) -> None:
        tax_a = {"a": 100}
        tax_b = {"b": 100}
        assert compute_specialization_score(tax_a, tax_b) > 0.95

    def test_symmetric(self) -> None:
        a = {"x": 70, "y": 30}
        b = {"x": 20, "y": 80}
        s_ab = compute_specialization_score(a, b)
        s_ba = compute_specialization_score(b, a)
        assert abs(s_ab - s_ba) < 1e-9

    def test_bounded_zero_one(self) -> None:
        a = {"x": 1, "y": 0, "z": 0}
        b = {"x": 0, "y": 0, "z": 1}
        score = compute_specialization_score(a, b)
        assert 0.0 <= score <= 1.0


# ──────────────────────────────────────────────────────────────────────────
# 2. classify_specialization
# ──────────────────────────────────────────────────────────────────────────


class TestClassify:
    def test_below_similar_threshold(self) -> None:
        assert classify_specialization(0.05) == "similar"

    def test_distinct_band(self) -> None:
        assert classify_specialization(0.20) == "distinct"

    def test_highly_specialized_above(self) -> None:
        assert classify_specialization(0.50) == "highly_specialized"

    def test_custom_thresholds(self) -> None:
        custom = (("low", 0.5), ("high", 1.01))
        assert classify_specialization(0.30, custom) == "low"
        assert classify_specialization(0.80, custom) == "high"

    def test_default_thresholds_exposed(self) -> None:
        assert isinstance(DEFAULT_THRESHOLDS, tuple)
        assert len(DEFAULT_THRESHOLDS) >= 2


# ──────────────────────────────────────────────────────────────────────────
# 3. compute_specialization_matrix
# ──────────────────────────────────────────────────────────────────────────


class TestMatrix:
    def test_returns_none_when_lt_two(self) -> None:
        assert compute_specialization_matrix({}) is None
        assert compute_specialization_matrix({"a": {"x": 1}}) is None

    def test_diagonal_zero(self) -> None:
        tax = {
            "a": {"x": 1, "y": 0},
            "b": {"x": 0, "y": 1},
        }
        m = compute_specialization_matrix(tax)
        for i in range(len(m["engines"])):
            assert m["matrix"][i][i] == 0.0

    def test_symmetric(self) -> None:
        tax = {
            "a": {"x": 1, "y": 0},
            "b": {"x": 0, "y": 1},
            "c": {"x": 1, "y": 1},
        }
        m = compute_specialization_matrix(tax)
        n = len(m["engines"])
        for i in range(n):
            for j in range(n):
                assert m["matrix"][i][j] == m["matrix"][j][i]

    def test_max_pair_identifies_most_specialized(self) -> None:
        # A vs B totalement disjoints, C similaire Γ  A.
        tax = {
            "a": {"x": 100, "y": 0},
            "b": {"x": 0, "y": 100},
            "c": {"x": 95, "y": 5},
        }
        m = compute_specialization_matrix(tax)
        # La paire la plus spΓ©cialisΓ©e doit Γͺtre (a, b)
        assert set(m["max_pair"]) == {"a", "b"}


# ──────────────────────────────────────────────────────────────────────────
# 4. top_specialized_pairs
# ──────────────────────────────────────────────────────────────────────────


class TestTop:
    def _matrix(self) -> dict:
        return compute_specialization_matrix({
            "a": {"x": 100, "y": 0},
            "b": {"x": 0, "y": 100},
            "c": {"x": 95, "y": 5},
        })

    def test_sorted_descending(self) -> None:
        pairs = top_specialized_pairs(self._matrix(), n=10)
        scores = [p["score"] for p in pairs]
        assert scores == sorted(scores, reverse=True)

    def test_caps_at_n(self) -> None:
        pairs = top_specialized_pairs(self._matrix(), n=1)
        assert len(pairs) == 1

    def test_min_score_filter(self) -> None:
        pairs = top_specialized_pairs(
            self._matrix(), n=10, min_score=0.99,
        )
        # Seules les paires (a,b) et Γ©ventuellement (b,c) au-dessus
        assert all(p["score"] >= 0.99 for p in pairs)

    def test_none_input_returns_empty(self) -> None:
        assert top_specialized_pairs(None) == []


# ──────────────────────────────────────────────────────────────────────────
# 5. Vue HTML
# ──────────────────────────────────────────────────────────────────────────


class TestRender:
    def test_empty_returns_empty(self) -> None:
        assert build_specialization_html(None) == ""
        assert build_specialization_html({}) == ""

    def test_single_engine_returns_empty(self) -> None:
        assert build_specialization_html({"a": {"x": 1}}) == ""

    def test_renders_table(self) -> None:
        tax = {
            "tess": {"visual_confusion": 80, "lacuna": 20},
            "pero": {"visual_confusion": 5, "lacuna": 95},
        }
        html = build_specialization_html(tax, _load_labels("fr"))
        assert "<table" in html
        assert "tess" in html
        assert "pero" in html
        # CatΓ©gorie traduite
        assert "Forte spΓ©cialisation" in html

    def test_anti_injection(self) -> None:
        tax = {
            "<script>alert(1)</script>": {"x": 100},
            "pero": {"y": 100},
        }
        html = build_specialization_html(tax, _load_labels("fr"))
        assert "<script>alert" not in html
        assert "&lt;script&gt;" in html

    def test_renders_in_english(self) -> None:
        tax = {
            "a": {"x": 100, "y": 0},
            "b": {"x": 0, "y": 100},
        }
        html = build_specialization_html(tax, _load_labels("en"))
        assert "Inter-engine specialisation" in html
        assert "Highly specialised" in html


# ──────────────────────────────────────────────────────────────────────────
# 6. ComplΓ©tude i18n
# ──────────────────────────────────────────────────────────────────────────


_KEYS = {
    "specialization_title", "specialization_note",
    "specialization_engine_a", "specialization_engine_b",
    "specialization_score", "specialization_category",
    "specialization_cat_similar", "specialization_cat_distinct",
    "specialization_cat_highly_specialized",
}


class TestI18n:
    def test_fr(self) -> None:
        d = _load_labels("fr")
        assert not _KEYS - d.keys()

    def test_en(self) -> None:
        d = _load_labels("en")
        assert not _KEYS - d.keys()