use crate::log_base::{
LOG_BASE, LogHead, base_frame, base_of, prev_path, read_log_head, snapshot_id,
write_snapshot_id,
};
use crate::tests_rewrite::apply_for_test;
use crate::{
Aof, Argv, Fsync, load_snapshot, replay_aof, save_snapshot, settle_snapshot, write_snapshot_tmp,
};
use kevy_store::Store;
use std::path::{Path, PathBuf};
fn dir(name: &str) -> PathBuf {
kevy_tmpdir::unique_dir(name)
}
fn store(items: &[&str]) -> Store {
let mut s = Store::new();
for i in items {
s.rpush(b"l", &[i.as_bytes()]).unwrap();
}
s
}
fn push(store: &mut Store, aof: &mut Aof, item: &str) {
store.rpush(b"l", &[item.as_bytes()]).unwrap();
aof.append(&Argv::from(vec![b"RPUSH".to_vec(), b"l".to_vec(), item.as_bytes().to_vec()]))
.unwrap();
}
fn restore(snap: &Path, log: &Path) -> Vec<Vec<u8>> {
let mut s = Store::new();
if settle_snapshot(snap, Some(log)).unwrap() {
load_snapshot(&mut s, snap).unwrap();
}
replay_aof(log, |args| apply_for_test(&mut s, &args)).unwrap();
s.lrange(b"l", 0, -1).unwrap()
}
fn items(s: &[&str]) -> Vec<Vec<u8>> {
s.iter().map(|i| i.as_bytes().to_vec()).collect()
}
fn occupied(path: &Path) {
std::fs::create_dir(path).unwrap();
std::fs::write(path.join("x"), b"x").unwrap();
}
fn log_of(path: &Path, frames: &[Argv]) {
let mut bytes = crate::record::AOF2_MAGIC.to_vec();
for f in frames {
crate::record::write_record_multibulk(&mut bytes, f, &mut Vec::new()).unwrap();
}
std::fs::write(path, bytes).unwrap();
}
#[test]
fn a_snapshot_id_is_read_only_from_a_whole_footer() {
let d = dir("fail-snapshot-id");
std::fs::write(d.join("short"), b"KEVYSNID").unwrap();
assert_eq!(snapshot_id(&d.join("short")).unwrap(), None, "shorter than a footer");
std::fs::write(d.join("untagged"), [7u8; 40]).unwrap();
assert_eq!(snapshot_id(&d.join("untagged")).unwrap(), None, "no tag");
assert!(snapshot_id(&d.join("untagged").join("under-a-file")).is_err());
occupied(&d.join("a-directory"));
assert!(snapshot_id(&d.join("a-directory")).is_err());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_footer_that_cannot_be_written_is_an_error() {
let d = dir("fail-footer");
std::fs::write(d.join("f"), b"").unwrap();
let mut read_only = std::fs::File::open(d.join("f")).unwrap();
assert!(write_snapshot_id(&mut read_only).is_err());
assert!(write_snapshot_tmp(&Store::new(), &d.join("no-such-dir").join("dump.rdb")).is_err());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_log_head_that_is_not_the_frame_is_read_as_legacy() {
let d = dir("fail-log-head");
let log = d.join("aof-0.aof");
let mut bytes = crate::record::AOF2_MAGIC.to_vec();
crate::record::write_record(&mut bytes, b"not resp").unwrap();
std::fs::write(&log, bytes).unwrap();
assert_eq!(read_log_head(&log).unwrap(), LogHead::Legacy);
let short = Argv::from(vec![LOG_BASE.to_vec(), b"123".to_vec()]);
assert_eq!(base_of(&short), None);
log_of(&log, &[short]);
assert_eq!(read_log_head(&log).unwrap(), LogHead::Legacy);
assert!(read_log_head(&log.join("under-a-file")).is_err());
occupied(&d.join("a-directory"));
assert!(read_log_head(&d.join("a-directory")).is_err());
assert!(settle_snapshot(&d.join("dump-0.rdb"), Some(&d.join("a-directory"))).is_err());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_kept_snapshot_is_dropped_or_put_back_by_what_the_log_says() {
let d = dir("fail-prev");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let prev = prev_path(&snap);
save_snapshot(&store(&["old"]), &prev).unwrap();
save_snapshot(&store(&["new"]), &snap).unwrap();
assert!(settle_snapshot(&snap, None).unwrap());
assert!(!prev.exists());
assert!(settle_snapshot(&snap, Some(&d.join("no-log.aof"))).unwrap());
std::fs::rename(&snap, &prev).unwrap();
assert!(settle_snapshot(&snap, None).unwrap());
assert!(snap.exists() && !prev.exists());
save_snapshot(&store(&["old"]), &prev).unwrap();
Aof::open(&log, Fsync::No).unwrap().rewrite_from(&store(&["image"])).unwrap();
assert!(!settle_snapshot(&snap, Some(&log)).unwrap());
assert!(!prev.exists());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_kept_snapshot_that_cannot_be_moved_is_an_error() {
let d = dir("fail-prev-stuck");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let prev = prev_path(&snap);
save_snapshot(&store(&["new"]), &snap).unwrap();
occupied(&prev);
assert!(settle_snapshot(&snap, None).is_err());
std::fs::remove_dir_all(&prev).unwrap();
std::fs::remove_file(&snap).unwrap();
save_snapshot(&store(&["old"]), &prev).unwrap();
occupied(&snap);
log_of(&log, &[]);
assert!(settle_snapshot(&snap, Some(&log)).is_err());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_snapshot_that_cannot_be_read_under_a_log_naming_one_is_an_error() {
let d = dir("fail-unreadable-snap");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
log_of(&log, &[base_frame(42)]);
occupied(&snap);
assert!(settle_snapshot(&snap, Some(&log)).is_err(), "no kept snapshot");
occupied(&prev_path(&snap));
assert!(settle_snapshot(&snap, Some(&log)).is_err(), "with a kept one");
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_reshard_refuses_a_log_whose_snapshot_is_gone() {
let d = dir("fail-reshard");
let rpush = Argv::from(vec![b"RPUSH".to_vec(), b"l".to_vec(), b"a".to_vec()]);
log_of(&crate::layout::aof_path(&d, 0), &[rpush]);
log_of(&crate::layout::aof_path(&d, 1), &[base_frame(42)]);
let mut temp = Store::new();
let lay = crate::reshard::StdLayout;
let mut replayed = 0;
let merged = crate::reshard::merge_sources(&d, 2, &lay, &mut temp, |_, _| replayed += 1);
assert!(merged.is_err());
assert_eq!(replayed, 1, "shard 0 was read before shard 1 refused");
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_begin_inside_a_transaction_drops_the_open_one() {
let d = dir("fail-nested-begin");
let log = d.join("aof-0.aof");
let rpush = |i: &str| Argv::from(vec![b"RPUSH".to_vec(), b"l".to_vec(), i.as_bytes().to_vec()]);
let begin = Argv::from(vec![Aof::TXN_BEGIN.to_vec()]);
let commit = Argv::from(vec![Aof::TXN_COMMIT.to_vec()]);
log_of(&log, &[begin.clone(), rpush("lost"), begin, rpush("kept"), commit]);
let mut seen = Vec::new();
replay_aof(&log, |args| seen.push(args[2].to_vec())).unwrap();
assert_eq!(seen, items(&["kept"]));
std::fs::remove_dir_all(&d).unwrap();
}
fn committed(name: &str) -> (PathBuf, PathBuf, PathBuf, Aof, Store) {
let d = dir(name);
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let mut s = Store::new();
let mut aof = Aof::open(&log, Fsync::No).unwrap();
push(&mut s, &mut aof, "a");
let reset = aof.begin_view_rewrite().unwrap();
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
aof.commit_snapshot(&tmp, &snap, &reset).unwrap();
push(&mut s, &mut aof, "b");
aof.sync_now().unwrap();
(d, snap, log, aof, s)
}
#[test]
fn a_commit_that_fails_leaves_the_pair_it_found() {
let (d, snap, log, mut aof, mut s) = committed("fail-commit");
let reset = |aof: &mut Aof| aof.begin_view_rewrite().unwrap();
let r = reset(&mut aof);
let plain = d.join("plain.rdb");
std::fs::write(&plain, {
let mut b = Vec::new();
crate::write_snapshot_to(&s, &mut b).unwrap();
b
})
.unwrap();
assert!(aof.commit_snapshot(&plain, &snap, &r).is_err());
let r = reset(&mut aof);
occupied(&d.join("unreadable"));
assert!(aof.commit_snapshot(&d.join("unreadable"), &snap, &r).is_err());
let r = reset(&mut aof);
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
occupied(&prev_path(&snap));
assert!(aof.commit_snapshot(&tmp, &snap, &r).is_err());
std::fs::remove_dir_all(prev_path(&snap)).unwrap();
let r = reset(&mut aof);
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
assert!(aof.commit_snapshot(&tmp, &snap, &d.join("no-such-dir").join("reset")).is_err());
let _ = r;
push(&mut s, &mut aof, "c");
aof.sync_now().unwrap();
drop(aof);
assert!(!prev_path(&snap).exists());
assert_eq!(restore(&snap, &log), items(&["a", "b", "c"]));
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_first_snapshot_whose_log_reset_fails_is_removed() {
let d = dir("fail-first-commit");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let mut s = Store::new();
let mut aof = Aof::open(&log, Fsync::No).unwrap();
push(&mut s, &mut aof, "a");
aof.begin_view_rewrite().unwrap();
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
assert!(aof.commit_snapshot(&tmp, &snap, &d.join("no-such-dir").join("reset")).is_err());
assert!(!snap.exists());
push(&mut s, &mut aof, "b");
drop(aof);
assert_eq!(restore(&snap, &log), items(&["a", "b"]));
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn a_reset_that_failed_after_the_log_named_the_snapshot_keeps_it() {
let d = dir("fail-late-reset");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let s = store(&["a"]);
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
let id = snapshot_id(&tmp).unwrap().unwrap();
log_of(&log, &[base_frame(id)]);
let mut aof = Aof::open(&log, Fsync::No).unwrap();
aof.begin_view_rewrite().unwrap();
assert!(aof.commit_snapshot(&tmp, &snap, &d.join("no-such-dir").join("reset")).is_err());
drop(aof);
assert_eq!(restore(&snap, &log), items(&["a"]));
let tmp = write_snapshot_tmp(&s, &snap).unwrap();
let mut aof = Aof::open(&log, Fsync::No).unwrap();
aof.begin_view_rewrite().unwrap();
std::fs::remove_file(&log).unwrap();
occupied(&log);
assert!(aof.commit_snapshot(&tmp, &snap, &d.join("no-such-dir").join("reset")).is_err());
std::fs::remove_dir_all(&d).unwrap();
}
#[test]
fn an_undo_that_fails_says_so() {
let d = dir("fail-undo");
let (snap, log) = (d.join("dump-0.rdb"), d.join("aof-0.aof"));
let s = store(&["a"]);
occupied(&snap);
let mut aof = Aof::open(&log, Fsync::No).unwrap();
aof.begin_view_rewrite().unwrap();
let tmp = write_snapshot_tmp(&s, &d.join("new.rdb")).unwrap();
let err = aof.commit_snapshot(&tmp, &snap, &d.join("no-such-dir").join("reset")).unwrap_err();
assert!(err.to_string().contains("restoring the previous snapshot"), "{err}");
std::fs::remove_dir_all(&d).unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn a_snapshot_that_cannot_be_renamed_into_place_puts_the_previous_back() {
let other = Path::new("/dev/shm");
let (d, snap, log, mut aof, s) = committed("fail-exdev");
let r = aof.begin_view_rewrite().unwrap();
let away = other.join(format!("kevy-exdev-{}.rdb", std::process::id()));
let tmp = write_snapshot_tmp(&s, &away).unwrap();
assert!(aof.commit_snapshot(&tmp, &snap, &r).is_err());
let _ = std::fs::remove_file(&tmp);
drop(aof);
assert!(snap.exists() && !prev_path(&snap).exists());
assert_eq!(restore(&snap, &log), items(&["a", "b"]));
std::fs::remove_file(&snap).unwrap();
let mut aof = Aof::open(&d.join("aof-1.aof"), Fsync::No).unwrap();
let r = aof.begin_view_rewrite().unwrap();
let tmp = write_snapshot_tmp(&s, &away).unwrap();
assert!(aof.commit_snapshot(&tmp, &snap, &r).is_err());
let _ = std::fs::remove_file(&tmp);
assert!(!snap.exists());
std::fs::remove_dir_all(&d).unwrap();
}