Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
73 changes: 55 additions & 18 deletions tests/test_doc_references.py
Original file line number Diff line number Diff line change
@@ -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"
Loading