#!/usr/bin/env python3 """Contract verification harness for XZBT_0-1_Format_Specification.md. Checks, over the whole document: 1. every ERR_*/WARN_*/INFO_* code used resolves to a row of the section 7 table; 2. every section 7 row is used at least once outside the table; 3. every numeric cross-reference (e.g. "section 19.5", "19.5", "17.16.14") resolves to a real heading or a numbered trace item; 4. code fences balance; 5. markdown table rows are well formed (consistent column count per table, discounting escaped pipes). Exit status is non-zero when any check fails. """ import io, os, re, sys, collections SPEC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "docs", "XZBT_0-1_Format_Specification.md") def main(): text = io.open(SPEC, encoding="utf-8").read() lines = text.split("\n") problems = [] # ---- fences ------------------------------------------------------- fence_lines = [i for i, l in enumerate(lines, 1) if l.strip().startswith("```")] if len(fence_lines) % 2: problems.append("unbalanced code fences: %d fence markers" % len(fence_lines)) in_fence = set() for a, b in zip(fence_lines[0::2], fence_lines[1::2]): in_fence.update(range(a, b + 1)) # ---- headings ----------------------------------------------------- headings = {} for i, l in enumerate(lines, 1): m = re.match(r"^#{2,4}\s+(\d+(?:\.\d+)*)\.?\s", l) if m: headings[m.group(1)] = i # numbered trace items inside a "Required traces" subsection trace_items = collections.defaultdict(set) current = None for i, l in enumerate(lines, 1): m = re.match(r"^#{2,4}\s+(\d+(?:\.\d+)*)\.?\s", l) if m: current = m.group(1) continue if current and re.match(r"^(\d+)\.\s", l) and i not in in_fence: trace_items[current].add(re.match(r"^(\d+)\.\s", l).group(1)) # ---- section 7 diagnostic table ------------------------------------ declared = set() sec7 = headings.get("7") if not sec7: problems.append("section 7 heading not found") else: i = sec7 while i < len(lines) and not re.match(r"^## 8\.", lines[i]): m = re.match(r"^\|\s*`((?:ERR|WARN|INFO)_[A-Z0-9_]+)`\s*\|", lines[i]) if m: declared.add(m.group(1)) i += 1 used = collections.Counter() for i, l in enumerate(lines, 1): if sec7 and sec7 <= i < sec7 + 60 and re.match(r"^\|\s*`(?:ERR|WARN|INFO)_", l): continue for code in re.findall(r"`((?:ERR|WARN|INFO)_[A-Z0-9_]+)`", l): used[code] += 1 for code in sorted(used): if code not in declared: problems.append("undeclared diagnostic code used: %s" % code) declared_only = sorted(c for c in declared if c not in used) # ---- cross references --------------------------------------------- xrefs = collections.Counter() for i, l in enumerate(lines, 1): if i in in_fence: continue if re.match(r"^\|?\s*`?(?:ERR|WARN|INFO)_", l): pass scan = re.sub(r"`[^`]*`", " ", l) # inline code spans hold data, not references for m in re.finditer(r"(?