"""The user documentation names every profile key, live key and on-screen label, and its links resolve.""" import dataclasses import re import unittest from pathlib import Path from typing import Any, Iterator from python_app.models.gui_profile_schema import GuiProfileModel from python_app.orchestration.live_processing_config import ProcessingLiveConfig ROOT = Path(__file__).resolve().parents[2] DOCS = ROOT / "docs" GUI = ROOT / "python_app" / "gui" # A full profile: every model section, both switches, geometry and the whole gui section. PROFILE = ROOT / "run_config_examples" / "run_config_librevna_multi_per_point.example.json" # Text the widgets show: constructor captions, and ("Label", widget) form rows of the sections and dialogs. CAPTION = re.compile(r'\b(?:QPushButton|QCheckBox|QGroupBox|QLabel|QToolButton|addRow|setWindowTitle|setTitle)\(\s*"([^"%{}]{2,})"') FORM_ROW = re.compile(r'\(\s*"([^"%{}]{1,60})"\s*,\s*[a-z_][a-z_0-9]*\s*\)') LINK = re.compile(r"\]\(([^)#\s]+)(?:#[^)]*)?\)") CODE = re.compile(r"(`+)(.+?)\1") BOLD = re.compile(r"\*\*(.+?)\*\*") HEADING = re.compile(r"^#+\s+(.+)$", re.MULTILINE) def leaf_names(value: Any) -> Iterator[str]: if isinstance(value, dict): for key, item in value.items(): if isinstance(item, (dict, list)) and item: yield from leaf_names(item) else: yield key elif isinstance(value, list) and isinstance(value[0], dict): yield from leaf_names(value[0]) def on_screen_labels() -> set[str]: labels: set[str] = set() for path in GUI.rglob("*.py"): source = path.read_text(encoding="utf-8") labels.update(CAPTION.findall(source)) if path.parent.name == "sections" or path.name.endswith(("_dialog.py", "_editor.py")): labels.update(FORM_ROW.findall(source)) return labels class DocumentationTest(unittest.TestCase): def setUp(self) -> None: self.chapters = {path: path.read_text(encoding="utf-8") for path in [ROOT / "README.md", *DOCS.glob("*.md")]} configuration = self.chapters[DOCS / "configuration.md"] self.configuration_names = {part for _, span in CODE.findall(configuration) for part in re.split(r"[.,*\s\[\]{}\"]+", span) if part} def test_configuration_names_every_profile_and_live_key(self) -> None: profile = GuiProfileModel.load_from_path(PROFILE).to_dict() live = [field.name for field in dataclasses.fields(ProcessingLiveConfig)] missing = sorted(set(leaf_names(profile)).union(live) - self.configuration_names) self.assertEqual(missing, [], "keys missing from docs/configuration.md") def test_every_on_screen_label_is_documented(self) -> None: # A label counts only as a whole bold span, code span or heading; a hint may continue with "…". text = "\n".join(self.chapters.values()) spans = {*BOLD.findall(text), *(span.strip() for _, span in CODE.findall(text)), *HEADING.findall(text)} def documented(label: str) -> bool: label = label.strip() return label in spans or any(span.startswith(label) and span[len(label):].lstrip().startswith("…") for span in spans) self.assertEqual(sorted(label for label in on_screen_labels() if not documented(label)), []) def test_relative_links_resolve(self) -> None: broken = [f"{path.name}: {target}" for path, text in self.chapters.items() for target in LINK.findall(text) if "://" not in target and not (path.parent / target).exists()] self.assertEqual(broken, []) if __name__ == "__main__": unittest.main()