text_to_solver_program/tests/test_charts.py
dschlueter 7459690bf5 refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00

138 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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("<svg")
return ET.fromstring(svg)
def test_bar_chart_is_valid_svg_with_one_bar_per_label():
root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio"))
assert len(root.findall(".//{http://www.w3.org/2000/svg}path")) == len(LABELS)
texts = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")]
for label in LABELS:
assert label in texts # Direktbeschriftung statt Legende (eine Serie)
assert root.get("role") == "img" and "Portfolio" in root.get("aria-label")
def test_bars_are_proportional_and_capped_in_thickness():
svg = charts.bar_chart(["groß", "klein"], [1.0, 0.5], "Test")
widths = [float(d.split("H")[1].split(" ")[0].split(",")[0]) for d in
[p.get("d") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]]
assert widths[0] > 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 '<figure class="viz-root">' in page and "<svg" in page
assert "<svg" not in to_markdown(doc)
def test_decomposition_table_gets_waterfall():
table = ResultTable(title="Zielwert-Zerlegung", columns=["Bestandteil", "Wert"],
rows=[["Rendite", 0.1277], ["Risiko", -0.0052], ["Kosten", -0.0256], ["Zielwert", 0.0969]],
objective_column="Wert")
page = to_html(build_document(make_result([table])))
assert page.count("var(--viz-total)") == 1 # Gesamtbalken nur im Wasserfall
def test_result_comes_first_and_headline_is_lead():
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
h2 = [b[1] for b in doc if b[0] == "h2"]
assert h2[0] == "Ergebnis" and h2.index("Ergebnis") < h2.index("Mathematisches Modell")
assert ("lead", "Das Portfolio erreicht 9,76 %.") in doc
assert '<p class="lead">' 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 "<svg" not in to_html(build_document(make_result([table])))
# --- Graphenzeichnung ---
def test_graph_chart_is_valid_svg_and_marks_the_path():
import xml.etree.ElementTree as ET
from reporting.charts import graph_chart
svg = graph_chart(["0", "1", "2", "3"],
[("0", "1", 8), ("0", "2", 18), ("0", "3", 31), ("1", "2", 10), ("1", "3", 21), ("2", "3", 12)],
path=["0", "1", "3"], title="Graph")
ET.fromstring(svg)
assert svg.count("<circle") == 4 # jeder Knoten einmal
assert svg.count("marker-end") == 6 # jeder Bogen einmal, und gerichtet
assert svg.count("--viz-pos") >= 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