Spaces:
Running
Running
Claude
feat(sprint-S8): cohΓ©rence finale β renames test dirs, /metrics endpoint, SBOM workflow
43478ec unverified | """Tests unitaires des helpers de rendu mutualisΓ©s. | |
| Couvre :func:`color_traffic_light`, :func:`color_single_gradient`, | |
| :func:`color_diverging`, :func:`text_color_for_bg`, :func:`build_grid_svg`. | |
| """ | |
| from __future__ import annotations | |
| from picarones.reports._helpers.render_helpers import ( | |
| DIVERGING_NEGATIVE_RGB, | |
| DIVERGING_POSITIVE_RGB, | |
| GRADIENT_GREEN_RGB, | |
| GRADIENT_RED_RGB, | |
| GRADIENT_YELLOW_RGB, | |
| build_grid_svg, | |
| color_diverging, | |
| color_single_gradient, | |
| color_traffic_light, | |
| text_color_for_bg, | |
| ) | |
| def _hex_to_rgb(hex_str: str) -> tuple[int, int, int]: | |
| s = hex_str.lstrip("#") | |
| return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # color_traffic_light | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestColorTrafficLight: | |
| def test_value_zero_high_is_good_returns_red(self) -> None: | |
| assert _hex_to_rgb(color_traffic_light(0.0)) == GRADIENT_RED_RGB | |
| def test_value_max_high_is_good_returns_green(self) -> None: | |
| assert _hex_to_rgb(color_traffic_light(1.0)) == GRADIENT_GREEN_RGB | |
| def test_value_mid_high_is_good_returns_yellow(self) -> None: | |
| assert _hex_to_rgb(color_traffic_light(0.5)) == GRADIENT_YELLOW_RGB | |
| def test_value_zero_low_is_good_returns_green(self) -> None: | |
| assert _hex_to_rgb(color_traffic_light(0.0, low_is_good=True)) == GRADIENT_GREEN_RGB | |
| def test_value_max_low_is_good_returns_red(self) -> None: | |
| assert _hex_to_rgb(color_traffic_light(1.0, low_is_good=True)) == GRADIENT_RED_RGB | |
| def test_clamping_above_max(self) -> None: | |
| c = color_traffic_light(2.0) | |
| assert _hex_to_rgb(c) == GRADIENT_GREEN_RGB | |
| def test_clamping_below_zero(self) -> None: | |
| c = color_traffic_light(-1.0) | |
| assert _hex_to_rgb(c) == GRADIENT_RED_RGB | |
| def test_custom_scale_max(self) -> None: | |
| # CER 0.30 = max β vert (avec low_is_good=True β vert au max) | |
| # On vΓ©rifie plutΓ΄t high_is_good : 0.30 β vert | |
| assert _hex_to_rgb(color_traffic_light(0.30, scale_max=0.30)) == GRADIENT_GREEN_RGB | |
| # 0.15 β milieu β jaune | |
| assert _hex_to_rgb(color_traffic_light(0.15, scale_max=0.30)) == GRADIENT_YELLOW_RGB | |
| def test_custom_scale_min_and_max(self) -> None: | |
| # Plage [10, 20] : 10 β rouge, 20 β vert, 15 β jaune | |
| assert _hex_to_rgb(color_traffic_light(10.0, scale_min=10, scale_max=20)) == GRADIENT_RED_RGB | |
| assert _hex_to_rgb(color_traffic_light(20.0, scale_min=10, scale_max=20)) == GRADIENT_GREEN_RGB | |
| assert _hex_to_rgb(color_traffic_light(15.0, scale_min=10, scale_max=20)) == GRADIENT_YELLOW_RGB | |
| def test_zero_span_returns_yellow(self) -> None: | |
| # scale_min == scale_max β milieu β jaune | |
| assert _hex_to_rgb(color_traffic_light(5.0, scale_min=10, scale_max=10)) == GRADIENT_YELLOW_RGB | |
| def test_format_is_hex_lowercase(self) -> None: | |
| c = color_traffic_light(0.7) | |
| assert c.startswith("#") | |
| assert len(c) == 7 | |
| assert c == c.lower() | |
| def test_nan_propagates_to_green(self) -> None: | |
| # VΓ©rification IEEE 754 : ``min(1.0, NaN)`` retourne ``1.0`` en | |
| # Python (la comparaison avec NaN est False, donc Python retient | |
| # le second argument). ``max(0.0, 1.0) = 1.0``. Donc f=1.0, | |
| # branche ``f > 0.5`` β vert max. Comportement dΓ©terministe et | |
| # testable (pas un test placebo "ne crash pas"). | |
| assert _hex_to_rgb(color_traffic_light(float("nan"))) == GRADIENT_GREEN_RGB | |
| # Avec low_is_good=True, l'inversion donne f=0 β rouge. | |
| assert _hex_to_rgb( | |
| color_traffic_light(float("nan"), low_is_good=True) | |
| ) == GRADIENT_RED_RGB | |
| def test_inf_clamped_to_max(self) -> None: | |
| # +inf > scale_max β clamp Γ scale_max β vert (high_is_good) | |
| assert _hex_to_rgb(color_traffic_light(float("inf"))) == GRADIENT_GREEN_RGB | |
| # -inf < 0 β clamp Γ 0 β rouge | |
| assert _hex_to_rgb(color_traffic_light(float("-inf"))) == GRADIENT_RED_RGB | |
| def test_inverted_scale_returns_yellow_neutral(self) -> None: | |
| # scale_min > scale_max β span <= 0 β branche "zero span" β f=0.5 | |
| # β frontiΓ¨re exacte rouge/jaune/vert β jaune neutre. | |
| assert _hex_to_rgb( | |
| color_traffic_light(5.0, scale_min=10, scale_max=5) | |
| ) == GRADIENT_YELLOW_RGB | |
| # Idem pour scale_min == scale_max (dΓ©jΓ couvert par | |
| # test_zero_span_returns_yellow plus haut, mais la cohΓ©rence | |
| # de comportement est explicite ici). | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # color_single_gradient | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestColorSingleGradient: | |
| def test_zero_returns_start(self) -> None: | |
| assert color_single_gradient(0.0, end_rgb=(30, 58, 138)) == "#ffffff" | |
| def test_max_returns_end(self) -> None: | |
| assert color_single_gradient(1.0, end_rgb=(30, 58, 138)) == "#1e3a8a" | |
| def test_half_is_midpoint(self) -> None: | |
| c = color_single_gradient(0.5, end_rgb=(30, 58, 138)) | |
| # ((255+30)/2, (255+58)/2, (255+138)/2) β (142, 156, 196) β ~#8e9cc4 | |
| r, g, b = _hex_to_rgb(c) | |
| assert abs(r - 142) <= 1 | |
| assert abs(g - 156) <= 1 | |
| assert abs(b - 196) <= 1 | |
| def test_above_max_clamped(self) -> None: | |
| assert color_single_gradient(2.0, end_rgb=(30, 58, 138)) == "#1e3a8a" | |
| def test_below_zero_clamped(self) -> None: | |
| assert color_single_gradient(-1.0, end_rgb=(30, 58, 138)) == "#ffffff" | |
| def test_custom_max_value(self) -> None: | |
| # value = 50, max = 100 β 0.5 β milieu | |
| c = color_single_gradient(50.0, end_rgb=(30, 58, 138), max_value=100.0) | |
| r, g, b = _hex_to_rgb(c) | |
| assert abs(r - 142) <= 1 | |
| def test_custom_start_rgb(self) -> None: | |
| c = color_single_gradient(0.0, end_rgb=(30, 58, 138), start_rgb=(0, 0, 0)) | |
| assert c == "#000000" | |
| def test_zero_max_value_returns_start(self) -> None: | |
| # Garde-fou contre division par zΓ©ro | |
| assert color_single_gradient(5.0, end_rgb=(30, 58, 138), max_value=0) == "#ffffff" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # color_diverging | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestColorDiverging: | |
| def test_zero_returns_neutral(self) -> None: | |
| c = color_diverging(0.0) | |
| # Neutre = vert par dΓ©faut | |
| assert _hex_to_rgb(c) == (130, 200, 130) | |
| def test_positive_max_returns_positive_color(self) -> None: | |
| c = color_diverging(1.0, max_abs=1.0) | |
| assert _hex_to_rgb(c) == DIVERGING_POSITIVE_RGB | |
| def test_negative_max_returns_negative_color(self) -> None: | |
| c = color_diverging(-1.0, max_abs=1.0) | |
| assert _hex_to_rgb(c) == DIVERGING_NEGATIVE_RGB | |
| def test_clamping_above_max_abs(self) -> None: | |
| assert _hex_to_rgb(color_diverging(5.0, max_abs=1.0)) == DIVERGING_POSITIVE_RGB | |
| def test_clamping_below_negative_max_abs(self) -> None: | |
| assert _hex_to_rgb(color_diverging(-5.0, max_abs=1.0)) == DIVERGING_NEGATIVE_RGB | |
| def test_zero_max_abs_returns_neutral(self) -> None: | |
| c = color_diverging(5.0, max_abs=0) | |
| assert _hex_to_rgb(c) == (130, 200, 130) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # text_color_for_bg | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestTextColorForBg: | |
| def test_low_intensity_dark_text(self) -> None: | |
| assert text_color_for_bg(0.2) == "#222" | |
| def test_high_intensity_white_text(self) -> None: | |
| assert text_color_for_bg(0.8) == "#fff" | |
| def test_threshold_boundary(self) -> None: | |
| assert text_color_for_bg(0.55) == "#222" | |
| assert text_color_for_bg(0.56) == "#fff" | |
| def test_custom_threshold(self) -> None: | |
| assert text_color_for_bg(0.4, threshold=0.3) == "#fff" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # build_grid_svg | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestBuildGridSvg: | |
| def test_empty_grid_returns_empty_string(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=0, | |
| n_cols=0, | |
| row_label_fn=lambda i: "", | |
| col_label_fn=lambda j: "", | |
| cell_color_fn=lambda i, j: "#fff", | |
| ) | |
| assert svg == "" | |
| def test_zero_rows_returns_empty(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=0, | |
| n_cols=3, | |
| row_label_fn=lambda i: "", | |
| col_label_fn=lambda j: str(j), | |
| cell_color_fn=lambda i, j: "#fff", | |
| ) | |
| assert svg == "" | |
| def test_minimal_grid_renders(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "row0", | |
| col_label_fn=lambda j: "col0", | |
| cell_color_fn=lambda i, j: "#abc123", | |
| ) | |
| assert "<svg" in svg | |
| assert "row0" in svg | |
| assert "col0" in svg | |
| assert "#abc123" in svg | |
| def test_cell_text_displayed_when_provided(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "r", | |
| col_label_fn=lambda j: "c", | |
| cell_color_fn=lambda i, j: "#fff", | |
| cell_text_fn=lambda i, j: "0.42", | |
| ) | |
| assert "0.42" in svg | |
| def test_cell_text_omitted_when_none(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "r", | |
| col_label_fn=lambda j: "c", | |
| cell_color_fn=lambda i, j: "#fff", | |
| cell_text_fn=lambda i, j: None, | |
| ) | |
| # Pas de chiffre dans la cellule | |
| assert ">0.<" not in svg | |
| def test_rotate_col_labels(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "r", | |
| col_label_fn=lambda j: "long_label", | |
| cell_color_fn=lambda i, j: "#fff", | |
| rotate_col_labels=True, | |
| ) | |
| assert "rotate(-45" in svg | |
| def test_x_axis_title(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "r", | |
| col_label_fn=lambda j: "c", | |
| cell_color_fn=lambda i, j: "#fff", | |
| x_axis_title="Position dans le document", | |
| ) | |
| assert "Position dans le document" in svg | |
| def test_html_escape_in_labels(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "<script>alert(1)</script>", | |
| col_label_fn=lambda j: "<b>bold</b>", | |
| cell_color_fn=lambda i, j: "#fff", | |
| cell_text_fn=lambda i, j: "<i>italic</i>", | |
| ) | |
| assert "<script>" not in svg | |
| assert "<script>" in svg | |
| assert "<b>" not in svg | |
| assert "<b>" in svg | |
| assert "<i>" not in svg | |
| def test_grid_dimensions(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=3, | |
| n_cols=4, | |
| row_label_fn=lambda i: f"r{i}", | |
| col_label_fn=lambda j: f"c{j}", | |
| cell_color_fn=lambda i, j: "#fff", | |
| ) | |
| # 12 cellules attendues (3Γ4) | |
| assert svg.count("<rect") == 12 | |
| # 3 Γ©tiquettes de ligne + 4 de colonne | |
| # On compte les <text> qui ne sont pas dans une cellule (donc qui sont des labels) : | |
| # 3 labels lignes + 4 labels colonnes = 7. Pas de cell_text_fn donc 0. | |
| assert svg.count("<text") == 7 | |
| def test_aria_label_present(self) -> None: | |
| svg = build_grid_svg( | |
| n_rows=1, | |
| n_cols=1, | |
| row_label_fn=lambda i: "r", | |
| col_label_fn=lambda j: "c", | |
| cell_color_fn=lambda i, j: "#fff", | |
| aria_label="Test heatmap", | |
| ) | |
| assert 'aria-label="Test heatmap"' in svg | |