#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::path::Path;
use chicago_tdd_tools::cli_proof::CliHarness;
use tempfile::TempDir;
fn ggen_bin() -> std::path::PathBuf {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_ggen") {
let p = std::path::PathBuf::from(path);
if p.exists() {
return p;
}
}
let target_root = std::env::var_os("CARGO_TARGET_DIR")
.map(std::path::PathBuf::from)
.or_else(|| {
let manifest_dir =
std::env::var_os("CARGO_MANIFEST_DIR").map(std::path::PathBuf::from)?;
let mut dir: &std::path::Path = manifest_dir.as_path();
loop {
if dir.join("Cargo.lock").exists() {
return Some(dir.join("target"));
}
match dir.parent() {
Some(p) => dir = p,
None => return None,
}
}
});
if let Some(target) = target_root {
for profile in &["debug", "release"] {
let candidate = target.join(profile).join("ggen");
if candidate.is_file() {
return candidate;
}
let candidate_exe = target.join(profile).join("ggen.exe");
if candidate_exe.is_file() {
return candidate_exe;
}
}
}
panic!(
"could not resolve the `ggen` binary: CARGO_BIN_EXE_ggen unset and no \
target/debug/ggen found; build it with `cargo build -p ggen-cli-lib --bin ggen`"
);
}
const GGEN_TOML: &str = r#"
[project]
name = "demo"
[ontology]
source = "ontology.ttl"
[templates]
dir = "templates"
"#;
const ONTOLOGY: &str = r#"
@prefix ex: <http://example.org/> .
ex:alice ex:name "alice" .
"#;
fn scaffold(root: &Path) {
std::fs::write(root.join("ggen.toml"), GGEN_TOML).expect("write ggen.toml");
std::fs::write(root.join("ontology.ttl"), ONTOLOGY).expect("write ontology");
std::fs::create_dir_all(root.join("templates")).expect("mkdir templates");
}
fn write_template(root: &Path, name: &str, content: &str) {
std::fs::write(root.join("templates").join(name), content).expect("write template");
}
fn run_sync(root: &Path) -> chicago_tdd_tools::cli_proof::CliOutput {
CliHarness::from_path(ggen_bin())
.args(["sync", "run"])
.current_dir(root)
.run()
.expect("spawn ggen sync run")
}
#[test]
fn to_path_escaping_the_project_root_is_refused() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(dir.path(), "f.tmpl", "---\nto: ../escape.txt\n---\nnope\n");
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("FM-WRITE-002");
assert!(!dir
.path()
.parent()
.expect("parent")
.join("escape.txt")
.exists());
}
#[test]
fn unless_exists_skips_a_file_that_already_exists() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(dir.path().join("out.txt"), "human content\n").expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\nunless_exists: true\n---\ngenerated content\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("out.txt"),
"human content\n",
"unless_exists must never touch a pre-existing file"
);
}
#[test]
fn skip_if_skips_when_the_marker_is_already_present() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(
dir.path().join("out.txt"),
"// DO-NOT-REGENERATE\nhand code\n",
)
.expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\nskip_if: \"DO-NOT-REGENERATE\"\n---\ngenerated content\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("out.txt"),
"// DO-NOT-REGENERATE\nhand code\n"
);
}
#[test]
fn freeze_always_skips_once_the_target_exists() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(dir.path().join("out.txt"), "scaffolded once\n").expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\nfreeze_policy: always\n---\nnew scaffold\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("out.txt"),
"scaffolded once\n"
);
}
#[test]
fn freeze_checksum_regenerates_until_a_human_edits_it_then_protects_it() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\nfreeze_policy: checksum\nfreeze_slots_dir: .slots\nforce: true\n---\ngenerated v1\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("v1"),
"generated v1\n"
);
std::fs::write(dir.path().join("out.txt"), "hand-edited\n").expect("human edit");
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("still hand-edited"),
"hand-edited\n",
"freeze_policy: checksum must detect the human edit and skip"
);
}
#[test]
fn inject_before_marker_inserts_content_and_backs_up_first() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(
dir.path().join("out.txt"),
"line one\n// MARKER\nline two\n",
)
.expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\ninject: true\nbefore: \"MARKER\"\nbackup: true\n---\ninjected line\n",
);
let _ = run_sync(dir.path()).assert_success();
let after = std::fs::read_to_string(dir.path().join("out.txt")).expect("out.txt");
assert_eq!(after, "line one\ninjected line\n// MARKER\nline two\n");
let backup = std::fs::read_to_string(dir.path().join("out.txt.bak")).expect("backup");
assert_eq!(
backup, "line one\n// MARKER\nline two\n",
"backup must hold the pre-inject content"
);
}
#[test]
fn inject_into_a_missing_target_is_refused() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\ninject: true\n---\ninjected\n",
);
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("FM-WRITE-003");
assert!(!dir.path().join("out.txt").exists());
}
#[test]
fn inject_with_a_missing_marker_is_refused() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(dir.path().join("out.txt"), "no markers here\n").expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\ninject: true\nbefore: \"NOPE\"\n---\ninjected\n",
);
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("FM-WRITE-004");
}
#[test]
fn force_overwrites_differing_content_and_backs_up_first() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
std::fs::write(dir.path().join("out.txt"), "old content\n").expect("pre-existing");
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\nforce: true\nbackup: true\n---\nnew content\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("out.txt"),
"new content\n"
);
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt.bak")).expect("backup"),
"old content\n"
);
}
#[test]
fn default_semantics_write_then_skip_then_refuse_on_drift() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(
dir.path(),
"f.tmpl",
"---\nto: out.txt\n---\nstable content\n",
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("first write"),
"stable content\n"
);
let _ = run_sync(dir.path()).assert_success();
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("unchanged"),
"stable content\n"
);
std::fs::write(dir.path().join("out.txt"), "manually diverged\n").expect("manual edit");
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("FM-WRITE-005");
assert_eq!(
std::fs::read_to_string(dir.path().join("out.txt")).expect("still diverged"),
"manually diverged\n",
"a refused write must never clobber the differing on-disk content"
);
}
#[test]
fn oversized_rendered_output_is_refused_over_the_cli_boundary() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(
dir.path(),
"huge.tmpl",
"---\nto: out.txt\n---\n{% for i in range(end=200000) %}0123456789012345678901234567890123456789012345678901234567890123\n{% endfor %}",
);
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("byte cap");
assert!(!dir.path().join("out.txt").exists());
}
#[test]
fn a_render_failure_in_one_template_leaves_no_writes_from_others_over_the_cli_boundary() {
let dir = TempDir::new().expect("tempdir");
scaffold(dir.path());
write_template(dir.path(), "a_good.tmpl", "---\nto: good.txt\n---\nfine\n");
write_template(
dir.path(),
"b_bad.tmpl",
"---\nto: bad.txt\n---\n{{ row.nuon }}\n",
);
let _ = run_sync(dir.path())
.assert_failure()
.assert_stderr_contains("render failed");
assert!(
!dir.path().join("good.txt").exists(),
"atomic sync must not write a_good's output"
);
assert!(!dir.path().join("bad.txt").exists());
}