2025-11-13 22:53:46 +01:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import tempfile
|
|
|
|
|
|
from pathlib import Path
|
2025-11-20 00:10:19 +01:00
|
|
|
|
from typing import Optional
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
2026-02-22 14:12:42 +01:00
|
|
|
|
from akkudoktoreos.core.coreabc import singletons_init
|
|
|
|
|
|
|
2025-11-13 22:53:46 +01:00
|
|
|
|
DIR_PROJECT_ROOT = Path(__file__).absolute().parent.parent
|
|
|
|
|
|
DIR_BUILD = DIR_PROJECT_ROOT / "build"
|
|
|
|
|
|
DIR_BUILD_DOCS = DIR_PROJECT_ROOT / "build" / "docs"
|
|
|
|
|
|
DIR_DOCS = DIR_PROJECT_ROOT / "docs"
|
|
|
|
|
|
DIR_SRC = DIR_PROJECT_ROOT / "src"
|
|
|
|
|
|
|
|
|
|
|
|
HASH_FILE = DIR_BUILD / ".sphinx_hash.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 16:38:53 +02:00
|
|
|
|
import os
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
from pathlib import Path
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
2026-07-15 16:38:53 +02:00
|
|
|
|
|
|
|
|
|
|
def find_sphinx_build() -> list[str]:
|
|
|
|
|
|
"""Return command to invoke sphinx-build via virtualenv, uv, or globally."""
|
|
|
|
|
|
candidates = []
|
|
|
|
|
|
|
|
|
|
|
|
# 1️⃣ Currently active virtualenv
|
|
|
|
|
|
venv = os.getenv("VIRTUAL_ENV")
|
|
|
|
|
|
if venv:
|
|
|
|
|
|
candidates.append(Path(venv))
|
|
|
|
|
|
|
|
|
|
|
|
# 2️⃣ uv‑managed virtualenv
|
|
|
|
|
|
uv_venv = Path(".uv") / "venv"
|
|
|
|
|
|
if uv_venv.exists():
|
|
|
|
|
|
candidates.append(uv_venv)
|
|
|
|
|
|
|
|
|
|
|
|
# 3️⃣ traditional .venv
|
|
|
|
|
|
dot_venv = Path(".venv")
|
|
|
|
|
|
if dot_venv.exists():
|
|
|
|
|
|
candidates.append(dot_venv)
|
|
|
|
|
|
|
|
|
|
|
|
# Check each candidate for the sphinx‑build binary
|
|
|
|
|
|
for base in candidates:
|
|
|
|
|
|
sphinx_build_path = base / ("Scripts" if os.name == "nt" else "bin") / (
|
|
|
|
|
|
"sphinx-build.exe" if os.name == "nt" else "sphinx-build"
|
|
|
|
|
|
)
|
|
|
|
|
|
if sphinx_build_path.exists():
|
|
|
|
|
|
return [str(sphinx_build_path)]
|
|
|
|
|
|
|
|
|
|
|
|
# 4️⃣ fallback to uv run sphinx‑build
|
|
|
|
|
|
try:
|
|
|
|
|
|
subprocess.run(
|
|
|
|
|
|
["uv", "run", "sphinx-build", "--version"],
|
|
|
|
|
|
check=True,
|
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
|
stderr=subprocess.DEVNULL,
|
|
|
|
|
|
)
|
|
|
|
|
|
return ["uv", "run", "sphinx-build"]
|
|
|
|
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 5️⃣ final fallback to system sphinx‑build
|
|
|
|
|
|
return ["sphinx-build"]
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
2025-11-20 00:10:19 +01:00
|
|
|
|
def sphinx_changed(version_and_hash) -> Optional[str]:
|
|
|
|
|
|
"""Returns new hash if any watched files have changed since last run.
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
Hash is stored in .sphinx_hash.json.
|
|
|
|
|
|
"""
|
2025-11-20 00:10:19 +01:00
|
|
|
|
new_hash = None
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
# Load previous hash
|
|
|
|
|
|
try:
|
|
|
|
|
|
previous = json.loads(HASH_FILE.read_text())
|
|
|
|
|
|
previous_hash = previous.get("hash")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
previous_hash = None
|
|
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
changed = (previous_hash != version_and_hash["hash_current"])
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
if changed:
|
|
|
|
|
|
new_hash = version_and_hash["hash_current"]
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
return new_hash
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSphinxDocumentation:
|
|
|
|
|
|
"""Test class to verify Sphinx documentation generation.
|
|
|
|
|
|
|
|
|
|
|
|
Ensures no major warnings are emitted.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-07-15 16:38:53 +02:00
|
|
|
|
SPHINX_CMD = find_sphinx_build() + [
|
2025-11-13 22:53:46 +01:00
|
|
|
|
"-M",
|
|
|
|
|
|
"html",
|
|
|
|
|
|
str(DIR_DOCS),
|
|
|
|
|
|
str(DIR_BUILD_DOCS),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def _cleanup_autosum_dirs(self):
|
|
|
|
|
|
"""Delete all *_autosum folders inside docs/."""
|
|
|
|
|
|
for folder in DIR_DOCS.rglob("*_autosum"):
|
|
|
|
|
|
if folder.is_dir():
|
|
|
|
|
|
shutil.rmtree(folder)
|
|
|
|
|
|
|
|
|
|
|
|
def _cleanup_build_dir(self):
|
|
|
|
|
|
"""Delete build/docs directory if present."""
|
|
|
|
|
|
if DIR_BUILD_DOCS.exists():
|
|
|
|
|
|
shutil.rmtree(DIR_BUILD_DOCS)
|
|
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
def test_sphinx_build(self, sphinx_changed: Optional[str], is_finalize: bool):
|
2025-11-13 22:53:46 +01:00
|
|
|
|
"""Build Sphinx documentation and ensure no major warnings appear in the build output."""
|
2026-02-22 14:12:42 +01:00
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
# Ensure docs folder exists
|
|
|
|
|
|
if not DIR_DOCS.exists():
|
|
|
|
|
|
pytest.skip(f"Skipping Sphinx build test - docs folder not present: {DIR_DOCS}")
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
if not sphinx_changed:
|
|
|
|
|
|
pytest.skip(f"Skipping Sphinx build — no relevant file changes detected: {HASH_FILE}")
|
|
|
|
|
|
|
2025-11-20 00:10:19 +01:00
|
|
|
|
if not is_finalize:
|
2026-02-22 14:12:42 +01:00
|
|
|
|
pytest.skip("Skipping Sphinx test — not finalize")
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
# Clean directories
|
|
|
|
|
|
self._cleanup_autosum_dirs()
|
|
|
|
|
|
self._cleanup_build_dir()
|
|
|
|
|
|
|
|
|
|
|
|
# Set environment for sphinx run (sphinx will make eos create a config file)
|
|
|
|
|
|
eos_tmp_dir = tempfile.TemporaryDirectory()
|
|
|
|
|
|
eos_dir = str(eos_tmp_dir.name)
|
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
|
env["EOS_DIR"] = eos_dir
|
|
|
|
|
|
env["EOS_CONFIG_DIR"] = eos_dir
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
process = subprocess.run(
|
|
|
|
|
|
self.SPHINX_CMD,
|
|
|
|
|
|
check=True,
|
|
|
|
|
|
env=env,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
2026-07-15 16:38:53 +02:00
|
|
|
|
cwd=DIR_PROJECT_ROOT, # use the existing constant
|
2025-11-13 22:53:46 +01:00
|
|
|
|
)
|
|
|
|
|
|
output = process.stdout + "\n" + process.stderr
|
|
|
|
|
|
returncode = process.returncode
|
2026-07-15 16:38:53 +02:00
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
|
output = e.stdout + "\n" + e.stderr if e.stdout else ""
|
|
|
|
|
|
returncode = e.returncode
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
output = f"Failed to execute command: {e}"
|
2025-11-13 22:53:46 +01:00
|
|
|
|
returncode = -1
|
|
|
|
|
|
|
|
|
|
|
|
# Remove temporary EOS_DIR
|
|
|
|
|
|
eos_tmp_dir.cleanup()
|
|
|
|
|
|
|
2026-02-22 14:12:42 +01:00
|
|
|
|
if returncode != 0:
|
|
|
|
|
|
pytest.fail(
|
|
|
|
|
|
f"Sphinx build failed with exit code {returncode}.\n"
|
|
|
|
|
|
f"{output}\n"
|
|
|
|
|
|
)
|
2025-11-13 22:53:46 +01:00
|
|
|
|
|
|
|
|
|
|
# Possible markers: ERROR: WARNING: TRACEBACK:
|
|
|
|
|
|
major_markers = ("ERROR:", "TRACEBACK:")
|
|
|
|
|
|
|
|
|
|
|
|
bad_lines = [
|
|
|
|
|
|
line for line in output.splitlines()
|
|
|
|
|
|
if any(marker in line for marker in major_markers)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
assert not bad_lines, f"Sphinx build contained errors:\n" + "\n".join(bad_lines)
|
2025-11-20 00:10:19 +01:00
|
|
|
|
|
|
|
|
|
|
# Update stored hash
|
|
|
|
|
|
HASH_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
HASH_FILE.write_text(json.dumps({"hash": sphinx_changed}, indent=2))
|