It claimed thirteen bundled plugins and listed thirteen; there are fifteen, and the two missing ones - backups and tools - were undiscoverable from the first page anyone reads. The count is gone rather than corrected, because a number typed here goes stale exactly the way it just did; the generated map carries it. The version and count guard now covers README.md and CONTRIBUTING.md, not just docs/. A rule that leaves the most-read file in the repository outside it is most of a rule. Also names the reference site as the reference site rather than by plant, adds a pointer to START-HERE, and corrects the distribution paragraph: it told a reader of the public mirror that the code is only available on an internal server, which is both wrong and unhelpful now that the installer ships as a release asset.
128 lines
5.4 KiB
Python
128 lines
5.4 KiB
Python
"""No document types a version or a count that the code already knows.
|
|
|
|
Nine documents carried a hand-typed contract version. Every one was stale, and
|
|
one of them was load-bearing: PLUGIN-EXTERNAL-REPO.md told an external author to
|
|
pin `>=0.13.0,<0.14.0` against a contract at 0.19.0, so a plugin built by
|
|
following the guide is refused by the loader at startup. The plugin count was
|
|
wrong in six more.
|
|
|
|
A number that is copied is a number that goes stale, and prose gives no signal
|
|
about which lines are still true. docs/PROJECT-MAP.md is generated for exactly
|
|
this, so a document points at it instead of restating it.
|
|
|
|
This test is the rule. Where a version genuinely belongs in prose - the contract
|
|
history table, a changelog entry, an ADR recording what was decided when - it is
|
|
recording the PAST, which does not go stale.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
DOCS = REPO / 'docs'
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not DOCS.is_dir(),
|
|
reason='no docs/ in this checkout - it is excluded from publication')
|
|
|
|
# The map is generated and the changelog records history; both are allowed to
|
|
# carry literal versions. CONTRACT-STABILITY's table is a history of what each
|
|
# contract version DID, which is the past and stays true.
|
|
EXEMPT = {'PROJECT-MAP.md', 'CONTRACT-STABILITY.md'}
|
|
|
|
ASSIGNMENT = re.compile(r"__(?:contract_)?version__\s*=\s*['\"]([0-9]+\.[0-9]+\.[0-9]+)['\"]")
|
|
NARRATED = re.compile(
|
|
r"__(?:contract_)?version__[^\n]{0,60}?\b([0-9]+\.[0-9]+\.[0-9]+)\b"
|
|
r"|\b(?:contract|version) is (?:at|currently)\s+\*{0,2}([0-9]+\.[0-9]+\.[0-9]+)")
|
|
PLUGIN_COUNT = re.compile(r'\b(\d+)\s+bundled plugins\b', re.I)
|
|
|
|
|
|
def documentation_files():
|
|
"""Everything published that a reader treats as documentation.
|
|
|
|
README.md and CONTRIBUTING.md are included deliberately: README is the first
|
|
page anyone sees on the mirror, and it was claiming thirteen bundled plugins
|
|
when there were fifteen. A rule that covers only docs/ leaves the most-read
|
|
file in the repository outside it.
|
|
"""
|
|
files = [p for p in DOCS.rglob('*.md') if p.name not in EXEMPT]
|
|
for name in ('README.md', 'CONTRIBUTING.md'):
|
|
top = REPO / name
|
|
if top.is_file():
|
|
files.append(top)
|
|
return sorted(files)
|
|
|
|
|
|
def current_versions():
|
|
text = (REPO / 'shopdb' / '__init__.py').read_text()
|
|
return {
|
|
name: re.search(r"^%s\s*=\s*['\"]([^'\"]+)['\"]" % name, text, re.M).group(1)
|
|
for name in ('__version__', '__contract_version__')
|
|
}
|
|
|
|
|
|
def bundled_plugin_count():
|
|
return len(list((REPO / 'plugins').glob('*/manifest.json')))
|
|
|
|
|
|
def test_no_document_declares_a_version_literal():
|
|
"""`__contract_version__ = '0.13.0'` in prose is a promise the code breaks."""
|
|
offenders = []
|
|
for path in documentation_files():
|
|
for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
|
|
if ASSIGNMENT.search(line):
|
|
offenders.append('%s:%d %s' % (path.relative_to(REPO), number, line.strip()[:90]))
|
|
assert not offenders, (
|
|
'These documents declare a version literal. Point at docs/PROJECT-MAP.md, '
|
|
'which is generated:\n ' + '\n '.join(offenders))
|
|
|
|
|
|
def test_a_narrated_version_matches_the_code():
|
|
"""Prose that states the CURRENT version has to be right."""
|
|
versions = set(current_versions().values())
|
|
offenders = []
|
|
for path in documentation_files():
|
|
# An ADR states the version a decision was taken AT. That is a record of
|
|
# the past, not a claim about today, and rewriting it would falsify the
|
|
# record this project keeps ADRs for.
|
|
if path.parent.name == 'adr':
|
|
continue
|
|
for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
|
|
for match in NARRATED.finditer(line):
|
|
found = match.group(1) or match.group(2)
|
|
if found and found not in versions:
|
|
offenders.append('%s:%d says %s %s'
|
|
% (path.relative_to(REPO), number, found, line.strip()[:70]))
|
|
assert not offenders, (
|
|
'These lines state a current version that no longer matches '
|
|
'shopdb/__init__.py:\n ' + '\n '.join(offenders))
|
|
|
|
|
|
def test_a_stated_plugin_count_matches_the_tree():
|
|
actual = bundled_plugin_count()
|
|
offenders = []
|
|
for path in documentation_files():
|
|
for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
|
|
for match in PLUGIN_COUNT.finditer(line):
|
|
if int(match.group(1)) != actual:
|
|
offenders.append('%s:%d says %s, tree has %d'
|
|
% (path.relative_to(REPO), number, match.group(1), actual))
|
|
assert not offenders, (
|
|
'These documents count plugins by hand. The count is in '
|
|
'docs/PROJECT-MAP.md:\n ' + '\n '.join(offenders))
|
|
|
|
|
|
def test_the_map_itself_is_current():
|
|
"""The pointer target has to be right, or every document pointing at it is
|
|
wrong at one remove."""
|
|
mapfile = DOCS / 'PROJECT-MAP.md'
|
|
assert mapfile.is_file(), 'docs/PROJECT-MAP.md is missing; run scripts/gen_project_map.py'
|
|
text = mapfile.read_text()
|
|
for name, version in current_versions().items():
|
|
assert version in text, (
|
|
'%s is %s in the code but the generated map does not carry it. '
|
|
'Run: venv/bin/python scripts/gen_project_map.py' % (name, version))
|
|
assert str(bundled_plugin_count()) in text
|