- 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
177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
#!/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()
|