sausage-vfs: tests per topic under WASI, nested differential tests, contract through Mounts
This commit is contained in:
@@ -1,9 +1,30 @@
|
||||
//! Exercises Rust filesystem clients through the virtual WASI filesystem.
|
||||
//! Runs independently selected WASI filesystem test topics.
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::ErrorKind,
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
};
|
||||
|
||||
/// Select one topic so host failures identify the failing contract.
|
||||
fn main() {
|
||||
match std::env::var("VFS_TOPIC").unwrap().as_str() {
|
||||
"basic" => basic(),
|
||||
"trees" => trees(),
|
||||
"options" => options(),
|
||||
"rename" => rename(),
|
||||
"sparse" => sparse(),
|
||||
"streams" => streams(),
|
||||
"mounts" => mounts(),
|
||||
"shared_write" => fs::write("/run/shared", b"shared between guests").unwrap(),
|
||||
"shared_read" => assert_eq!(fs::read("/run/shared").unwrap(), b"shared between guests"),
|
||||
"sqlite" => sqlite(),
|
||||
"tempfile" => temporary(),
|
||||
topic => panic!("unknown topic: {topic}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic operations preserve file contents, sizes, and relative path resolution.
|
||||
fn basic() {
|
||||
fs::create_dir("/work").unwrap();
|
||||
fs::write("/work/a", b"hello").unwrap();
|
||||
let mut f = OpenOptions::new()
|
||||
@@ -15,25 +36,171 @@ fn main() {
|
||||
f.write_all(b"A").unwrap();
|
||||
f.set_len(4).unwrap();
|
||||
f.sync_all().unwrap();
|
||||
OpenOptions::new()
|
||||
.append(true)
|
||||
.open("/work/a")
|
||||
assert_eq!(fs::read("work/../work/a").unwrap(), b"hAll");
|
||||
assert_eq!(fs::metadata("/work/a").unwrap().len(), 4);
|
||||
fs::remove_file("/work/a").unwrap();
|
||||
fs::remove_dir("/work").unwrap();
|
||||
}
|
||||
|
||||
/// Recursive folder operations and directory entries expose the complete tree.
|
||||
fn trees() {
|
||||
fs::create_dir_all("/tree/a/b").unwrap();
|
||||
fs::create_dir_all("/tree/a/b").unwrap();
|
||||
fs::write("/tree/a/file", b"contents").unwrap();
|
||||
let entries = fs::read_dir("/tree/a")
|
||||
.unwrap()
|
||||
.write_all(b"!")
|
||||
.unwrap();
|
||||
assert_eq!(fs::read("/work/a").unwrap(), b"hAll!");
|
||||
assert_eq!(fs::metadata("/work/a").unwrap().len(), 5);
|
||||
fs::rename("/work/a", "/work/b").unwrap();
|
||||
assert_eq!(fs::read("/work/../work/b").unwrap(), b"hAll!");
|
||||
assert_eq!(fs::read("work/b").unwrap(), b"hAll!");
|
||||
assert_eq!(fs::read_dir("/work").unwrap().count(), 1);
|
||||
.map(Result::unwrap)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(entries.len(), 2);
|
||||
for entry in entries {
|
||||
let metadata = entry.metadata().unwrap();
|
||||
let directory = entry.file_name() == "b";
|
||||
assert_eq!(entry.file_type().unwrap().is_dir(), directory);
|
||||
assert_eq!(metadata.is_dir(), directory);
|
||||
assert_eq!(metadata.is_file(), !directory);
|
||||
if !directory {
|
||||
assert_eq!(metadata.len(), 8);
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all("/tree").unwrap();
|
||||
assert_eq!(
|
||||
fs::write("/proc/no", b"no").unwrap_err().kind(),
|
||||
std::io::ErrorKind::ReadOnlyFilesystem
|
||||
fs::metadata("/tree").unwrap_err().kind(),
|
||||
ErrorKind::NotFound
|
||||
);
|
||||
}
|
||||
|
||||
/// Open flags and filesystem errors match Rust's filesystem contract.
|
||||
fn options() {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open("/file")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open("/file")
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ErrorKind::AlreadyExists
|
||||
);
|
||||
fs::write("/file", b"old").unwrap();
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open("/file")
|
||||
.unwrap();
|
||||
assert!(fs::read("/file").unwrap().is_empty());
|
||||
let mut append = OpenOptions::new().append(true).open("/file").unwrap();
|
||||
append.write_all(b"one").unwrap();
|
||||
append.seek(SeekFrom::Start(0)).unwrap();
|
||||
append.write_all(b"two").unwrap();
|
||||
assert_eq!(fs::read("/file").unwrap(), b"onetwo");
|
||||
assert_eq!(
|
||||
fs::read("/missing").unwrap_err().kind(),
|
||||
ErrorKind::NotFound
|
||||
);
|
||||
assert_eq!(
|
||||
fs::rename("/work/b", "/run/b").unwrap_err().kind(),
|
||||
std::io::ErrorKind::CrossesDevices
|
||||
fs::write("/missing/child", b"x").unwrap_err().kind(),
|
||||
ErrorKind::NotFound
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read("/file/child").unwrap_err().kind(),
|
||||
ErrorKind::NotADirectory
|
||||
);
|
||||
fs::create_dir("/folder").unwrap();
|
||||
fs::write("/folder/child", b"x").unwrap();
|
||||
assert_eq!(
|
||||
fs::remove_dir("/folder").unwrap_err().kind(),
|
||||
ErrorKind::DirectoryNotEmpty
|
||||
);
|
||||
}
|
||||
|
||||
/// Renames replace files and preserve the contents of moved folders.
|
||||
fn rename() {
|
||||
fs::write("/source", b"new").unwrap();
|
||||
fs::write("/target", b"old").unwrap();
|
||||
fs::rename("/source", "/target").unwrap();
|
||||
assert_eq!(fs::read("/target").unwrap(), b"new");
|
||||
fs::create_dir_all("/folder/nested").unwrap();
|
||||
fs::write("/folder/nested/file", b"nested").unwrap();
|
||||
fs::rename("/folder", "/moved").unwrap();
|
||||
assert_eq!(fs::read("/moved/nested/file").unwrap(), b"nested");
|
||||
assert_eq!(
|
||||
fs::rename("/target", "/absent/target").unwrap_err().kind(),
|
||||
ErrorKind::NotFound
|
||||
);
|
||||
assert_eq!(fs::read("/target").unwrap(), b"new");
|
||||
}
|
||||
|
||||
/// A write after a seek beyond EOF fills the gap with zeros.
|
||||
fn sparse() {
|
||||
let mut f = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open("/sparse")
|
||||
.unwrap();
|
||||
f.write_all(b"start").unwrap();
|
||||
f.seek(SeekFrom::Start(8192)).unwrap();
|
||||
f.write_all(b"end").unwrap();
|
||||
let bytes = fs::read("/sparse").unwrap();
|
||||
assert_eq!(&bytes[..5], b"start");
|
||||
assert!(bytes[5..8192].iter().all(|b| *b == 0));
|
||||
assert_eq!(&bytes[8192..], b"end");
|
||||
}
|
||||
|
||||
/// One write_all and one read_to_end transfer several MiB through WASI streams.
|
||||
fn streams() {
|
||||
use wasip2::filesystem::{
|
||||
preopens,
|
||||
types::{DescriptorFlags, OpenFlags, PathFlags},
|
||||
};
|
||||
let root = preopens::get_directories().remove(0).0;
|
||||
let file = root
|
||||
.open_at(
|
||||
PathFlags::empty(),
|
||||
"large",
|
||||
OpenFlags::CREATE,
|
||||
DescriptorFlags::READ | DescriptorFlags::WRITE,
|
||||
)
|
||||
.unwrap();
|
||||
let bytes = (0..3 * 1024 * 1024 + 137)
|
||||
.map(|i| (i % 251) as u8)
|
||||
.collect::<Vec<_>>();
|
||||
let mut output = file.write_via_stream(0).unwrap();
|
||||
Write::write_all(&mut output, &bytes).unwrap();
|
||||
Write::flush(&mut output).unwrap();
|
||||
drop(output);
|
||||
let mut input = file.read_via_stream(0).unwrap();
|
||||
let mut received = Vec::new();
|
||||
Read::read_to_end(&mut input, &mut received).unwrap();
|
||||
let checksum = |data: &[u8]| {
|
||||
data.iter()
|
||||
.fold(0u64, |sum, b| sum.wrapping_mul(31).wrapping_add(*b as u64))
|
||||
};
|
||||
assert_eq!(received.len(), bytes.len());
|
||||
assert_eq!(checksum(&received), checksum(&bytes));
|
||||
assert_eq!(received, bytes);
|
||||
}
|
||||
|
||||
/// Mount roots remain folders and read-only mounts reject mutations.
|
||||
fn mounts() {
|
||||
assert_eq!(
|
||||
fs::write("/", b"x").unwrap_err().kind(),
|
||||
ErrorKind::IsADirectory
|
||||
);
|
||||
assert_eq!(
|
||||
fs::write("/proc", b"x").unwrap_err().kind(),
|
||||
ErrorKind::ReadOnlyFilesystem
|
||||
);
|
||||
assert_eq!(
|
||||
fs::write("/run", b"x").unwrap_err().kind(),
|
||||
ErrorKind::IsADirectory
|
||||
);
|
||||
assert_eq!(
|
||||
fs::write("/proc/no", b"x").unwrap_err().kind(),
|
||||
ErrorKind::ReadOnlyFilesystem
|
||||
);
|
||||
let root = fs::read_dir("/")
|
||||
.unwrap()
|
||||
@@ -43,24 +210,48 @@ fn main() {
|
||||
assert!(root.iter().any(|n| n == name));
|
||||
}
|
||||
assert_eq!(fs::read("/proc/value").unwrap(), b"snapshot");
|
||||
fs::write("/run/test", b"memory").unwrap();
|
||||
assert_eq!(fs::read("/run/test").unwrap(), b"memory");
|
||||
fs::remove_file("/work/b").unwrap();
|
||||
fs::remove_dir("/work").unwrap();
|
||||
// WASI std does not implement env::temp_dir, so select the folder explicitly.
|
||||
let mut temp = tempfile::tempfile_in("/tmp").unwrap();
|
||||
temp.write_all(b"temporary").unwrap();
|
||||
temp.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut text = String::new();
|
||||
temp.read_to_string(&mut text).unwrap();
|
||||
assert_eq!(text, "temporary");
|
||||
let db = rusqlite::Connection::open("/database.sqlite").unwrap();
|
||||
db.execute_batch("CREATE TABLE data (value TEXT); INSERT INTO data VALUES ('row');")
|
||||
fs::write("/file", b"root write").unwrap();
|
||||
assert_eq!(fs::read("/proc/../file").unwrap(), b"root write");
|
||||
assert_eq!(
|
||||
fs::rename("/file", "/run/file").unwrap_err().kind(),
|
||||
ErrorKind::CrossesDevices
|
||||
);
|
||||
fs::write("/run/file", b"memory").unwrap();
|
||||
assert_eq!(fs::read("/run/file").unwrap(), b"memory");
|
||||
}
|
||||
|
||||
/// SQLite commits many rows in one transaction and reads them after reopen.
|
||||
fn sqlite() {
|
||||
let mut db = rusqlite::Connection::open("/database.sqlite").unwrap();
|
||||
db.execute_batch("CREATE TABLE data (value INTEGER)")
|
||||
.unwrap();
|
||||
let tx = db.transaction().unwrap();
|
||||
for i in 0..1000 {
|
||||
tx.execute("INSERT INTO data VALUES (?1)", [i]).unwrap();
|
||||
}
|
||||
tx.commit().unwrap();
|
||||
drop(db);
|
||||
let db = rusqlite::Connection::open("/database.sqlite").unwrap();
|
||||
let row: String = db
|
||||
.query_row("SELECT value FROM data", [], |r| r.get(0))
|
||||
let result: (i64, i64) = db
|
||||
.query_row("SELECT count(*), sum(value) FROM data", [], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(row, "row");
|
||||
assert_eq!(result, (1000, 999 * 1000 / 2));
|
||||
}
|
||||
|
||||
/// Named temporary files persist their bytes at the selected destination.
|
||||
fn temporary() {
|
||||
let mut unnamed = tempfile::tempfile_in("/tmp").unwrap();
|
||||
unnamed.write_all(b"unnamed").unwrap();
|
||||
unnamed.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
unnamed.read_to_end(&mut bytes).unwrap();
|
||||
assert_eq!(bytes, b"unnamed");
|
||||
let mut named = tempfile::NamedTempFile::new_in("/tmp").unwrap();
|
||||
let original = named.path().to_owned();
|
||||
named.write_all(b"persisted").unwrap();
|
||||
named.persist("/persisted").unwrap();
|
||||
assert!(!original.exists());
|
||||
assert_eq!(fs::read("/persisted").unwrap(), b"persisted");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user