CI / shell (shellcheck + syntax) (push) Successful in 9s
CI / python 3.11 (push) Successful in 14s
CI / python 3.12 (push) Successful in 16s
CI / python 3.13 (push) Successful in 17s
TrueNAS compatibility / compat (push) Successful in 11s
Release / release (push) Successful in 14s
install.sh chmod +x's update.sh, and git recorded update.sh as 100644 -- so the chmod was a TRACKED modification, and update.sh refuses to run over a dirty tree. Install once and you could never update again. The error even told you to 'git checkout -- .', which just undoes the exec bit so the next install can re-dirty it. Found on the real box, which had been sitting on v0.4.1 for exactly this reason. Fixed on both sides: the scripts install.sh chmods are executable in git (so the chmod is a no-op), and update.sh's dirty check now looks at CONTENT, not mode -- git diff --numstat reports 0 0 for a mode-only change. A test asserts every script in install.sh's chmod loop is already 100755 in git.
134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
"""The docs must not lie about themselves.
|
|
|
|
The README was 969 lines with the install instructions at line 517. Splitting it into
|
|
docs/ fixed that and broke every cross-reference in the process -- which is the normal
|
|
outcome of moving Markdown around, and exactly why this is a test rather than a
|
|
careful afternoon.
|
|
|
|
A dead link in a recovery doc is worse than a dead link anywhere else: the person
|
|
following it is, by definition, already having a bad day.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
|
|
import pytest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
DOCS = os.path.join(ROOT, "docs")
|
|
|
|
LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
|
HEADING_RE = re.compile(r"^#{1,6}\s+(.*)$", re.M)
|
|
|
|
|
|
def markdown_files():
|
|
files = [os.path.join(ROOT, "README.md"), os.path.join(ROOT, "CHANGELOG.md")]
|
|
if os.path.isdir(DOCS):
|
|
files += [os.path.join(DOCS, f) for f in sorted(os.listdir(DOCS))
|
|
if f.endswith(".md")]
|
|
return files
|
|
|
|
|
|
def anchors(text):
|
|
"""GitHub/Gitea slugs for every heading in `text`."""
|
|
out = set()
|
|
for h in HEADING_RE.findall(text):
|
|
slug = re.sub(r"[^a-z0-9 -]", "", h.lower()).replace(" ", "-")
|
|
out.add(slug)
|
|
return out
|
|
|
|
|
|
@pytest.mark.parametrize("path", markdown_files(), ids=os.path.basename)
|
|
def test_every_internal_link_resolves(path):
|
|
with open(path, encoding="utf-8") as fh:
|
|
text = fh.read()
|
|
here = anchors(text)
|
|
base = os.path.dirname(path)
|
|
|
|
broken = []
|
|
for label, target in LINK_RE.findall(text):
|
|
if target.startswith(("http://", "https://", "mailto:")):
|
|
continue
|
|
rel, _, anchor = target.partition("#")
|
|
|
|
if not rel: # same-file anchor
|
|
if anchor and anchor not in here:
|
|
broken.append(f"[{label}](#{anchor}) — no such heading here")
|
|
continue
|
|
|
|
dest = os.path.normpath(os.path.join(base, rel))
|
|
if not os.path.exists(dest):
|
|
broken.append(f"[{label}]({target}) — file does not exist")
|
|
continue
|
|
|
|
if anchor and dest.endswith(".md"):
|
|
with open(dest, encoding="utf-8") as fh:
|
|
if anchor not in anchors(fh.read()):
|
|
broken.append(f"[{label}]({target}) — no such heading there")
|
|
|
|
assert not broken, "broken links in {}:\n {}".format(
|
|
os.path.basename(path), "\n ".join(broken)
|
|
)
|
|
|
|
|
|
class TestTheReadmeStaysAReadme:
|
|
def test_install_is_near_the_top(self):
|
|
# It was at line 517 of 969, under a wall of internals. Somebody deciding
|
|
# whether to use this should not have to scroll past the boot sequence.
|
|
with open(os.path.join(ROOT, "README.md"), encoding="utf-8") as fh:
|
|
lines = fh.read().splitlines()
|
|
install = next(i for i, ln in enumerate(lines, 1) if ln.startswith("## Install"))
|
|
assert install < 40, f"## Install is at line {install}"
|
|
|
|
def test_the_readme_does_not_grow_back(self):
|
|
with open(os.path.join(ROOT, "README.md"), encoding="utf-8") as fh:
|
|
n = len(fh.read().splitlines())
|
|
assert n < 300, (
|
|
f"README is {n} lines. Detail belongs in docs/ — the README is what "
|
|
f"someone reads before they trust this with their backups."
|
|
)
|
|
|
|
def test_the_minimum_version_is_stated_before_the_install_command(self):
|
|
with open(os.path.join(ROOT, "README.md"), encoding="utf-8") as fh:
|
|
text = fh.read()
|
|
assert "24.10" in text[:text.index("## Install")], (
|
|
"the minimum TrueNAS version must be visible above the install steps"
|
|
)
|
|
|
|
|
|
class TestInstallDoesNotDirtyTheCheckout:
|
|
"""install.sh chmod +x's scripts. If git records them as 100644, that chmod is a
|
|
TRACKED MODIFICATION -- and update.sh refuses to run over a dirty tree.
|
|
|
|
So installing once permanently blocked updating, for every user, with a message
|
|
telling them to `git checkout -- .` (which would just undo the exec bit and let
|
|
the next install re-dirty it). Found on a real box that had been stuck on an old
|
|
version for exactly this reason.
|
|
|
|
Every script install.sh makes executable must already be executable in git.
|
|
"""
|
|
|
|
def test_every_chmodded_script_is_already_executable_in_git(self):
|
|
import re
|
|
import subprocess
|
|
|
|
with open(os.path.join(ROOT, "install.sh"), encoding="utf-8") as fh:
|
|
m = re.search(r"^for _exe in (.+?); do", fh.read(), re.M)
|
|
assert m, "could not find install.sh's chmod loop"
|
|
scripts = m.group(1).split()
|
|
|
|
out = subprocess.run(
|
|
["git", "ls-files", "-s", *scripts],
|
|
cwd=ROOT, capture_output=True, text=True, check=True,
|
|
).stdout
|
|
|
|
not_exec = [
|
|
line.split("\t")[-1] for line in out.strip().splitlines()
|
|
if not line.startswith("100755")
|
|
]
|
|
assert not not_exec, (
|
|
"install.sh chmod +x's these, but git records them as non-executable — "
|
|
"so installing dirties the checkout and update.sh then refuses to run:\n "
|
|
+ "\n ".join(not_exec)
|
|
)
|