"""Inline-SVG-Diagramme: gültiges SVG, korrekte Geometrie, Einbindung in den Report.""" import xml.etree.ElementTree as ET import pytest from reporting import charts from core.model_schema import PipelineResult, ResultTable, SolverRun, SolverStatus from reporting.report import build_document, to_html, to_markdown from test_model_checks import make_spec from test_result_tables import PORTFOLIO_TABLE from test_review import make_review LABELS = ["A02", "A08", "A03"] VALUES = [0.03433, 0.03093, 0.02587] def parse(svg: str): assert svg.startswith(" widths[1] heights = {float(p.get("d").split("V")[1].split(" ")[0]) for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")} assert charts.BAR_HEIGHT <= 24 def test_negative_values_use_the_other_colour_role(): svg = charts.bar_chart(["Ertrag", "Kosten"], [1.0, -0.4], "Test") fills = [p.get("fill") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")] assert fills == ["var(--viz-pos)", "var(--viz-neg)"] def test_waterfall_has_step_per_term_plus_total(): svg = charts.waterfall_chart(["Rendite", "Risiko", "Kosten"], [0.1277, -0.0052, -0.0256], "Zielwert", 0.0969, "Zielwert-Zerlegung", " ") root = parse(svg) fills = [p.get("fill") for p in root.findall(".//{http://www.w3.org/2000/svg}path")] assert fills == ["var(--viz-pos)", "var(--viz-neg)", "var(--viz-neg)", "var(--viz-total)"] assert "Zielwert" in [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")] def test_tooltip_per_mark(): root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio")) titles = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}title")] assert len(titles) == len(LABELS) and titles[0].startswith("A02: ") def make_result(tables): return PipelineResult( task="Aufgabe", specification=make_spec(), review=make_review(), revisions=0, solver_run=SolverRun(status=SolverStatus.OPTIMAL, objective_value=0.09756, tables=tables, headline="Das Portfolio erreicht 9,76 %.", solution={"w[A02]": 0.3}), ) def test_html_report_embeds_chart_and_markdown_does_not(): doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)])) page = to_html(doc) assert '
' in page and "' in to_html(doc) @pytest.mark.parametrize("table", [ ResultTable(title="Ohne Zahlen", columns=["A", "B"], rows=[["x", "1,5 %"]], objective_column="B"), ResultTable(title="Nur eine Zeile", columns=["A", "B"], rows=[["x", 1.0]], objective_column="B"), ResultTable(title="Ohne Beitragsspalte", columns=["A", "B"], rows=[["x", 1.0], ["y", 2.0]]), ]) def test_no_chart_without_usable_numbers(table): assert "= 2 # Weg und Knoten des Weges hervorgehoben def test_graph_chart_refuses_unknown_nodes_and_huge_graphs(): from reporting.charts import graph_chart assert graph_chart(["a"], [("a", "b", 1)]) == "" # Bogen ins Nichts assert graph_chart([str(i) for i in range(20)], [("0", "1", 1)]) == "" # zu groß zum Zeichnen assert graph_chart([], []) == "" def test_graph_section_explains_before_it_shows(): from core.model_schema import GraphSolution, SolverRun, SolverStatus from reporting.report import _graph_section run = SolverRun(status=SolverStatus.OPTIMAL, objective_value=29, graph=GraphSolution.model_validate({ "nodes": ["0", "1", "3"], "arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}], "path": ["0", "1", "3"], "steps": [{"node": "0", "label": 0}, {"node": "1", "label": 8}, {"node": "3", "label": 29}], "node_meaning": "ein Zeitpunkt – das Ende von Jahr i.", "arc_meaning": "Kauf am Ende von Jahr i und Nutzung bis Ende von Jahr j.", })) doc = _graph_section(run) arten = [block[0] for block in doc] assert arten[0] == "p" and "Ende von Jahr" in doc[0][1] # Erklärung steht vor der Zeichnung assert "svg" in arten assert any(b[0] == "table" and "Markierungsverlauf" in str(doc[arten.index("table") - 1]) for b in doc) assert any("8 + 21 = 29" in str(b) for b in doc) # Rechenweg ausgeschrieben