"""Guard the README against drifting from the real CLI. This test parses README.md and checks it against the actual sources of truth instead of against a hand-maintained copy: * client ``--help`` output -> ``src/client/usage.c`` (``print_usage``) * server ``--help`` output -> ``src/server/server.c`` (``print_server_usage``) * ``FASTSYNC_*`` env vars -> ``getenv("...")`` call sites under ``src/`` It is deliberately offline and read-only: no server is started, no transfer is performed. Each binary is invoked at most once per test session and the result is cached. """ import functools import os import re import subprocess import sys import pytest sys.path.insert(0, os.path.dirname(__file__)) from common import BUILD_DIR, PROJECT_ROOT pytestmark = pytest.mark.ci README_PATH = os.path.join(PROJECT_ROOT, "README.md") SRC_DIR = os.path.join(PROJECT_ROOT, "src") # --------------------------------------------------------------------------- # Markdown helpers # --------------------------------------------------------------------------- _HEADING_RE = re.compile(r"^(#+)\s+(.*?)\s*$") _BACKTICK_RE = re.compile(r"`([^`]*)`") # A documented option may carry an argument annotation that is not part of the # option name itself: ``--out=FILE``, ``--exclude ``, ``--threads[=N]``, # ``--copy-as=USER[:GROUP]``. Cut the name loose from the first such marker. _OPTION_SUFFIX_RE = re.compile(r"[=\s<\[(].*$") _OPTION_TOKEN_RE = re.compile(r"^--?[A-Za-z][A-Za-z0-9-]*$") def _readme_lines(): with open(README_PATH, encoding="utf-8") as fh: return fh.read().splitlines() def _heading_level(line): match = _HEADING_RE.match(line) return len(match.group(1)) if match else 0 def _section(lines, heading): """Return ``(lineno, line)`` pairs under the first exact ``heading``. The section runs until the next heading of the same or higher level, so a ``##`` section includes its ``###`` subsections. Line numbers are 1-based to match what a reader sees in an editor. """ target_level = _heading_level(heading) for index, line in enumerate(lines): if line.strip() != heading: continue start = index + 1 for end in range(start, len(lines)): level = _heading_level(lines[end]) if level and level <= target_level: return [(n + 1, lines[n]) for n in range(start, end)] return [(n + 1, lines[n]) for n in range(start, len(lines))] raise AssertionError( f"README heading not found (has the README been restructured?): {heading!r}" ) def _first_column_spans(section_lines): """Backticked spans from the first column of every markdown table row.""" spans = [] for lineno, line in section_lines: stripped = line.strip() if not stripped.startswith("|"): continue cells = stripped.split("|") if len(cells) < 2: continue first = cells[1] if set(first.strip()) <= set("-: "): continue # header separator row, e.g. |---|---| for match in _BACKTICK_RE.finditer(first): spans.append((match.group(1), lineno)) return spans def _documented_option_tokens(section_lines): """``(token, lineno, raw_cell)`` for each CLI option in a section's tables.""" found = [] for raw, lineno in _first_column_spans(section_lines): for piece in re.split(r"[,\s]+", raw): name = _OPTION_SUFFIX_RE.sub("", piece).strip() if _OPTION_TOKEN_RE.match(name): found.append((name, lineno, raw)) return found # --------------------------------------------------------------------------- # Sources of truth # --------------------------------------------------------------------------- _GETENV_RE = re.compile(r'getenv\s*\(\s*"([^"]+)"\s*\)') _FASTSYNC_ENV_RE = re.compile(r"FASTSYNC_[A-Z0-9_]+") def _getenv_names(): """Every string literal passed to ``getenv()`` anywhere under ``src/``.""" names = set() for root, _dirs, files in os.walk(SRC_DIR): for filename in files: if not filename.endswith((".c", ".h")): continue path = os.path.join(root, filename) with open(path, encoding="utf-8", errors="replace") as fh: names.update(_GETENV_RE.findall(fh.read())) return names @functools.lru_cache(maxsize=None) def _help_stdout(binary_name): """Cached `` --help`` stdout; skipped (not failed) if unbuilt.""" binary = os.path.join(BUILD_DIR, binary_name) if not (os.path.isfile(binary) and os.access(binary, os.X_OK)): pytest.skip( f"{binary} is not built; run " "`cmake -B build -S . && cmake --build build` first" ) try: result = subprocess.run( [binary, "--help"], capture_output=True, text=True, timeout=30 ) except OSError as exc: pytest.skip(f"could not execute {binary}: {exc}") assert result.returncode == 0, ( f"{binary} --help exited {result.returncode}: " f"{(result.stderr or result.stdout).strip()[:200]}" ) return result.stdout def _mentions_option(help_text, token): """True when ``token`` appears as a standalone option in ``help_text``. A plain substring test would let a removed token hide behind a longer one (``--del`` inside ``--delete``); requiring a non-word boundary on both sides keeps every documented token individually accountable. """ return ( re.search(r"(?