use crate::common::{run_rsconstruct, setup_project_with_config, write_file};
use std::fs;
use tempfile::TempDir;
#[test]
fn missing_src_dirs_is_a_config_error() {
let temp_dir =
setup_project_with_config("[processor.tera]\nsrc_dirs = [\"missing.templates\"]\n");
let output = run_rsconstruct(temp_dir.path(), &["build"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(2),
"a missing src_dirs entry must fail as a config error: {stderr}"
);
assert!(
stderr.contains("processor.tera") && stderr.contains("missing.templates"),
"error must name the processor and the entry: {stderr}"
);
}
#[test]
fn allow_missing_src_dirs_does_not_disturb_sibling_instances() {
let temp_dir = setup_project_with_config(concat!(
"[build]\n",
"allow_missing_src_dirs = true\n",
"\n",
"[processor.tera.real]\n",
"src_dirs = [\"tera.templates\"]\n",
"\n",
"[processor.tera.ghost]\n",
"src_dirs = [\"missing.templates\"]\n",
));
let project = temp_dir.path();
write_file(project, "tera.templates/hello.txt.tera", "hello");
let output = run_rsconstruct(project, &["build"]);
assert!(
output.status.success(),
"build should skip the missing dir: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(project.join("hello.txt").exists());
}
#[test]
fn tool_check_is_deferred_to_processors_with_products() {
let temp_dir = setup_project_with_config(concat!(
"[processor.tera]\n",
"src_dirs = [\"tera.templates\"]\n",
"\n",
"[processor.script.ghost_check]\n",
"command = \"scripts/does_not_exist.py\"\n",
"src_dirs = [\"ghost_src\"]\n",
"src_extensions = [\".md\"]\n",
));
let project = temp_dir.path();
write_file(project, "tera.templates/out.txt.tera", "ok");
fs::create_dir_all(project.join("ghost_src")).unwrap();
let output = run_rsconstruct(project, &["build"]);
assert!(
output.status.success(),
"tool-less zero-product processor should not fail the build: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(project.join("out.txt").exists());
}
#[test]
fn missing_tool_fails_when_processor_has_products() {
let temp_dir = setup_project_with_config(concat!(
"[processor.script.real_check]\n",
"command = \"scripts/does_not_exist.py\"\n",
"src_dirs = [\"checked\"]\n",
"src_extensions = [\".md\"]\n",
));
let project = temp_dir.path();
write_file(project, "checked/doc.md", "# doc");
let output = run_rsconstruct(project, &["build"]);
assert!(
!output.status.success(),
"missing tool must fail when the processor has products"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Missing required tools"),
"expected strict tool preflight, got: {stderr}"
);
}
#[test]
fn tools_install_skips_repo_local_script_commands() {
let temp_dir = setup_project_with_config(concat!(
"[processor.script.check_md]\n",
"command = \"scripts/check_md.py\"\n",
"src_dirs = [\"checked\"]\n",
"src_extensions = [\".md\"]\n",
));
let project = temp_dir.path();
write_file(project, "checked/doc.md", "# doc");
write_file(project, "scripts/check_md.py", "#!/usr/bin/env python3\n");
let output = run_rsconstruct(project, &["tools", "install"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"a repo-local script command is not an installable tool: {stderr}"
);
assert!(
!stderr.contains("No install method"),
"script path must not be resolved against the tool registry: {stderr}"
);
}
#[test]
fn local_overlay_disables_processor() {
let temp_dir = setup_project_with_config("[processor.tera]\nsrc_dirs = [\"tera.templates\"]\n");
let project = temp_dir.path();
write_file(project, "tera.templates/gen.txt.tera", "generated");
fs::write(
project.join("rsconstruct.local.toml"),
"[processor.tera]\nenabled = false\n",
)
.unwrap();
let output = run_rsconstruct(project, &["build"]);
assert!(
output.status.success(),
"build failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(!project.join("gen.txt").exists());
}
#[test]
fn local_overlay_adds_sections() {
let temp_dir = setup_project_with_config("[processor.tera]\nsrc_dirs = [\"tera.templates\"]\n");
let project = temp_dir.path();
write_file(project, "tera.templates/gen.txt.tera", "generated");
fs::write(
project.join("rsconstruct.local.toml"),
concat!(
"[build]\n",
"max_discovery_passes = 7\n",
"\n",
"[processor.zspell]\n",
"src_dirs = [\"empty_docs\"]\n",
),
)
.unwrap();
fs::create_dir_all(project.join("empty_docs")).unwrap();
let output = run_rsconstruct(project, &["build"]);
assert!(
output.status.success(),
"build failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(project.join("gen.txt").exists());
let cfg = run_rsconstruct(project, &["processors", "config", "zspell"]);
assert!(
cfg.status.success(),
"overlay-added [processor.zspell] should exist in the merged config: {}",
String::from_utf8_lossy(&cfg.stderr)
);
}
#[test]
fn local_overlay_field_wins_over_main() {
let temp_dir = setup_project_with_config("[processor.tera]\ndep_inputs = [\"config/a.py\"]\n");
let project = temp_dir.path();
fs::write(
project.join("rsconstruct.local.toml"),
"[processor.tera]\ndep_inputs = [\"config/b.py\"]\n",
)
.unwrap();
let output = run_rsconstruct(project, &["processors", "config", "tera"]);
assert!(
output.status.success(),
"processors config failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("b.py"),
"local value should win, got: {stdout}"
);
assert!(
!stdout.contains("a.py"),
"main value should be replaced, got: {stdout}"
);
}
#[test]
fn local_overlay_without_main_config_fails() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("rsconstruct.local.toml"),
"[processor.tera]\n",
)
.unwrap();
let output = run_rsconstruct(temp_dir.path(), &["build"]);
assert!(
!output.status.success(),
"build should fail without a main config"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("rsconstruct.local.toml found without rsconstruct.toml"),
"expected overlay-without-main error, got: {stderr}"
);
}