Files
tryton-db-migration/model_diff.py
T
uha4 c0dd24b6c5 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
2026-08-22 10:52:13 +02:00

139 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
model_diff.py
Vergleicht zwei mit extract_model_tree.py erzeugte Modellbäume (z. B.
model_tree_5.0.json gegen model_tree_8.0.json) auf Modell- und Feldebene
und sortiert jedes Modell in eine von vier Kategorien:
- entfernt: Modell gibt es in der ersten Version, aber nicht mehr
in der zweiten.
- neu: Modell gibt es erst in der zweiten Version.
- geändert: Modell existiert in beiden, aber mind. ein Feld wurde
entfernt oder hat sich (Typ/Relation/required)
geändert. Das sind die Kandidaten, bei denen sich ein
echter Testlauf gegen beide Versionen lohnt.
- unverändert / nur ergänzt: Modell ist identisch geblieben oder hat nur
zusätzliche Felder bekommen - hier ist über den reinen
Code-Vergleich hinaus erstmal nichts Auffälliges zu
erwarten (Laufzeitverhalten wie Constraints/Defaults
kann natürlich trotzdem abweichen, das sieht man mit
dieser rein statischen Analyse nicht).
Nutzung:
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
"""
import argparse
import json
from pathlib import Path
def diff_field(fa, fb):
changes = []
if fa["type"] != fb["type"]:
changes.append(f"type: {fa['type']} -> {fb['type']}")
if fa["relation"] != fb["relation"]:
changes.append(f"relation: {fa['relation']} -> {fb['relation']}")
if fa["required"] != fb["required"]:
changes.append(f"required: {fa['required']} -> {fb['required']}")
return changes
def diff_trees(tree_a, tree_b):
result = {}
for model_name in sorted(set(tree_a) | set(tree_b)):
if model_name not in tree_b:
result[model_name] = {"category": "entfernt", "added": [], "removed": [], "changed": {}}
continue
if model_name not in tree_a:
result[model_name] = {"category": "neu", "added": [], "removed": [], "changed": {}}
continue
fields_a = tree_a[model_name]["fields"]
fields_b = tree_b[model_name]["fields"]
added = sorted(set(fields_b) - set(fields_a))
removed = sorted(set(fields_a) - set(fields_b))
changed = {}
for name in sorted(set(fields_a) & set(fields_b)):
c = diff_field(fields_a[name], fields_b[name])
if c:
changed[name] = c
if removed or changed:
category = "geändert"
elif added:
category = "nur ergänzt"
else:
category = "unverändert"
result[model_name] = {
"category": category,
"added": added,
"removed": removed,
"changed": changed,
}
return result
def write_report(diff, out_path, label_a, label_b):
by_category = {}
for model_name, info in diff.items():
by_category.setdefault(info["category"], []).append(model_name)
lines = [f"# Modellbaum-Diff: {label_a} -> {label_b}\n"]
for category, count_note in [
("entfernt", "Modell existiert nicht mehr"),
("geändert", "Feld(er) entfernt oder geändert - Kandidat für einen echten Vergleichslauf"),
("neu", f"Modell gibt es erst ab {label_b}"),
("nur ergänzt", "nur neue Felder dazugekommen, Rest unverändert"),
("unverändert", "komplett identisch geblieben"),
]:
models = sorted(by_category.get(category, []))
lines.append(f"\n## {category} ({len(models)}) - {count_note}\n")
for model_name in models:
info = diff[model_name]
if category == "geändert":
lines.append(f"\n### `{model_name}`")
if info["removed"]:
lines.append(f"- entfernte Felder: {', '.join(info['removed'])}")
for field_name, changes in info["changed"].items():
lines.append(f"- **{field_name}**: {'; '.join(changes)}")
if info["added"]:
lines.append(f"- (zusätzlich neue Felder: {', '.join(info['added'])})")
else:
lines.append(f"- `{model_name}`")
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--tree-a", required=True)
parser.add_argument("--tree-b", required=True)
parser.add_argument("--label-a", default="A")
parser.add_argument("--label-b", default="B")
parser.add_argument("--output", default="reports/model_diff.json")
parser.add_argument("--report", default="reports/model_diff.md")
args = parser.parse_args()
tree_a = json.loads(Path(args.tree_a).read_text(encoding="utf-8"))
tree_b = json.loads(Path(args.tree_b).read_text(encoding="utf-8"))
diff = diff_trees(tree_a, tree_b)
Path(args.output).write_text(json.dumps(diff, indent=2, ensure_ascii=False, sort_keys=True), encoding="utf-8")
write_report(diff, Path(args.report), args.label_a, args.label_b)
counts = {}
for info in diff.values():
counts[info["category"]] = counts.get(info["category"], 0) + 1
summary = ", ".join(f"{v} {k}" for k, v in counts.items())
print(f"{summary} -> {args.output}, {args.report}")
if __name__ == "__main__":
main()