crates/appliance-pid1/tests/quiesce_for_power_off.rs
//! **A clean power-off leaves the data disk's log CLEAN** (2026-10-01, the KVM proof of
//! test2: `XFS (vda3): Starting recovery` after a stop that froze /data). Linux 6.12's XFS
//! freeze only QUIESCES the log (`xfs_fs_freeze` → `xfs_log_quiesce`, no unmount record);
//! the record that lets the next mount skip recovery is written by a READ-ONLY REMOUNT
//! (`xfs_remount_ro` → `xfs_log_clean`). So: `sync(2)`, then this process's own writers on
//! the data mount released (pointed at /dev/null), then the remount read-only — and the
//! freeze only when the remount is refused. Each step bounded, each said.
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use appliance_pid1::power::{quiesce_for_power_off, QuiesceOps};
#[derive(Default)]
struct Fake {
order: Mutex<Vec<&'static str>>,
remount_refused: bool,
hangs: Option<&'static str>,
}
impl Fake {
fn step(&self, s: &'static str) {
self.order.lock().unwrap().push(s);
if self.hangs == Some(s) {
std::thread::sleep(Duration::from_secs(30));
}
}
}
impl QuiesceOps for Fake {
fn sync_all(&self) {
self.step("sync");
}
fn release_writers(&self, _data: &Path) -> usize {
self.step("release");
2
}
fn remount_readonly(&self, _data: &Path) -> std::io::Result<()> {
self.step("remount");
if self.remount_refused {
Err(std::io::Error::from_raw_os_error(16))
} else {
Ok(())
}
}
fn freeze(&self, _data: &Path) -> std::io::Result<()> {
self.step("freeze");
Ok(())
}
}
fn run(f: Arc<Fake>) -> (Vec<String>, Vec<String>, Duration) {
let said = Mutex::new(Vec::<String>::new());
let t0 = Instant::now();
let steps = quiesce_for_power_off(f, Path::new("/data"), Duration::from_millis(500), &|l: &str| said.lock().unwrap().push(l.to_string()));
(steps, said.into_inner().unwrap(), t0.elapsed())
}
#[test]
fn sync_then_writers_released_then_a_read_only_remount_and_no_freeze_when_it_takes() {
let f = Arc::new(Fake::default());
let (steps, said, _) = run(f.clone());
assert_eq!(*f.order.lock().unwrap(), vec!["sync", "release", "remount"], "the remount cleans the log; no freeze after it");
assert!(steps.iter().any(|s| s.contains("read-only") && s.contains("clean")), "{steps:?}");
assert_eq!(said, steps, "every step is said as it happens");
}
#[test]
fn a_refused_remount_falls_back_to_the_freeze_and_says_the_log_is_not_clean() {
let f = Arc::new(Fake { remount_refused: true, ..Default::default() });
let (steps, _, _) = run(f.clone());
assert_eq!(*f.order.lock().unwrap(), vec!["sync", "release", "remount", "freeze"]);
assert!(steps.iter().any(|s| s.contains("NOT remounted")), "{steps:?}");
assert!(steps.iter().any(|s| s.contains("frozen") && s.contains("replay")), "a freeze does not clean the log, and says so: {steps:?}");
}
#[test]
fn a_step_that_hangs_does_not_hold_the_power_off() {
let f = Arc::new(Fake { hangs: Some("remount"), ..Default::default() });
let (steps, _, took) = run(f);
assert!(took < Duration::from_secs(5), "held {took:?}");
assert!(steps.iter().any(|s| s.contains("not back within")), "{steps:?}");
}