"""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