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:
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
model_coverage.py
|
||||
|
||||
Vergleicht den per extract_model_tree.py ermittelten Modellbaum (was gibt
|
||||
es in der 5.0-Codebasis wirklich an Modellen/Feldern) mit dem aktuellen
|
||||
Stand einer aus *.rst-Szenarien gemergten dataset.yaml (was wird davon
|
||||
schon durch Testdaten abgedeckt).
|
||||
|
||||
Zeigt:
|
||||
- Modelle, die es in 5.0 gibt, aber für die noch KEIN Record in der
|
||||
dataset.yaml existiert (komplett unabgedeckt).
|
||||
- Pro abgedecktem Modell: setzbare Felder (keine reinen Function-Felder
|
||||
ohne setter), die von keinem Record verwendet werden - required
|
||||
zuerst, da dort eher etwas fehlt, das für's Anlegen nötig wäre.
|
||||
|
||||
Für die Liste der komplett unabgedeckten Modelle werden per Default drei
|
||||
Kategorien ausgeblendet, die man ohnehin nie händisch mit Testdaten
|
||||
befüllt (jeweils per Flag reaktivierbar):
|
||||
- ir.*/res.*-Kernmodelle (--include-framework)
|
||||
- Wizard-/Transient-Formulare, die nur ModelView statt ModelSQL erben,
|
||||
laut extract_model_tree.py also "persisted": false (--include-transient)
|
||||
- automatisch verwaltete Many2Many-Verknüpfungstabellen, erkennbar am
|
||||
Bindestrich im Modellnamen wie "party.party-party.category"
|
||||
(--include-relations)
|
||||
|
||||
Mit --diff (Ausgabe von model_diff.py, z. B. model_diff.json aus einem
|
||||
5.0->8.0-Vergleich) wird zusätzlich pro Modell vermerkt, ob es sich
|
||||
zwischen den Versionen überhaupt geändert hat, und danach sortiert:
|
||||
"geändert"/"entfernt" zuerst (echte Kandidaten für einen Vergleichslauf),
|
||||
"nur ergänzt"/"unverändert" zuletzt (dort ist über den Codevergleich
|
||||
hinaus erstmal nichts Auffälliges zu erwarten).
|
||||
|
||||
Nutzung:
|
||||
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
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
def load_records(path):
|
||||
text = Path(path).read_text(encoding="utf-8")
|
||||
if yaml is not None:
|
||||
return yaml.safe_load(text)["records"]
|
||||
return json.loads(text)["records"]
|
||||
|
||||
|
||||
def used_fields_by_model(records):
|
||||
used = {}
|
||||
for r in records:
|
||||
used.setdefault(r["model"], set()).update(r["fields"].keys())
|
||||
return used
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--model-tree", required=True, help="JSON von extract_model_tree.py")
|
||||
parser.add_argument("--dataset", required=True, help="Gemergte dataset.yaml/json")
|
||||
parser.add_argument("--report", required=True, help="Ziel-Markdown-Datei")
|
||||
parser.add_argument(
|
||||
"--include-framework", action="store_true",
|
||||
help="ir.*/res.*-Kernmodelle in der Liste unabgedeckter Modelle NICHT herausfiltern",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-transient", action="store_true",
|
||||
help="Wizard-/Transient-Formulare (persisted=false) NICHT herausfiltern",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-relations", action="store_true",
|
||||
help="Many2Many-Verknüpfungstabellen (Bindestrich im Namen) NICHT herausfiltern",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diff", default=None,
|
||||
help="Optional: model_diff.json - blendet die Diff-Kategorie ein und "
|
||||
"sortiert 'geändert'/'entfernt' nach oben",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
tree = json.loads(Path(args.model_tree).read_text(encoding="utf-8"))
|
||||
records = load_records(args.dataset)
|
||||
used = used_fields_by_model(records)
|
||||
|
||||
diff = json.loads(Path(args.diff).read_text(encoding="utf-8")) if args.diff else {}
|
||||
PRIORITY = {"geändert": 0, "entfernt": 1, "neu": 2, "nur ergänzt": 3, "unverändert": 4}
|
||||
|
||||
def priority(model_name):
|
||||
if not diff:
|
||||
return 0 # ohne --diff: keine Umsortierung, alphabetische Reihenfolge bleibt
|
||||
category = diff.get(model_name, {}).get("category")
|
||||
return PRIORITY.get(category, 5)
|
||||
|
||||
def diff_note(model_name):
|
||||
if not diff:
|
||||
return ""
|
||||
category = diff.get(model_name, {}).get("category", "nicht in beiden Bäumen")
|
||||
return f" _[{category}]_"
|
||||
|
||||
uncovered_models = []
|
||||
partially_covered = []
|
||||
skipped = {"framework": 0, "transient": 0, "relation": 0}
|
||||
|
||||
for model_name in sorted(tree):
|
||||
entry = tree[model_name]
|
||||
fields = entry["fields"]
|
||||
settable = {name for name, info in fields.items() if info["settable"]}
|
||||
if not settable:
|
||||
continue # z.B. reine Log-/History-Modelle ohne setzbare Felder
|
||||
|
||||
if model_name not in used:
|
||||
if not args.include_framework and (
|
||||
model_name.startswith("ir.") or model_name.startswith("res.")
|
||||
):
|
||||
skipped["framework"] += 1
|
||||
continue
|
||||
if not args.include_transient and not entry["persisted"]:
|
||||
skipped["transient"] += 1
|
||||
continue
|
||||
if not args.include_relations and "-" in model_name:
|
||||
skipped["relation"] += 1
|
||||
continue
|
||||
uncovered_models.append(model_name)
|
||||
continue
|
||||
|
||||
missing = settable - used[model_name]
|
||||
if missing:
|
||||
missing_required = sorted(f for f in missing if fields[f]["required"] is True)
|
||||
missing_optional = sorted(f for f in missing if fields[f]["required"] is not True)
|
||||
partially_covered.append((model_name, missing_required, missing_optional))
|
||||
|
||||
uncovered_models.sort(key=priority)
|
||||
partially_covered.sort(key=lambda item: priority(item[0]))
|
||||
|
||||
lines = ["# Testdaten-Abdeckung gegen den 5.0-Modellbaum\n"]
|
||||
|
||||
lines.append(f"\n## Komplett unabgedeckte Modelle ({len(uncovered_models)})\n")
|
||||
lines.append("Für diese Modelle gibt es noch gar keinen Record in der dataset.yaml:\n")
|
||||
for m in uncovered_models:
|
||||
lines.append(f"- `{m}`{diff_note(m)}")
|
||||
|
||||
lines.append(f"\n\n## Teilweise abgedeckte Modelle ({len(partially_covered)})\n")
|
||||
lines.append("Modell hat mind. einen Record, aber nicht alle setzbaren Felder werden benutzt:\n")
|
||||
for model_name, missing_required, missing_optional in partially_covered:
|
||||
lines.append(f"\n### `{model_name}`{diff_note(model_name)}")
|
||||
if missing_required:
|
||||
lines.append(f"- **required, aber ungenutzt:** {', '.join(missing_required)}")
|
||||
if missing_optional:
|
||||
lines.append(f"- optional, ungenutzt: {', '.join(missing_optional)}")
|
||||
|
||||
Path(args.report).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
skip_parts = []
|
||||
if skipped["framework"]:
|
||||
skip_parts.append(f"{skipped['framework']} ir.*/res.*")
|
||||
if skipped["transient"]:
|
||||
skip_parts.append(f"{skipped['transient']} Wizard/Transient")
|
||||
if skipped["relation"]:
|
||||
skip_parts.append(f"{skipped['relation']} Verknüpfungstabellen")
|
||||
note = f" ({', '.join(skip_parts)} ausgeblendet)" if skip_parts else ""
|
||||
|
||||
print(
|
||||
f"{len(uncovered_models)} Modelle komplett unabgedeckt{note}, "
|
||||
f"{len(partially_covered)} teilweise abgedeckt -> {args.report}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user