//! The end-to-end proof of the defining requirement: the real `beeping` //! binary is SIGKILLed at random points during a backup, over and over, //! and the eventually-completed backup must restore byte-for-byte, //! metadata and all. //! //! Everything runs on synthetic trees under a temp directory. mod common; use std::{ path::Path, process::{Command, Stdio}, time::Duration, }; use common::{assert_trees_equal, deterministic_bytes}; const PASSWORD: &str = "crash-test-password"; fn beeping(dir: &Path) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_beeping")); command .current_dir(dir) .env("BEEPING_PASSWORD", PASSWORD) .stdout(Stdio::piped()) .stderr(Stdio::null()); command } fn run_to_completion(mut command: Command, what: &str) -> String { let output = command.output().unwrap(); assert!( output.status.success(), "{what} failed: {}", String::from_utf8_lossy(&output.stdout) ); String::from_utf8(output.stdout).unwrap() } #[test] fn backup_survives_repeated_sigkill() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("tree"); // A tree big enough that a backup takes meaningfully longer than the // kill delays below for outer in 0..30 { let sub = root.join(format!("dir-{outer:02}")); std::fs::create_dir_all(&sub).unwrap(); for file in 0..25 { std::fs::write( sub.join(format!("file-{file:02}.dat")), deterministic_bytes(30_000, (outer * 1000 + file) as u64), ) .unwrap(); } } #[cfg(unix)] std::os::unix::fs::symlink("dir-00/file-00.dat", root.join("link")).unwrap(); run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args(["init", "--repository", "repo"]); cmd }, "init", ); let backup_args = [ "backup", "tree", "--repository", "repo", "--state-dir", "state", "--plain", ]; // Kill the backup at a spread of points: during repository // unlocking, mid-scan, mid-chunking, and (as delays grow) possibly // during finalization. let mut completed = None; for round in 0..12u64 { let mut child = { let mut cmd = beeping(dir.path()); cmd.args(backup_args); cmd.spawn().unwrap() }; std::thread::sleep(Duration::from_millis(150 + round * 120)); // SIGKILL: no signal handler, no cleanup, no goodbye let killed = child.kill().is_ok(); let status = child.wait().unwrap(); if !killed || status.success() { // It finished before the kill landed let mut stdout = String::new(); use std::io::Read as _; if let Some(mut pipe) = child.stdout.take() { let _ = pipe.read_to_string(&mut stdout); } if status.success() { completed = Some(stdout); break; } } } // However the kill rounds went, a final undisturbed run completes // the backup. let stdout = match completed { Some(stdout) => stdout, None => run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args(backup_args); cmd }, "final backup", ), }; let snapshot = stdout .lines() .find_map(|line| line.strip_prefix("done: snapshot ")) .unwrap_or_else(|| panic!("no snapshot id in output: {stdout}")) .trim() .to_string(); // The tree never changed, so the restored result must match it // exactly — contents, symlinks, modes, and mtimes. run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args([ "restore", &snapshot, "restored", "--repository", "repo", "--state-dir", "restore-state", "--plain", ]); cmd }, "restore", ); assert_trees_equal(&root, &dir.path().join("restored")); } #[test] fn restore_survives_repeated_sigkill() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().join("tree"); for outer in 0..20 { let sub = root.join(format!("dir-{outer:02}")); std::fs::create_dir_all(&sub).unwrap(); for file in 0..20 { std::fs::write( sub.join(format!("file-{file:02}.dat")), deterministic_bytes(40_000, (outer * 77 + file) as u64), ) .unwrap(); } } run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args(["init", "--repository", "repo"]); cmd }, "init", ); let stdout = run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args([ "backup", "tree", "--repository", "repo", "--state-dir", "state", "--plain", ]); cmd }, "backup", ); let snapshot = stdout .lines() .find_map(|line| line.strip_prefix("done: snapshot ")) .unwrap() .trim() .to_string(); let restore_args = [ "restore", &snapshot, "restored", "--repository", "repo", "--state-dir", "restore-state", "--plain", ]; for round in 0..8u64 { let mut child = { let mut cmd = beeping(dir.path()); cmd.args(restore_args); cmd.spawn().unwrap() }; std::thread::sleep(Duration::from_millis(150 + round * 130)); let _ = child.kill(); let status = child.wait().unwrap(); if status.success() { break; } } run_to_completion( { let mut cmd = beeping(dir.path()); cmd.args(restore_args); cmd }, "final restore", ); assert_trees_equal(&root, &dir.path().join("restored")); }