#![allow(dead_code)]
use crate::{
buffer::{ExtentBuffer, HEADER_SIZE, ITEM_SIZE},
filesystem::Filesystem,
items,
path::BtrfsPath,
search::{self, SearchIntent},
transaction::Transaction,
};
use btrfs_disk::tree::{DiskKey, KeyType};
use std::{
collections::BTreeMap,
fs::File,
io,
path::{Path, PathBuf},
process::Command,
sync::mpsc,
thread,
time::Duration,
};
fn find_our_mkfs() -> PathBuf {
let exe =
std::env::current_exe().expect("cannot determine test binary path");
let target_dir = exe
.parent()
.and_then(Path::parent)
.expect("cannot determine target directory from test binary path");
let mkfs = target_dir.join("btrfs-mkfs");
assert!(
mkfs.exists(),
"btrfs-mkfs not found at {}; run `cargo build -p btrfs-mkfs` first",
mkfs.display()
);
mkfs
}
pub struct TestFixture {
_dir: tempfile::TempDir,
pub path: PathBuf,
}
impl Default for TestFixture {
fn default() -> Self {
Self::new()
}
}
impl TestFixture {
#[must_use]
pub fn new() -> Self {
let dir = tempfile::TempDir::new().expect("failed to create temp dir");
let img_path = dir.path().join("test.img");
let file =
File::create(&img_path).expect("failed to create image file");
file.set_len(256 * 1024 * 1024)
.expect("failed to set image size");
drop(file);
let mkfs = find_our_mkfs();
let status = Command::new(&mkfs)
.args(["-f", "-q"])
.arg(&img_path)
.status()
.unwrap_or_else(|e| {
panic!("btrfs-mkfs at {} failed to run: {e}", mkfs.display())
});
assert!(status.success(), "btrfs-mkfs failed with {status}");
Self {
_dir: dir,
path: img_path,
}
}
pub fn open(&self) -> io::Result<Filesystem<File>> {
let file = File::options().read(true).write(true).open(&self.path)?;
Filesystem::open(file)
}
pub fn assert_check(&self) {
let output = Command::new("btrfs")
.args(["check", "--readonly"])
.arg(&self.path)
.output()
.expect("btrfs check not found");
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
panic!(
"btrfs check failed:\n--- stderr ---\n{stderr}\n--- stdout ---\n{stdout}"
);
}
}
}
pub fn insert_test_items<R: io::Read + io::Write + io::Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
tree_id: u64,
start_oid: u64,
count: usize,
data_size: usize,
) -> io::Result<usize> {
let data = vec![0xAB; data_size];
for i in 0..count {
let key = DiskKey {
objectid: start_oid + i as u64,
key_type: KeyType::TemporaryItem,
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *trans),
fs_info,
tree_id,
&key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + data.len()) as u32),
true,
)?;
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("no leaf"))?;
items::insert_item(leaf, path.slots[0], &key, &data)?;
fs_info.mark_dirty(leaf);
path.release();
}
Ok(count)
}
pub fn validate_leaf_offsets<R: io::Read + io::Write + io::Seek>(
fs_info: &mut Filesystem<R>,
root_bytenr: u64,
) -> io::Result<()> {
let eb = fs_info.read_block(root_bytenr)?;
if eb.level() == 0 {
validate_single_leaf(&eb)?;
} else {
for i in 0..eb.nritems() as usize {
let child_bytenr = eb.key_ptr_blockptr(i);
validate_leaf_offsets(fs_info, child_bytenr)?;
}
}
Ok(())
}
fn validate_single_leaf(eb: &ExtentBuffer) -> io::Result<()> {
let nritems = eb.nritems() as usize;
if nritems == 0 {
return Ok(());
}
let first_end = eb.item_offset(0) + eb.item_size(0);
let expected_end = eb.nodesize() - HEADER_SIZE as u32;
if first_end != expected_end {
return Err(io::Error::other(format!(
"leaf at {}: item[0] data end={first_end} != expected={expected_end}",
eb.logical()
)));
}
for i in 0..nritems - 1 {
if eb.item_offset(i) < eb.item_offset(i + 1) {
return Err(io::Error::other(format!(
"leaf at {}: offset[{i}]={} < offset[{}]={}",
eb.logical(),
eb.item_offset(i),
i + 1,
eb.item_offset(i + 1)
)));
}
}
for i in 0..nritems - 1 {
let k1 = eb.item_key(i);
let k2 = eb.item_key(i + 1);
if crate::buffer::key_cmp(&k1, &k2) != std::cmp::Ordering::Less {
return Err(io::Error::other(format!(
"leaf at {}: key[{i}]={:?} not < key[{}]={:?}",
eb.logical(),
k1,
i + 1,
k2
)));
}
}
Ok(())
}
pub const PLAYGROUND_TREE: u64 = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TestKey {
pub objectid: u8,
pub offset: u8,
}
impl TestKey {
#[must_use]
pub fn to_disk(self) -> DiskKey {
DiskKey {
objectid: u64::from(self.objectid),
key_type: KeyType::TemporaryItem,
offset: u64::from(self.offset),
}
}
}
#[derive(Debug, Clone)]
pub enum Op {
Insert { key: TestKey, value: Vec<u8> },
Update { key: TestKey, value: Vec<u8> },
Delete { key: TestKey },
AllocBlock,
DropOwnedBlock { idx: usize },
RemovePlaygroundRoot,
Commit,
Abort,
Reopen,
}
#[derive(Debug, Default, Clone)]
pub struct Model {
pub kv: BTreeMap<TestKey, Vec<u8>>,
pub owned_blocks: Vec<(u64, u8)>,
pub playground_removed: bool,
pub committed_kv: BTreeMap<TestKey, Vec<u8>>,
pub committed_playground_removed: bool,
}
#[derive(Debug)]
pub enum Failure {
OpFailed { step: usize, op: Op, err: io::Error },
ModelMismatch { step: usize, detail: String },
Hang { timeout: Duration },
CheckFailed { stderr: String },
Other(String),
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OpFailed { step, op, err } => {
write!(f, "op {step} {op:?} failed: {err}")
}
Self::ModelMismatch { step, detail } => {
write!(f, "model mismatch after step {step}: {detail}")
}
Self::Hang { timeout } => {
write!(f, "sequence hung (timeout {timeout:?})")
}
Self::CheckFailed { stderr } => {
write!(f, "btrfs check failed: {stderr}")
}
Self::Other(s) => f.write_str(s),
}
}
}
impl std::error::Error for Failure {}
#[allow(clippy::result_unit_err)]
pub fn with_watchdog<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let _ = tx.send(f());
});
rx.recv_timeout(timeout).map_err(|_| ())
}
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
fn apply_op<R: io::Read + io::Write + io::Seek>(
op: &Op,
trans: &mut Transaction<R>,
fs: &mut Filesystem<R>,
model: &mut Model,
) -> io::Result<bool> {
match op {
Op::Insert { key, value } => {
apply_insert(*key, value, trans, fs, model)?;
Ok(false)
}
Op::Update { key, value } => {
apply_update(*key, value, trans, fs, model)?;
Ok(false)
}
Op::Delete { key } => {
apply_delete(*key, trans, fs, model)?;
Ok(false)
}
Op::AllocBlock => {
if model.playground_removed {
return Ok(false);
}
let bytenr = trans.alloc_tree_block(fs, PLAYGROUND_TREE, 0)?;
model.owned_blocks.push((bytenr, 0));
Ok(false)
}
Op::DropOwnedBlock { idx } => {
if model.owned_blocks.is_empty() {
return Ok(false);
}
let real_idx = *idx % model.owned_blocks.len();
let (bytenr, level) = model.owned_blocks.swap_remove(real_idx);
trans
.delayed_refs
.drop_ref(bytenr, true, PLAYGROUND_TREE, level);
trans.pin_block(bytenr);
Ok(false)
}
Op::RemovePlaygroundRoot => {
let model_dirty = model.kv != model.committed_kv
|| !model.owned_blocks.is_empty();
let tree_root_cowed = fs
.changed_roots()
.iter()
.any(|(tid, _, _)| *tid == PLAYGROUND_TREE);
let dirty_uncommitted = model_dirty || tree_root_cowed;
if !model.playground_removed && !dirty_uncommitted {
fs.remove_root(PLAYGROUND_TREE);
model.playground_removed = true;
}
Ok(false)
}
Op::Commit | Op::Abort | Op::Reopen => {
Ok(true)
}
}
}
fn drain_owned_blocks<R: io::Read + io::Write + io::Seek>(
trans: &mut Transaction<R>,
model: &mut Model,
) {
for (bytenr, level) in model.owned_blocks.drain(..) {
trans
.delayed_refs
.drop_ref(bytenr, true, PLAYGROUND_TREE, level);
trans.pin_block(bytenr);
}
}
fn apply_insert<R: io::Read + io::Write + io::Seek>(
key: TestKey,
value: &[u8],
trans: &mut Transaction<R>,
fs: &mut Filesystem<R>,
model: &mut Model,
) -> io::Result<()> {
if model.playground_removed || model.kv.contains_key(&key) {
return Ok(());
}
let dk = key.to_disk();
let mut path = BtrfsPath::new();
let needed = (ITEM_SIZE + value.len()) as u32;
let found = search::search_slot(
Some(&mut *trans),
fs,
PLAYGROUND_TREE,
&dk,
&mut path,
SearchIntent::Insert(needed),
true,
)?;
if found {
path.release();
return Ok(());
}
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("no leaf after search"))?;
items::insert_item(leaf, path.slots[0], &dk, value)?;
fs.mark_dirty(leaf);
path.release();
model.kv.insert(key, value.to_vec());
Ok(())
}
fn apply_update<R: io::Read + io::Write + io::Seek>(
key: TestKey,
value: &[u8],
trans: &mut Transaction<R>,
fs: &mut Filesystem<R>,
model: &mut Model,
) -> io::Result<()> {
if model.playground_removed || !model.kv.contains_key(&key) {
return Ok(());
}
apply_delete(key, trans, fs, model)?;
apply_insert(key, value, trans, fs, model)?;
Ok(())
}
fn apply_delete<R: io::Read + io::Write + io::Seek>(
key: TestKey,
trans: &mut Transaction<R>,
fs: &mut Filesystem<R>,
model: &mut Model,
) -> io::Result<()> {
if model.playground_removed || !model.kv.contains_key(&key) {
return Ok(());
}
let dk = key.to_disk();
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *trans),
fs,
PLAYGROUND_TREE,
&dk,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(
"model says key exists but tree disagrees",
));
}
let slot = path.slots[0];
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("no leaf after search"))?;
items::del_items(leaf, slot, 1);
fs.mark_dirty(leaf);
path.release();
model.kv.remove(&key);
Ok(())
}
fn check_model_matches<R: io::Read + io::Write + io::Seek>(
fs: &mut Filesystem<R>,
model: &Model,
) -> Result<(), String> {
if model.playground_removed {
return Ok(());
}
for (key, want) in &model.kv {
let dk = key.to_disk();
let mut path = BtrfsPath::new();
let found = search::search_slot(
None,
fs,
PLAYGROUND_TREE,
&dk,
&mut path,
SearchIntent::ReadOnly,
false,
)
.map_err(|e| format!("search_slot({key:?}) failed: {e}"))?;
if !found {
path.release();
return Err(format!("key {key:?} missing from playground tree"));
}
let leaf = path.nodes[0].as_ref().expect("leaf after found");
let got = leaf.item_data(path.slots[0]).to_vec();
path.release();
if got != *want {
return Err(format!(
"key {key:?}: tree value {got:?} != model value {want:?}"
));
}
}
Ok(())
}
pub fn run_sequence(ops: &[Op]) -> Result<(), Failure> {
let fixture = TestFixture::new();
let mut fs = fixture
.open()
.map_err(|e| Failure::Other(format!("open: {e}")))?;
if fs.root_bytenr(PLAYGROUND_TREE).is_none() {
let mut setup = Transaction::start(&mut fs)
.map_err(|e| Failure::Other(format!("setup start: {e}")))?;
setup
.create_empty_tree(&mut fs, PLAYGROUND_TREE)
.map_err(|e| {
Failure::Other(format!("create playground tree: {e}"))
})?;
setup
.commit(&mut fs)
.map_err(|e| Failure::Other(format!("setup commit: {e}")))?;
}
let mut trans = Transaction::start(&mut fs)
.map_err(|e| Failure::Other(format!("start: {e}")))?;
let mut model = Model::default();
for (step, op) in ops.iter().enumerate() {
let restart =
apply_op(op, &mut trans, &mut fs, &mut model).map_err(|err| {
Failure::OpFailed {
step,
op: op.clone(),
err,
}
})?;
if restart {
if matches!(op, Op::Commit | Op::Abort | Op::Reopen) {
drain_owned_blocks(&mut trans, &mut model);
}
match op {
Op::Commit => {
trans.commit(&mut fs).map_err(|err| Failure::OpFailed {
step,
op: op.clone(),
err,
})?;
model.committed_kv = model.kv.clone();
model.committed_playground_removed =
model.playground_removed;
model.owned_blocks.clear();
trans = Transaction::start(&mut fs)
.map_err(|e| Failure::Other(format!("restart: {e}")))?;
}
Op::Abort => {
trans.abort(&mut fs);
model.kv = model.committed_kv.clone();
model.playground_removed =
model.committed_playground_removed;
model.owned_blocks.clear();
trans = Transaction::start(&mut fs)
.map_err(|e| Failure::Other(format!("restart: {e}")))?;
}
Op::Reopen => {
trans.abort(&mut fs);
drop(fs);
fs = fixture
.open()
.map_err(|e| Failure::Other(format!("reopen: {e}")))?;
model.kv = model.committed_kv.clone();
model.playground_removed =
model.committed_playground_removed;
model.owned_blocks.clear();
trans = Transaction::start(&mut fs)
.map_err(|e| Failure::Other(format!("restart: {e}")))?;
}
_ => unreachable!("only lifecycle ops set restart=true"),
}
}
check_model_matches(&mut fs, &model)
.map_err(|detail| Failure::ModelMismatch { step, detail })?;
}
drain_owned_blocks(&mut trans, &mut model);
trans
.commit(&mut fs)
.map_err(|e| Failure::Other(format!("final commit: {e}")))?;
drop(fs);
let mut fs2 = fixture
.open()
.map_err(|e| Failure::Other(format!("final reopen: {e}")))?;
check_model_matches(&mut fs2, &model).map_err(|detail| {
Failure::ModelMismatch {
step: ops.len(),
detail,
}
})?;
drop(fs2);
let output = Command::new("btrfs")
.args(["check", "--readonly"])
.arg(&fixture.path)
.output()
.map_err(|e| Failure::Other(format!("btrfs check spawn: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(Failure::CheckFailed { stderr });
}
Ok(())
}
pub fn run_sequence_watchdogged(
ops: Vec<Op>,
timeout: Duration,
) -> Result<(), Failure> {
match with_watchdog(timeout, move || run_sequence(&ops)) {
Ok(result) => result,
Err(()) => Err(Failure::Hang { timeout }),
}
}