use std::path::Path;
use std::process::Command;
use diesel::connection::SimpleConnection as _;
use diesel::{Connection as _, QueryableByName, RunQueryDsl as _, SqliteConnection, sql_query};
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
#[derive(QueryableByName)]
struct Count {
#[diesel(sql_type = diesel::sql_types::BigInt)]
n: i64,
}
fn open(db: &Path) -> SqliteConnection {
SqliteConnection::establish(&db.to_string_lossy()).expect("open sqlite")
}
fn row_count(db: &Path) -> i64 {
let rows: Vec<Count> = sql_query("SELECT COUNT(*) AS n FROM notes")
.load(&mut open(db))
.expect("count");
rows.first().map_or(-1, |r| r.n)
}
fn run_autumn(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
let empty = dir.join("no-tools");
std::fs::create_dir_all(&empty).expect("mkdir");
let out = Command::new(autumn_bin())
.args(args)
.current_dir(dir)
.env("PATH", &empty)
.env("AUTUMN_PG_BIN_DIR", &empty)
.env("AUTUMN_ENV", "dev")
.output()
.expect("run autumn");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(combined, out.status.code())
}
fn only_run_dir(root: &Path) -> std::path::PathBuf {
let mut runs: Vec<_> = std::fs::read_dir(root.join("backups").join("dev"))
.expect("profile dir")
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
assert_eq!(runs.len(), 1, "exactly one run directory: {runs:?}");
runs.pop().expect("one run")
}
#[test]
fn sqlite_backup_and_restore_round_trip_without_any_postgres_tools() {
let project = tempfile::tempdir().expect("tempdir");
let root = project.path();
std::fs::write(
root.join("autumn.toml"),
"[database]\nurl = \"sqlite://app.db\"\n",
)
.expect("write autumn.toml");
let db = root.join("app.db");
open(&db)
.batch_execute(
"PRAGMA journal_mode = WAL; \
CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL); \
INSERT INTO notes (body) VALUES ('first'), ('second');",
)
.expect("seed");
assert_eq!(row_count(&db), 2);
let (out, code) = run_autumn(root, &["db", "backup", "--profile", "dev"]);
assert_eq!(code, Some(0), "backup must succeed with no PATH:\n{out}");
assert!(
out.contains("SQLite snapshot"),
"the run must report the snapshot it took:\n{out}"
);
let run_dir = only_run_dir(root);
let artifact = run_dir.join("control.sqlite");
assert!(artifact.is_file(), "artifact missing in {run_dir:?}");
let manifest = std::fs::read_to_string(run_dir.join("manifest.json")).expect("manifest");
assert!(
manifest.contains("\"backend\": \"sqlite\""),
"the manifest must record the backend:\n{manifest}"
);
assert_eq!(row_count(&artifact), 2);
open(&db)
.batch_execute("DELETE FROM notes;")
.expect("delete");
assert_eq!(row_count(&db), 0);
let (out, code) = run_autumn(
root,
&[
"db",
"restore",
run_dir.to_str().expect("utf-8"),
"--profile",
"dev",
],
);
assert_eq!(code, Some(0), "restore must succeed with no PATH:\n{out}");
assert_eq!(row_count(&db), 2, "the restore must bring the rows back");
}
#[test]
fn a_live_writer_is_neither_blocked_nor_corrupted_by_a_backup() {
let project = tempfile::tempdir().expect("tempdir");
let root = project.path();
std::fs::write(
root.join("autumn.toml"),
"[database]\nurl = \"sqlite://app.db\"\n",
)
.expect("write autumn.toml");
let db = root.join("app.db");
let mut live = open(&db);
live.batch_execute(
"PRAGMA journal_mode = WAL; \
CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL); \
INSERT INTO notes (body) VALUES ('first');",
)
.expect("seed");
let (out, code) = run_autumn(root, &["db", "backup", "--profile", "dev"]);
assert_eq!(code, Some(0), "a live database must back up:\n{out}");
live.batch_execute("INSERT INTO notes (body) VALUES ('second');")
.expect("the live app must still write after a backup");
assert_eq!(row_count(&db), 2);
let snapshot = only_run_dir(root).join("control.sqlite");
assert_eq!(
row_count(&snapshot),
1,
"the snapshot is the point in time the backup ran"
);
}
#[test]
fn an_in_memory_database_is_refused_with_an_actionable_message() {
let project = tempfile::tempdir().expect("tempdir");
let root = project.path();
std::fs::write(
root.join("autumn.toml"),
"[database]\nurl = \"sqlite::memory:\"\n",
)
.expect("write autumn.toml");
let (out, code) = run_autumn(root, &["db", "backup", "--profile", "dev"]);
assert_eq!(code, Some(1), "an in-memory target must fail:\n{out}");
assert!(
out.contains("in-memory") && out.contains("sqlite://"),
"the refusal must name the reason and the fix:\n{out}"
);
assert!(
!root.join("backups").join("dev").exists()
|| std::fs::read_dir(root.join("backups").join("dev"))
.into_iter()
.flatten()
.next()
.is_none(),
"a failed backup must leave no run directory behind"
);
}