use crate::common::{make_executable, run_rsconstruct_json_with_env};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_tool(project: &Path, name: &str, body: &str) {
let bin_dir = project.join("toolbin");
fs::create_dir_all(&bin_dir).unwrap();
let tool = bin_dir.join(name);
fs::write(&tool, format!("#!/bin/sh\n{body}")).unwrap();
make_executable(&tool);
}
fn build(project: &Path) -> crate::common::BuildResult {
let path_env = format!(
"{}:{}",
project.join("toolbin").display(),
std::env::var("PATH").unwrap_or_default()
);
run_rsconstruct_json_with_env(
project,
&["build"],
&[("NO_COLOR", "1"), ("PATH", &path_env)],
)
}
#[test]
fn generated_source_is_analyzed_before_its_consumer_runs() {
let temp_dir = TempDir::new().unwrap();
let project = temp_dir.path();
write_tool(
project,
"mkdoc",
"{ cat \"$1\"; echo; echo ''; } > \"$2\"\n",
);
write_tool(project, "copy", "cp \"$1\" \"$2\"\n");
write_tool(
project,
"needpic",
"test -f img/pic.png || { echo 'img/pic.png missing' >&2; exit 1; }\ncp \"$1\" \"$2\"\n",
);
fs::write(
project.join("rsconstruct.toml"),
r#"[build]
hash_tool_versions = false
[analyzer.markdown]
[processor.generator.generic.doc]
command = "mkdoc"
output_dir = "gen"
output_extension = "md"
batch = false
src_extensions = [".txt"]
src_dirs = ["src"]
[processor.generator.generic.stage]
command = "copy"
output_dir = "stage"
output_extension = "mid"
batch = false
src_extensions = [".raw"]
src_dirs = ["raw"]
[processor.generator.generic.pic]
command = "copy"
output_dir = "img"
output_extension = "png"
batch = false
src_extensions = [".mid"]
src_dirs = ["stage"]
[processor.generator.generic.page]
command = "needpic"
output_dir = "out/page"
output_extension = "html"
batch = false
src_extensions = [".md"]
src_dirs = ["gen"]
"#,
)
.unwrap();
fs::create_dir_all(project.join("src")).unwrap();
fs::create_dir_all(project.join("raw")).unwrap();
fs::write(project.join("src/doc.txt"), "# Doc\n").unwrap();
fs::write(project.join("raw/pic.raw"), "pixels v1\n").unwrap();
fs::write(project.join("README.md"), "# readme\n").unwrap();
let first = build(project);
assert!(
first.exit_success,
"clean build must order the page after the picture it references: {:?}",
first.errors
);
assert_eq!(first.success, 4, "all four products build: {first:?}");
let second = build(project);
assert!(second.exit_success, "{:?}", second.errors);
assert_eq!(
second.success, 0,
"the page was cached under its full input set, so nothing reruns: {second:?}"
);
fs::write(project.join("raw/pic.raw"), "pixels v2\n").unwrap();
let third = build(project);
assert!(third.exit_success, "{:?}", third.errors);
let status_of = |processor: &str| {
third
.products
.iter()
.find(|p| p.processor == processor)
.map(|p| p.status.as_str())
};
assert_eq!(
status_of("processor.generator.generic.page"),
Some("success"),
"a changed picture must rebuild the page: {third:?}"
);
assert_eq!(
status_of("processor.generator.generic.doc"),
Some("skipped"),
"the document itself did not change: {third:?}"
);
}
#[test]
fn own_output_matched_by_dep_inputs_is_not_an_input() {
let temp_dir = TempDir::new().unwrap();
let project = temp_dir.path();
write_tool(project, "copy", "cp \"$1\" \"$2\"\n");
fs::write(
project.join("rsconstruct.toml"),
r#"[build]
hash_tool_versions = false
[processor.generator.generic]
command = "copy"
output_dir = "out/g"
output_extension = "txt"
batch = false
src_extensions = [".src"]
src_dirs = ["src"]
dep_inputs = ["out/g/*.txt"]
"#,
)
.unwrap();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/a.src"), "a\n").unwrap();
let first = build(project);
assert!(first.exit_success, "{:?}", first.errors);
assert_eq!(first.success, 1);
let second = build(project);
assert!(second.exit_success, "{:?}", second.errors);
assert_eq!(
second.success, 0,
"the product's own output must not make its key unstable: {second:?}"
);
assert_eq!(second.skipped, 1, "{second:?}");
}
#[test]
fn input_rewritten_with_mtime_restored_rebuilds() {
let temp_dir = TempDir::new().unwrap();
let project = temp_dir.path();
write_tool(project, "copy", "cp \"$1\" \"$2\"\n");
fs::write(
project.join("rsconstruct.toml"),
r#"[build]
hash_tool_versions = false
[processor.generator.generic]
command = "copy"
output_dir = "out/g"
output_extension = "txt"
batch = false
src_extensions = [".src"]
src_dirs = ["src"]
"#,
)
.unwrap();
fs::create_dir_all(project.join("src")).unwrap();
let input = project.join("src/a.src");
fs::write(&input, "aaaa\n").unwrap();
let old_mtime = std::time::SystemTime::now() - std::time::Duration::from_secs(3600);
let set_old_mtime = || {
fs::File::options()
.write(true)
.open(&input)
.unwrap()
.set_modified(old_mtime)
.unwrap();
};
set_old_mtime();
std::thread::sleep(std::time::Duration::from_millis(2_500));
let first = build(project);
assert!(first.exit_success, "{:?}", first.errors);
assert_eq!(first.success, 1);
fs::write(&input, "bbbb\n").unwrap();
set_old_mtime();
let second = build(project);
assert!(second.exit_success, "{:?}", second.errors);
assert_eq!(
second.success, 1,
"rewritten content must rebuild despite the restored mtime: {second:?}"
);
assert_eq!(
fs::read_to_string(project.join("out/g/a.txt")).unwrap(),
"bbbb\n"
);
}
#[test]
fn input_edited_while_running_is_not_cached() {
let temp_dir = TempDir::new().unwrap();
let project = temp_dir.path();
write_tool(
project,
"copy_then_touch",
"cp \"$1\" \"$2\"\necho edited >> \"$1\"\n",
);
fs::write(
project.join("rsconstruct.toml"),
r#"[build]
hash_tool_versions = false
[processor.generator.generic]
command = "copy_then_touch"
output_dir = "out/g"
output_extension = "txt"
batch = false
src_extensions = [".src"]
src_dirs = ["src"]
"#,
)
.unwrap();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(project.join("src/a.src"), "a\n").unwrap();
let first = build(project);
assert!(first.exit_success, "{:?}", first.errors);
assert_eq!(first.success, 1);
let second = build(project);
assert!(second.exit_success, "{:?}", second.errors);
assert_eq!(
second.success, 1,
"outputs built from content that was edited mid-run must not be reused: {second:?}"
);
}