# Modellbaum-Vergleich (5.0 ↔ 8.0) Werkzeug für die [Testdaten-Pipeline](TESTDATEN.md): Bevor Testdaten von Hand ergänzt werden, erstmal statisch (kein laufendes Tryton nötig) Schwarz auf Weiß sehen, welche Modelle/Felder es in einer Version überhaupt gibt - damit weder Felder aus 8.0 übernommen werden, die es in 5.0 noch gar nicht gibt (würde beim Anlegen nur Fehler werfen), noch Aufwand in Modelle gesteckt wird, die sich zwischen den Versionen gar nicht verändert haben. ## Workflow ### 1. `extract_model_tree.py` – Modellbaum aus dem Quellcode ziehen Scannt den Python-Code ausgewählter Module per AST (kein Import, keine laufende Instanz) und baut pro Modell (`__name__`) die Liste aller per `fields.X(...)` definierten Felder, mit Typ, Relationsziel (Many2One/One2Many/Many2Many/One2One/Reference) und - falls literal im Code erkennbar - `required`. Modelle werden über PoolMeta-Erweiterungen hinweg zusammengeführt: Fügt z. B. `stock_split` eine Klasse mit `__name__ = 'stock.move'` hinzu, landen deren Felder unter demselben Modell-Eintrag, mit Vermerk, aus welchem Modul sie stammen. Pro Modell wird zusätzlich `persisted` erkannt (erbt die Klasse `ModelSQL` o. ä., oder nur `ModelView` - dann ist es ein reines Wizard-/ Transient-Formular ohne eigene Tabelle). Verfolgt zusätzlich ganz normale **Python-Vererbung innerhalb derselben Datei**: erbt eine Klasse mit eigenem `__name__` von einer anderen lokalen Klasse - z. B. `class BOMOutput(BOMInput): __name__ = 'production.bom.output'` in `production/bom.py`, die selbst keine einzige `fields.X(...)`-Zeile deklariert, sondern alle Felder von `BOMInput` übernimmt - werden die geerbten Felder mit aufgenommen (eigene Felder überschreiben geerbte bei Namensgleichheit). Ohne das sähe ein rein über Vererbung wiederverwendetes Modell wie eines OHNE Felder aus, und Änderungen daran (z. B. eine Feldumbenennung im Elternteil) blieben komplett unsichtbar - genau das hat `sql_dump_diff.py` (Schritt 4) einmal live aufgedeckt. Die Basisklassen-Auflösung ist bewusst auf "gleiche Datei" beschränkt (nicht "gleiches Modul"): derselbe lokale Klassenname wie `Cron` taucht innerhalb eines Moduls oft mehrfach für völlig unterschiedliche Klassen auf (Tryton-Konvention: eine PoolMeta-Erweiterung heißt oft genauso wie das Original, z. B. `currency/currency.py` definiert `class Cron(ModelSQL, ModelView): __name__ = 'currency.cron'`, während `currency/ir.py` unabhängig davon eine andere Klasse `class Cron(metaclass=PoolMeta):` zur Erweiterung von `ir.cron` definiert) - ein modulweites Registry würde sich da gegenseitig überschreiben. ```bash python3 extract_model_tree.py --repo /mnt/tryton \ --modules company country currency party product ... \ --output reports/model_tree_5.0.json --report reports/model_tree_5.0.md ``` Für eine zweite Version denselben Befehl gegen einen zweiten Checkout laufen lassen (z. B. per `git worktree add /tmp/tryton-8.0 8.0`, ohne den Haupt-Checkout umzuschalten). ### 2. `model_diff.py` – zwei Modellbäume vergleichen Sortiert jedes Modell in eine von fünf Kategorien: - **entfernt** – Modell gibt es in Version A, nicht mehr in B - **geändert** – Feld(er) entfernt oder Typ/Relation/required geändert → echter Kandidat für einen Vergleichslauf - **neu** – Modell gibt es erst in B - **nur ergänzt** – nur neue Felder dazugekommen, Rest unverändert - **unverändert** – komplett identisch geblieben ```bash python3 model_diff.py --tree-a reports/model_tree_5.0.json --label-a 5.0 \ --tree-b reports/model_tree_8.0.json --label-b 8.0 \ --output reports/model_diff.json --report reports/model_diff.md ``` ### 3. `model_coverage.py` – gegen die aktuelle Testdaten-Abdeckung abgleichen Vergleicht einen Modellbaum mit einer gemergten `dataset.yaml` (`reports/_testdata_merged.yaml`, siehe [TESTDATEN.md](TESTDATEN.md)) und zeigt komplett unabgedeckte Modelle sowie pro teilweise abgedecktem Modell die ungenutzten Felder (required zuerst). Drei Kategorien werden aus der Liste unabgedeckter Modelle per Default herausgefiltert (nie von Hand testdatenrelevant, jeweils per Flag wieder einschaltbar): - `ir.*`/`res.*`-Kernmodelle (`--include-framework`) - Wizard-/Transient-Formulare, `persisted: false` (`--include-transient`) - automatisch verwaltete Many2Many-Verknüpfungstabellen, erkennbar am Bindestrich im Namen wie `party.party-party.category` (`--include-relations`) Mit `--diff reports/model_diff.json` wird zusätzlich pro Modell die Diff-Kategorie aus Schritt 2 eingeblendet und die Liste danach sortiert: `geändert`/`entfernt` zuerst (lohnt sich, dort Testdaten zu ergänzen), `nur ergänzt`/`unverändert` zuletzt (dort ist über den reinen Codevergleich hinaus erstmal nichts Auffälliges zu erwarten). ```bash python3 model_coverage.py --model-tree reports/model_tree_5.0.json \ --dataset reports/5_testdata_merged.yaml --diff reports/model_diff.json \ --report reports/coverage_5.0.md ``` ### 4. `sql_dump_diff.py` – gegen den echten DB-Zustand validieren Alles bisherige ist reine Code-Analyse. Zum Cross-Check: zwei mit `pg_dump` (Klartext-Format) erzeugte Dumps frisch initialisierter (leerer) Datenbanken beider Versionen strukturell vergleichen - Tabellen entfernt/neu, Spalten hinzugefügt/entfernt/Typ geändert, Zeilenzahl (z. B. mehr XML-Fixture-Daten in der neueren Version). Zeilen werden bereits vollständig geparst (für eine spätere inhaltliche Vergleichs-Erweiterung, sobald es Snapshots mit echten Testdaten gibt), aktuell nutzt der Report davon aber nur die Zeilenzahl. Mit `--model-diff reports/model_diff.json` wird pro betroffener Tabelle zusätzlich geprüft, ob die rein statische Code-Analyse (Schritt 2) dieselbe Änderung schon vorhergesagt hatte (Modellname -> Tabellenname: Punkte durch Unterstriche ersetzen, wie `extract_updates.py`/ `compare_snapshots.py` es auch tun) - Übereinstimmung bzw. Abweichung wird markiert. ```bash python3 sql_dump_diff.py --dump-a data/leer5.sql --label-a 5.0 \ --dump-b data/leer8.sql --label-b 8.0 \ --model-diff reports/model_diff.json \ --output reports/dump_diff_5.0_8.0.json --report reports/dump_diff_5.0_8.0.md ``` **Stand:** 78 von 92 vergleichbaren Fällen stimmen überein (~85%). Die verbleibenden Abweichungen gehen meist in eine erwartbare Richtung: die Code-Analyse findet MEHR Veränderung, als im leeren DB-Schema sichtbar ist (z. B. ein entferntes Function-Feld ohne eigene Spalte, oder Selection-Wertänderungen ohne Typänderung) - kein Fehler, sondern zwei Werkzeuge, die unterschiedliche Ebenen messen (Python-Feld-Definition vs. physisches SQL-Schema). Der `production.bom.output`-Fund oben (Schritt 1) kam genau aus einer vorherigen Abweichung dieser Art zustande. ## Bekannte Grenzen - Rein statische Analyse: Felder, die nicht als einfaches `name = fields.X(...)` auf Klassenebene stehen (z. B. per Schleife/Helper dynamisch erzeugt), werden nicht erkannt. - Python-Vererbung wird nur INNERHALB derselben Datei aufgelöst (s. o.). Erbt eine Klasse von einer Basisklasse aus einer anderen Datei desselben oder eines anderen Moduls (Import statt lokaler Definition), werden deren Felder nicht gefunden - unauffällig, aber ein bekannter blinder Fleck. - `persisted`-Erkennung ist eine Textsuche über die Basisklassen-Ausdrücke (`ModelSQL`/`Workflow`/`DeactivableMixin`) - deckt den Normalfall ab, ist aber kein echtes Type-Checking. - Erkennt nur Unterschiede auf Ebene der Feld-**Definition** im Code (Typ/Relation/required). Laufzeitverhalten wie Constraints, Defaults oder DB-Spaltenpräzision kann trotzdem abweichen, ohne hier aufzufallen - dafür bleibt der eigentliche Testlauf über `create_dataset.py`/ `compare_snapshots.py` nötig.