aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_docs.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-27 15:17:57 +0200
committeruser <user@clank>2026-08-27 15:17:57 +0200
commitee5790225261fb3d473a16a365bedc15619f93cc (patch)
treeca1821d2643d5eeea9af47dcf0ba52ab29ebaf8d /tests/test_docs.py
parenttest_splash: 'landed' is the row in its final form (diff)
downloadslosh-ee5790225261fb3d473a16a365bedc15619f93cc.tar.gz
slosh-ee5790225261fb3d473a16a365bedc15619f93cc.tar.xz
slosh-ee5790225261fb3d473a16a365bedc15619f93cc.zip
a manpage: slosh(1), installed, shipped, and held to the source
The distributables were a bare binary and a directory of Markdown, which is fine until somebody types 'man slosh' -- the unix-traditional place to ask. docs/slosh.1 is hand-written man(7), pure ASCII roff, no toolchain: the synopsis, the options, the session commands, the default keys, the mouse (including the new drag-past-an-edge scrolling), where the configuration lives, what SLOSH_* means in a pane's environment, files, exit status. The prose docs stay the long form; the page says so and points at them. 'make install' puts it under PREFIX/share/man/man1 (and uninstall takes it away), the release tarballs carry it beside the LICENSE -- a tarball is what a packager starts from -- and the Homebrew formula installs it when the commit it is building has one, guarded because the formula must be able to build commits older than the page. Prose about a program drifts unless something holds them together, so test_docs.py holds the page to the source the way it already holds the site to docs/nav: every option main.c's parser compares against and every word command must appear in the page, every SLOSH_* variable the code reads or sets must be documented, the roff must be pure ASCII (it travels through implementations that never heard of preconv), and where groff exists the page must lint clean at CHECKSTYLE=3 with -ww. Adding a flag without documenting it is now a failing test, which is the only kind of documentation policy that survives contact with a deadline.
Diffstat (limited to 'tests/test_docs.py')
-rwxr-xr-xtests/test_docs.py67
1 files changed, 67 insertions, 0 deletions
diff --git a/tests/test_docs.py b/tests/test_docs.py
index a61e37c..def76ff 100755
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -237,6 +237,70 @@ def test_html_in_the_source_is_escaped():
check("and not a tag", "<script>" not in got, got[:400])
+# ---- the manpage ----------------------------------------------------------
+#
+# docs/slosh.1 is hand-written roff, and prose about a program drifts from the
+# program unless something holds them together. The same idea as the nav
+# checks above: the source of truth for what the CLI accepts is main.c's
+# parser and for what the environment means is the code that reads it, so the
+# manpage is checked against those rather than against a copy of itself.
+
+MAN = os.path.join(DOCS, "slosh.1")
+
+
+def test_the_manpage_covers_the_cli():
+ man = open(MAN).read()
+ main_c = open(os.path.join(ROOT, "src", "main.c")).read()
+
+ # Every option main.c compares against, plus the word commands. -h/--help
+ # print the usage line and are exempt: a manpage documenting the flag that
+ # asks for less documentation would be circular.
+ opts = set(re.findall(r'strcmp\(a, "(-[^"]+)"\)', main_c)) - {"-h", "--help", "--"}
+ words = set(re.findall(r'strcmp\(a, "([a-z]+)"\)', main_c))
+ for o in sorted(opts):
+ check(f"the manpage documents {o}", o.replace("-", "\\-") in man, o)
+ for w in sorted(words):
+ check(f"the manpage documents the {w} command", f"\n.B {w}" in man, w)
+
+
+def test_the_manpage_covers_the_environment():
+ """Every SLOSH_* variable the program reads or sets is a promise to
+ somebody's script, and the manpage is where they were promised."""
+ man = open(MAN).read()
+ names = set()
+ for f in os.listdir(os.path.join(ROOT, "src")):
+ if not f.endswith(".c"):
+ continue
+ src = open(os.path.join(ROOT, "src", f)).read()
+ names |= set(re.findall(r'(?:getenv|setenv|unsetenv)\("(SLOSH\w*)"', src))
+ check("main.c really was scanned", "SLOSH_SESSION" in names, str(names))
+ for n in sorted(names):
+ check(f"the manpage documents ${n}", n in man, n)
+
+
+def test_the_manpage_is_clean_roff():
+ """Pure ASCII (a manpage travels through implementations that never heard
+ of preconv) and, where groff is present, no warnings at its strictest."""
+ raw = open(MAN, "rb").read()
+ bad = [i for i, b in enumerate(raw) if b > 0x7F]
+ check("the roff is pure ASCII", not bad, f"non-ASCII at byte {bad[:3]}")
+
+ import shutil
+
+ if not shutil.which("groff"):
+ return # the ASCII check above still ran; lint where lint exists
+ r = subprocess.run(
+ ["groff", "-man", "-rCHECKSTYLE=3", "-ww", "-z", MAN],
+ capture_output=True,
+ text=True,
+ )
+ check(
+ "groff -ww has nothing to say",
+ r.returncode == 0 and not r.stderr.strip(),
+ r.stderr.strip()[:300],
+ )
+
+
if __name__ == "__main__":
test_it_renders_every_page_in_the_nav()
test_every_internal_link_lands_somewhere()
@@ -246,4 +310,7 @@ if __name__ == "__main__":
test_the_markdown_subset()
test_markup_inside_a_code_span_is_not_markup()
test_html_in_the_source_is_escaped()
+ test_the_manpage_covers_the_cli()
+ test_the_manpage_covers_the_environment()
+ test_the_manpage_is_clean_roff()
sys.exit(report())