use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
fn run_autumn(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(autumn_bin())
.args(args)
.current_dir(dir)
.output()
.expect("failed to run autumn")
}
fn run_autumn_ok(dir: &Path, args: &[&str]) {
let output = run_autumn(dir, args);
assert!(
output.status.success(),
"autumn {args:?} failed (exit={:?})\nstdout: {}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
fn api_project(name: &str, extra: &[&str]) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let mut args = vec!["new", name, "--api"];
args.extend_from_slice(extra);
run_autumn_ok(tmp.path(), &args);
let project = tmp.path().join(name);
(tmp, project)
}
fn all_files(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in fs::read_dir(&dir).expect("read_dir") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
stack.push(path);
} else {
out.push(path);
}
}
}
out
}
#[test]
fn api_scaffold_has_no_html_css_template_artifacts() {
let (_tmp, project) = api_project("api-artifacts-app", &[]);
for missing in [
"tailwind.config.js",
"static/css/input.css",
"static/css/app.css",
] {
assert!(
!project.join(missing).exists(),
"--api scaffold must not create {missing}"
);
}
assert!(
!project.join("static").exists(),
"--api scaffold must not create a static/ directory"
);
for path in all_files(&project) {
let Ok(content) = fs::read_to_string(&path) else {
continue; };
for marker in ["tailwind", "input.css", "maud::html"] {
assert!(
!content.contains(marker),
"--api generated file {} contains forbidden marker {marker:?}:\n{content}",
path.display(),
);
}
}
}
#[test]
fn api_scaffold_build_and_docker_have_no_css_step() {
let (_tmp, project) = api_project("api-build-app", &[]);
let build_rs = fs::read_to_string(project.join("build.rs")).unwrap();
assert!(
!build_rs.contains("tailwind") && !build_rs.contains("input.css"),
"--api build.rs must not compile CSS:\n{build_rs}"
);
assert!(
build_rs.contains("emit_build_provenance"),
"--api build.rs must keep build/git provenance emission:\n{build_rs}"
);
let dockerfile = fs::read_to_string(project.join("Dockerfile")).unwrap();
assert!(
!dockerfile.to_lowercase().contains("tailwind"),
"--api Dockerfile must not download/run Tailwind:\n{dockerfile}"
);
assert!(
!dockerfile.contains("COPY static"),
"--api Dockerfile must not copy a static/ tree:\n{dockerfile}"
);
}
#[test]
fn api_scaffold_main_rs_has_json_handler_and_routes() {
let (_tmp, project) = api_project("api-main-app", &[]);
let main_rs = fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(
main_rs.contains("-> Json<"),
"--api main.rs must define at least one Json<...> handler:\n{main_rs}"
);
assert!(
main_rs.contains("routes![index, hello_name]"),
"--api main.rs must register handlers via routes![]:\n{main_rs}"
);
assert!(
main_rs.contains("use autumn_web::prelude::*;"),
"--api main.rs must import the prelude (re-exports Json):\n{main_rs}"
);
for forbidden in ["maud", "layout(", "<link", "nav_bar("] {
assert!(
!main_rs.contains(forbidden),
"--api main.rs must not contain {forbidden:?}:\n{main_rs}"
);
}
}
#[test]
fn api_scaffold_cargo_toml_drops_view_stack() {
let (_tmp, project) = api_project("api-cargo-app", &[]);
let cargo = fs::read_to_string(project.join("Cargo.toml")).unwrap();
assert!(
!cargo.contains("maud ="),
"--api Cargo.toml must not depend on maud:\n{cargo}"
);
assert!(
cargo.contains("default-features = false"),
"--api Cargo.toml must disable autumn-web default (view) features:\n{cargo}"
);
assert!(
cargo.contains("\"db\""),
"--api Cargo.toml must keep the db feature so migrations compile:\n{cargo}"
);
}
#[test]
fn api_scaffold_composes_with_i18n_and_seed() {
let (_tmp_i18n, i18n) = api_project("api-i18n-app", &["--with-i18n"]);
assert!(
i18n.join("i18n/en.ftl").is_file(),
"--api --with-i18n must scaffold i18n/en.ftl"
);
let i18n_main = fs::read_to_string(i18n.join("src/main.rs")).unwrap();
assert!(
i18n_main.contains(".i18n_auto()") && i18n_main.contains("-> Json<"),
"--api --with-i18n main.rs must wire i18n and stay JSON-first:\n{i18n_main}"
);
assert!(
fs::read_to_string(i18n.join("Cargo.toml"))
.unwrap()
.contains("\"i18n\""),
"--api --with-i18n must enable the i18n feature"
);
let (_tmp_seed, seed) = api_project("api-seed-app", &["--with-seed"]);
assert!(
seed.join("src/bin/seed.rs").is_file(),
"--api --with-seed must scaffold src/bin/seed.rs"
);
let seed_cargo = fs::read_to_string(seed.join("Cargo.toml")).unwrap();
assert!(
seed_cargo.contains("\"seed\"") && seed_cargo.contains("name = \"seed\""),
"--api --with-seed must enable the seed feature and [[bin]] entry:\n{seed_cargo}"
);
}
#[test]
fn api_scaffold_with_i18n_copies_i18n_into_docker_image() {
let (_tmp, project) = api_project("api-i18n-docker-app", &["--with-i18n"]);
let dockerfile = fs::read_to_string(project.join("Dockerfile")).unwrap();
assert!(
dockerfile.contains("COPY i18n ./i18n"),
"--api --with-i18n Dockerfile must copy i18n/ into the builder stage:\n{dockerfile}"
);
assert!(
dockerfile.contains("COPY --from=builder /app/i18n /app/i18n"),
"--api --with-i18n Dockerfile must copy i18n/ into the runtime stage:\n{dockerfile}"
);
assert!(
!dockerfile.contains("__AUTUMN_I18N"),
"--api --with-i18n Dockerfile must not leave anchor markers:\n{dockerfile}"
);
}
#[test]
fn api_scaffold_without_i18n_has_no_i18n_docker_copy() {
let (_tmp, project) = api_project("api-no-i18n-docker-app", &[]);
let dockerfile = fs::read_to_string(project.join("Dockerfile")).unwrap();
assert!(
!dockerfile.contains("COPY i18n ./i18n"),
"non-i18n --api Dockerfile must not copy i18n/ (build context has no i18n/ dir):\n{dockerfile}"
);
assert!(
!dockerfile.contains("/app/i18n"),
"non-i18n --api Dockerfile must not reference /app/i18n:\n{dockerfile}"
);
assert!(
!dockerfile.contains("__AUTUMN_I18N"),
"non-i18n --api Dockerfile must not leave anchor markers:\n{dockerfile}"
);
}
#[test]
fn api_scaffold_conflicts_with_daemon_flavors() {
let tmp = tempfile::tempdir().expect("tempdir");
for bad in [
["new", "api-daemon-bad", "--api", "--daemon"],
["new", "api-bundled-bad", "--api", "--bundled-pg"],
] {
let output = run_autumn(tmp.path(), &bad);
assert!(
!output.status.success(),
"autumn {bad:?} should be rejected"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--api") && stderr.contains("incompatible"),
"rejection message should explain the --api conflict, got:\n{stderr}"
);
}
}
#[test]
#[ignore = "slow: cargo-checks a fresh project — run with `cargo test -p autumn-cli -- --ignored`"]
fn api_scaffold_cargo_checks() {
use std::fmt::Write as _;
let (_tmp, project) = api_project("api-check-app", &[]);
let cargo_toml_path = project.join("Cargo.toml");
let mut content = fs::read_to_string(&cargo_toml_path).unwrap();
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root");
let autumn_web = workspace_root.join("autumn");
let _ = write!(
content,
"\n[patch.crates-io]\nautumn-web = {{ path = \"{}\" }}\n",
autumn_web.display().to_string().replace('\\', "/")
);
fs::write(&cargo_toml_path, content).unwrap();
let check = Command::new("cargo")
.args(["check", "--tests"])
.current_dir(&project)
.output()
.unwrap();
assert!(
check.status.success(),
"cargo check on the --api scaffold failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&check.stdout),
String::from_utf8_lossy(&check.stderr),
);
}