use mongreldb_core::{Table, Value};
use std::io::BufRead;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
use tempfile::TempDir;
fn writer_exe() -> PathBuf {
if let Ok(p) = std::env::var("CARGO_BIN_EXE_crash_writer") {
return PathBuf::from(p);
}
let exe = std::env::current_exe().expect("current_exe");
exe.parent()
.and_then(|deps| deps.parent())
.map(|target| target.join("crash_writer"))
.expect("locate crash_writer")
}
fn spawn_and_kill(mode: &str, ready: &str, db_dir: &std::path::Path) {
let mut child = Command::new(writer_exe())
.arg(db_dir)
.arg(mode)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn crash_writer");
let stdout = child.stdout.take().expect("piped stdout");
let (tx, rx) = mpsc::channel::<String>();
let reader = std::thread::spawn(move || {
for line in std::io::BufReader::new(stdout)
.lines()
.map_while(Result::ok)
{
if tx.send(line).is_err() {
break;
}
}
});
let mut saw_ready = false;
let deadline = std::time::Instant::now() + Duration::from_secs(60);
while std::time::Instant::now() < deadline {
match rx.recv_timeout(Duration::from_secs(60)) {
Ok(line) if line == ready => {
saw_ready = true;
break;
}
Ok(other) => eprintln!("[crash_writer] {other}"),
Err(mpsc::RecvTimeoutError::Timeout) => break,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
let kill_ok = child.kill().is_ok();
let _ = child.wait();
let _ = reader.join();
assert!(
saw_ready,
"writer never signalled {ready} (killed={kill_ok})"
);
}
fn values(db: &Table) -> Vec<i64> {
let rows = db.visible_rows(db.snapshot()).expect("visible_rows");
let mut vals: Vec<i64> = rows
.iter()
.filter_map(|r| match r.columns.get(&1) {
Some(Value::Int64(v)) => Some(*v),
_ => None,
})
.collect();
vals.sort_unstable();
vals
}
#[test]
fn committed_row_survives_real_kill9() {
let dir = TempDir::new().unwrap();
spawn_and_kill("committed", "COMMITTED_READY", dir.path());
let db = Table::open(dir.path()).expect("reopen after kill -9");
assert_eq!(
values(&db),
vec![4242],
"committed row must survive kill -9"
);
assert_eq!(db.count(), 1);
}
#[test]
fn committed_burst_survives_real_kill9() {
let dir = TempDir::new().unwrap();
spawn_and_kill("committed-burst", "BURST_READY", dir.path());
let db = Table::open(dir.path()).expect("reopen after kill -9");
assert_eq!(
values(&db),
vec![10, 20, 30],
"all committed rows must survive kill -9"
);
assert_eq!(db.count(), 3);
}
#[test]
fn uncommitted_row_stays_invisible_after_real_kill9() {
let dir = TempDir::new().unwrap();
spawn_and_kill("uncommitted", "UNCOMMITTED_READY", dir.path());
let db = Table::open(dir.path()).expect("reopen after kill -9");
assert!(
values(&db).is_empty(),
"uncommitted row must not be visible after kill -9"
);
assert_eq!(db.count(), 0);
}
#[test]
fn flushed_run_survives_real_kill9() {
let dir = TempDir::new().unwrap();
spawn_and_kill("flush-spill", "FLUSH_READY", dir.path());
let db = Table::open(dir.path()).expect("reopen after kill -9");
assert_eq!(
values(&db),
vec![7777],
"flushed (run + rotated WAL) row must survive kill -9"
);
assert_eq!(db.count(), 1);
}