fix: Solver-Fehler gilt als unzuverlässig; Aufräumrunde ohne LLM
- Status error nach allen Reparaturen -> reliable=False („Solver-Fehler: …“, auch bei --resolve); vorher Exit-Code 0 ohne Warnkasten. Benchmark zählt einen Absturz nicht als ehrliches Scheitern. - Report: „längster Weg“ bei maximize; Harness druckt „Gefundener Weg“. - check_requested_method: Verneinung kurz davor im selben Satz („ohne/nicht/statt/ anders als Dijkstra“) ist keine Forderung. - Reparatur-Prompt enthält die Fehlertexte aller früheren Versuche. - ruff (F, E9) als Test tests/test_lint.py, Konfiguration in pyproject.toml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
29d058d8f1
commit
f101b63947
13 changed files with 149 additions and 13 deletions
|
|
@ -270,6 +270,7 @@ Der Report folgt dem Weg von der Aufgabe zur Lösung. Ganz oben stehen die **Lau
|
||||||
| Zulässigkeitsprüfung | Die gemeldete Lösung verletzt Restriktionen, Schranken oder Ganzzahligkeit. | `--von solver` versuchen. |
|
| Zulässigkeitsprüfung | Die gemeldete Lösung verletzt Restriktionen, Schranken oder Ganzzahligkeit. | `--von solver` versuchen. |
|
||||||
| Datenherkunft | Das Programm hat Eingabedaten erzeugt, ohne sie auszuweisen. | Daten beilegen (`--daten`) oder fehlende Werte in den Text schreiben. |
|
| Datenherkunft | Das Programm hat Eingabedaten erzeugt, ohne sie auszuweisen. | Daten beilegen (`--daten`) oder fehlende Werte in den Text schreiben. |
|
||||||
| Szenario ohne Ergebnis | Eine verglichene Konfiguration hat keinen Wert, ist aber nicht als unlösbar belegt (z. B. Zeitlimit). | `--von solver`. |
|
| Szenario ohne Ergebnis | Eine verglichene Konfiguration hat keinen Wert, ist aber nicht als unlösbar belegt (z. B. Zeitlimit). | `--von solver`. |
|
||||||
|
| Solver-Fehler | Das erzeugte Programm ist auch nach allen Reparaturversuchen gescheitert (Absturz, Zeitlimit, kein Ergebnis). Die Fehlermeldung steht im Abschnitt „Ergebnis“. | `--von solver`; bei Zeitlimit `SOLVER_TIMEOUT` erhöhen. |
|
||||||
| Unlösbar, Gegenprobe uneindeutig | Der Solver meldet „unlösbar“, eine unabhängige zweite Modellierung konnte das weder bestätigen noch widerlegen. | Aufgabe auf widersprüchliche Bedingungen prüfen. |
|
| Unlösbar, Gegenprobe uneindeutig | Der Solver meldet „unlösbar“, eine unabhängige zweite Modellierung konnte das weder bestätigen noch widerlegen. | Aufgabe auf widersprüchliche Bedingungen prüfen. |
|
||||||
|
|
||||||
### Besondere Ergebnisse
|
### Besondere Ergebnisse
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,11 @@ def _assess_run(result: PipelineResult) -> None:
|
||||||
"""Abnahme eines Solver-Laufs: Befunde, die das Ergebnis unzuverlässig machen, in `result` übernehmen."""
|
"""Abnahme eines Solver-Laufs: Befunde, die das Ergebnis unzuverlässig machen, in `result` übernehmen."""
|
||||||
run = result.solver_run
|
run = result.solver_run
|
||||||
log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else ""))
|
log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else ""))
|
||||||
|
if run.status == SolverStatus.ERROR:
|
||||||
|
# Auch nach allen Reparaturen kein Ergebnis – bisher blieb reliable=True und die CLI endete mit 0
|
||||||
|
result.reliable = False
|
||||||
|
result.reliability_issues.append(f"Solver-Fehler: {solver_error_summary(run)}")
|
||||||
|
log("ERGEBNIS UNZUVERLÄSSIG – das Solver-Programm ist auch nach allen Reparaturversuchen gescheitert.")
|
||||||
if run.feasibility:
|
if run.feasibility:
|
||||||
result.reliable = False
|
result.reliable = False
|
||||||
result.reliability_issues += [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility]
|
result.reliability_issues += [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility]
|
||||||
|
|
@ -233,6 +238,12 @@ def _assess_run(result: PipelineResult) -> None:
|
||||||
log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).")
|
log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).")
|
||||||
|
|
||||||
|
|
||||||
|
def solver_error_summary(run) -> str:
|
||||||
|
"""Erste aussagekräftige Zeile des Fehlers – der volle Text steht im Report unter „Ergebnis“."""
|
||||||
|
zeilen = [z.strip() for z in (run.error or "").splitlines() if z.strip()]
|
||||||
|
return (zeilen[-1] if zeilen else "das Programm meldete selbst Status 'error'")[:200]
|
||||||
|
|
||||||
|
|
||||||
def _log_usage(session, result: PipelineResult) -> None:
|
def _log_usage(session, result: PipelineResult) -> None:
|
||||||
result.usage = session.usage
|
result.usage = session.usage
|
||||||
cost = f", Kosten ${session.usage.cost_usd:.4f}" if session.usage.cost_usd is not None else ""
|
cost = f", Kosten ${session.usage.cost_usd:.4f}" if session.usage.cost_usd is not None else ""
|
||||||
|
|
@ -394,8 +405,11 @@ def resolve_result(result_json: str, data_file: str | None = None, solver_timeou
|
||||||
run.warnings += check_multistart(run, result.specification.objective.sense.value)
|
run.warnings += check_multistart(run, result.specification.objective.sense.value)
|
||||||
|
|
||||||
result.solver_run = run
|
result.solver_run = run
|
||||||
# Frühere Befunde (Zielgröße, Abbildung, Datenherkunft) gelten weiter – nur die Zulässigkeit ist neu geprüft
|
# Frühere Befunde (Zielgröße, Abbildung, Datenherkunft) gelten weiter – nur Lauf und Zulässigkeit sind neu geprüft
|
||||||
result.reliability_issues = ([p for p in result.reliability_issues if not p.startswith("Zulässigkeitsprüfung: ")]
|
result.reliability_issues = ([p for p in result.reliability_issues
|
||||||
|
if not p.startswith(("Zulässigkeitsprüfung: ", "Solver-Fehler: "))]
|
||||||
|
+ ([f"Solver-Fehler: {solver_error_summary(run)}"]
|
||||||
|
if run.status == SolverStatus.ERROR else [])
|
||||||
+ [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility])
|
+ [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility])
|
||||||
result.reliable = not result.reliability_issues
|
result.reliable = not result.reliability_issues
|
||||||
result.usage = None # keine LLM-Nutzung in diesem Lauf
|
result.usage = None # keine LLM-Nutzung in diesem Lauf
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ def observed_behaviors(result) -> set:
|
||||||
gezeigt = set()
|
gezeigt = set()
|
||||||
if result.rejected:
|
if result.rejected:
|
||||||
return {"rejected"}
|
return {"rejected"}
|
||||||
if not result.reliable:
|
# Ein Absturz ist kein Eingeständnis: nur Befunde außer „Solver-Fehler“ zählen als ehrlich unzuverlässig
|
||||||
|
if not result.reliable and any(not i.startswith("Solver-Fehler: ") for i in result.reliability_issues):
|
||||||
gezeigt.add("unreliable")
|
gezeigt.add("unreliable")
|
||||||
if result.solver_code is not None and result.solver_code.data_assumptions:
|
if result.solver_code is not None and result.solver_code.data_assumptions:
|
||||||
gezeigt.add("assumed_data")
|
gezeigt.add("assumed_data")
|
||||||
|
|
|
||||||
|
|
@ -503,12 +503,22 @@ REQUESTED_METHOD = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# „ohne Dijkstra“, „nicht mit dem Simplex“, „statt Branch-and-Bound“: Erwähnung, keine Forderung. Nur im selben
|
||||||
|
# Satz und kurz davor – „Lösen Sie mit Dijkstra. Ohne Rechenweg gibt es keine Punkte.“ bleibt eine Forderung.
|
||||||
|
NEGATION_BEFORE = re.compile(r"\b(?:ohne|nicht|keine?[nmrs]?|statt|anstelle|anstatt|außer|ausser|anders als)\b[^.!?;\n]{0,30}$")
|
||||||
|
|
||||||
|
|
||||||
|
def _requested(name: str, text: str) -> bool:
|
||||||
|
"""Kommt das Verfahren mindestens einmal ohne vorangehende Verneinung vor?"""
|
||||||
|
return any(not NEGATION_BEFORE.search(text[:m.start()]) for m in re.finditer(re.escape(name), text))
|
||||||
|
|
||||||
|
|
||||||
def check_requested_method(spec: OptimizationModelSpecification, task: str) -> List[CheckIssue]:
|
def check_requested_method(spec: OptimizationModelSpecification, task: str) -> List[CheckIssue]:
|
||||||
"""Ein im Aufgabentext ausdrücklich verlangtes Verfahren muss auch verwendet werden."""
|
"""Ein im Aufgabentext ausdrücklich verlangtes Verfahren muss auch verwendet werden."""
|
||||||
text = (task or "").lower()
|
text = (task or "").lower()
|
||||||
issues = []
|
issues = []
|
||||||
for name, stacks in REQUESTED_METHOD.items():
|
for name, stacks in REQUESTED_METHOD.items():
|
||||||
if name in text and spec.target_stack not in stacks:
|
if _requested(name, text) and spec.target_stack not in stacks:
|
||||||
issues.append(CheckIssue(
|
issues.append(CheckIssue(
|
||||||
severity=CheckSeverity.ERROR, code="REQUESTED_METHOD_IGNORED",
|
severity=CheckSeverity.ERROR, code="REQUESTED_METHOD_IGNORED",
|
||||||
message=f"Die Aufgabe verlangt ausdrücklich '{name}', das Modell nutzt aber "
|
message=f"Die Aufgabe verlangt ausdrücklich '{name}', das Modell nutzt aber "
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
pythonpath = [".", "tests"]
|
pythonpath = [".", "tests"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
extend-exclude = ["output", "benchmark/results"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Fehlerklassen, die tatsächlich Fehler sind: unbenutzte/undefinierte Namen (F), Syntaxfehler (E9).
|
||||||
|
# Stilregeln bewusst nicht – der Code folgt eigenen Konventionen (deutsche Namen, lange Zeilen).
|
||||||
|
select = ["F", "E9"]
|
||||||
|
|
|
||||||
|
|
@ -128,20 +128,24 @@ def split_math(text: str) -> list:
|
||||||
return _join_text(parts)
|
return _join_text(parts)
|
||||||
|
|
||||||
|
|
||||||
def _graph_section(run) -> list:
|
def _graph_section(run, sense: str = "minimize") -> list:
|
||||||
"""Zeichnung des Graphen mit hervorgehobenem Weg, darüber die Bedeutung von Knoten und Bogen."""
|
"""Zeichnung des Graphen mit hervorgehobenem Weg, darüber die Bedeutung von Knoten und Bogen.
|
||||||
|
|
||||||
|
Bei maximize (längster Weg, z. B. kritischer Pfad) heißt der Weg nicht „kürzester“.
|
||||||
|
"""
|
||||||
g = run.graph
|
g = run.graph
|
||||||
if g is None:
|
if g is None:
|
||||||
return []
|
return []
|
||||||
|
weg = "längste" if sense == "maximize" else "kürzeste"
|
||||||
doc = [("p", f"Knoten des Graphen: {g.node_meaning} Ein Bogen: {g.arc_meaning}")]
|
doc = [("p", f"Knoten des Graphen: {g.node_meaning} Ein Bogen: {g.arc_meaning}")]
|
||||||
zeichnung = charts.graph_chart([n for n in g.nodes], [(a.source, a.target, a.weight) for a in g.arcs],
|
zeichnung = charts.graph_chart([n for n in g.nodes], [(a.source, a.target, a.weight) for a in g.arcs],
|
||||||
path=list(g.path), title="Graph mit kürzestem Weg")
|
path=list(g.path), title=f"Graph mit {weg}m Weg")
|
||||||
if zeichnung:
|
if zeichnung:
|
||||||
doc.append(("svg", zeichnung))
|
doc.append(("svg", zeichnung))
|
||||||
if g.path and run.objective_value is not None:
|
if g.path and run.objective_value is not None:
|
||||||
auf_weg = set(zip(g.path, g.path[1:]))
|
auf_weg = set(zip(g.path, g.path[1:]))
|
||||||
summanden = " + ".join(_fmt_number(a.weight) for a in g.arcs if (a.source, a.target) in auf_weg)
|
summanden = " + ".join(_fmt_number(a.weight) for a in g.arcs if (a.source, a.target) in auf_weg)
|
||||||
doc.append(("p", "Hervorgehoben ist der kürzeste Weg " + " → ".join(g.path)
|
doc.append(("p", f"Hervorgehoben ist der {weg} Weg " + " → ".join(g.path)
|
||||||
+ f" mit der Länge {summanden} = {_fmt_number(run.objective_value)}."))
|
+ f" mit der Länge {summanden} = {_fmt_number(run.objective_value)}."))
|
||||||
if g.steps:
|
if g.steps:
|
||||||
doc += [("h4", "Markierungsverlauf des Dijkstra-Algorithmus"),
|
doc += [("h4", "Markierungsverlauf des Dijkstra-Algorithmus"),
|
||||||
|
|
@ -182,7 +186,7 @@ def _result_section(result) -> list:
|
||||||
f"{len(run.variables)} Variablen wurden aus der Lösung nachgerechnet und eingehalten."))
|
f"{len(run.variables)} Variablen wurden aus der Lösung nachgerechnet und eingehalten."))
|
||||||
# Die Erklärung („Wie das Modell die Aufgabe sieht“) steht als eigener Abschnitt vor dem Modell, also schon
|
# Die Erklärung („Wie das Modell die Aufgabe sieht“) steht als eigener Abschnitt vor dem Modell, also schon
|
||||||
# vor diesem Ergebnis – die Fachbegriffe der Tabellen sind damit eingeführt, bevor sie auftauchen.
|
# vor diesem Ergebnis – die Fachbegriffe der Tabellen sind damit eingeführt, bevor sie auftauchen.
|
||||||
doc += _graph_section(run)
|
doc += _graph_section(run, spec.objective.sense.value)
|
||||||
for table in run.tables:
|
for table in run.tables:
|
||||||
doc += [("h3", table.title),
|
doc += [("h3", table.title),
|
||||||
("table", table.columns, [[_fmt_cell(c) for c in row] for row in table.rows])]
|
("table", table.columns, [[_fmt_cell(c) for c in row] for row in table.rows])]
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,4 @@ pandas>=2.2
|
||||||
markdown-it-py>=3.0 # Aufgabentext im HTML-Report (bisher nur indirekt über rich)
|
markdown-it-py>=3.0 # Aufgabentext im HTML-Report (bisher nur indirekt über rich)
|
||||||
numpy
|
numpy
|
||||||
pytest
|
pytest
|
||||||
|
ruff # tests/test_lint.py: unbenutzte und undefinierte Namen
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ def report(status, objective_value, solution, tables=None, headline=None, constr
|
||||||
print(" Ein Knoten ist: " + str(graph.get("node_meaning", "")))
|
print(" Ein Knoten ist: " + str(graph.get("node_meaning", "")))
|
||||||
print(" Ein Bogen ist: " + str(graph.get("arc_meaning", "")))
|
print(" Ein Bogen ist: " + str(graph.get("arc_meaning", "")))
|
||||||
if graph.get("path"):
|
if graph.get("path"):
|
||||||
print(" Kürzester Weg: " + " → ".join(str(k) for k in graph["path"]))
|
print(" Gefundener Weg: " + " → ".join(str(k) for k in graph["path"])) # kürzester oder längster
|
||||||
for schritt in graph.get("steps") or []:
|
for schritt in graph.get("steps") or []:
|
||||||
print(f" Marke endgültig: {{schritt.get('node')}} = {{schritt.get('label')}}")
|
print(f" Marke endgültig: {{schritt.get('node')}} = {{schritt.get('label')}}")
|
||||||
if solution:
|
if solution:
|
||||||
|
|
@ -644,7 +644,7 @@ def solver_prompt() -> str:
|
||||||
def generate_solver_code(
|
def generate_solver_code(
|
||||||
session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
previous: Optional[SolverCode] = None, previous_error: Optional[str] = None, understanding=None,
|
previous: Optional[SolverCode] = None, previous_error: Optional[str] = None, understanding=None,
|
||||||
data_prompt: str = "",
|
data_prompt: str = "", earlier_errors=(),
|
||||||
) -> SolverCode:
|
) -> SolverCode:
|
||||||
content = f"AUFGABENTEXT:\n{task}\n\nMODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}"
|
content = f"AUFGABENTEXT:\n{task}\n\nMODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}"
|
||||||
if data_prompt:
|
if data_prompt:
|
||||||
|
|
@ -655,9 +655,13 @@ def generate_solver_code(
|
||||||
content += ("\n\nZU BEANTWORTENDE TEILFRAGEN (für jede genau ein Eintrag in answers, "
|
content += ("\n\nZU BEANTWORTENDE TEILFRAGEN (für jede genau ein Eintrag in answers, "
|
||||||
f"question_id wie angegeben):\n{fragen}")
|
f"question_id wie angegeben):\n{fragen}")
|
||||||
if previous is not None:
|
if previous is not None:
|
||||||
|
# Ältere Fehlversuche nur als Fehlertext (nicht der ganze Code): sonst behebt die Reparatur den letzten Fehler
|
||||||
|
# und führt den vorletzten wieder ein – die Versuche gehen im Kreis.
|
||||||
|
frueher = "".join(f"\n- Versuch {n}: {fehler[:400]}" for n, fehler in enumerate(earlier_errors, 1))
|
||||||
content += (
|
content += (
|
||||||
f"\n\nDER VORHERIGE CODE IST FEHLGESCHLAGEN.\nCODE:\n{previous.code}\n\nFEHLER:\n{previous_error}\n\n"
|
f"\n\nDER VORHERIGE CODE IST FEHLGESCHLAGEN.\nCODE:\n{previous.code}\n\nFEHLER:\n{previous_error}\n\n"
|
||||||
"Behebe den Fehler. Das Modell selbst darf dabei nicht verändert werden."
|
+ (f"FRÜHERE FEHLVERSUCHE (schon einmal aufgetreten – nicht wieder einführen):{frueher}\n\n" if frueher else "")
|
||||||
|
+ "Behebe den Fehler. Das Modell selbst darf dabei nicht verändert werden."
|
||||||
)
|
)
|
||||||
return session.create(
|
return session.create(
|
||||||
SolverCode,
|
SolverCode,
|
||||||
|
|
@ -1176,6 +1180,7 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
zerlegung = check_objective_decomposition(run, spec)
|
zerlegung = check_objective_decomposition(run, spec)
|
||||||
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
|
||||||
attempt = 1
|
attempt = 1
|
||||||
|
fehlerverlauf = [] # Fehler aller bisherigen Versuche, für die Reparatur
|
||||||
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
|
||||||
or einheiten or zerlegung or verschwiegen) and attempt <= retries:
|
or einheiten or zerlegung or verschwiegen) and attempt <= retries:
|
||||||
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
|
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
|
||||||
|
|
@ -1190,7 +1195,9 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
|
||||||
if befunde)
|
if befunde)
|
||||||
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
|
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
|
||||||
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,
|
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,
|
||||||
understanding=understanding, data_prompt=data_prompt)
|
understanding=understanding, data_prompt=data_prompt,
|
||||||
|
earlier_errors=fehlerverlauf)
|
||||||
|
fehlerverlauf.append(fehler)
|
||||||
solver_code.code = clean_generated_code(solver_code.code)
|
solver_code.code = clean_generated_code(solver_code.code)
|
||||||
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
|
||||||
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files)
|
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files)
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,14 @@ def test_line_breaks_of_the_task_are_kept():
|
||||||
from reporting.report import task_html
|
from reporting.report import task_html
|
||||||
seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5")
|
seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5")
|
||||||
assert seite.count("<br") == 2
|
assert seite.count("<br") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_longest_path_is_not_called_shortest():
|
||||||
|
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"], "path": ["0", "1", "3"], "node_meaning": "-", "arc_meaning": "-",
|
||||||
|
"arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}]}))
|
||||||
|
assert "kürzeste" in str(_graph_section(run)) and "längste" not in str(_graph_section(run))
|
||||||
|
lang = str(_graph_section(run, "maximize"))
|
||||||
|
assert "Hervorgehoben ist der längste Weg" in lang and "kürzest" not in lang
|
||||||
|
|
|
||||||
19
tests/test_lint.py
Normal file
19
tests/test_lint.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
"""Statische Prüfung mit ruff: unbenutzte Imports und undefinierte Namen fallen im Test auf, nicht erst im Review.
|
||||||
|
|
||||||
|
Anlass: bei der Fehlerdurchsicht am 18.09.2026 fand ruff 8 tote Imports und einen undefinierten Namen in einem
|
||||||
|
Testhelfer; nichts davon hatte ein Test bemerkt. Regeln in pyproject.toml ([tool.ruff.lint]).
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def test_ruff_finds_nothing():
|
||||||
|
pytest.importorskip("ruff")
|
||||||
|
lauf = subprocess.run([sys.executable, "-m", "ruff", "check", "--no-cache", "--output-format", "concise", str(ROOT)],
|
||||||
|
capture_output=True, text=True, cwd=ROOT)
|
||||||
|
assert lauf.returncode == 0, lauf.stdout + lauf.stderr
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from checks.model_checks import check_spec, has_errors
|
from checks.model_checks import check_spec, has_errors
|
||||||
from core.model_schema import OptimizationModelSpecification
|
from core.model_schema import OptimizationModelSpecification
|
||||||
|
|
||||||
|
|
@ -495,3 +497,18 @@ def test_heading_markers_win_over_numbered_lists():
|
||||||
"### Teil 1: Formulierung\nKlassifizieren Sie das Problem.\n\n### Teil 2: Lösung\nLösen Sie das Modell.")
|
"### Teil 1: Formulierung\nKlassifizieren Sie das Problem.\n\n### Teil 2: Lösung\nLösen Sie das Modell.")
|
||||||
assert question_ids_in(text) == {"1", "2"}
|
assert question_ids_in(text) == {"1", "2"}
|
||||||
assert question_ids_in("1. Berechnen Sie x.\n2. Deuten Sie y.\n3. Vergleichen Sie.") == {"1", "2", "3"}
|
assert question_ids_in("1. Berechnen Sie x.\n2. Deuten Sie y.\n3. Vergleichen Sie.") == {"1", "2", "3"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text, verlangt", [
|
||||||
|
("Lösen Sie das Problem ohne den Dijkstra-Algorithmus, als Flussmodell.", False),
|
||||||
|
("Anders als beim Dijkstra-Verfahren sind hier negative Kosten möglich – nicht mit Dijkstra rechnen.", False),
|
||||||
|
("Verwenden Sie statt Dijkstra ein lineares Programm.", False),
|
||||||
|
("b) Lösen Sie es mit dem Dijkstra-Algorithmus!", True),
|
||||||
|
("Lösen Sie mit Dijkstra. Ohne Rechenweg gibt es keine Punkte.", True), # Verneinung im nächsten Satz
|
||||||
|
("Nicht jede Kante ist befahrbar. Bestimmen Sie den Weg mit Dijkstra.", True),
|
||||||
|
])
|
||||||
|
def test_negated_method_mention_is_not_a_request(text, verlangt):
|
||||||
|
spec = make_spec(problem_class="Mixed-Integer Linear Programming (MILP)", target_stack="HiGHS",
|
||||||
|
variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
|
||||||
|
gemeldet = "REQUESTED_METHOD_IGNORED" in {i.code for i in check_spec(spec, text)}
|
||||||
|
assert gemeldet is verlangt
|
||||||
|
|
|
||||||
|
|
@ -820,3 +820,36 @@ def test_missing_value_without_declared_assumption_is_repaired_or_unreliable():
|
||||||
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3)
|
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3)
|
||||||
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||||
assert not result.reliable and any(i.startswith("Datenherkunft") for i in result.reliability_issues)
|
assert not result.reliable and any(i.startswith("Datenherkunft") for i in result.reliability_issues)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Solver-Fehler macht das Ergebnis unzuverlässig ---
|
||||||
|
|
||||||
|
KAPUTT = "def main():\n raise RuntimeError('Modell nicht lösbar gebaut')\n\n\nif __name__ == '__main__':\n main()\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_solver_error_after_all_repairs_is_unreliable():
|
||||||
|
# bisher blieb reliable=True: CLI-Exit 0, Report ohne Warnkasten
|
||||||
|
session = FakeSession([GOOD], [OK], [SolverCode(code=KAPUTT)] * 3)
|
||||||
|
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||||
|
assert result.solver_run.status == SolverStatus.ERROR
|
||||||
|
assert not result.reliable
|
||||||
|
assert any(i.startswith("Solver-Fehler: ") and "Modell nicht lösbar gebaut" in i for i in result.reliability_issues)
|
||||||
|
|
||||||
|
|
||||||
|
def test_crash_is_not_an_honest_failure():
|
||||||
|
from benchmark.benchmark import observed_behaviors
|
||||||
|
session = FakeSession([GOOD], [OK], [SolverCode(code=KAPUTT)] * 3)
|
||||||
|
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
|
||||||
|
assert observed_behaviors(result) == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_repair_knows_the_first_error():
|
||||||
|
# Die Reparatur sah nur den letzten Fehler – und konnte den vorletzten wieder einführen
|
||||||
|
from solving.solver_stage import solve
|
||||||
|
erster = "def main():\n raise RuntimeError('Fehler A: Index')\n\n\nif __name__ == '__main__':\n main()\n"
|
||||||
|
zweiter = "def main():\n raise RuntimeError('Fehler B: Typ')\n\n\nif __name__ == '__main__':\n main()\n"
|
||||||
|
session = FakeSession([], [], [SolverCode(code=erster), SolverCode(code=zweiter), SolverCode(code=STRUCTURED_CODE)])
|
||||||
|
solve(session, "Aufgabe", GOOD, retries=2)
|
||||||
|
reparaturen = [inhalt for stufe, inhalt in session.contents if stufe == "Solver-Reparatur"]
|
||||||
|
assert "FRÜHERE FEHLVERSUCHE" not in reparaturen[0] # beim ersten gibt es noch keinen Verlauf
|
||||||
|
assert "Fehler B: Typ" in reparaturen[1] and "Versuch 1: " in reparaturen[1] and "Fehler A: Index" in reparaturen[1]
|
||||||
|
|
|
||||||
|
|
@ -109,3 +109,13 @@ def test_resolve_keeps_earlier_reliability_issues(tmp_path):
|
||||||
neu = resolve_result(str(pfad))
|
neu = resolve_result(str(pfad))
|
||||||
assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt
|
assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt
|
||||||
assert not neu.reliable
|
assert not neu.reliable
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_with_a_failing_program_is_unreliable(tmp_path):
|
||||||
|
import json
|
||||||
|
from TextToModel import resolve_result
|
||||||
|
daten = tmp_path / "daten.json"
|
||||||
|
daten.write_text(json.dumps({"kosten": "keine Zahl"}), encoding="utf-8")
|
||||||
|
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)), str(daten))
|
||||||
|
assert neu.solver_run.status == SolverStatus.ERROR
|
||||||
|
assert not neu.reliable and neu.reliability_issues[0].startswith("Solver-Fehler: ")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue