use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::error::Result;
use fs_ext4::Filesystem;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
#[track_caller]
fn copy_to_tmp(name: &str, tag: &str) -> String {
let src = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), name);
let dst =
fs_ext4_test_support::temp_path!("fs_ext4_commit_cache_{tag}_{}.img", std::process::id());
std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} -> {dst}: {e}"));
dst
}
fn root_names(fs: &Filesystem) -> Vec<String> {
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
let ino = fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, "/").expect("lookup");
let (inode, _) = fs.read_inode_verified(ino).unwrap();
let data = fs_ext4::file_io::read_all(fs, &inode).unwrap();
let bs = fs.sb.block_size() as usize;
let mut out: Vec<String> = data
.chunks(bs)
.flat_map(|b| fs_ext4::dir::parse_block(b, true).unwrap_or_default())
.map(|e| String::from_utf8_lossy(&e.name).into_owned())
.collect();
out.sort();
out
}
#[test]
fn journaled_writes_do_not_pin_blocks_past_the_cache_capacity() {
const CAPACITY: usize = 8;
const FILES: usize = 64;
let image = copy_to_tmp("ext4-basic.img", "bounded");
{
let fs =
Filesystem::mount_with_cache(Arc::new(FileDevice::open_rw(&image).unwrap()), CAPACITY)
.expect("mount rw");
assert!(
fs.journal.is_some(),
"the writes must go through the journal"
);
for i in 0..FILES {
let path = format!("/f{i:03}");
fs.apply_create(&path, 0o644).expect("create");
fs.apply_replace_file_content(&path, &vec![i as u8; 5000])
.expect("write");
}
let pinned = fs.cache_pinned_blocks();
assert!(
pinned <= CAPACITY,
"{FILES} journaled file writes left {pinned} blocks pinned in a cache of capacity {CAPACITY}"
);
assert_eq!(pinned, 0, "checkpointed blocks are still pinned");
let names = root_names(&fs);
for i in 0..FILES {
assert!(
names.contains(&format!("f{i:03}")),
"f{i:03} missing: {names:?}"
);
}
}
let fs = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).expect("remount");
let names = root_names(&fs);
for i in 0..FILES {
assert!(
names.contains(&format!("f{i:03}")),
"f{i:03} lost: {names:?}"
);
}
drop(fs);
let _ = std::fs::remove_file(&image);
}
struct CutAfterDirtyJournal {
inner: Arc<dyn BlockDevice>,
armed: AtomicBool,
flushes: AtomicUsize,
}
impl BlockDevice for CutAfterDirtyJournal {
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
self.inner.read_at(offset, buf)
}
fn size_bytes(&self) -> u64 {
self.inner.size_bytes()
}
fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
if self.armed.load(Ordering::SeqCst) && self.flushes.load(Ordering::SeqCst) >= 2 {
return Ok(());
}
self.inner.write_at(offset, buf)
}
fn flush(&self) -> Result<()> {
if self.armed.load(Ordering::SeqCst) {
self.flushes.fetch_add(1, Ordering::SeqCst);
}
self.inner.flush()
}
fn is_writable(&self) -> bool {
true
}
}
#[test]
fn a_read_only_replay_keeps_its_pins_through_fresh_read() {
let image = copy_to_tmp("ext4-basic.img", "ro_replay");
{
let dev = Arc::new(CutAfterDirtyJournal {
inner: Arc::new(FileDevice::open_rw(&image).unwrap()),
armed: AtomicBool::new(false),
flushes: AtomicUsize::new(0),
});
let fs = Filesystem::mount(dev.clone()).expect("mount rw");
assert!(
fs.journal.is_some(),
"the mkdir must go through the journal"
);
fs.apply_mkdir("/warmup", 0o755).expect("warm-up mkdir");
dev.armed.store(true, Ordering::SeqCst);
fs.apply_mkdir("/committed", 0o755).expect("mkdir");
}
let before = std::fs::read(&image).unwrap();
let mut ro = Filesystem::mount(Arc::new(FileDevice::open(&image).unwrap())).expect("mount ro");
assert!(
ro.journal.is_none(),
"a read-only mount has no journal writer"
);
let pinned = ro.cache_pinned_blocks();
assert!(
pinned > 0,
"the read-only mount replayed nothing into the cache"
);
assert!(
root_names(&ro).contains(&"committed".to_string()),
"the replayed view lost the committed mkdir"
);
let fresh = ro.fresh_read();
assert!(
fresh.is_err(),
"fresh_read claimed a physical readback the device cannot give"
);
assert_eq!(
ro.cache_pinned_blocks(),
pinned,
"fresh_read released blocks that exist only in the cache"
);
assert!(
root_names(&ro).contains(&"committed".to_string()),
"after fresh_read the committed mkdir is gone"
);
drop(ro);
assert!(
std::fs::read(&image).unwrap() == before,
"a read-only mount wrote to the device"
);
let _ = std::fs::remove_file(&image);
}