use super::{CountingFs, FileWrites, WriteOrigin};
use crate::fs::{Fault, FaultFs, FaultOp, FaultRule, Fs, FsOpenOptions, MemFs};
use std::io::{Read as _, Write as _};
use std::path::Path;
use std::sync::Arc;
fn writable(origin: Option<WriteOrigin>) -> FsOpenOptions {
let opts = FsOpenOptions::new().write(true).create_new(true);
match origin {
Some(origin) => opts.write_origin(origin),
None => opts,
}
}
#[test]
fn writes_are_counted_under_the_origin_they_were_opened_with() -> crate::io::Result<()> {
let fs = CountingFs::new(Arc::new(MemFs::new()));
fs.create_dir_all(Path::new("/db"))?;
let mut table = fs.open(Path::new("/db/1"), &writable(Some(WriteOrigin::FlushTable)))?;
table.write_all(&[7; 100])?;
table.write_all(&[8; 28])?;
table.sync_all()?;
let mut stray = fs.open(Path::new("/db/2"), &writable(None))?;
stray.write_all(&[9; 5])?;
let reading = fs.stats().snapshot();
let flush = reading.origin(WriteOrigin::FlushTable);
assert_eq!(flush.bytes, 128);
assert_eq!(flush.writes, 2);
assert_eq!(flush.opened, 1);
assert_eq!(flush.created, 1);
assert_eq!(flush.syncs, 1);
assert_eq!(reading.unattributed.bytes, 5);
assert_eq!(
reading.origin(WriteOrigin::CompactionTable),
FileWrites::default()
);
assert_eq!(reading.files().bytes, 133);
assert_eq!(reading.directory.dirs_created, 1);
Ok(())
}
#[test]
fn a_file_opened_for_reading_is_not_counted() -> crate::io::Result<()> {
let fs = CountingFs::new(Arc::new(MemFs::new()));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(Path::new("/db/1"), &writable(Some(WriteOrigin::Manifest)))?;
file.write_all(b"abc")?;
drop(file);
let before = fs.stats().snapshot();
let mut file = fs.open(Path::new("/db/1"), &FsOpenOptions::new().read(true))?;
let mut read = Vec::new();
file.read_to_end(&mut read)?;
assert_eq!(read, b"abc");
assert_eq!(fs.stats().snapshot(), before);
Ok(())
}
#[test]
fn a_short_write_counts_the_bytes_taken_once() -> crate::io::Result<()> {
let faulty = FaultFs::new(MemFs::new());
faulty
.injector()
.arm(FaultRule::new(FaultOp::Write, Fault::ShortWrite(3)).once());
let fs = CountingFs::new(Arc::new(faulty));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(Path::new("/db/1"), &writable(Some(WriteOrigin::Repair)))?;
file.write_all(&[1; 10])?;
let repair = fs.stats().snapshot().origin(WriteOrigin::Repair);
assert_eq!(repair.bytes, 10, "3 taken short, the other 7 on the retry");
assert_eq!(repair.writes, 2);
assert_eq!(repair.short_writes, 1);
assert_eq!(repair.write_errors, 0);
Ok(())
}
#[test]
fn failed_writes_and_syncs_are_counted_as_failures() -> crate::io::Result<()> {
let faulty = FaultFs::new(MemFs::new());
let injector = faulty.injector();
let fs = CountingFs::new(Arc::new(faulty));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(Path::new("/db/1"), &writable(Some(WriteOrigin::Metadata)))?;
injector.arm(FaultRule::new(FaultOp::Write, Fault::Error(crate::io::ErrorKind::Other)).once());
assert!(file.write(&[1; 4]).is_err());
injector
.arm(FaultRule::new(FaultOp::SyncAll, Fault::Error(crate::io::ErrorKind::Other)).once());
assert!(file.sync_all().is_err());
file.sync_all()?;
let metadata = fs.stats().snapshot().origin(WriteOrigin::Metadata);
assert_eq!(metadata.bytes, 0);
assert_eq!(metadata.write_errors, 1);
assert_eq!(metadata.syncs, 2);
assert_eq!(metadata.sync_errors, 1);
Ok(())
}
#[test]
fn a_started_writeback_is_counted_apart_from_syncs() -> crate::io::Result<()> {
let fs = CountingFs::new(Arc::new(MemFs::new()));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(
Path::new("/db/1"),
&writable(Some(WriteOrigin::CompactionTable)),
)?;
file.write_all(&[1; 64])?;
file.start_writeback(0, 64)?;
file.sync_data()?;
let compaction = fs.stats().snapshot().origin(WriteOrigin::CompactionTable);
assert_eq!(compaction.writebacks, 1);
assert_eq!(compaction.syncs, 1);
assert_eq!(compaction.bytes, 64);
Ok(())
}
#[test]
fn directory_operations_are_counted() -> crate::io::Result<()> {
let faulty = FaultFs::new(MemFs::new());
let injector = faulty.injector();
let fs = CountingFs::new(Arc::new(faulty));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(Path::new("/db/1"), &writable(Some(WriteOrigin::Checkpoint)))?;
file.write_all(&[0; 64])?;
drop(file);
fs.hard_link(Path::new("/db/1"), Path::new("/db/2"))?;
fs.rename(Path::new("/db/2"), Path::new("/db/3"))?;
fs.remove_file(Path::new("/db/3"))?;
assert!(fs.remove_file(Path::new("/db/absent")).is_err());
fs.sync_directory(Path::new("/db"))?;
injector.arm(
FaultRule::new(
FaultOp::SyncDirectory,
Fault::Error(crate::io::ErrorKind::Other),
)
.once(),
);
assert!(fs.sync_directory(Path::new("/db")).is_err());
fs.sync_device(Path::new("/db"))?;
injector.arm(
FaultRule::new(
FaultOp::SyncDevice,
Fault::Error(crate::io::ErrorKind::Other),
)
.once(),
);
assert!(fs.sync_device(Path::new("/db")).is_err());
let reading = fs.stats().snapshot();
assert_eq!(reading.directory.device_syncs, 2);
assert_eq!(reading.directory.device_sync_errors, 1);
assert_eq!(reading.directory.linked, 1);
assert_eq!(reading.directory.renamed, 1);
assert_eq!(
reading.directory.removed, 1,
"the failed removal is not one"
);
assert_eq!(reading.directory.syncs, 2);
assert_eq!(reading.directory.sync_errors, 1);
assert_eq!(reading.files().bytes, 64, "the link copied nothing");
assert_eq!(reading.syncs(), 4);
Ok(())
}
#[test]
fn an_existing_directory_is_not_counted_as_created() -> crate::io::Result<()> {
let fs = CountingFs::new(Arc::new(MemFs::new()));
fs.create_dir_all(Path::new("/db/tables"))?;
fs.create_dir_all(Path::new("/db/tables"))?;
fs.create_dir_all(Path::new("/db"))?;
assert!(fs.create_dir(Path::new("/db")).is_err());
assert_eq!(fs.stats().snapshot().directory.dirs_created, 1);
Ok(())
}
#[test]
fn readings_from_two_lifetimes_have_no_difference() -> crate::io::Result<()> {
let backend: Arc<dyn Fs> = Arc::new(MemFs::new());
backend.create_dir_all(Path::new("/db"))?;
let first = CountingFs::new(Arc::clone(&backend));
let mut file = first.open(Path::new("/db/1"), &writable(Some(WriteOrigin::Manifest)))?;
file.write_all(&[0; 10])?;
let early = first.stats().snapshot();
file.write_all(&[0; 6])?;
let late = first.stats().snapshot();
let between = late
.since(&early)
.map(|d| d.origin(WriteOrigin::Manifest).bytes);
assert_eq!(between, Some(6), "one lifetime");
let reopened = CountingFs::new(backend).stats().snapshot();
assert!(reopened.since(&late).is_none(), "the counters started over");
Ok(())
}
#[test]
fn every_origin_has_its_own_name() {
let names: Vec<&str> = WriteOrigin::ALL.iter().map(|o| o.name()).collect();
assert_eq!(
names,
[
"flush_table",
"flush_blob",
"ingest_table",
"ingest_blob",
"compaction_table",
"compaction_blob",
"manifest",
"metadata",
"repair",
"checkpoint",
]
);
}
#[test]
fn what_the_wrapper_hands_on_answers_as_the_backend_does() -> crate::io::Result<()> {
use crate::fs::{BlockBuf, BlockRead};
use std::io::{IoSlice, Seek as _, SeekFrom};
let backend: Arc<dyn Fs> = Arc::new(MemFs::new());
let fs = CountingFs::new(Arc::clone(&backend));
fs.create_dir_all(Path::new("/db"))?;
let mut file = fs.open(
Path::new("/db/1"),
&writable(Some(WriteOrigin::Repair)).read(true),
)?;
let written = file.write_vectored(&[IoSlice::new(b"ab"), IoSlice::new(b"cd")])?;
file.write_all(b"abcd".get(written..).unwrap_or_default())?;
file.seek(SeekFrom::Start(0))?;
let mut read = [0; 4];
file.read_exact(&mut read)?;
assert_eq!(&read, b"abcd");
let (mut a, mut b) = ([0; 2], [0; 2]);
file.read_many(&mut [(0, &mut a[..]), (2, &mut b[..])])?;
assert_eq!((&a, &b), (b"ab", b"cd"));
let inner = backend.open(Path::new("/db/1"), &FsOpenOptions::new().read(true))?;
assert_eq!(file.backing_fd(), inner.backing_fd());
file.lock_exclusive()?;
let mut buf = [0; 4];
let mut reqs = [BlockRead {
file: &*inner,
offset: 0,
buf: BlockBuf::new(&mut buf),
}];
fs.read_blocks_batched(&mut reqs)?;
assert_eq!(&buf, b"abcd");
assert_eq!(fs.reads_by_descriptor(), backend.reads_by_descriptor());
let check = |fs: &dyn Fs| {
let mut buf = [0; 4];
let reqs = [BlockRead {
file: &*inner,
offset: 0,
buf: BlockBuf::new(&mut buf),
}];
fs.check_descriptor_batch(&reqs).is_ok()
};
assert_eq!(check(&fs), check(&*backend));
let link = (
fs.symlink(Path::new("/db/1"), Path::new("/db/l"), false)
.is_ok(),
fs.symlink_to_dir(Path::new("/db/l")).ok(),
);
let clone = fs
.reflink_file(Path::new("/db/1"), Path::new("/db/c"))
.is_ok();
let holes = (
fs.extent_is_hole(Path::new("/db/1"), 0, 4).ok(),
fs.extent_contains_hole(Path::new("/db/1"), 0, 4).ok(),
);
let reading = fs.stats().snapshot();
assert_eq!(reading.directory.symlinked, u64::from(link.0));
assert_eq!(reading.directory.cloned, u64::from(clone));
assert_eq!(
reading.directory.cloned_bytes,
if clone { 4 } else { 0 },
"a clone counts the length of what it made, copied or shared"
);
assert_eq!(
holes,
(
backend.extent_is_hole(Path::new("/db/1"), 0, 4).ok(),
backend.extent_contains_hole(Path::new("/db/1"), 0, 4).ok(),
)
);
let repair = reading.origin(WriteOrigin::Repair);
assert_eq!(repair.bytes, 4);
assert_eq!(repair.writes, 1 + u64::from(written < 4));
Ok(())
}