110 lines
3.5 KiB
Python
110 lines
3.5 KiB
Python
"""Docs-as-tests: README.md stays in sync with the real commands and file tree."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from discord.ext import commands
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
README = REPO_ROOT / "README.md"
|
|
|
|
BINARY_SUFFIXES = {
|
|
".bin",
|
|
".db",
|
|
".gif",
|
|
".ico",
|
|
".jpeg",
|
|
".jpg",
|
|
".mp3",
|
|
".onnx",
|
|
".png",
|
|
".wav",
|
|
}
|
|
|
|
|
|
def _git(args: list[str]) -> list[str]:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return result.stdout.splitlines()
|
|
|
|
|
|
def _committed_files() -> set[str]:
|
|
"""Files that make up the current tree: index minus deletions, plus untracked."""
|
|
deleted: set[str] = set()
|
|
for line in _git(["status", "--porcelain"]):
|
|
if "D" in line[:2]:
|
|
deleted.add(line[3:])
|
|
tracked = set(_git(["ls-files"])) - deleted
|
|
untracked = set(_git(["ls-files", "--others", "--exclude-standard"]))
|
|
files: set[str] = set()
|
|
for path in tracked | untracked:
|
|
parts = Path(path).parts
|
|
if any(part.startswith(".") for part in parts):
|
|
continue
|
|
if Path(path).suffix.lower() in BINARY_SUFFIXES:
|
|
continue
|
|
files.add(path)
|
|
return files
|
|
|
|
|
|
def _readme_tree_paths() -> set[str]:
|
|
"""Reconstruct the relative paths (and directory entries) from the README tree."""
|
|
text = README.read_text(encoding="utf-8")
|
|
match = re.search(r"## File Structure\s*```text\n(.*?)```", text, re.DOTALL)
|
|
assert match is not None, "File Structure tree block not found in README"
|
|
lines = match.group(1).splitlines()
|
|
assert lines, "File Structure tree block is empty"
|
|
|
|
root = lines[0].split("#")[0].strip().rstrip("/")
|
|
assert root == REPO_ROOT.name, f"Tree root {root!r} != repo dir {REPO_ROOT.name!r}"
|
|
stack: list[str] = []
|
|
paths: set[str] = set()
|
|
entry_re = re.compile(
|
|
r"^(?P<prefix>(?:[│ ] )*)(?:├── |└── )(?P<name>\S.*?)(?:\s+#.*)?$"
|
|
)
|
|
for line in lines[1:]:
|
|
m = entry_re.match(line)
|
|
assert m is not None, f"Unparseable tree line: {line!r}"
|
|
depth = len(m.group("prefix")) // 4
|
|
name = m.group("name").strip()
|
|
if name.endswith("/"):
|
|
stack = stack[:depth] + [name.rstrip("/")]
|
|
paths.add("/".join(stack) + "/")
|
|
else:
|
|
paths.add("/".join(stack[:depth] + [name]))
|
|
return paths
|
|
|
|
|
|
def test_readme_documents_every_registered_command(bot: commands.Bot) -> None:
|
|
"""Every command registered on the bot is documented in README.md."""
|
|
readme = README.read_text(encoding="utf-8")
|
|
assert len(bot.commands) >= 11
|
|
missing = [cmd.name for cmd in bot.commands if cmd.name not in readme]
|
|
assert missing == [], f"Commands missing from README: {missing}"
|
|
|
|
|
|
def test_readme_file_tree_matches_actual_tree() -> None:
|
|
"""The README tree covers every committed non-dotfile, non-binary file."""
|
|
actual = _committed_files()
|
|
readme_paths = _readme_tree_paths()
|
|
|
|
missing_in_readme = actual - readme_paths
|
|
assert (
|
|
missing_in_readme == set()
|
|
), f"Files missing from the README tree: {sorted(missing_in_readme)}"
|
|
|
|
for path in readme_paths:
|
|
target = REPO_ROOT / path
|
|
if path.endswith("/"):
|
|
assert target.is_dir(), f"README tree lists missing directory: {path}"
|
|
else:
|
|
assert target.is_file(), f"README tree lists missing file: {path}"
|