# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Tests for the `update-docs` orchestration and this repository's docs wiring."""
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import tomlrt
from click_extra import ClickException
from repomatic.config import Config, DocsConfig
from repomatic.docs import _run_docs_tool, update_docs, validate_docs_script_path
REPO_ROOT = Path(__file__).parent.parent
[docs]
def test_docs_group_carries_the_test_group() -> None:
"""The Sphinx environment imports the test suite it documents.
autodoc imports every module it renders a page for, `docs/tests.md`
documents each test module, and each of those imports pytest. Without the
`test` group, all of them fail to import and their pages publish empty. The
build reports that as a warning, so nothing else catches it.
"""
groups = tomlrt.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="UTF-8"))[
"dependency-groups"
]
included = {
entry["include-group"] for entry in groups["docs"] if not isinstance(entry, str)
}
assert "test" in included, (
"[dependency-groups] docs must include the test group, or every "
"tests.* documentation page builds empty."
)
def _docs_config(update_script: str = "") -> Config:
"""Build a Config carrying just the `docs` fields under test.
`update_script` defaults to empty here (not the shipped
`./docs/docs_update.py`) so orchestration tests skip the script phase
unless they opt in.
"""
return Config(docs=DocsConfig(update_script=update_script))
[docs]
def test_validate_docs_script_path_empty_returns_none(tmp_path):
"""An empty configured path disables the script phase."""
assert validate_docs_script_path("", tmp_path) is None
[docs]
def test_validate_docs_script_path_valid(tmp_path):
"""A `.py` file under `docs/` resolves to its absolute path."""
resolved = validate_docs_script_path("docs/docs_update.py", tmp_path)
assert resolved == (tmp_path / "docs" / "docs_update.py").resolve()
[docs]
@pytest.mark.parametrize(
("script", "match"),
(
pytest.param("../outside.py", "escapes repository root", id="escapes-root"),
pytest.param("scripts/build.py", "must be under docs/", id="outside-docs"),
pytest.param("docs/notes.txt", "must be a .py file", id="not-python"),
),
)
def test_validate_docs_script_path_rejects(tmp_path, script, match):
"""Paths outside `docs/`, outside the repo, or non-`.py` are rejected."""
with pytest.raises(ClickException, match=match):
validate_docs_script_path(script, tmp_path)
[docs]
def test_update_docs_noop_without_sphinx(tmp_path, monkeypatch):
"""A project with no Sphinx configuration runs no docs tooling."""
monkeypatch.chdir(tmp_path)
meta = SimpleNamespace(is_sphinx=False)
with (
patch("repomatic.docs.Metadata", return_value=meta),
patch("repomatic.docs._run_docs_tool") as mock_tool,
):
update_docs(_docs_config())
mock_tool.assert_not_called()
[docs]
def test_update_docs_runs_apidoc_for_autodoc_project(tmp_path, monkeypatch):
"""An autodoc-enabled Sphinx project regenerates its apidoc stubs."""
(tmp_path / "docs").mkdir()
monkeypatch.chdir(tmp_path)
meta = SimpleNamespace(is_sphinx=True, active_autodoc=True, uses_myst=True)
with (
patch("repomatic.docs.Metadata", return_value=meta),
patch("repomatic.docs.convert_rst_files_in_directory", return_value=[]),
patch("repomatic.docs._run_docs_tool") as mock_tool,
):
update_docs(_docs_config())
# Only the sphinx-apidoc phase runs: no update script, no directive blocks.
assert mock_tool.call_count == 1
assert mock_tool.call_args.args[0] == "sphinx-apidoc"
[docs]
def test_update_docs_skips_apidoc_without_active_autodoc(tmp_path, monkeypatch):
"""Without an active autodoc extension, no docs tooling runs."""
(tmp_path / "docs").mkdir()
monkeypatch.chdir(tmp_path)
meta = SimpleNamespace(is_sphinx=True, active_autodoc=False, uses_myst=False)
with (
patch("repomatic.docs.Metadata", return_value=meta),
patch("repomatic.docs._run_docs_tool") as mock_tool,
):
update_docs(_docs_config())
mock_tool.assert_not_called()
[docs]
def test_update_docs_check_skips_writes_and_propagates(tmp_path, monkeypatch):
"""`check=True` skips the write phases and runs the rest in check mode."""
docs = tmp_path / "docs"
docs.mkdir()
(docs / "docs_update.py").write_text("", encoding="UTF-8")
(docs / "page.md").write_text(
"<!-- mirror -->\n\n<!-- mirror-end -->\n", encoding="UTF-8"
)
monkeypatch.chdir(tmp_path)
meta = SimpleNamespace(is_sphinx=True, active_autodoc=True, uses_myst=True)
with (
patch("repomatic.docs.Metadata", return_value=meta),
patch(
"repomatic.docs.convert_rst_files_in_directory", return_value=[]
) as mock_convert,
patch("repomatic.docs._run_docs_tool", return_value=0) as mock_tool,
):
update_docs(_docs_config("docs/docs_update.py"), check=True)
# Phases 1-2 (which write) are skipped: no RST conversion, no apidoc.
mock_convert.assert_not_called()
labels = [call.args[0] for call in mock_tool.call_args_list]
assert "sphinx-apidoc" not in labels
# Phases 3-4 run in check mode, each forwarding the --check flag.
assert mock_tool.call_count == 2
for call in mock_tool.call_args_list:
assert call.kwargs.get("check") is True
assert "--check" in call.args
[docs]
def test_update_docs_check_raises_on_drift(tmp_path, monkeypatch):
"""A non-zero exit from a check phase raises with an "out of date" message."""
docs = tmp_path / "docs"
docs.mkdir()
(docs / "docs_update.py").write_text("", encoding="UTF-8")
monkeypatch.chdir(tmp_path)
meta = SimpleNamespace(is_sphinx=True, active_autodoc=False, uses_myst=False)
with (
patch("repomatic.docs.Metadata", return_value=meta),
patch("repomatic.docs._run_docs_tool", return_value=1),
pytest.raises(ClickException, match="out of date"),
):
update_docs(_docs_config("docs/docs_update.py"), check=True)