#!/usr/bin/env python3 """Refresh the coverage percentage in the root README's project inventory. The root README's inventory table carries the code coverage of each promoted project in its "Build Status" column. A re-split or a repaired audit trail changes that number, so the column is recomputed here from the sources themselves: the code bytes are the bytes the audit comments claim, and the denominator is the size of the PRG image. Usage: python3 _tools/refresh_readme_coverage.py [--write] """ import argparse import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent # the audit comment of every instruction. A data row carries one too, and # its bytes are not code, so the line must start with a mnemonic. AUDIT = re.compile( r"^ [A-Za-z@][A-Za-z0-9_]*\s+.*?CPU:\\?\$[0-9A-F]{4,6} PRG:\\?\$?" r"[0-9A-F]{4,6}\s+((?:[0-9A-F]{2} ?)+)") # a row of the inventory table ROW = re.compile(r"^\| (\d+) \| (.+?) \| (.*?) \| (.*?) \| (.*?) \| (.*?) \| (.*?) \| (.*?) \|$") def coverage(proj): d = ROOT / proj prg = (d / "prg.bin").read_bytes() code = 0 for f in ("prg.s", "prg.6.s", "prg.07.s", "prg.0.s", "prg.1.s"): p = d / f if not p.is_file(): continue for line in p.read_text().split("\n"): m = AUDIT.search(line) if m: code += len(m.group(1).split()) return 100.0 * code / len(prg) def shape(proj): """(lines, instructions, code bytes) of a project's source.""" d = ROOT / proj lines = instructions = code = 0 for f in ("prg.s", "prg.6.s", "prg.07.s", "prg.0.s", "prg.1.s"): p = d / f if not p.is_file(): continue text = p.read_text().split("\n") lines += len(text) for line in text: m = AUDIT.match(line) if m: instructions += 1 code += len(m.group(1).split()) return lines, instructions, code def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--write", action="store_true") args = ap.parse_args() readme = ROOT / "README.md" lines = readme.read_text().split("\n") out, changed = [], 0 for line in lines: m = ROW.match(line) if m and m.group(2).strip() and "% code" in m.group(7): name = m.group(2).strip() proj = None for base in ("NROM", "CNROM", "UNROM", "MAPPER8"): if (ROOT / base / name).is_dir(): proj = f"{base}/{name}" break if proj: pct = coverage(proj) status = re.sub(r"\d+\.\d+% code", f"{pct:.1f}% code", m.group(7)) if status != m.group(7): changed += 1 line = f"| {m.group(1)} | {m.group(2)} | {m.group(3)} | {m.group(4)} | {m.group(5)} | {m.group(6)} | {status} | {m.group(8)} |" out.append(line) # The per-project detail sections carry the same figure in prose. Each # section belongs to the project its heading names, and only a percentage # in a line that also says "code" is the coverage. A section ends at # the next heading of any level, so a table further down the file is # never given the last project's figure. text = "\n".join(out) top = re.split(r"(?m)^(## .*)$", text) for k in range(1, len(top), 2): head, body = top[k], top[k + 1] subs = re.split(r"(?m)^(### \d+\. .*)$", body) for j in range(1, len(subs), 2): shead, sbody = subs[j], subs[j + 1] m = re.match(r"### \d+\. (.+)$", shead) if not m: continue name = m.group(1).strip() proj = None for base in ("NROM", "CNROM", "UNROM", "MAPPER8"): if (ROOT / base / name).is_dir(): proj = f"{base}/{name}" break if not proj: continue pct = coverage(proj) def fix(mo, pct=pct): nonlocal changed changed += 1 return f"{pct:.1f}% code" sbody = re.sub(r"\d+\.\d+% code", fix, sbody) # the "Source:" line describes the shape of the file, so its # line count, instruction count and byte count are recomputed # too - a re-split changes all three. nlines, ninsn, nbytes = shape(proj) def srcline(mo, nlines=nlines, ninsn=ninsn, nbytes=nbytes, pct=pct): nonlocal changed line = mo.group(0) new = line new = re.sub(r"(~?)([\d,]+) lines", lambda m: f"{m.group(1)}{nlines:,} lines", new) new = re.sub(r"[\d,]+ instructions\b", f"{ninsn:,} instructions", new) new = re.sub(r"[\d,]+ instruction lines", f"{ninsn:,} instruction lines", new) new = re.sub(r"[\d,]+ code bytes", f"{nbytes:,} code bytes", new) new = re.sub(r"[\d,]+ bytes", f"{nbytes:,} bytes", new) # a bare percentage in the same line is the coverage new = re.sub(r"= \d+\.\d+%", f"= {pct:.1f}%", new) if new != line: changed += 1 return new sbody = re.sub(r"^- \*\*Source:\*\*.*$", srcline, sbody, flags=re.M) subs[j + 1] = sbody body = "".join(subs) top[k + 1] = body text = "".join(top) print(f"{changed} figure(s) updated") if args.write: readme.write_text(text) print("written") if __name__ == "__main__": main()