use std::fs;
use tempfile::TempDir;
use crate::common::{run_rsconstruct, setup_project_with_config, write_file};
#[test]
fn missing_src_dirs_scans_nothing() {
let temp_dir = setup_project_with_config(
"[processor.tera]\nsrc_dirs = [\"missing.templates\"]\n",
);
let output = run_rsconstruct(temp_dir.path(), &["build"]);
assert!(
output.status.success(),
"build should succeed with a missing src_dirs entry: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn missing_src_dirs_does_not_disturb_sibling_instances() {
let temp_dir = setup_project_with_config(concat!(
"[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");
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 = [\"absent_docs\"]\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());
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}"
);
}