"""Shared helper utilities for the PMP reference node test suite. These helpers deliberately test *behaviour* rather than incidental representation details, so that an interoperating implementation of the protocol could reuse most of this suite. Two conventions are encoded here: * ``blob()`` renders any structure into a deterministic string so tests can make substring assertions about content without coupling to exact field names inside evidence payloads. * ``signalled_failure()`` accepts both idiomatic ways an implementation may signal a verification failure: raising a ``PMPError`` subclass, or returning a falsy result. The protocol mandates that tampering MUST be detected; it does not mandate the Python idiom used to report it. """ from __future__ import annotations import json from pathlib import Path from typing import Any, Callable from pmp.errors import PMPError def blob(obj: Any) -> str: """Render any structure to a deterministic string for substring checks.""" return json.dumps(obj, sort_keys=True, default=str, ensure_ascii=False) def evidence_ops(node) -> list: """All operations in a node's log whose type marks them as evidence.""" return [op for op in node.log if "evidence" in op.op_type] def signalled_failure(fn: Callable[[], Any]) -> bool: """Run ``fn``; return True iff it signalled a verification failure. Failure is signalled either by raising a ``PMPError`` (or subclass), or by returning an explicitly falsy value. A return of ``None`` is treated as *success* (the common "verify() raises on failure, returns nothing on success" idiom). """ try: result = fn() except PMPError: return True if result is None: return False return not bool(result) def find_log_file(root: Path) -> Path: """Locate the on-disk operation log file under a node directory. The reference node stores the log as line-delimited JSON. We locate it by content rather than by hard-coding a file name, so the tests keep working if the storage layout under the node directory is reorganised. """ markers = ('"op_type"', '"op_id"', '"prev"', '"sig"') candidates: list[Path] = [] for path in sorted(root.rglob("*")): if not path.is_file(): continue try: head = path.read_text(encoding="utf-8")[:8000] except (UnicodeDecodeError, OSError): continue if any(marker in head for marker in markers): candidates.append(path) if not candidates: raise AssertionError( f"could not locate an operation log file under {root}; " f"expected a JSON-lines file containing operation records" ) # If several files match (e.g. an index plus the log), prefer the largest: # the append-only log dominates in size. return max(candidates, key=lambda p: p.stat().st_size)