use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
const SECRET_PW: &str = "s3cr3t_pw_do_not_leak";
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
fn run_autumn(dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> (String, String, Option<i32>) {
let output = Command::new(autumn_bin())
.args(args)
.current_dir(dir)
.envs(envs.iter().copied())
.output()
.expect("failed to run autumn");
(
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
output.status.code(),
)
}
fn run_autumn_ok(dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> (String, String) {
let (stdout, stderr, code) = run_autumn(dir, args, envs);
assert_eq!(
code,
Some(0),
"autumn {args:?} failed (exit={code:?})\nstdout: {stdout}\nstderr: {stderr}",
);
(stdout, stderr)
}
fn run_autumn_fail(dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> (String, String) {
let (stdout, stderr, code) = run_autumn(dir, args, envs);
assert_ne!(
code,
Some(0),
"autumn {args:?} should have failed but exited 0\nstdout: {stdout}\nstderr: {stderr}",
);
(stdout, stderr)
}
fn fresh_project(name: &str) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
run_autumn_ok(tmp.path(), &["new", name], &[]);
let project = tmp.path().join(name);
(tmp, project)
}
fn patch_generated_cargo_toml(project_dir: &Path) {
let cargo_toml_path = project_dir.join("Cargo.toml");
let mut content = std::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");
write!(
content,
"\n[patch.crates-io]\nautumn-web = {{ path = \"{}\" }}\n",
autumn_web.display().to_string().replace('\\', "/")
)
.unwrap();
std::fs::write(&cargo_toml_path, content).unwrap();
}
fn write_migration(project: &Path, name: &str, up_sql: &str, down_sql: &str) {
let dir = project.join("migrations").join(name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("up.sql"), up_sql).unwrap();
std::fs::write(dir.join("down.sql"), down_sql).unwrap();
}
async fn start_postgres() -> (
testcontainers::ContainerAsync<testcontainers_modules::postgres::Postgres>,
String,
u16,
) {
use testcontainers::runners::AsyncRunner as _;
use testcontainers_modules::postgres::Postgres;
let container = Postgres::default()
.with_password(SECRET_PW)
.start()
.await
.expect("failed to start Postgres testcontainer — is Docker running?");
let host = container.get_host().await.unwrap().to_string();
let port = container.get_host_port_ipv4(5432).await.unwrap();
(container, host, port)
}
fn cargo_check(project: &Path) -> (bool, String) {
let output = Command::new(env!("CARGO"))
.args(["check", "--quiet"])
.current_dir(project)
.output()
.expect("failed to run cargo check");
let combined = format!(
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
(output.status.success(), combined)
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers) + cargo check; run with -- --ignored"]
async fn db_pull_regenerates_models_from_existing_schema_and_compiles() {
let (_container, host, port) = start_postgres().await;
let url = format!("postgres://postgres:{SECRET_PW}@{host}:{port}/postgres");
let envs = [("AUTUMN_DATABASE__URL", url.as_str())];
let (_tmp, project) = fresh_project("pull_app");
patch_generated_cargo_toml(&project);
run_autumn_ok(
&project,
&[
"generate",
"model",
"Post",
"title:String",
"body:Text",
"views:i32",
"score:f64",
"token:Uuid",
"published:bool",
"summary:Option<String>",
],
&envs,
);
run_autumn_ok(&project, &["migrate"], &envs);
std::fs::remove_dir_all(project.join("src/models")).unwrap();
std::fs::remove_file(project.join("src/schema.rs")).unwrap();
let migrations_before = std::fs::read_dir(project.join("migrations"))
.unwrap()
.count();
let (stdout, _) = run_autumn_ok(&project, &["db", "pull", "--with-repository"], &envs);
assert!(
stdout.contains("post.rs"),
"pull should report files:\n{stdout}"
);
assert!(!project.join("src/models/autumn_job.rs").exists());
assert!(!project.join("src/models/api_token.rs").exists());
let model = std::fs::read_to_string(project.join("src/models/post.rs")).unwrap();
assert!(model.contains("#[autumn_web::model]"));
assert!(model.contains("#[id]"));
assert!(model.contains("pub id: i64,"));
assert!(model.contains("pub title: String,"));
assert!(model.contains("pub views: i32,"));
assert!(model.contains("pub score: f64,"));
assert!(model.contains("pub token: uuid::Uuid,"));
assert!(model.contains("pub published: bool,"));
assert!(model.contains("pub summary: Option<String>,"));
assert!(model.contains("#[default]\n pub created_at: chrono::NaiveDateTime,"));
let schema = std::fs::read_to_string(project.join("src/schema.rs")).unwrap();
assert!(schema.contains("posts (id)"));
assert!(schema.contains("summary -> Nullable<Text>,"));
let repo = std::fs::read_to_string(project.join("src/repositories/post.rs")).unwrap();
assert!(
repo.contains("#[autumn_web::repository(Post, table = \"posts\", api = \"/api/posts\")]")
);
let main = std::fs::read_to_string(project.join("src/main.rs")).unwrap();
assert!(main.contains("mod models;"));
assert!(main.contains("mod schema;"));
assert!(main.contains("mod repositories;"));
let migrations_after = std::fs::read_dir(project.join("migrations"))
.unwrap()
.count();
assert_eq!(
migrations_before, migrations_after,
"db pull must not emit migrations"
);
let (ok, output) = cargo_check(&project);
assert!(ok, "generated app must `cargo check` cleanly:\n{output}");
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers); run with -- --ignored"]
async fn db_pull_fails_loudly_on_unsupported_column_type() {
let (_container, host, port) = start_postgres().await;
let url = format!("postgres://postgres:{SECRET_PW}@{host}:{port}/postgres");
let envs = [("AUTUMN_DATABASE__URL", url.as_str())];
let (_tmp, project) = fresh_project("pull_unsupported_app");
write_migration(
&project,
"20260101000000_create_ledgers",
"CREATE TABLE ledgers (id BIGSERIAL PRIMARY KEY, amount NUMERIC NOT NULL, \
created_at TIMESTAMP NOT NULL DEFAULT NOW());",
"DROP TABLE ledgers;",
);
run_autumn_ok(&project, &["migrate"], &envs);
let (stdout, stderr) = run_autumn_fail(&project, &["db", "pull", "ledgers"], &envs);
assert!(
stderr.contains("ledgers.amount"),
"error must name the column: {stderr}"
);
assert!(
stderr.contains("Supported:"),
"error must list supported types: {stderr}"
);
assert!(
!stdout.contains(SECRET_PW) && !stderr.contains(SECRET_PW),
"credentials leaked!\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(!project.join("src/models/ledger.rs").exists());
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers); run with -- --ignored"]
async fn db_pull_unscoped_skips_unsupported_tables_and_pulls_the_rest() {
let (_container, host, port) = start_postgres().await;
let url = format!("postgres://postgres:{SECRET_PW}@{host}:{port}/postgres");
let envs = [("AUTUMN_DATABASE__URL", url.as_str())];
let (_tmp, project) = fresh_project("pull_mixed_app");
patch_generated_cargo_toml(&project);
run_autumn_ok(
&project,
&["generate", "model", "Post", "title:String"],
&envs,
);
write_migration(
&project,
"20260101000000_create_ledgers",
"CREATE TABLE ledgers (id BIGSERIAL PRIMARY KEY, amount NUMERIC NOT NULL);",
"DROP TABLE ledgers;",
);
run_autumn_ok(&project, &["migrate"], &envs);
std::fs::remove_dir_all(project.join("src/models")).unwrap();
std::fs::remove_file(project.join("src/schema.rs")).unwrap();
let (_, stderr) = run_autumn_ok(&project, &["db", "pull"], &envs);
assert!(
stderr.contains("Skipping table 'ledgers'"),
"unsupported table should be skipped with a notice: {stderr}"
);
assert!(project.join("src/models/post.rs").exists());
assert!(!project.join("src/models/ledger.rs").exists());
}