import ast import subprocess import sys import warnings import pytest from model_schema import PipelineResult, SolverCode, SolverRun, SolverStatus from report import build_document, to_html from script_export import build_solver_script from solver_stage import CodeRejected, assemble_script, run_solver_code, validate_structure from test_model_checks import make_spec from test_review import make_review # min 3x + 2y s.t. x + y >= 4, 0 <= x, y <= 3 -> Zielwert 9 STRUCTURED_CODE = ''' from scipy.optimize import linprog # Kosten je Tonne Rohstoff X und Y [Tsd. €] COSTS = [3, 2] def solve_model(): """Baut das LP auf und löst es.""" # Bedarf: x + y >= 4 -> -x - y <= -4 return linprog(COSTS, A_ub=[[-1, -1]], b_ub=[-4], bounds=[(0, 3), (0, 3)], method="highs") def main(): """Löst das Modell und gibt das Ergebnis aus.""" res = solve_model() x, y = float(res.x[0]), float(res.x[1]) report( "optimal" if res.status == 0 else "error", res.fun, {"x": x, "y": y}, constraints=[{"name": "Bedarf", "lhs": x + y, "sense": ">=", "rhs": 4}], variables=[{"name": "x", "value": x, "var_type": "continuous", "lb": 0, "ub": 3}, {"name": "y", "value": y, "var_type": "continuous", "lb": 0, "ub": 3}], ) if __name__ == "__main__": main() ''' def make_result(task="Minimiere die Kosten für mindestens 4 t Rohstoff.", code=STRUCTURED_CODE): return PipelineResult( task=task, specification=make_spec(), review=make_review(), revisions=0, solver_code=SolverCode(code=code, data_assumptions=["Lieferfrist: keine (nicht im Text)"]), solver_run=SolverRun(status=SolverStatus.OPTIMAL, objective_value=9.0), ) def test_script_layout_order(): script = build_solver_script(make_result()) assert script.startswith('#!/usr/bin/env python3\nr"""\n') tree = ast.parse(script) docstring = ast.get_docstring(tree) positions = [docstring.index(h) for h in ("AUFGABE", "MATHEMATISCHES MODELL", "ALGORITHMUS")] assert positions == sorted(positions) assert "Minimiere die Kosten" in docstring assert "\\sum_{j \\in J} x_j \\ge b" in docstring # Restriktion in LaTeX assert "Branch-and-Cut" not in docstring # SciPy-Stack, nicht HiGHS-Text assert "linprog" in docstring assert "Lieferfrist" in docstring # angenommene Daten # Imports vor report(), report() vor dem abschließenden main-Guard names = [type(n).__name__ + ":" + getattr(n, "name", "") for n in tree.body] guard_index = len(names) - 1 assert names.index("ImportFrom:") < names.index("FunctionDef:main") < names.index("FunctionDef:report") < guard_index assert isinstance(tree.body[guard_index], ast.If) and script.rstrip().endswith("main()") def test_script_is_standalone_runnable(tmp_path): path = tmp_path / "solve.py" path.write_text(build_solver_script(make_result()), encoding="utf-8") proc = subprocess.run([sys.executable, str(path)], capture_output=True, text=True, timeout=60) assert proc.returncode == 0, proc.stderr assert "Status: optimal" in proc.stdout assert "Zielwert: 9.0" in proc.stdout assert proc.stdout.strip().splitlines()[-1].startswith("RESULT_JSON:") def test_latex_backslashes_and_quotes_do_not_break_script(): task = 'Text mit \\sum, \\( x \\), """ Anführungszeichen """ und Backslash am Ende \\' script = build_solver_script(make_result(task=task)) with warnings.catch_warnings(): warnings.simplefilter("error") # SyntaxWarning (ungültige Escapes) wäre ein Fehler compile(script, "solve.py", "exec") def test_run_solver_code_with_structured_code(): run = run_solver_code(STRUCTURED_CODE) assert run.status == SolverStatus.OPTIMAL assert run.objective_value == pytest.approx(9) def test_assemble_without_guard_prepends_helpers(): script = assemble_script("x = 1") assert "def report(" in script and "_apply_data_override(globals())" in script assert script.index("def report(") < script.index("x = 1") def test_valid_structure_passes(): validate_structure(STRUCTURED_CODE) @pytest.mark.parametrize("code, reason", [ ("def solve():\n pass\n\nif __name__ == '__main__':\n solve()\n", "main()"), ("def main():\n pass\n\nmain()\n", "__main__"), ("def main():\n pass\n\nif __name__ == '__main__':\n main()\n\nx = 1\n", "__main__"), ]) def test_invalid_structure_is_rejected(code, reason): with pytest.raises(CodeRejected, match=reason.replace("(", r"\(").replace(")", r"\)")): validate_structure(code) def test_html_report_has_copy_button_with_full_script(): page = to_html(build_document(make_result())) assert page.count('class="copy-btn"') == 1 assert "navigator.clipboard" in page and "execCommand('copy')" in page assert "#!/usr/bin/env python3" in page assert "if __name__ == "__main__":" in page def test_inline_math_delimiters_are_removed_from_docstring(): spec = make_spec(indices=["Tage: \\(d \\in D = \\{Mo,Di\\}\\)"]) result = make_result() result.specification = spec docstring = ast.get_docstring(ast.parse(build_solver_script(result))) assert "Tage: d \\in D = \\{Mo,Di\\}" in docstring assert "\\(" not in docstring and "\\)" not in docstring def test_every_stack_and_class_has_algorithm_text(): # Synchron-Regel: neuer TargetStack/ProblemClass braucht Texte in script_export from model_schema import ProblemClass, TargetStack from script_export import ALGORITHMS, CLASS_FIT assert set(ALGORITHMS) == set(TargetStack) assert set(CLASS_FIT) == set(ProblemClass)