Notebook-Tests: nbmake + Subprocess-Zellen + inkrementeller Test

Alle 25 Notebooks sind nun fehlerfrei testbar (25 passed). Dafür drei
Änderungen im Build und eine neue Test-Infrastruktur:

1. pyproject.toml: nbmake>=1.5 in der dev-Gruppe.

2. build_version_04.py: Notebooks bekommen zwei Setup-Zellen und
   Subprocess-Zellen für Programme, die nicht direkt im Kernel laufen:
   - NOTEBOOK_SETUP_2: __file__ definieren, sys.path für or_kern,
     multiprocessing auf fork (Spawn findet Kernel-Funktionen nicht).
   - _braucht_subprocess: erkennt highspy (Solver-Konflikt), pytest
     (SystemExit), get_context('spawn'), subprocess+__file__ (sucht
     .py-Dateien), sys.exit (SystemExit). Diese Programme erscheinen
     als Markdown-Codeblock (sichtbar, nicht ausführbar) plus einer
     Subprocess-Zelle mit NO_COLOR=1 (verhindert ANSI-Codes, die der
     Parser von Mutationstest.py nicht verarbeitet).
   - Solver-Konflikt-Erkennung: wenn ein Kapitel sowohl ortools als
     auch highspy importiert, laufen auch reine ortools-Programme als
     Subprocess (highspy schon im Kernel).

3. teste_code_04.py: inkrementeller Test — nur geänderte/neue Programme
   und Notebooks (via git status). Netzabhängige Dateien (yfinance)
   werden automatisch erkannt und nur getestet, wenn Internet verfügbar.

4. PLAN.md Abschnitt 10: neuer Verifikationsschritt 4 (pytest --nbmake).
   PROGRESS.md und CLAUDE.md aktualisiert.

Gefundene und behobene Probleme:
- or_kern-Import: sys.path im Notebook erweitert.
- __file__ nicht definiert: zweite Setup-Zelle setzt es.
- multiprocessing spawn: fork erzwungen (Ein_System_Vier_Ansaetze.py).
- ortools/highspy-Konflikt: Subprocess für beide Solver.
- pytest SystemExit: Subprocess für test_or_kern.py und Mutationstest.py.
- sys.exit(0): Subprocess für Installationstest.py.
- ANSI-Codes in pytest-Ausgabe: NO_COLOR=1 in Subprocess-Zelle.
This commit is contained in:
dschlueter 2026-09-11 22:38:59 +02:00
commit a4fc247116
70 changed files with 4073 additions and 305 deletions

View file

