use crate::common::run_rsconstruct_with_env;
use std::fs;
use tempfile::TempDir;
fn setup_shared_site_project() -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
let mkdocs_script = project_path.join("mkdocs.sh");
fs::write(
&mkdocs_script,
concat!(
"#!/bin/bash\n",
"set -e\n",
"mkdir -p _site/assets\n",
"echo 'mkdocs-index' > _site/index.html\n",
"echo 'mkdocs-css' > _site/assets/style.css\n",
),
)
.unwrap();
let pandoc_script = project_path.join("pandoc.sh");
fs::write(
&pandoc_script,
concat!(
"#!/bin/bash\n",
"set -e\n",
"mkdir -p _site\n",
"echo 'pandoc-about' > _site/about.html\n",
),
)
.unwrap();
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&mkdocs_script, fs::Permissions::from_mode(0o755)).unwrap();
fs::set_permissions(&pandoc_script, fs::Permissions::from_mode(0o755)).unwrap();
}
fs::write(project_path.join("site.manifest"), "mkdocs\n").unwrap();
fs::write(project_path.join("about.manifest"), "pandoc\n").unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
concat!(
"[processor.creator.mkdocs]\n",
"command = \"./mkdocs.sh\"\n",
"src_extensions = [\"site.manifest\"]\n",
"src_dirs = [\".\"]\n",
"output_dirs = [\"_site\"]\n",
"\n",
"[processor.explicit.pandoc]\n",
"command = \"./pandoc.sh\"\n",
"inputs = [\"about.manifest\"]\n",
"output_files = [\"_site/about.html\"]\n",
"src_dirs = [\".\"]\n",
),
)
.unwrap();
temp_dir
}
#[test]
fn shared_dir_both_build_successfully() {
let temp_dir = setup_shared_site_project();
let project_path = temp_dir.path();
let output = run_rsconstruct_with_env(project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"Build should succeed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
assert!(
project_path.join("_site/index.html").exists(),
"mkdocs should have created _site/index.html"
);
assert!(
project_path.join("_site/assets/style.css").exists(),
"mkdocs should have created _site/assets/style.css"
);
assert!(
project_path.join("_site/about.html").exists(),
"pandoc (explicit) should have created _site/about.html; \
when the Creator runs after the Generator, it must NOT wipe the shared _site/ dir."
);
assert_eq!(
fs::read_to_string(project_path.join("_site/index.html"))
.unwrap()
.trim(),
"mkdocs-index"
);
assert_eq!(
fs::read_to_string(project_path.join("_site/about.html"))
.unwrap()
.trim(),
"pandoc-about"
);
}
#[test]
fn shared_dir_clean_and_restore_preserves_ownership() {
let temp_dir = setup_shared_site_project();
let project_path = temp_dir.path();
let build = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(
build.status.success(),
"Initial build failed: stderr={}",
String::from_utf8_lossy(&build.stderr)
);
let clean = run_rsconstruct_with_env(project_path, &["clean", "outputs"], &[("NO_COLOR", "1")]);
assert!(
clean.status.success(),
"Clean failed: stderr={}",
String::from_utf8_lossy(&clean.stderr)
);
assert!(
!project_path.join("_site").exists(),
"_site should be gone after clean"
);
let restore =
run_rsconstruct_with_env(project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(
restore.status.success(),
"Restore build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&restore.stdout),
String::from_utf8_lossy(&restore.stderr)
);
assert!(
project_path.join("_site/index.html").exists(),
"mkdocs file should be restored"
);
assert!(
project_path.join("_site/assets/style.css").exists(),
"mkdocs nested file should be restored"
);
assert!(
project_path.join("_site/about.html").exists(),
"pandoc file should be restored"
);
assert_eq!(
fs::read_to_string(project_path.join("_site/about.html"))
.unwrap()
.trim(),
"pandoc-about",
"about.html must still have pandoc's content — the Creator must not have claimed it"
);
}
#[test]
fn creator_tree_does_not_include_foreign_outputs() {
let temp_dir = setup_shared_site_project();
let project_path = temp_dir.path();
let build = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(
build.status.success(),
"Initial build failed: stderr={}",
String::from_utf8_lossy(&build.stderr)
);
let clean = run_rsconstruct_with_env(project_path, &["clean", "outputs"], &[("NO_COLOR", "1")]);
assert!(clean.status.success());
let restore_mkdocs_only = run_rsconstruct_with_env(
project_path,
&["build", "-p", "creator.mkdocs", "--verbose"],
&[("NO_COLOR", "1")],
);
assert!(
restore_mkdocs_only.status.success(),
"Partial build failed: stdout={}, stderr={}",
String::from_utf8_lossy(&restore_mkdocs_only.stdout),
String::from_utf8_lossy(&restore_mkdocs_only.stderr)
);
assert!(
project_path.join("_site/index.html").exists(),
"mkdocs's index.html should be restored"
);
assert!(
project_path.join("_site/assets/style.css").exists(),
"mkdocs's style.css should be restored"
);
assert!(
!project_path.join("_site/about.html").exists(),
"_site/about.html must NOT be restored by the Creator alone \
— it is owned by the explicit.pandoc processor and must not \
appear in the Creator's tree descriptor."
);
}
#[test]
fn two_processors_declaring_same_output_file_errors() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::write(project_path.join("a.manifest"), "a\n").unwrap();
fs::write(project_path.join("b.manifest"), "b\n").unwrap();
let script = project_path.join("touch.sh");
fs::write(&script, "#!/bin/bash\ntouch \"$1\"\n").unwrap();
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
}
fs::write(
project_path.join("rsconstruct.toml"),
concat!(
"[processor.explicit.a]\n",
"command = \"./touch.sh\"\n",
"args = [\"_site/index.html\"]\n",
"inputs = [\"a.manifest\"]\n",
"output_files = [\"_site/index.html\"]\n",
"src_dirs = [\".\"]\n",
"\n",
"[processor.explicit.b]\n",
"command = \"./touch.sh\"\n",
"args = [\"_site/index.html\"]\n",
"inputs = [\"b.manifest\"]\n",
"output_files = [\"_site/index.html\"]\n",
"src_dirs = [\".\"]\n",
),
)
.unwrap();
let output = run_rsconstruct_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(
!output.status.success(),
"Build must fail when two products declare the same output path. \
stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
assert!(
combined.to_lowercase().contains("output conflict"),
"Expected an 'Output conflict' error in the output, got: {}",
combined,
);
assert_eq!(
output.status.code(),
Some(4),
"Expected GraphError exit code (4), got {:?}. Output: {}",
output.status.code(),
combined
);
}