use super::*;
use crate::tests::temp_file;
use std::fs::OpenOptions;
fn cmd(parts: &[&[u8]]) -> Argv {
Argv::from(parts.iter().map(|p| p.to_vec()).collect::<Vec<_>>())
}
#[test]
fn aof_append_and_replay() {
let path = temp_file("aof");
{
let mut aof = Aof::open(&path, Fsync::Always).unwrap();
aof.append(&cmd(&[b"SET", b"a", b"1"])).unwrap();
aof.append(&cmd(&[b"INCR", b"a"])).unwrap();
aof.append(&cmd(&[b"SET", b"b", b"hello world"])).unwrap();
}
let mut got: Vec<Argv> = Vec::new();
replay_aof(&path, |args| got.push(args)).unwrap();
assert_eq!(got.len(), 3);
assert_eq!(got[0], cmd(&[b"SET", b"a", b"1"]));
assert_eq!(got[1], cmd(&[b"INCR", b"a"]));
assert_eq!(got[2], cmd(&[b"SET", b"b", b"hello world"]));
let _ = std::fs::remove_file(&path);
}
#[test]
fn aof_group_commit_defers_then_flushes() {
let path = temp_file("aofgroup");
let mut aof = Aof::open(&path, Fsync::Always).unwrap();
aof.begin_group();
aof.append(&cmd(&[b"SET", b"a", b"1"])).unwrap();
aof.append(&cmd(&[b"SET", b"b", b"2"])).unwrap();
aof.append(&cmd(&[b"SET", b"c", b"3"])).unwrap();
let mut mid: Vec<Argv> = Vec::new();
replay_aof(&path, |a| mid.push(a)).unwrap();
assert!(mid.is_empty(), "group commit must defer until end_group, saw {}", mid.len());
aof.end_group().unwrap();
let mut after: Vec<Argv> = Vec::new();
replay_aof(&path, |a| after.push(a)).unwrap();
assert_eq!(after, vec![
cmd(&[b"SET", b"a", b"1"]),
cmd(&[b"SET", b"b", b"2"]),
cmd(&[b"SET", b"c", b"3"]),
]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn aof_truncated_tail_ignored() {
let path = temp_file("aoftail");
{
let mut aof = Aof::open(&path, Fsync::No).unwrap();
aof.append(&cmd(&[b"SET", b"a", b"1"])).unwrap();
}
let mut f = OpenOptions::new().append(true).open(&path).unwrap();
f.write_all(b"*2\r\n$3\r\nSET\r\n$5\r\nhal").unwrap(); drop(f);
let mut got: Vec<Argv> = Vec::new();
replay_aof(&path, |args| got.push(args)).unwrap();
assert_eq!(got, vec![cmd(&[b"SET", b"a", b"1"])]); let _ = std::fs::remove_file(&path);
}
#[test]
fn aof_truncate_clears() {
let path = temp_file("aoftrunc");
let mut aof = Aof::open(&path, Fsync::No).unwrap();
aof.append(&cmd(&[b"SET", b"a", b"1"])).unwrap();
aof.truncate().unwrap();
aof.append(&cmd(&[b"SET", b"b", b"2"])).unwrap();
drop(aof);
let mut got: Vec<Argv> = Vec::new();
replay_aof(&path, |args| got.push(args)).unwrap();
assert_eq!(got, vec![cmd(&[b"SET", b"b", b"2"])]); let _ = std::fs::remove_file(&path);
}
#[test]
fn replay_missing_file_is_ok() {
let path = temp_file("nofile");
let mut n = 0;
replay_aof(&path, |_| n += 1).unwrap();
assert_eq!(n, 0);
}
#[test]
fn replay_aof_with_ssh_stderr_head_does_not_panic() {
use std::io::Write;
let path = temp_file("ssh_warning_head");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(
b"Warning: Permanently added 't02.golia.jp' (ED25519) to the list of known hosts.\r\n",
).unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\nk\r\n$1\r\nv\r\n").unwrap();
drop(f);
let mut n = 0;
replay_aof(&path, |_| n += 1).expect("replay must not panic on junk input");
assert!(n >= 2, "saw at least the inline junk + the SET, got {n}");
let _ = std::fs::remove_file(&path);
}
#[test]
fn fresh_aof_has_magic_header_and_replays_cleanly() {
use std::io::Read;
let path = temp_aof("magic-fresh");
{
let mut aof = Aof::open(&path, Fsync::No).unwrap();
aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))
.unwrap();
}
let mut f = std::fs::File::open(&path).unwrap();
let mut buf = [0u8; 9];
f.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"KEVYAOF1\n");
let mut seen: Vec<Argv> = Vec::new();
replay_aof(&path, |args| seen.push(args)).unwrap();
assert_eq!(seen.len(), 1);
assert_eq!(seen[0].get(0).unwrap(), b"SET");
let _ = std::fs::remove_file(&path);
}
#[test]
fn legacy_aof_without_magic_still_replays() {
use std::io::Write;
let path = temp_aof("magic-legacy");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\nk\r\n$1\r\nv\r\n").unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\nx\r\n$1\r\ny\r\n").unwrap();
drop(f);
let mut seen: Vec<Argv> = Vec::new();
replay_aof(&path, |args| seen.push(args)).unwrap();
assert_eq!(seen.len(), 2);
let _ = std::fs::remove_file(&path);
}
#[test]
fn truncate_preserves_magic_header() {
use std::io::Read;
let path = temp_aof("magic-truncate");
let mut aof = Aof::open(&path, Fsync::No).unwrap();
aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))
.unwrap();
aof.truncate().unwrap();
assert_eq!(aof.size_bytes(), 9);
drop(aof);
let mut f = std::fs::File::open(&path).unwrap();
let mut buf = Vec::new();
f.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"KEVYAOF1\n");
let _ = std::fs::remove_file(&path);
}
#[test]
fn replay_aof_with_real_corrupt_frame_keeps_prefix() {
use std::io::Write;
let path = temp_file("real_corrupt_mid");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\n1\r\n").unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\nb\r\n$1\r\n2\r\n").unwrap();
f.write_all(b"*BAD\r\n").unwrap();
f.write_all(b"*3\r\n$3\r\nSET\r\n$1\r\nc\r\n$1\r\n3\r\n").unwrap();
drop(f);
let mut n = 0;
replay_aof(&path, |_| n += 1).expect("replay must not panic on corrupt frame");
assert_eq!(n, 2, "prefix replays; corrupt frame stops the loop; tail dropped");
let _ = std::fs::remove_file(&path);
}
pub(crate) fn temp_aof(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let uniq = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("kevy-{name}-{uniq}.aof"));
p
}