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
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_sphinx_build() -> str:
|
|
|
|
|
venv = os.getenv("VIRTUAL_ENV")
|
|
|
|
|
paths = [Path(venv)] if venv else []
|
|
|
|
|
paths.append(DIR_PROJECT_ROOT / ".venv")
|
|
|
|
|
|
|
|
|
|
for base in paths:
|
|
|
|
|
cmd = base / ("Scripts" if os.name == "nt" else "bin") / ("sphinx-build.exe" if os.name == "nt" else "sphinx-build")
|
|
|
|
|
if cmd.exists():
|
|
|
|
|
return str(cmd)
|
|
|
|
|
return "sphinx-build"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
SPHINX_CMD = [
|
|
|
|
|
find_sphinx_build(),
|
|
|
|
|
"-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."""
|
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:
|
|
|
|
|
pytest.skip("Skipping Sphinx test — not full run")
|
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:
|
|
|
|
|
# Run sphinx-build
|
|
|
|
|
project_dir = Path(__file__).parent.parent
|
|
|
|
|
process = subprocess.run(
|
|
|
|
|
self.SPHINX_CMD,
|
|
|
|
|
check=True,
|
|
|
|
|
env=env,
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
text=True,
|
|
|
|
|
cwd=project_dir,
|
|
|
|
|
)
|
|
|
|
|
# Combine output
|
|
|
|
|
output = process.stdout + "\n" + process.stderr
|
|
|
|
|
returncode = process.returncode
|
|
|
|
|
except:
|
|
|
|
|
output = f"ERROR: Could not start sphinx-build - {self.SPHINX_CMD}"
|
|
|
|
|
returncode = -1
|
|
|
|
|
|
|
|
|
|
# Remove temporary EOS_DIR
|
|
|
|
|
eos_tmp_dir.cleanup()
|
|
|
|
|
|
|
|
|
|
assert returncode == 0
|
|
|
|
|
|
|
|
|
|
# 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))
|