use std::fs;
use std::path::PathBuf;
use std::process::Command;
fn temporary_root(label: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"arcature-cli-{label}-{}-{}",
std::process::id(),
unique_suffix()
))
}
fn unique_suffix() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos())
}
fn arc() -> Command {
Command::new(env!("CARGO_BIN_EXE_arc"))
}
const BASE_FILES: &[&str] = &[
".gitignore",
"Cargo.toml",
"arcature.toml",
"s.script",
"bootstrap/main.rs",
"app/lib.rs",
"app/run.rs",
"app/application.rs",
"app/proxy.rs",
"app/errors.rs",
"app/root_document.rs",
"app/frontend_asset.rs",
"app/bin/arcature-contract.rs",
"app/bin/arcature-metadata.rs",
"app/routes/mod.rs",
"app/home/mod.rs",
"app/home/home_controller.rs",
"app/home/home_page.rs",
"tests/smoke.rs",
"resources/index.html",
"resources/tsconfig.json",
"resources/pnpm-workspace.yaml",
"resources/src/style.css",
];
const DB_FILES: &[&str] = &[
".env.example",
"app/state.rs",
"app/links/mod.rs",
"app/links/link.rs",
"app/links/link_entity.rs",
"app/links/link_resource.rs",
"app/links/link_service.rs",
"app/links/links_controller.rs",
"app/links/store_link_request.rs",
"database/migrations/Cargo.toml",
"database/migrations/src/lib.rs",
"database/migrations/src/main.rs",
"database/migrations/src/m20260814_create_links.rs",
];
#[test]
fn generates_complete_react_and_vue_projects() {
let root = temporary_root("generation");
fs::create_dir_all(&root).expect("test root should be created");
for (name, frontend, entry, page) in [
("react-app", None, "app.tsx", "Home.tsx"),
("vue-app", Some("vue"), "app.ts", "Home.vue"),
] {
let target = root.join(name);
let mut command = arc();
command.arg("new").arg(&target);
if let Some(value) = frontend {
command.args(["--frontend", value]);
}
assert!(command.status().expect("arc should run").success());
for path in BASE_FILES {
assert!(target.join(path).is_file(), "missing {path}");
}
for path in DB_FILES {
assert!(target.join(path).is_file(), "missing {path}");
}
assert!(target.join("resources/package.json").is_file());
assert!(!target.join("resources/pnpm-lock.yaml").exists());
assert!(target.join("resources/vite.config.ts").is_file());
assert!(target.join("resources/src").join(entry).is_file());
assert!(target.join("resources/src/pages").join(page).is_file());
assert!(
!target.join("src").exists(),
"canonical generated app must not have a root src/ directory"
);
assert!(
!target.join("frontend").exists(),
"canonical generated app must not have a root frontend/ directory"
);
for entry in walk(&target) {
let text = fs::read_to_string(&entry).expect("generated text should be readable");
assert!(!text.contains("__PROJECT_NAME__"));
assert!(!text.contains("__RUST_NAME__"));
assert!(!text.contains("__FRONTEND__"));
assert!(!text.contains("__ENTRY_EXT__"));
assert!(!text.contains("__REACT_REFRESH__"));
}
}
fs::remove_dir_all(root).expect("test root should be removed");
}
#[test]
fn generates_no_db_project_without_migration_package() {
let root = temporary_root("no-db");
fs::create_dir_all(&root).expect("test root should be created");
let target = root.join("no-db-app");
let mut command = arc();
command.arg("new").arg(&target).arg("--no-db");
assert!(command.status().expect("arc should run").success());
assert!(
!target.join("database/migrations").exists(),
"database/migrations/ should not exist for --no-db"
);
assert!(
!target.join(".env.example").exists(),
".env.example should not exist for --no-db"
);
let cargo_toml = fs::read_to_string(target.join("Cargo.toml")).expect("Cargo.toml readable");
assert!(
!cargo_toml.contains("\"db\""),
"no-db project should not enable the db feature"
);
assert!(
!cargo_toml.contains("[workspace]"),
"no-db project should not be a workspace"
);
let run_rs = fs::read_to_string(target.join("app/run.rs")).expect("app/run.rs readable");
assert!(
!run_rs.contains("arcature::db"),
"no-db app/run.rs should not reference arcature::db"
);
fs::remove_dir_all(root).expect("test root should be removed");
}
#[test]
fn generates_db_project_with_migration_package_by_default() {
let root = temporary_root("db-default");
fs::create_dir_all(&root).expect("test root should be created");
let target = root.join("db-app");
let mut command = arc();
command.arg("new").arg(&target);
assert!(command.status().expect("arc should run").success());
assert!(
target.join("database/migrations/Cargo.toml").is_file(),
"database/migrations/Cargo.toml should exist"
);
assert!(
target.join("database/migrations/src/lib.rs").is_file(),
"database/migrations/src/lib.rs should exist"
);
assert!(
target.join("database/migrations/src/main.rs").is_file(),
"database/migrations/src/main.rs should exist"
);
assert!(
target.join(".env.example").is_file(),
".env.example should exist"
);
let cargo_toml = fs::read_to_string(target.join("Cargo.toml")).expect("Cargo.toml readable");
assert!(
cargo_toml.contains("\"db\""),
"db project should enable the db feature on arcature"
);
assert!(
cargo_toml.contains("[workspace]"),
"db project should be a workspace"
);
let run_rs = fs::read_to_string(target.join("app/run.rs")).expect("app/run.rs readable");
assert!(
run_rs.contains("arcature::db"),
"db app/run.rs should reference arcature::db"
);
assert!(
run_rs.contains("DbConfig"),
"db app/run.rs should reference DbConfig"
);
fs::remove_dir_all(root).expect("test root should be removed");
}
#[test]
fn refuses_invalid_names_and_existing_targets() {
let root = temporary_root("safety");
fs::create_dir_all(&root).expect("test root should be created");
let existing = root.join("existing");
fs::create_dir(&existing).expect("existing target should be created");
fs::write(existing.join("owned.txt"), "keep").expect("sentinel should be written");
assert!(
!arc()
.args(["new"])
.arg(&existing)
.status()
.expect("arc should run")
.success()
);
assert_eq!(
fs::read_to_string(existing.join("owned.txt")).expect("sentinel should remain"),
"keep"
);
assert!(
!arc()
.args(["new"])
.arg(root.join("bad_name"))
.status()
.expect("arc should run")
.success()
);
fs::remove_dir_all(root).expect("test root should be removed");
}
fn walk(root: &std::path::Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut pending = vec![root.to_path_buf()];
while let Some(directory) = pending.pop() {
for entry in fs::read_dir(directory).expect("generated directory should be readable") {
let entry = entry.expect("generated entry should be readable");
if entry
.file_type()
.expect("entry type should be readable")
.is_dir()
{
pending.push(entry.path());
} else {
files.push(entry.path());
}
}
}
files
}