Proteus-Zweig: dataset.yaml-Pipeline und 5.0/8.0-Modellbaumvergleich

- create_dataset.py, compare_snapshots.py, rst_to_dataset.py: Proteus-
  Testdatensatz anlegen/vergleichen bzw. aus *.rst-Szenarien extrahieren
- create_dataset.py: Feld-Alias-Fallback (FIELD_RENAMES) für bekannte
  Umbenennungen wie stock.move.uom -> unit, bidirektional
- merge_dedup_rsts.py: Extrahierte YAMLs pro Version zusammenführen,
  versionsinterne Duplikate entfernen, abhängigkeitskorrekt sortieren
- extract_model_tree.py: Modellbaum (Modelle/Felder) statisch aus dem
  Tryton-Quellcode ziehen, für 5.0 und 8.0
- model_diff.py, model_coverage.py: 5.0<->8.0-Modelländerungen finden und
  gegen die aktuelle Testdatenabdeckung abgleichen, priorisiert nach
  tatsächlicher Änderung
- reports/*_merged.yaml: Extraktionsergebnisse für die aktiven
  Module (rohe *.rst-Quellen bewusst nicht mitversioniert)
- Ordner doc ergänzt
This commit is contained in:
2026-08-22 10:52:13 +02:00
parent b549063ff6
commit c0dd24b6c5
38 changed files with 26136 additions and 20 deletions
+96
View File
@@ -0,0 +1,96 @@
# 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).
```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/<version>_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
```
## 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.
- `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.
+105
View File
@@ -0,0 +1,105 @@
# Proteus-Testdaten-Pipeline
Ergänzender Ansatz zur [Codeanalyse](UPDATEANALYSE.md): Statt den
`__register__`-Code zu lesen, wird ein deklarativ definierter Testdatensatz
über Proteus in zwei frisch installierten Tryton-Instanzen (5.0 und 8.0)
tatsächlich angelegt. Ziel ist, dieselbe Datei gegen beide Versionen laufen
zu lassen, damit sowohl die Skript-Ausgabe (`compare_snapshots.py`) als
auch anschließend die SQL-Dumps beider Datenbanken vergleichbar sind -
dafür müssen inhaltlich identische Daten in derselben Reihenfolge
eingespielt werden.
## Workflow
```
tests/scenario_*.rst (Tryton-Repo, pro Modul)
│ rst_to_dataset.py
▼
rsts/<version>e/*.yaml (ein YAML-Record pro angelegtem Proteus-Objekt)
│ rsts_merge_dedup.py
▼
reports/<version>_testdata_merged.yaml (dedupliziert, abhängigkeitskorrekt sortiert)
│ create_dataset.py --database ...
▼
snapshot_<version>.json
│ compare_snapshots.py snapshot_a.json snapshot_b.json
▼
Fund-Liste: status_changed / value_changed / missing_record
```
### 1. `rst_to_dataset.py` – Testdaten aus den Modul-eigenen Tests extrahieren
Tryton-Module bringen eigene `tests/scenario_*.rst`-Doctests mit (ausführbarer
Proteus-Code in `>>>`-Zeilen). Das Skript parst diese Dateien per AST und
erkennt:
```
>>> Party = Model.get('party.party')
>>> party = Party(name='Testfirma GmbH')
>>> party.vat_number = '123'
>>> party.save()
```
→ wird zu einem YAML-Record mit `key`, `model`, `fields`. Referenzen auf
zuvor im selben File angelegte Records werden zu `{"$ref": "..."}`,
`Decimal(...)`/`date(...)` zu `{"$decimal": ...}`/`{"$date": ...}`. Alles
nicht automatisch Auflösbare (Wizards, `.click(...)`, Helper-Funktionsaufrufe)
wird als `{"$manual": "<Ausdruck>"}` markiert oder ganz übersprungen -
bewusst sichtbar unvollständig statt still falsch.
Record-Keys werden mit dem Dateinamen präfixiert, damit beim
Zusammenführen mehrerer Module keine Kollisionen entstehen.
### 2. `rsts_merge_dedup.py` – pro Version zusammenführen
Liest alle `rsts/<version>e/*.yaml` eines Verzeichnisses ein und entfernt
Records mit identischem `model`+`fields` (z. B. dasselbe
Basis-`product.template`, das in vielen Szenarien wiederverwendet wird).
`$ref`-Verweise auf entfernte Duplikate werden auf den behaltenen Record
umgeschrieben. Danach wird per Kahn-Algorithmus so sortiert, dass
`$ref`-Abhängigkeiten immer VOR dem referenzierenden Record stehen, aber
innerhalb der Freiheitsgrade weiterhin nach `model` gruppiert wird.
**Achtung:** Dedupliziert bewusst GLOBAL, auch innerhalb derselben Datei.
Bei Szenarien, die zwei absichtlich gleich aussehende Records anlegen (z. B.
`scenario_party_replace.rst`: zwei Parteien namens "Pam", um eine zu
ersetzen), verändert das den Sinn des Szenarios - für einen groben
Testdatensatz ist das in Ordnung, für szenario-treue Tests nicht.
### 3. `create_dataset.py` – gegen eine echte Instanz anlegen
Legt die Records der Reihe nach per Proteus an, liest jeden Wert danach
FRISCH VOM SERVER zurück (nicht aus dem lokalen Proteus-Cache), damit
automatische Reformatierung sichtbar wird. Ergebnis: ein JSON-Snapshot
(`status`/`error`/`stored` pro Record).
**Feld-Umbenennungen zwischen Versionen** (`FIELD_RENAMES`): Da dieselbe
YAML gegen mehrere Versionen laufen soll, aber Felder umbenannt werden
können (bestätigt: `stock.move.uom` → `stock.move.unit` zwischen 5.0 und
8.0), prüft das Skript pro Modell zuerst per `fields_get()`, ob der
YAML-Feldname auf dem jeweiligen Server existiert, und weicht sonst auf
einen bekannten, bidirektionalen Alias aus. Ein wirklich unbekanntes Feld
bleibt trotzdem ein Fehler - das ist ja gerade das gesuchte Signal für eine
echte Schema-Änderung. Weitere Umbenennungen bei Bedarf in `FIELD_RENAMES`
ergänzen, sobald ein Lauf sie aufdeckt.
### 4. `compare_snapshots.py` – zwei Versionen gegenüberstellen
Vergleicht zwei Snapshots (gleiche YAML, zwei Versionen/Datenbanken) und
markiert `status_changed` (ein Record schlägt nur in einer Version fehl -
Kandidat für Format-/Constraint-/Pflichtfeld-Änderung), `value_changed`
(gespeicherter Wert weicht trotz gleichem Input ab - automatische
Reformatierung) und `missing_record`.
## Stand
- `rst_to_dataset.py` und `rsts_merge_dedup.py` sind gegen echte
`scenario_*.rst`-Dateien aus `/mnt/tryton` (5.0- und 8.0-Branch) getestet,
siehe `reports/5_testdata_merged.yaml` / `reports/8_testdata_merged.yaml`
(jeweils NUR aus den Szenarien der jeweiligen Version extrahiert, noch
NICHT zu einer gemeinsamen Datei zusammengeführt).
- `create_dataset.py` und `compare_snapshots.py` sind noch nicht gegen eine
echte Tryton-Instanz gelaufen (nur syntaktisch geprüft).
- Welche Modelle/Felder überhaupt Testdaten brauchen bzw. wo sich 5.0 und
8.0 tatsächlich unterscheiden, siehe [MODELLVERGLEICH.md](MODELLVERGLEICH.md)
- das steuert, wo als Nächstes von Hand YAML-Records ergänzt werden.
+78
View File
@@ -0,0 +1,78 @@
# Tryton DB-Migrationsanalyse (AST-Zweig)
Analysiert per statischem Code-Scan, welche Datenbank-Operationen die
`__register__`-Methoden aller Tryton-Module über mehrere Versionen hinweg
ausführen – ohne die Module tatsächlich zu installieren/upzudaten.
## Workflow
```bash
./run.sh
```
Checkt der Reihe nach jeden Versions-Branch (`5.0` bis `8.0`) im
Tryton-Repo aus, lässt `extract_updates.py` pro Version über alle konfigurierten
Module laufen (Ergebnis: `data/<version>/<modul>.json`), und ruft danach
`compare_updates.py` auf, das daraus die aggregierten Reports in `reports/updateChanges/*.md`
erzeugt (ein Report pro Modul, gegliedert nach Tabelle und Version).
Einzeln aufrufbar:
```bash
python3 extract_updates.py --repo <pfad-zum-tryton-clone> --output data
python3 compare_updates.py
```
## Was `extract_updates.py` erkennt
- Erkannte DB-Operationen (`column_rename`, `add_column`, `update`,
`insert`, `execute`, ... – siehe `INTERESTING_OPS`) inkl. Tabelle(n),
Zeile, Bedingung (`if`-Kontext), Phase (`before_super`/`after_super`).
- Steigt in `for`/`while`/`with`/`try` ab (nicht nur `if`).
- Löst Aufrufe an Helper-Methoden derselben Klasse auf
(`cls._migrate_x(...)`) und markiert die gefundenen Operationen mit
`"via": [{"method": ..., "line": ...}]`.
- Loggt alle sonstigen, nicht erkannten Aufrufe zusätzlich als
`"unresolved": true` – Rohmaterial für die manuelle Sichtung, gefiltert
um bekanntes Rauschen (`Pool()`, `Transaction()`, `cursor`, `.append`,
`pool.get`, `logger.*`, `__table__`/`__table_handler__`).
## Was `extract_updates.py` NICHT erkennt (bewusste Grenzen)
- **Cross-Datei-Aufrufe**: Ruft `__register__` eine Funktion aus einem
anderen Modul/Utility-Modul auf (z. B. `migrate_property(...)` aus
`trytond.model.modelstorage`), wird das nicht verfolgt – landet als
`unresolved`, der eigentliche DB-Zugriff bleibt unsichtbar.
- **XML-Datendateien** (`ir.model.data`) – bewusst ausgeklammert, da
Konfigurationsdaten ohnehin manuell geprüft werden müssen.
- **`.sql`-Dateien** in Modulen – werden nicht gescannt.
- **Implizite Schema-Änderungen** durch reine Feldtyp-Änderungen im
Python-Code (ohne expliziten `add_column`/`alter_size`-Aufruf).
## `compare_updates.py` – Aggregation & Dedup
Fasst pro Tabelle die Operationen aller Versionen zusammen und zeigt eine
Operation nur dann erneut an, wenn sie sich vom **tatsächlichen,
vollständigen Bestand der unmittelbar vorherigen Version** unterscheidet
(nicht: irgendwann schon mal gesehen). Das behandelt korrekt sowohl den
Normalfall (Migrationscode bleibt über viele Versionen unverändert – wird
nur einmal angezeigt) als auch den Fall, dass ein Block zwischenzeitlich
durch etwas anderes ersetzt und später identisch wieder eingeführt wird
(taucht dann korrekt erneut auf).
Zeigt keine "wurde entfernt"-Markierung – nur Neues/Verändertes.
## Konfiguration
- Modul-/Repo-Defaults: oben in `extract_updates.py` (`DEFAULT_REPO`,
`DEFAULT_MODULES`).
- Versions-Range: `START`/`END` in `run.sh`.
- Rauschfilter für `unresolved`: `IGNORED_UNRESOLVED_*` in `extract_updates.py` –
bei Bedarf erweitern, wenn sich beim Sichten neue, eindeutig irrelevante
Muster zeigen (siehe Git-Historie für die bisherige Herleitung).
## Verwandter Zweig
Ergänzender, unabhängiger Ansatz: Proteus-basierter Vergleich (echte
Testdaten in zwei frisch installierten Versionen anlegen, Fehler/
Reformatierung erkennen) – siehe [doc/TESTDATEN.md](TESTDATEN.md) und
[doc/MODELLVERGLEICH.md](MODELLVERGLEICH.md).