#![cfg(feature = "test-failpoints")]
use std::sync::atomic::{AtomicBool, Ordering};
use stoolap::test_failpoints;
use stoolap::Database;
use tempfile::tempdir;
fn failpoint_guard() -> test_failpoints::FailpointGuard {
test_failpoints::FailpointGuard::new()
}
#[test]
fn test_wal_write_fail_returns_error() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let db = Database::open(&format!("file://{}", dir.path().display()))
.expect("Failed to open database");
db.execute("CREATE TABLE fp_wal (id INTEGER PRIMARY KEY, val TEXT)", ())
.expect("CREATE should succeed");
test_failpoints::WAL_WRITE_FAIL.store(true, Ordering::Release);
let result = db.execute("INSERT INTO fp_wal VALUES (1, 'hello')", ());
assert!(
result.is_err(),
"INSERT should fail with WAL write failpoint armed"
);
test_failpoints::WAL_WRITE_FAIL.store(false, Ordering::Release);
db.execute("INSERT INTO fp_wal VALUES (2, 'after_fail')", ())
.expect("INSERT should succeed after disarming failpoint");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_wal", ())
.expect("COUNT should work");
assert!(count >= 1, "At least one row should exist, got {}", count);
}
#[test]
fn test_wal_write_fail_mid_transaction_atomicity() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let db = Database::open(&format!("file://{}", dir.path().display()))
.expect("Failed to open database");
db.execute(
"CREATE TABLE fp_wal_tx (id INTEGER PRIMARY KEY, val INTEGER)",
(),
)
.expect("CREATE should succeed");
db.execute("INSERT INTO fp_wal_tx VALUES (1, 100)", ())
.expect("Initial insert should succeed");
db.execute("BEGIN", ()).expect("BEGIN should succeed");
db.execute("UPDATE fp_wal_tx SET val = 200 WHERE id = 1", ())
.expect("UPDATE should succeed within transaction");
test_failpoints::WAL_WRITE_FAIL.store(true, Ordering::Release);
let result = db.execute("COMMIT", ());
if result.is_err() {
let _ = db.execute("ROLLBACK", ());
}
test_failpoints::WAL_WRITE_FAIL.store(false, Ordering::Release);
let val: i64 = db
.query_one("SELECT val FROM fp_wal_tx WHERE id = 1", ())
.expect("SELECT should work");
assert_eq!(val, 100, "Value should be unchanged after failed commit");
}
#[test]
fn test_wal_write_fail_recovery_after_disarm() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_wal_rec (id INTEGER PRIMARY KEY, val TEXT)",
(),
)
.expect("CREATE should succeed");
db.execute("INSERT INTO fp_wal_rec VALUES (1, 'before')", ())
.expect("INSERT should succeed");
}
{
let db = Database::open(&path).expect("Failed to reopen database");
let val: String = db
.query_one("SELECT val FROM fp_wal_rec WHERE id = 1", ())
.expect("SELECT should work");
assert_eq!(val, "before");
test_failpoints::WAL_WRITE_FAIL.store(true, Ordering::Release);
let _ = db.execute("INSERT INTO fp_wal_rec VALUES (2, 'during_fail')", ());
test_failpoints::WAL_WRITE_FAIL.store(false, Ordering::Release);
db.execute("INSERT INTO fp_wal_rec VALUES (3, 'after_fail')", ())
.expect("INSERT should succeed after disarming");
}
{
let db = Database::open(&path).expect("Failed to reopen after failpoint");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_wal_rec", ())
.expect("COUNT should work");
assert!(
count >= 2,
"At least rows 1 and 3 should exist, got {}",
count
);
}
}
#[test]
fn test_wal_sync_fail_returns_error() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let db = Database::open(&format!("file://{}", dir.path().display()))
.expect("Failed to open database");
db.execute(
"CREATE TABLE fp_sync (id INTEGER PRIMARY KEY, val TEXT)",
(),
)
.expect("CREATE should succeed");
test_failpoints::WAL_SYNC_FAIL.store(true, Ordering::Release);
let result = db.execute("INSERT INTO fp_sync VALUES (1, 'test')", ());
test_failpoints::WAL_SYNC_FAIL.store(false, Ordering::Release);
let insert_result = db.execute("INSERT INTO fp_sync VALUES (2, 'after')", ());
if result.is_err() {
let _ = db.execute("INSERT INTO fp_sync VALUES (1, 'retry')", ());
}
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_sync", ())
.expect("COUNT should work after sync recovery");
assert!(count >= 1, "At least one row should exist");
drop(insert_result);
}
#[test]
fn test_snapshot_write_fail_during_checkpoint() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_snap (id INTEGER PRIMARY KEY, val INTEGER)",
(),
)
.expect("CREATE should succeed");
for i in 0..10 {
db.execute(
&format!("INSERT INTO fp_snap VALUES ({}, {})", i, i * 10),
(),
)
.expect("INSERT should succeed");
}
test_failpoints::SNAPSHOT_WRITE_FAIL.store(true, Ordering::Release);
let vacuum_result = db.execute("VACUUM", ());
test_failpoints::SNAPSHOT_WRITE_FAIL.store(false, Ordering::Release);
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_snap", ())
.expect("COUNT should work");
assert_eq!(count, 10, "All 10 rows should be accessible");
let sum: f64 = db
.query_one("SELECT SUM(val) FROM fp_snap", ())
.expect("SUM should work");
assert_eq!(sum, 450.0, "Sum should be 0+10+20+...+90 = 450");
drop(vacuum_result);
}
#[test]
fn test_snapshot_write_fail_recovery_on_reopen() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_snap_rec (id INTEGER PRIMARY KEY, val INTEGER)",
(),
)
.expect("CREATE should succeed");
for i in 0..5 {
db.execute(
&format!("INSERT INTO fp_snap_rec VALUES ({}, {})", i, i),
(),
)
.expect("INSERT should succeed");
}
test_failpoints::SNAPSHOT_WRITE_FAIL.store(true, Ordering::Release);
let _ = db.execute("VACUUM", ());
test_failpoints::SNAPSHOT_WRITE_FAIL.store(false, Ordering::Release);
}
{
let db = Database::open(&path).expect("Recovery should succeed");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_snap_rec", ())
.expect("COUNT should work after recovery");
assert_eq!(count, 5, "All 5 rows should be recovered from WAL");
}
}
#[test]
fn test_snapshot_sync_fail_during_finalize() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_ssync (id INTEGER PRIMARY KEY, val TEXT)",
(),
)
.expect("CREATE should succeed");
for i in 0..5 {
db.execute(
&format!("INSERT INTO fp_ssync VALUES ({}, 'row_{}')", i, i),
(),
)
.expect("INSERT should succeed");
}
test_failpoints::SNAPSHOT_SYNC_FAIL.store(true, Ordering::Release);
let _ = db.execute("VACUUM", ());
test_failpoints::SNAPSHOT_SYNC_FAIL.store(false, Ordering::Release);
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_ssync", ())
.expect("COUNT should work");
assert_eq!(count, 5);
}
#[test]
fn test_snapshot_rename_fail_atomicity() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_rename (id INTEGER PRIMARY KEY, val INTEGER)",
(),
)
.expect("CREATE should succeed");
for i in 0..10 {
db.execute(
&format!("INSERT INTO fp_rename VALUES ({}, {})", i, i * 100),
(),
)
.expect("INSERT should succeed");
}
test_failpoints::SNAPSHOT_RENAME_FAIL.store(true, Ordering::Release);
let _ = db.execute("VACUUM", ());
test_failpoints::SNAPSHOT_RENAME_FAIL.store(false, Ordering::Release);
let sum: f64 = db
.query_one("SELECT SUM(val) FROM fp_rename", ())
.expect("SUM should work");
assert_eq!(sum, 4500.0);
}
{
let db = Database::open(&path).expect("Recovery should succeed");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_rename", ())
.expect("COUNT should work");
assert_eq!(count, 10, "All rows should survive failed snapshot rename");
}
}
#[test]
fn test_checkpoint_write_fail() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_ckpt (id INTEGER PRIMARY KEY, val TEXT)",
(),
)
.expect("CREATE should succeed");
db.execute("INSERT INTO fp_ckpt VALUES (1, 'first')", ())
.expect("INSERT should succeed");
test_failpoints::CHECKPOINT_WRITE_FAIL.store(true, Ordering::Release);
for i in 2..=5 {
let _ = db.execute(
&format!("INSERT INTO fp_ckpt VALUES ({}, 'row_{}')", i, i),
(),
);
}
test_failpoints::CHECKPOINT_WRITE_FAIL.store(false, Ordering::Release);
}
{
let db = Database::open(&path).expect("Recovery should succeed");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_ckpt", ())
.expect("COUNT should work");
assert!(
count >= 1,
"At least the first row should exist, got {}",
count
);
}
}
#[test]
fn test_multiple_failpoints_sequential() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_multi (id INTEGER PRIMARY KEY, val INTEGER)",
(),
)
.expect("CREATE should succeed");
test_failpoints::WAL_WRITE_FAIL.store(true, Ordering::Release);
let _ = db.execute("INSERT INTO fp_multi VALUES (1, 100)", ());
test_failpoints::WAL_WRITE_FAIL.store(false, Ordering::Release);
db.execute("INSERT INTO fp_multi VALUES (2, 200)", ())
.expect("Should succeed after disarming WAL failpoint");
test_failpoints::SNAPSHOT_WRITE_FAIL.store(true, Ordering::Release);
let _ = db.execute("VACUUM", ());
test_failpoints::SNAPSHOT_WRITE_FAIL.store(false, Ordering::Release);
db.execute("INSERT INTO fp_multi VALUES (3, 300)", ())
.expect("Should succeed after disarming snapshot failpoint");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_multi", ())
.expect("COUNT should work");
assert!(
count >= 2,
"At least rows 2 and 3 should exist, got {}",
count
);
}
#[test]
fn test_failpoint_does_not_corrupt_existing_data() {
let _guard = failpoint_guard();
let dir = tempdir().unwrap();
let path = format!("file://{}", dir.path().display());
{
let db = Database::open(&path).expect("Failed to open database");
db.execute(
"CREATE TABLE fp_preserve (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
(),
)
.expect("CREATE should succeed");
for i in 0..20 {
db.execute(
&format!("INSERT INTO fp_preserve VALUES ({}, 'data_{}')", i, i),
(),
)
.expect("INSERT should succeed");
}
}
{
let db = Database::open(&path).expect("Reopen should succeed");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_preserve", ())
.expect("COUNT should work");
assert_eq!(count, 20);
let failpoints: &[&AtomicBool] = &[
&test_failpoints::WAL_WRITE_FAIL,
&test_failpoints::WAL_SYNC_FAIL,
&test_failpoints::SNAPSHOT_WRITE_FAIL,
&test_failpoints::SNAPSHOT_SYNC_FAIL,
&test_failpoints::SNAPSHOT_RENAME_FAIL,
&test_failpoints::CHECKPOINT_WRITE_FAIL,
];
for fp in failpoints {
fp.store(true, Ordering::Release);
let _ = db.execute("INSERT INTO fp_preserve VALUES (999, 'fail')", ());
let _ = db.execute("DELETE FROM fp_preserve WHERE id = 999", ());
let _ = db.execute("VACUUM", ());
fp.store(false, Ordering::Release);
}
let count_after: i64 = db
.query_one("SELECT COUNT(*) FROM fp_preserve WHERE id < 20", ())
.expect("COUNT should work");
assert_eq!(count_after, 20, "Original 20 rows should be preserved");
}
{
let db = Database::open(&path).expect("Final reopen should succeed");
let count: i64 = db
.query_one("SELECT COUNT(*) FROM fp_preserve WHERE id < 20", ())
.expect("COUNT should work");
assert_eq!(
count, 20,
"All original rows should survive failpoint storm"
);
}
}