use crate::common::{
BuildResult, require_tool, run_rsconstruct_json_with_env, run_rsconstruct_with_env, write_file,
};
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
const CONF_PY: &str = "import os
import sys
sys.path.insert(0, os.path.abspath(\"../src\"))
project = \"Fixture\"
extensions = [
\"sphinx.ext.autodoc\",
\"sphinx.ext.viewcode\",
\"sphinx.ext.githubpages\",
]
html_static_path = [\"_static\"]
";
const INDEX_RST: &str = "Fixture\n=======\n\n.. image:: pic.svg\n\n.. toctree::\n\n api\n";
const API_RST: &str = "API\n===\n\n.. automodule:: fixturepkg.core\n :members:\n";
const CORE_PY: &str = "def add(a, b):\n \"\"\"Add two numbers.\"\"\"\n return a + b\n";
const CONFIG: &str = "[processor.mass_generator.sphinx]\ndep_inputs = [\"src/fixturepkg/*.py\"]\n";
fn setup(config: &str) -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let root = temp_dir.path();
write_file(root, "sphinx/conf.py", CONF_PY);
write_file(root, "sphinx/index.rst", INDEX_RST);
write_file(root, "sphinx/api.rst", API_RST);
write_file(
root,
"sphinx/pic.svg",
"<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n",
);
write_file(root, "sphinx/_static/custom.css", "body { color: red }\n");
write_file(root, "src/fixturepkg/__init__.py", "");
write_file(root, "src/fixturepkg/core.py", CORE_PY);
write_file(root, "rsconstruct.toml", config);
temp_dir
}
fn files_below(dir: &Path) -> BTreeSet<String> {
let mut out = BTreeSet::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(current) = stack.pop() {
for entry in fs::read_dir(¤t).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
stack.push(path);
} else {
out.insert(
path.strip_prefix(dir)
.unwrap()
.to_string_lossy()
.into_owned(),
);
}
}
}
out
}
fn planned(root: &Path) -> BTreeSet<String> {
let output = run_rsconstruct_with_env(
root,
&["graph", "show", "--format", "json"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"graph show failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let graph: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
graph["products"]
.as_array()
.unwrap()
.iter()
.flat_map(|p| p["outputs"].as_array().unwrap().clone())
.map(|o| {
o.as_str()
.unwrap()
.strip_prefix("docs/")
.expect("every planned output lives in docs/")
.to_string()
})
.collect()
}
fn build_ok(root: &Path) -> BuildResult {
let result = run_rsconstruct_json_with_env(root, &["build"], &[("NO_COLOR", "1")]);
assert!(result.exit_success, "build failed: {:?}", result.products);
result
}
fn assert_no_scratch_left(root: &Path) {
let dir = root.join(".rsconstruct/mass_generator");
let left: Vec<String> = fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| !name.ends_with(".json"))
.collect();
assert!(left.is_empty(), "scratch directories left behind: {left:?}");
}
#[test]
fn sphinx_plan_matches_what_sphinx_writes() {
require_tool("sphinx-build");
let temp_dir = setup(CONFIG);
let root = temp_dir.path();
let status = std::process::Command::new("sphinx-build")
.current_dir(root)
.args([
"-q",
"-b",
"html",
"-E",
"-a",
"-d",
"reference-doctrees",
"sphinx",
"reference",
])
.output()
.unwrap();
assert!(
status.status.success(),
"fixture must build with plain sphinx-build: {}",
String::from_utf8_lossy(&status.stderr)
);
let written = files_below(&root.join("reference"));
fs::remove_dir_all(root.join("reference")).unwrap();
fs::remove_dir_all(root.join("reference-doctrees")).unwrap();
let plan = planned(root);
let missing: Vec<_> = written.difference(&plan).collect();
let extra: Vec<_> = plan.difference(&written).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"plan and sphinx-build disagree\n written by sphinx-build, not planned: {missing:?}\n planned, not written: {extra:?}"
);
for expected in [
"index.html",
"api.html",
"_sources/api.rst.txt",
"_modules/index.html",
"_modules/fixturepkg/core.html",
"py-modindex.html",
"genindex.html",
"search.html",
"searchindex.js",
"objects.inv",
".buildinfo",
".nojekyll",
"_images/pic.svg",
"_static/custom.css",
"_static/basic.css",
] {
assert!(
plan.contains(expected),
"{expected} missing from the plan: {plan:?}"
);
}
assert_no_scratch_left(root);
}
#[test]
fn sphinx_products_build_restore_and_follow_their_sources() {
require_tool("sphinx-build");
let temp_dir = setup(CONFIG);
let root = temp_dir.path();
let count = planned(root).len();
let result = build_ok(root);
assert_eq!(
result.count_status("success"),
count,
"{:?}",
result.products
);
assert!(root.join("docs/index.html").is_file());
assert!(root.join("docs/_modules/fixturepkg/core.html").is_file());
assert!(root.join("docs/.nojekyll").is_file());
assert!(
!root.join("docs/.doctrees").exists(),
"doctrees stay out of the output directory"
);
assert_no_scratch_left(root);
let again = build_ok(root);
assert_eq!(again.count_status("success"), 0, "{:?}", again.products);
let clean = run_rsconstruct_with_env(root, &["clean", "outputs"], &[("NO_COLOR", "1")]);
assert!(
clean.status.success(),
"{}",
String::from_utf8_lossy(&clean.stderr)
);
assert!(!root.join("docs/index.html").exists());
let restored = build_ok(root);
assert_eq!(restored.restored, count, "{:?}", restored.products);
assert_eq!(
restored.count_status("success"),
0,
"{:?}",
restored.products
);
assert!(root.join("docs/_static/custom.css").is_file());
write_file(
root,
"src/fixturepkg/core.py",
"def add(a, b):\n \"\"\"Add two numbers.\"\"\"\n return a + b\n\n\ndef subtract(a, b):\n \"\"\"Subtract the second number.\"\"\"\n return a - b\n",
);
let changed = build_ok(root);
assert_eq!(
changed.count_status("success"),
count,
"{:?}",
changed.products
);
assert!(
fs::read_to_string(root.join("docs/api.html"))
.unwrap()
.contains("Subtract the second number"),
"api.html must be Sphinx's fresh output, not the cached page"
);
write_file(root, "sphinx/extra.rst", "Extra\n=====\n\nMore.\n");
write_file(
root,
"sphinx/index.rst",
"Fixture\n=======\n\n.. image:: pic.svg\n\n.. toctree::\n\n api\n extra\n",
);
let extended = build_ok(root);
assert_eq!(
extended.count_status("success"),
count + 2,
"{:?}",
extended.products
);
assert!(root.join("docs/extra.html").is_file());
assert!(root.join("docs/_sources/extra.rst.txt").is_file());
assert_no_scratch_left(root);
}
#[test]
fn sphinx_refuses_the_arguments_it_owns() {
let temp_dir =
setup("[processor.mass_generator.sphinx]\nargs = [\"-W\", \"-Dproject=Other\"]\n");
let output = run_rsconstruct_with_env(temp_dir.path(), &["build"], &[("NO_COLOR", "1")]);
assert!(!output.status.success(), "a -D override must be refused");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("args entry '-Dproject=Other' is not allowed"),
"{stderr}"
);
}