use std::fs;
use std::path::Path;
use std::process::Command;
use pgevolve_testkit::ephemeral_pg::{EphemeralPostgres, default_pg_version, docker_available};
fn cargo_bin() -> std::path::PathBuf {
let mut p = std::path::PathBuf::from(env!("CARGO_BIN_EXE_pgevolve"));
if !p.exists() {
p = std::path::PathBuf::from("target/debug/pgevolve");
}
p
}
fn write(path: &Path, contents: &str) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn end_to_end_init_plan_apply_status() {
if !docker_available() {
eprintln!("skipping: docker unavailable");
return;
}
let pg = EphemeralPostgres::start(default_pg_version())
.await
.unwrap();
let project = tempfile::tempdir().unwrap();
let project_path = project.path();
let status = Command::new(cargo_bin())
.args(["init", "--dir"])
.arg(project_path)
.status()
.expect("run init");
assert!(status.success(), "init exited non-zero");
assert!(project_path.join("pgevolve.toml").exists());
let cfg = format!(
"[project]\nname = \"e2e\"\nschema_dir = \"schema\"\nplan_dir = \"plans\"\nlayout_profile = \"schema-mirror\"\n\n\
[managed]\nschemas = [\"app\"]\n\n\
[planner]\nstrategy = \"online\"\n\n\
[environments.dev]\nurl = \"{}\"\n",
pg.dsn()
);
fs::write(project_path.join("pgevolve.toml"), cfg).unwrap();
write(
&project_path.join("schema/app/0001-init.sql"),
"-- @pgevolve schema=app\nCREATE SCHEMA app;\nCREATE TABLE app.users (id bigint NOT NULL, CONSTRAINT users_pkey PRIMARY KEY (id));\n",
);
let status = Command::new(cargo_bin())
.current_dir(project_path)
.args(["bootstrap", "--db", "dev"])
.status()
.expect("run bootstrap");
assert!(status.success());
let out = Command::new(cargo_bin())
.current_dir(project_path)
.args(["plan", "--db", "dev"])
.output()
.expect("run plan");
assert!(
out.status.success(),
"plan stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("Wrote plan"), "got: {stdout}");
let plan_dir_line = stdout
.lines()
.find(|l| l.starts_with("Wrote plan"))
.expect("plan output");
let plan_dir_str = plan_dir_line
.split(" to ")
.nth(1)
.unwrap()
.split(' ')
.next()
.unwrap();
let plan_dir_rel = Path::new(plan_dir_str).to_path_buf();
let plan_dir_abs = project_path.join(&plan_dir_rel);
assert!(
plan_dir_abs.is_dir(),
"plan dir not found at {}",
plan_dir_abs.display(),
);
let out = Command::new(cargo_bin())
.current_dir(project_path)
.args(["apply"])
.arg(&plan_dir_rel)
.args(["--db", "dev"])
.output()
.expect("run apply");
assert!(
out.status.success(),
"apply failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let client = pg.connect().await.unwrap();
let row = client
.query_one("SELECT to_regclass('app.users')::text IS NOT NULL", &[])
.await
.unwrap();
let exists: bool = row.get(0);
assert!(exists, "app.users not present after apply");
let out = Command::new(cargo_bin())
.current_dir(project_path)
.args(["status", "--db", "dev"])
.output()
.expect("run status");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("status=succeeded"), "status said: {stdout}");
}
#[tokio::test]
async fn help_lists_all_nine_commands() {
let out = Command::new(cargo_bin())
.arg("--help")
.output()
.expect("run --help");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
for cmd in [
"init",
"lint",
"validate",
"diff",
"plan",
"apply",
"status",
"dump",
"bootstrap",
] {
assert!(stdout.contains(cmd), "--help missing `{cmd}`:\n{stdout}");
}
}