Files
tryton-db-migration/model_coverage.py
T

177 lines
7.0 KiB
Python
Raw Normal View History

#!/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()