use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use gize_core::ModelSpec;
use gize_generator::{Options, Plan, Report, Writer, scaffold};
const TS: &str = "20260101000000";
fn unique_tmpdir() -> PathBuf {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("gize-it-{}-{nanos}-{n}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
dir
}
fn apply(root: &Path, plan: &Plan, opts: Options) -> Report {
Writer::new(opts).apply(root, plan).expect("apply plan")
}
fn product_model() -> ModelSpec {
ModelSpec::parse(
"Product",
&[
"name:String".to_string(),
"price:i32".to_string(),
"active:bool".to_string(),
],
)
.unwrap()
}
#[test]
fn new_project_materializes_expected_tree_on_disk() {
let root = unique_tmpdir();
apply(
&root,
&scaffold::new_project("shop", true, TS),
Options::default(),
);
for rel in [
"Cargo.toml",
"gize.toml",
".env.example",
".gitignore",
"src/main.rs",
"src/state.rs",
"src/router.rs",
"src/config/mod.rs",
"src/app/mod.rs",
"src/app/users/mod.rs",
"src/app/users/model.rs",
"src/app/users/handler.rs",
] {
assert!(root.join(rel).is_file(), "expected generated file `{rel}`");
}
assert!(
root.join(format!("migrations/{TS}_create_users.sql"))
.is_file(),
"expected the users migration"
);
for dir in ["src/database", "src/middleware", "src/shared", "migrations"] {
assert!(root.join(dir).is_dir(), "expected directory `{dir}`");
}
}
#[test]
fn regenerating_is_idempotent_and_preserves_hand_edits() {
let root = unique_tmpdir();
let plan = scaffold::new_project("shop", true, TS);
apply(&root, &plan, Options::default());
let edited = root.join("src/app/users/service.rs");
let marker = "// hand-written business logic — must survive re-generation\n";
let original = fs::read_to_string(&edited).unwrap();
fs::write(&edited, format!("{marker}{original}")).unwrap();
let report = apply(&root, &plan, Options::default());
assert!(report.created.is_empty(), "re-run should create nothing");
assert!(
report.overwritten.is_empty(),
"re-run without --force must overwrite nothing"
);
assert!(
!report.skipped.is_empty(),
"existing files should be skipped"
);
assert!(
fs::read_to_string(&edited).unwrap().starts_with(marker),
"the hand edit must survive re-generation"
);
}
#[test]
fn force_overwrites_and_dry_run_writes_nothing() {
let root = unique_tmpdir();
let plan = scaffold::new_project("shop", false, TS);
apply(&root, &plan, Options::default());
let sentinel = root.join("Cargo.toml");
let generated = fs::read_to_string(&sentinel).unwrap();
fs::write(&sentinel, "TOUCHED").unwrap();
let report = apply(
&root,
&plan,
Options {
force: true,
dry_run: true,
},
);
assert!(
!report.overwritten.is_empty(),
"dry-run should still report overwrites"
);
assert_eq!(
fs::read_to_string(&sentinel).unwrap(),
"TOUCHED",
"dry-run must not write to disk"
);
apply(
&root,
&plan,
Options {
force: true,
dry_run: false,
},
);
assert_eq!(fs::read_to_string(&sentinel).unwrap(), generated);
}
#[test]
fn make_crud_lands_resource_with_declared_fields() {
let root = unique_tmpdir();
apply(
&root,
&scaffold::new_project("shop", false, TS),
Options::default(),
);
apply(
&root,
&scaffold::make_crud(&product_model(), TS),
Options::default(),
);
let model_rs = fs::read_to_string(root.join("src/app/products/model.rs")).unwrap();
assert!(model_rs.contains("pub struct Product"));
assert!(model_rs.contains("pub price: i32"));
assert!(model_rs.contains("pub active: bool"));
let migration =
fs::read_to_string(root.join(format!("migrations/{TS}_create_products.sql"))).unwrap();
assert!(migration.contains("CREATE TABLE products"));
assert!(migration.contains("price INTEGER NOT NULL"));
}
fn snapshots_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
}
fn slug(rel: &str) -> String {
rel.replace(['/', '.', ' '], "_")
}
fn assert_snapshot(name: &str, actual: &str) {
let path = snapshots_dir().join(format!("{name}.snap"));
if std::env::var_os("UPDATE_SNAPSHOTS").is_some() || !path.exists() {
fs::create_dir_all(snapshots_dir()).unwrap();
fs::write(&path, actual).unwrap();
return;
}
let expected = fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {name}.snap: {e}"));
assert_eq!(
actual, expected,
"generated output for `{name}` drifted from its snapshot. \
If intentional, re-run with UPDATE_SNAPSHOTS=1 and review the diff."
);
}
fn content<'a>(plan: &'a Plan, rel: &str) -> &'a str {
plan.ops
.iter()
.find(|o| o.path.to_string_lossy() == rel)
.map(|o| o.contents.as_str())
.unwrap_or_else(|| panic!("plan has no file `{rel}`"))
}
#[test]
fn project_skeleton_matches_snapshots() {
let plan = scaffold::new_project("shop", false, TS);
for rel in [
"Cargo.toml",
"gize.toml",
"src/main.rs",
"src/router.rs",
"src/state.rs",
"src/config/mod.rs",
"src/app/mod.rs",
] {
assert_snapshot(&format!("project__{}", slug(rel)), content(&plan, rel));
}
}
#[test]
fn crud_slice_matches_snapshots() {
let plan = scaffold::make_crud(&product_model(), TS);
for rel in [
"src/app/products/mod.rs",
"src/app/products/model.rs",
"src/app/products/dto.rs",
"src/app/products/error.rs",
"src/app/products/repository.rs",
"src/app/products/service.rs",
"src/app/products/handler.rs",
"src/app/products/routes.rs",
"src/app/products/tests.rs",
] {
let leaf = Path::new(rel).file_name().unwrap().to_string_lossy();
assert_snapshot(&format!("crud__{}", slug(&leaf)), content(&plan, rel));
}
assert_snapshot(
"crud__create_products_sql",
content(&plan, &format!("migrations/{TS}_create_products.sql")),
);
}