#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::needless_raw_string_hashes,
clippy::duration_suboptimal_units,
clippy::branches_sharing_code,
clippy::used_underscore_binding,
clippy::single_char_pattern,
clippy::ignore_without_reason,
clippy::cloned_ref_to_slice_refs,
clippy::doc_overindented_list_items,
clippy::match_wildcard_for_single_variants,
clippy::ignored_unit_patterns,
clippy::needless_collect,
clippy::unnecessary_map_or,
clippy::manual_flatten,
clippy::manual_strip,
clippy::future_not_send,
clippy::unnested_or_patterns,
clippy::no_effect_underscore_binding,
clippy::literal_string_with_formatting_args
)]
use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn ggen() -> Command {
Command::cargo_bin("ggen").expect("ggen binary not found")
}
const SCAFFOLD_FILES: &[&str] = &[
"ggen.toml",
"schema/domain.ttl",
"Makefile",
"templates/example.txt.tera",
"scripts/startup.sh",
".gitignore",
"README.md",
];
const SCAFFOLD_DIRS: &[&str] = &["schema", "templates", "scripts"];
fn stdout_json(bytes: &[u8]) -> Value {
let s = String::from_utf8_lossy(bytes);
let lines: Vec<&str> = s.lines().collect();
for start in 0..lines.len() {
if !lines[start].trim_start().starts_with('{') {
continue;
}
let mut depth: i32 = 0;
let mut buf = String::new();
for line in &lines[start..] {
buf.push_str(line);
buf.push('\n');
depth += line.matches('{').count() as i32;
depth -= line.matches('}').count() as i32;
if depth <= 0 {
break;
}
}
if let Ok(v) = serde_json::from_str::<Value>(buf.trim()) {
if v.get("status").is_some() {
return v;
}
}
}
panic!("init stdout had no JSON object carrying a \"status\" field; got: {s}")
}
#[test]
fn init_prepares_real_durable_scaffold_on_disk() {
let dir = TempDir::new().expect("tempdir");
let root = dir.path();
let assert = ggen()
.current_dir(root)
.args(["init", "--path", ".", "--skip_hooks", "true"])
.assert()
.success();
let json = stdout_json(&assert.get_output().stdout);
assert_eq!(
json["status"].as_str(),
Some("success"),
"fresh init must report status=success: {json}"
);
for f in SCAFFOLD_FILES {
assert!(
root.join(f).is_file(),
"fresh init must create durable scaffold file {f} on disk"
);
}
for d in SCAFFOLD_DIRS {
assert!(
root.join(d).is_dir(),
"fresh init must create scaffold directory {d}/ on disk"
);
}
let manifest = fs::read_to_string(root.join("ggen.toml")).expect("read ggen.toml");
assert!(
manifest.contains("[project]") && manifest.contains("[ontology]"),
"ggen.toml must be a real, parseable boundary with [project] and [ontology]: {manifest}"
);
let ttl = fs::read_to_string(root.join("schema/domain.ttl")).expect("read domain.ttl");
assert!(
ttl.contains("@prefix"),
"schema/domain.ttl must contain a real RDF seed (@prefix ...): {ttl}"
);
}
#[test]
fn init_refuses_to_clobber_existing_boundary_without_force() {
let dir = TempDir::new().expect("tempdir");
let root = dir.path();
ggen()
.current_dir(root)
.args(["init", "--path", ".", "--skip_hooks", "true"])
.assert()
.success();
let manifest_path = root.join("ggen.toml");
let sentinel = "# USER-EDITED-SENTINEL — must survive a no-force re-init\n";
fs::write(&manifest_path, sentinel).expect("overwrite manifest with sentinel");
let assert = ggen()
.current_dir(root)
.args(["init", "--path", ".", "--skip_hooks", "true"])
.assert();
let output = assert.get_output().clone();
let json = stdout_json(&output.stdout);
assert_eq!(
json["status"].as_str(),
Some("error"),
"re-init without --force must report status=error (no-clobber): {json}"
);
let err = json["error"].as_str().unwrap_or("");
assert!(
err.contains("already initialized"),
"re-init refusal must explain it is already initialized: {err}"
);
let after = fs::read_to_string(&manifest_path).expect("read manifest after re-init");
assert_eq!(
after, sentinel,
"no-force re-init must NOT overwrite the existing boundary"
);
}
#[test]
fn init_force_rescaffolds_but_preserves_user_files() {
let dir = TempDir::new().expect("tempdir");
let root = dir.path();
let user_readme = "# My Real Project\n\nDo not clobber me.\n";
let user_gitignore = "# my ignores\ntarget/\n*.log\n";
fs::write(root.join("README.md"), user_readme).expect("write user README");
fs::write(root.join(".gitignore"), user_gitignore).expect("write user .gitignore");
ggen()
.current_dir(root)
.args(["init", "--path", ".", "--skip_hooks", "true"])
.assert()
.success();
fs::write(root.join("ggen.toml"), "# stale\n").expect("mutate manifest");
let assert = ggen()
.current_dir(root)
.args([
"init",
"--path",
".",
"--skip_hooks",
"true",
"--force",
"true",
])
.assert()
.success()
.stdout(predicate::str::contains("\"status\": \"success\""));
let json = stdout_json(&assert.get_output().stdout);
let manifest = fs::read_to_string(root.join("ggen.toml")).expect("read manifest");
assert!(
manifest.contains("[project]") && manifest.contains("[ontology]"),
"--force must restore the canonical ggen.toml seed, not leave the stale stub: {manifest}"
);
assert_eq!(
fs::read_to_string(root.join("README.md")).expect("read README"),
user_readme,
"--force must PRESERVE the user's existing README.md"
);
assert_eq!(
fs::read_to_string(root.join(".gitignore")).expect("read .gitignore"),
user_gitignore,
"--force must PRESERVE the user's existing .gitignore"
);
let preserved: Vec<&str> = json["files_preserved"]
.as_array()
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
assert!(
preserved.contains(&"README.md") && preserved.contains(&".gitignore"),
"force re-init must report README.md and .gitignore as preserved: {json}"
);
}
#[test]
fn init_into_nonexistent_subdir_creates_boundary_there() {
let dir = TempDir::new().expect("tempdir");
let root = dir.path();
let nested = "fresh-project";
assert!(
!root.join(nested).exists(),
"precondition: target subdir must not exist yet"
);
ggen()
.current_dir(root)
.args(["init", "--path", nested, "--skip_hooks", "true"])
.assert()
.success();
let proj = root.join(nested);
for f in SCAFFOLD_FILES {
assert!(
proj.join(f).is_file(),
"init --path {nested} must place {f} under the requested subdir"
);
}
assert!(
!root.join("ggen.toml").exists(),
"init --path <subdir> must not scaffold into the parent working dir"
);
}
#[allow(dead_code)]
fn _path_import_anchor(_p: &Path) {}