Введение
Справка, как и код, подвержена «регрессиям» — рефакторинг переименовывает страницу, а ссылки на старое имя остаются; новая функция добавляется в код, но соответствующее ключевое слово в индексе справки не добавляется. Без автоматизированной проверки эти расхождения обнаруживаются только при ручном тестировании или, хуже, жалобами пользователей. В статье построим набор автоматизированных проверок целостности справки, запускаемых в CI при каждом изменении документации.
Концепция
Базовые проверки целостности справки делятся на несколько категорий: (1) все файлы, перечисленные в <files> .qhp, действительно существуют на диске; (2) все внутренние ссылки в HTML-страницах (<a href="...">) ведут на существующие файлы и существующие якоря (id) внутри них; (3) каждая страница, перечисленная в <files>, достижима хотя бы из одного узла TOC (нет «осиротевших» страниц, до которых нельзя добраться через навигацию); (4) опционально — каждое ключевое слово, упомянутое в коде приложения (через систему контекстной справки из статьи 164), действительно присутствует в индексе <keywords> .qhp.
Пример кода: CI-проверка целостности справки
#!/usr/bin/env python3
# check_help_integrity.py — проверка целостности проекта справки перед сборкой
import sys
import os
import re
from xml.etree import ElementTree as ET
from html.parser import HTMLParser
class LinkExtractor(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
self.ids = set()
def handle_starttag(self, tag, attrs):
attr_dict = dict(attrs)
if tag == "a" and "href" in attr_dict:
self.links.append(attr_dict["href"])
if "id" in attr_dict:
self.ids.add(attr_dict["id"])
def check_files_exist(qhp_path, base_dir):
errors = []
tree = ET.parse(qhp_path)
declared_files = {f.text for f in tree.findall(".//files/file")}
for declared in declared_files:
full_path = os.path.join(base_dir, declared)
if not os.path.isfile(full_path):
errors.append(f"Файл объявлен в .qhp, но отсутствует на диске: {declared}")
toc_refs = {section.get("ref").split("#")[0] for section in tree.findall(".//section")}
orphan_files = declared_files - toc_refs - {".css", ".png", ".jpg"} # упрощённая фильтрация
html_orphans = {f for f in orphan_files if f.endswith(".html")}
for orphan in html_orphans:
errors.append(f"Страница не достижима из TOC (orphan page): {orphan}")
return errors, declared_files
def check_internal_links(declared_files, base_dir):
errors = []
page_ids = {}
for filepath in declared_files:
if not filepath.endswith(".html"):
continue
full_path = os.path.join(base_dir, filepath)
if not os.path.isfile(full_path):
continue
with open(full_path, encoding="utf-8") as f:
parser = LinkExtractor()
parser.feed(f.read())
page_ids[filepath] = parser.ids
for link in parser.links:
if link.startswith("http://") or link.startswith("https://"):
continue # внешние ссылки не проверяем здесь
target_file, _, anchor = link.partition("#")
resolved = target_file if target_file else filepath
resolved_path = os.path.normpath(os.path.join(os.path.dirname(filepath), resolved))
if resolved_path not in declared_files and target_file:
errors.append(f"{filepath}: ссылка на несуществующий файл '{target_file}'")
return errors
def main():
qhp_path = sys.argv[1]
base_dir = os.path.dirname(qhp_path)
file_errors, declared_files = check_files_exist(qhp_path, base_dir)
link_errors = check_internal_links(declared_files, base_dir)
all_errors = file_errors + link_errors
if all_errors:
print(f"Найдено {len(all_errors)} проблем целостности справки:")
for error in all_errors:
print(f" - {error}")
sys.exit(1)
print("Проверка целостности справки пройдена успешно")
sys.exit(0)
if __name__ == "__main__":
main()
# .github/workflows/check_help.yml — интеграция проверки в CI
name: Help Integrity Check
on:
pull_request:
paths:
- 'docs/**'
jobs:
check-help:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Проверка целостности справки
run: python3 scripts/check_help_integrity.py docs/myapp.qhp
Пояснения к коду
check_files_exist() сначала сверяет список файлов, заявленных в <files> .qhp, с реально существующими на диске — это ловит ситуацию, когда файл был удалён или переименован, но запись в .qhp забыли обновить. Дополнительно вычисляются «осиротевшие» страницы — те, что перечислены в <files>, но ни на одну из них не ссылается ни один узел <section> в TOC, что означает, что пользователь не может добраться до этой страницы через обычную навигацию (она доступна только напрямую по URL или через индекс, если там тоже есть ссылка).
check_internal_links() разбирает каждую HTML-страницу через LinkExtractor, собирает все внутренние ссылки (<a href="...">) и проверяет, что целевой файл действительно входит в список заявленных файлов справки — внешние ссылки (http:///https://) явно исключаются из этой проверки, поскольку их доступность не относится к целостности самой справки. Workflow для github-actions-%d0%bf%d0%b5%d1%80%d0%b2%d1%8b%d0%b9-workflow-%d0%b4%d0%bb%d1%8f-%d1%81%d0%b1%d0%be%d1%80%d0%ba%d0%b8-qt-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b0/" class="qth-seo-autolink">GitHub Actions запускает проверку только при изменении файлов внутри docs/, что экономит время CI для pull request’ов, не затрагивающих документацию.
Подводные камни
- Упрощённая фильтрация «осиротевших» файлов в примере (
{".css", ".png", ".jpg"}) — не полноценное решение для всех типов нетекстовых ресурсов; в реальном проекте список расширений, не подлежащих проверке на достижимость через TOC (поскольку они являются вложенными ресурсами, а не самостоятельными страницами навигации), должен быть точнее настроен под структуру конкретного проекта справки. - Проверка якорей (
id) не реализована в примере полностью — для строгой проверки нужно также сопоставлять каждую ссылку с конкретным якорем (#install) с фактически существующимidв целевом файле, не только с существованием самого файла; это требует двухпроходного анализа (сначала собрать всеidсо всех страниц, затем проверить все ссылки с якорями). - Ложные срабатывания на динамически генерируемых ссылках. Если часть ссылок формируется не статически в исходном HTML, а программно через JavaScript (для интерактивных справок на основе
QWebEngineView), статический анализатор ссылок не сможет их обнаружить и проверить — для таких случаев нужен либо отдельный механизм валидации на этапе выполнения, либо явное соглашение о том, что динамические ссылки не покрываются автоматической проверкой. - Запуск проверки только при изменении
docs/**может пропустить ситуацию, когда изменение кода приложения (переименование функции с контекстной справкой) рассинхронизирует ключевые слова в справке, но сам каталогdocs/физически не изменился — для полной защиты от этого класса рассинхронизации нужна отдельная, более широкая проверка, сопоставляющая ключевые слова контекстной справки в коде (статья 164) с индексом<keywords>в .qhp, независимо от того, что именно было изменено в pull request.