File size: 8,943 Bytes
94e0210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9011070
94e0210
 
 
 
d109222
9011070
94e0210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17cc547
 
 
 
9011070
17cc547
94e0210
 
17cc547
94e0210
17cc547
94e0210
 
9011070
17cc547
94e0210
 
 
17cc547
94e0210
 
9011070
17cc547
94e0210
 
 
17cc547
94e0210
 
 
9011070
17cc547
94e0210
 
 
 
 
17cc547
94e0210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Tests Sprint 95 โ€” B.4 : visualisation DAG d'un pipeline composรฉ.

Couvre :

1. ``build_pipeline_dag_html`` :
   - vide / None โ†’ ``""``
   - 1 nล“ud โ†’ SVG sans arรชte
   - 2 nล“uds + 1 arรชte
   - 3 nล“uds chaรฎnรฉs
   - arรชtes auto-dรฉduites si non fournies
   - couleur selon seuil de la mรฉtrique
   - mode higher_is_better
2. Anti-injection sur nom de nล“ud, type d'artefact, nom de
   mรฉtrique.
3. Affichage de la valeur de mรฉtrique formatรฉe.
4. Complรฉtude i18n FR/EN.
"""

from __future__ import annotations

import json
from pathlib import Path

from picarones.reports.html.renderers.pipeline_dag import build_pipeline_dag_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. build_pipeline_dag_html
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestRender:
    def test_empty_returns_empty(self) -> None:
        assert build_pipeline_dag_html(None) == ""
        assert build_pipeline_dag_html([]) == ""

    def test_single_node_renders_svg_no_edge(self) -> None:
        nodes = [{"name": "tess", "output_types": ["TEXT"]}]
        html = build_pipeline_dag_html(nodes, _load_labels("fr"))
        assert "<svg" in html
        assert "tess" in html
        # Pas de flรจche tracรฉe (pas d'arรชte)
        assert "marker-end" not in html

    def test_two_nodes_one_edge(self) -> None:
        nodes = [
            {"name": "ocr", "output_types": ["TEXT"]},
            {"name": "llm", "input_types": ["TEXT"]},
        ]
        edges = [{"from": "ocr", "to": "llm",
                  "artifact_type": "TEXT",
                  "metric_name": "cer",
                  "metric_value": 0.04}]
        html = build_pipeline_dag_html(
            nodes, _load_labels("fr"), edges=edges,
        )
        # Nล“uds prรฉsents
        assert "ocr" in html
        assert "llm" in html
        # ร‰tiquettes d'arรชte
        assert "TEXT" in html
        assert "cer" in html
        assert "4.0%" in html
        # Flรจche prรฉsente
        assert "marker-end" in html

    def test_three_nodes_chain(self) -> None:
        nodes = [
            {"name": "a"}, {"name": "b"}, {"name": "c"},
        ]
        edges = [
            {"from": "a", "to": "b", "metric_value": 0.05},
            {"from": "b", "to": "c", "metric_value": 0.10},
        ]
        html = build_pipeline_dag_html(nodes, edges=edges)
        # Deux flรจches
        assert html.count("marker-end") == 2

    def test_auto_edges_when_missing(self) -> None:
        # Pas d'arรชtes fournies โ†’ auto-dรฉduit sรฉquentielles
        nodes = [{"name": "a"}, {"name": "b"}, {"name": "c"}]
        html = build_pipeline_dag_html(nodes)
        assert html.count("marker-end") == 2

    def test_colour_green_for_low_cer(self) -> None:
        # Sprint A7 (m-5) : palette Okabe-Ito (daltonien-friendly).
        # Le test valide la sรฉmantique ยซ โ‰ค 0.05 โ†’ bon ยป sans coder en
        # dur le hex (qui peut รฉvoluer avec la palette).  Comparaison
        # via ``COLOR_GREEN`` du module canonique.
        from picarones.reports._helpers.colors import COLOR_GREEN

        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b",
                  "metric_value": 0.02}]  # โ‰ค 0.05 โ†’ bon
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert COLOR_GREEN in html

    def test_colour_yellow(self) -> None:
        from picarones.reports._helpers.colors import COLOR_YELLOW

        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b", "metric_value": 0.10}]
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert COLOR_YELLOW in html

    def test_colour_red_for_high_cer(self) -> None:
        from picarones.reports._helpers.colors import COLOR_RED

        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b", "metric_value": 0.30}]
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert COLOR_RED in html

    def test_higher_is_better_inverts(self) -> None:
        # F1 = 0.95 = bonne qualitรฉ (haut)
        from picarones.reports._helpers.colors import COLOR_GREEN

        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b", "metric_value": 0.96}]
        html = build_pipeline_dag_html(
            nodes, edges=edges, higher_is_better=True,
        )
        assert COLOR_GREEN in html

    def test_unknown_node_in_edge_skipped(self) -> None:
        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [
            {"from": "a", "to": "b", "metric_value": 0.05},
            {"from": "ghost", "to": "b", "metric_value": 0.01},
        ]
        html = build_pipeline_dag_html(nodes, edges=edges)
        # Une seule flรจche valide
        assert html.count("marker-end") == 1

    def test_handles_missing_metric_value(self) -> None:
        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b",
                  "artifact_type": "TEXT",
                  "metric_name": "cer"}]  # pas de valeur
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert "โ€”" in html or "cer" in html


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. Anti-injection
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestAntiInjection:
    def test_node_name(self) -> None:
        nodes = [{"name": "<script>alert(1)</script>"}]
        html = build_pipeline_dag_html(nodes, _load_labels("fr"))
        assert "<script>alert" not in html
        assert "&lt;script&gt;" in html

    def test_artifact_type(self) -> None:
        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b",
                  "artifact_type": "<img/>",
                  "metric_value": 0.05}]
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert "<img/>" not in html
        assert "&lt;img" in html

    def test_metric_name(self) -> None:
        nodes = [{"name": "a"}, {"name": "b"}]
        edges = [{"from": "a", "to": "b",
                  "metric_name": "<script>x",
                  "metric_value": 0.05}]
        html = build_pipeline_dag_html(nodes, edges=edges)
        assert "<script>x" not in html
        assert "&lt;script&gt;" in html

    def test_input_output_types(self) -> None:
        nodes = [{"name": "a", "input_types": ["<svg/>"],
                  "output_types": ["<x>"]}]
        html = build_pipeline_dag_html(nodes, _load_labels("fr"))
        assert "<svg/>" not in html
        assert "&lt;svg" in html


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. Rendu en anglais
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


class TestI18nRendering:
    def test_english(self) -> None:
        nodes = [{"name": "a"}]
        html = build_pipeline_dag_html(nodes, _load_labels("en"))
        assert "Inspection tool" in html or "source of truth" in html


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 4. Complรฉtude i18n
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€


_KEYS = {
    "dag_title", "dag_note", "dag_legend",
    "dag_legend_green", "dag_legend_yellow", "dag_legend_red",
}


class TestI18nCompleteness:
    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()