77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""check_roundtrip.py - is every field of the live state in the saved file?
|
|
|
|
python check_roundtrip.py saved.json state_fields.json [--runtime selection,_cache]
|
|
|
|
saved.json the file the app wrote (any JSON; a top-level "state" key is unwrapped)
|
|
state_fields.json JSON array of field paths of the LIVE state, e.g.
|
|
["objects[].id", "objects[].position[]", "settings.grid", "selection"]
|
|
(how to produce it: js.md / python.md, section "Completeness")
|
|
--runtime comma-separated paths that are intentionally not saved
|
|
|
|
Exit 1 if a state field is missing from the file (and not in --runtime), with the list.
|
|
Also lists file fields that the state does not have (stale keys from old versions).
|
|
"""
|
|
import json, sys
|
|
|
|
|
|
def paths(obj, prefix=""):
|
|
if isinstance(obj, dict):
|
|
if not obj:
|
|
yield prefix
|
|
for k, v in obj.items():
|
|
yield from paths(v, f"{prefix}.{k}" if prefix else k)
|
|
elif isinstance(obj, list):
|
|
if not obj:
|
|
yield prefix # empty array: can't know the element shape
|
|
for v in obj:
|
|
yield from paths(v, prefix + "[]")
|
|
else:
|
|
yield prefix
|
|
|
|
|
|
def normalize(p):
|
|
"""'objects[].children[].id' and 'objects[].id' describe the same record type when
|
|
children hold the same records; compare on the path with repeated '[]' segments kept."""
|
|
return p
|
|
|
|
|
|
def main(argv):
|
|
runtime = set()
|
|
if "--runtime" in argv:
|
|
i = argv.index("--runtime"); runtime = set(argv[i + 1].split(",")); del argv[i:i + 2]
|
|
if len(argv) != 2:
|
|
print(__doc__); return 2
|
|
saved = json.load(open(argv[0], encoding="utf-8"))
|
|
if isinstance(saved, dict) and "state" in saved and isinstance(saved["state"], dict):
|
|
saved = saved["state"]
|
|
want = set(json.load(open(argv[1], encoding="utf-8")))
|
|
have = set(paths(saved))
|
|
# a field counts as present if the file has it at that path or any ancestor is an empty container
|
|
def present(p):
|
|
if p in have:
|
|
return True
|
|
parts = p.replace("[]", ".[]").split(".")
|
|
for n in range(len(parts) - 1, 0, -1):
|
|
anc = ".".join(parts[:n]).replace(".[]", "[]")
|
|
if anc in have and any(h == anc for h in have): # ancestor exists as a leaf = empty list/dict
|
|
return True
|
|
return False
|
|
missing = sorted(p for p in want if not present(p) and p not in runtime
|
|
and not any(p == r or p.startswith(r + ".") or p.startswith(r + "[]") for r in runtime))
|
|
extra = sorted(h for h in have if h not in want)
|
|
if missing:
|
|
print(f"MISSING in the file ({len(missing)}): fields the live state has but the save does not")
|
|
for p in missing:
|
|
print(" -", p)
|
|
if extra:
|
|
print(f"only in the file ({len(extra)}): not in the live state (old keys? wrong source?)")
|
|
for p in extra:
|
|
print(" -", p)
|
|
print("ok - every state field is in the file" if not missing else f"{len(missing)} field(s) lost on save")
|
|
return 1 if missing else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|