diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 473aad1af..5001de0a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -169,7 +169,7 @@ will fail if: 6. **Update documentation** if needed - Update README.md for user-facing changes - Update relevant docs in `docs/` - - Add entry to `docs/CHANGELOG.md` under `[Unreleased]` + - Add entry to `docs/changelog.md` under `[Unreleased]` 7. **Submit a pull request** and fill out the PR template — including the automated tests, manual testing, and "why it was required" sections diff --git a/docs/changelog.md b/docs/changelog.md index f8e4e385d..0d59e6d5c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -25,8 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Doc paths named from the Python package resolve again.** The K-LD7 OPS-bin penalty warning, the cloud client and session-logger comments, and the two-ray module docstring pointed at flat `docs/*.md` files that - moved in the docs restructure. A new test scans `src/openflight` for - `docs/…​.md` references and fails on any that do not exist. + moved in the docs restructure. A new test scans `src/openflight`, + `scripts/`, `.github/`, and the top-level guides for `docs/…​.md` + references and fails on any that do not exist. - **`openflight-cloud` honors `--config`/`--log-dir` before the subcommand.** The subcommand's defaults overwrote options given before it, so `openflight-cloud --log-dir X push` scanned the default session directory. diff --git a/tests/test_doc_references.py b/tests/test_doc_references.py index d536679a2..a9467971b 100644 --- a/tests/test_doc_references.py +++ b/tests/test_doc_references.py @@ -1,40 +1,77 @@ -"""Doc paths mentioned from the Python package must exist. - -Log messages, docstrings, and comments under ``src/openflight`` point users -at ``docs/...md`` pages. The docs restructure moved those pages into -sectioned directories and several pointers were left naming files that no -longer exist (see the kld7 OPS-bin penalty warning, which sent users to a -404 for months). This test fails on any such reference so a future docs -move cannot silently strand them again. +"""Doc paths mentioned outside ``docs/`` must exist. + +Log messages, docstrings, shell ``warn`` lines, issue templates, and the +top-level guides all point users at ``docs/...md`` pages. The docs +restructure moved those pages into sectioned directories and left a dozen +pointers naming files that no longer exist, including a production WARNING +in the K-LD7 estimator and the setup script every new builder runs. Those +were fixed by hand (PRs #294 and #295); this test fails on any such +reference so a future docs move cannot silently strand them again. + +Links *inside* ``docs/`` are not scanned here: the Zensical build runs with +``--strict`` in CI and already fails on broken intra-docs links. """ +import os import re from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parent.parent -PACKAGE_ROOT = REPO_ROOT / "src" / "openflight" DOC_REF = re.compile(r"docs/[A-Za-z0-9_./-]+\.md") +# (root, glob) pairs scanned for docs/*.md mentions. Keep this list explicit +# so a new tree that references docs is added deliberately, not by accident. +SCAN_TARGETS: tuple[tuple[str, str], ...] = ( + ("src/openflight", "**/*.py"), + ("scripts", "**/*.py"), + ("scripts", "**/*.sh"), + (".github", "**/*.yml"), + (".github", "**/*.md"), + (".", "README.md"), + (".", "CONTRIBUTING.md"), + (".", "CLAUDE.md"), + (".", "AGENTS.md"), + ("ui", "README.md"), +) + + +def _exists_with_exact_case(rel_path: str) -> bool: + """``Path.is_file`` with exact on-disk casing for every component. + + macOS and Windows filesystems are case-insensitive, so ``docs/CHANGELOG.md`` + looks fine there and only breaks on the Linux CI runners (and on GitHub's + rendered links). Check each component against its parent's real listing. + """ + current = REPO_ROOT + for part in Path(rel_path).parts: + if part not in os.listdir(current): + return False + current = current / part + return current.is_file() + def _doc_references() -> list[tuple[str, str]]: - found: list[tuple[str, str]] = [] - for path in sorted(PACKAGE_ROOT.rglob("*.py")): - text = path.read_text(encoding="utf-8") - for match in sorted(set(DOC_REF.findall(text))): - found.append((str(path.relative_to(REPO_ROOT)), match)) - return found + found: set[tuple[str, str]] = set() + for root, pattern in SCAN_TARGETS: + for path in (REPO_ROOT / root).glob(pattern): + if not path.is_file() or "__pycache__" in path.parts: + continue + text = path.read_text(encoding="utf-8", errors="replace") + for match in DOC_REF.findall(text): + found.add((str(path.relative_to(REPO_ROOT)), match)) + return sorted(found) _REFERENCES = _doc_references() -def test_package_mentions_at_least_one_doc(): +def test_scan_finds_references(): """Guard against the scan silently matching nothing.""" - assert _REFERENCES, "expected src/openflight to reference docs/*.md somewhere" + assert len(_REFERENCES) > 10, f"expected many docs/*.md references, found {len(_REFERENCES)}" @pytest.mark.parametrize(("source", "doc"), _REFERENCES, ids=[f"{s}:{d}" for s, d in _REFERENCES]) def test_referenced_doc_exists(source, doc): - assert (REPO_ROOT / doc).is_file(), f"{source} points at {doc}, which does not exist" + assert _exists_with_exact_case(doc), f"{source} points at {doc}, which does not exist"