@ -12,7 +12,11 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": [
"nbmake-skip"
]
},
"outputs": [],
"source": [
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
@ -21,6 +25,27 @@
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Notebook-spezifisches Setup: __file__ definieren (Programme nutzen es\n",
"# fuer OUTPUT_DIR), Programmverzeichnis in den Suchpfad aufnehmen (or_kern)\n",
"# und multiprocessing auf 'fork' stellen (Spawn findet Kernel-Funktionen nicht).\n",
"import os, sys\n",
"__file__ = os.path.join(os.getcwd(), '_notebook.py')\n",
"for _p in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" if os.path.isfile(os.path.join(_p, 'or_kern.py')):\n",
" sys.path.insert(0, _p); break\n",
"import multiprocessing as _mp\n",
"try: _mp.set_start_method('fork', force=True)\n",
"except (RuntimeError, ValueError): pass"
]
},
{
"cell_type": "markdown",
"metadata": {},
@ -31,11 +56,14 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"## Falle 1 — Infeasibility{idx:Infeasibility}\n",
"\n",
"`Infeasibility_Diagnose.py`\n",
"\n",
"```python\n",
"#!/usr/bin/env python3\n",
"\n",
"# Infeasibility_Diagnose.py\n",
@ -147,7 +175,36 @@
" print(\"\\nDer Anwender bekommt jetzt einen Plan PLUS eine konkrete Ursache -\")\n",
" print(\"und kann handeln: Vertretung von aussen holen, Stunde verlegen,\")\n",
" print(\"oder die Klasse zusammenlegen.\")\n",
" print(\"=\" * 78)"
" print(\"=\" * 78)\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# In einem Notebook kann 'Infeasibility_Diagnose.py' nicht direkt ausgefuehrt werden\n",
"# (ortools/highspy-Konflikt im selben Kernel, pytest oder multiprocessing).\n",
"# Deshalb als Subprocess — so laeuft es genauso wie im Terminal.\n",
"import subprocess, sys, os\n",
"# NO_COLOR/TERM=dumb verhindern ANSI-Codes in der Ausgabe (pytest\n",
"# im Jupyter-Kernel wuerde sonst farbige Escape-Sequenzen ausgeben).\n",
"_env = dict(os.environ, NO_COLOR='1', TERM='dumb')\n",
"for _d in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" _p = os.path.join(_d, 'Infeasibility_Diagnose.py')\n",
" if os.path.isfile(_p):\n",
" _r = subprocess.run([sys.executable, _p], cwd=_d, env=_env,\n",
" capture_output=True, text=True)\n",
" if _r.stdout: print(_r.stdout)\n",
" if _r.stderr: print(_r.stderr)\n",
" _r.check_returncode()\n",
" break\n",
"else:\n",
" print('Infeasibility_Diagnose.py nicht gefunden')"
]
},
{
@ -160,11 +217,14 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"## Falle 2 — Der Black-Box-Effekt\n",
"\n",
"`Erklaerbarkeit.py`\n",
"\n",
"```python\n",
"#!/usr/bin/env python3\n",
"\n",
"# Erklaerbarkeit.py\n",
@ -249,7 +309,45 @@
" print(f\" -> Die Obergrenze blockiert zwar {blockiert}, kostet aber gerade NICHTS:\")\n",
" print(\" Selbst ohne sie waere die Loesung dieselbe.\")\n",
" print(\" Blockierend und kostenrelevant sind zwei verschiedene Dinge.\")\n",
" print(\"=\" * 78)"
" print(\"=\" * 78)\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# In einem Notebook kann 'Erklaerbarkeit.py' nicht direkt ausgefuehrt werden\n",
"# (ortools/highspy-Konflikt im selben Kernel, pytest oder multiprocessing).\n",
"# Deshalb als Subprocess — so laeuft es genauso wie im Terminal.\n",
"import subprocess, sys, os\n",
"# NO_COLOR/TERM=dumb verhindern ANSI-Codes in der Ausgabe (pytest\n",
"# im Jupyter-Kernel wuerde sonst farbige Escape-Sequenzen ausgeben).\n",
"_env = dict(os.environ, NO_COLOR='1', TERM='dumb')\n",
"for _d in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" _p = os.path.join(_d, 'Erklaerbarkeit.py')\n",
" if os.path.isfile(_p):\n",
" _r = subprocess.run([sys.executable, _p], cwd=_d, env=_env,\n",
" capture_output=True, text=True)\n",
" if _r.stdout: print(_r.stdout)\n",
" if _r.stderr: print(_r.stderr)\n",
" _r.check_returncode()\n",
" break\n",
"else:\n",
" print('Erklaerbarkeit.py nicht gefunden')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Constraint Attribution{idx:Constraint Attribution}: welche Bedingung kostet wie viel?\n",
"\n",
"`Constraint_Attribution.py`\n"
]
},
{
@ -584,11 +682,14 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"## Ein Enum für fünf Bibliotheken\n",
"\n",
"`or_kern.py`\n",
"\n",
"```python\n",
"#!/usr/bin/env python3\n",
"\n",
"# or_kern.py\n",
@ -1092,7 +1193,36 @@
" print(\"\\nAlle drei scheitern beim EINLESEN - nicht erst beim Loesen und\")\n",
" print(\"schon gar nicht erst im Bericht. Das ist der ganze Zweck der\")\n",
" print(\"Domaenenschicht.\")\n",
" print(\"=\" * 78)"
" print(\"=\" * 78)\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# In einem Notebook kann 'or_kern.py' nicht direkt ausgefuehrt werden\n",
"# (ortools/highspy-Konflikt im selben Kernel, pytest oder multiprocessing).\n",
"# Deshalb als Subprocess — so laeuft es genauso wie im Terminal.\n",
"import subprocess, sys, os\n",
"# NO_COLOR/TERM=dumb verhindern ANSI-Codes in der Ausgabe (pytest\n",
"# im Jupyter-Kernel wuerde sonst farbige Escape-Sequenzen ausgeben).\n",
"_env = dict(os.environ, NO_COLOR='1', TERM='dumb')\n",
"for _d in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" _p = os.path.join(_d, 'or_kern.py')\n",
" if os.path.isfile(_p):\n",
" _r = subprocess.run([sys.executable, _p], cwd=_d, env=_env,\n",
" capture_output=True, text=True)\n",
" if _r.stdout: print(_r.stdout)\n",
" if _r.stderr: print(_r.stderr)\n",
" _r.check_returncode()\n",
" break\n",
"else:\n",
" print('or_kern.py nicht gefunden')"
]
},
{
@ -1105,11 +1235,14 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"## Der Solverwechsel in der Praxis\n",
"\n",
"`Solverwechsel_CPSAT_HiGHS.py`\n",
"\n",
"```python\n",
"#!/usr/bin/env python3\n",
"\n",
"# Solverwechsel_CPSAT_HiGHS.py\n",
@ -1433,7 +1566,36 @@
" print(\"gleich teure Loesungen, darf jeder Solver eine andere davon liefern.\")\n",
" print(\"Hier stimmen sie zufaellig ueberein - darauf zu testen waere trotzdem\")\n",
" print(\"ein unzuverlaessiger Test (siehe JobShop_Intervalle.py).\")\n",
" print(\"=\" * 82)"
" print(\"=\" * 82)\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# In einem Notebook kann 'Solverwechsel_CPSAT_HiGHS.py' nicht direkt ausgefuehrt werden\n",
"# (ortools/highspy-Konflikt im selben Kernel, pytest oder multiprocessing).\n",
"# Deshalb als Subprocess — so laeuft es genauso wie im Terminal.\n",
"import subprocess, sys, os\n",
"# NO_COLOR/TERM=dumb verhindern ANSI-Codes in der Ausgabe (pytest\n",
"# im Jupyter-Kernel wuerde sonst farbige Escape-Sequenzen ausgeben).\n",
"_env = dict(os.environ, NO_COLOR='1', TERM='dumb')\n",
"for _d in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" _p = os.path.join(_d, 'Solverwechsel_CPSAT_HiGHS.py')\n",
" if os.path.isfile(_p):\n",
" _r = subprocess.run([sys.executable, _p], cwd=_d, env=_env,\n",
" capture_output=True, text=True)\n",
" if _r.stdout: print(_r.stdout)\n",
" if _r.stderr: print(_r.stderr)\n",
" _r.check_returncode()\n",
" break\n",
"else:\n",
" print('Solverwechsel_CPSAT_HiGHS.py nicht gefunden')"
]
},
{
@ -1446,11 +1608,14 @@
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"## Finde den Denkfehler\n",
"\n",
"`Betriebsueberwachung.py`\n",
"\n",
"```python\n",
"#!/usr/bin/env python3\n",
"\n",
"# Betriebsueberwachung.py\n",
@ -1630,7 +1795,36 @@
" print()\n",
" print(\"Die dritte ist die frueheste Warnung: Sie steigt, lange bevor der Gap\")\n",
" print(\"sichtbar wird, und gibt Zeit zum Handeln.\")\n",
" print(\"=\" * 84)"
" print(\"=\" * 84)\n",
"```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# In einem Notebook kann 'Betriebsueberwachung.py' nicht direkt ausgefuehrt werden\n",
"# (ortools/highspy-Konflikt im selben Kernel, pytest oder multiprocessing).\n",
"# Deshalb als Subprocess — so laeuft es genauso wie im Terminal.\n",
"import subprocess, sys, os\n",
"# NO_COLOR/TERM=dumb verhindern ANSI-Codes in der Ausgabe (pytest\n",
"# im Jupyter-Kernel wuerde sonst farbige Escape-Sequenzen ausgeben).\n",
"_env = dict(os.environ, NO_COLOR='1', TERM='dumb')\n",
"for _d in [os.path.join(os.path.dirname(os.getcwd()),\n",
" 'Operations_Research_mit_Python_Version_04_Programme'),\n",
" os.path.dirname(os.getcwd()), os.getcwd()]:\n",
" _p = os.path.join(_d, 'Betriebsueberwachung.py')\n",
" if os.path.isfile(_p):\n",
" _r = subprocess.run([sys.executable, _p], cwd=_d, env=_env,\n",
" capture_output=True, text=True)\n",
" if _r.stdout: print(_r.stdout)\n",
" if _r.stderr: print(_r.stderr)\n",
" _r.check_returncode()\n",
" break\n",
"else:\n",
" print('Betriebsueberwachung.py nicht gefunden')"
]
}
],