snapshot.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. """Tiny golden-snapshot helper for case-replay tests (V2-M0C).
  2. Zero third-party deps (no syrupy). A snapshot stores an approved JSON projection
  3. of replay output; later runs compare against it and fail on drift. Regenerate
  4. intentionally with UPDATE_SNAPSHOTS=1.
  5. from tests.snapshot import assert_matches
  6. assert_matches("real_id45/decision_summary", summary, subset_keys=["rejected_content_count"])
  7. Golden files live under tests/fixtures/snapshots/{name}.json.
  8. """
  9. from __future__ import annotations
  10. import json
  11. import os
  12. from pathlib import Path
  13. from typing import Any
  14. SNAPSHOT_DIR = Path("tests/fixtures/snapshots")
  15. def _updating() -> bool:
  16. return os.environ.get("UPDATE_SNAPSHOTS", "") not in {"", "0", "false", "False"}
  17. def _canonical(obj: Any) -> str:
  18. return json.dumps(obj, ensure_ascii=False, indent=2, sort_keys=True)
  19. def _project(actual: Any, subset_keys: list[str] | None) -> Any:
  20. """Keep only the given dot-path keys (avoids snapshotting volatile fields)."""
  21. if subset_keys is None:
  22. return actual
  23. out: dict[str, Any] = {}
  24. for dotted in subset_keys:
  25. cur: Any = actual
  26. for part in dotted.split("."):
  27. cur = cur.get(part) if isinstance(cur, dict) else None
  28. out[dotted] = cur
  29. return out
  30. def _first_diff(expected: str, actual: str) -> str:
  31. exp_lines = expected.splitlines()
  32. act_lines = actual.splitlines()
  33. for i in range(max(len(exp_lines), len(act_lines))):
  34. e = exp_lines[i] if i < len(exp_lines) else "<missing>"
  35. a = act_lines[i] if i < len(act_lines) else "<missing>"
  36. if e != a:
  37. return f"line {i + 1}:\n expected: {e}\n actual: {a}"
  38. return "<no line diff>"
  39. def assert_matches(name: str, actual: Any, *, subset_keys: list[str] | None = None) -> None:
  40. golden = SNAPSHOT_DIR / f"{name}.json"
  41. projected = _canonical(_project(actual, subset_keys))
  42. if not golden.exists():
  43. if _updating():
  44. golden.parent.mkdir(parents=True, exist_ok=True)
  45. golden.write_text(projected + "\n", encoding="utf-8")
  46. return
  47. raise AssertionError(
  48. f"snapshot missing: {golden}. Run with UPDATE_SNAPSHOTS=1 to create it."
  49. )
  50. expected = golden.read_text(encoding="utf-8").rstrip("\n")
  51. if projected == expected:
  52. return
  53. if _updating():
  54. golden.write_text(projected + "\n", encoding="utf-8")
  55. return
  56. raise AssertionError(
  57. f"snapshot mismatch for {name}:\n{_first_diff(expected, projected)}\n"
  58. f"(run UPDATE_SNAPSHOTS=1 to accept the new output)"
  59. )