Files
2026-10-16 18:35:02 +03:00

81 lines
3.6 KiB
Python

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