Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/rust/wasm32-wasip3/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion tests/rust/wasm32-wasip3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024"

[dependencies]
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen.git" }
futures = "0.3.31"
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen.git" }
3 changes: 3 additions & 0 deletions tests/rust/wasm32-wasip3/src/bin/filesystem-io.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dirs": ["fs-tests.dir"]
}
221 changes: 221 additions & 0 deletions tests/rust/wasm32-wasip3/src/bin/filesystem-io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use futures::join;
use std::process;
extern crate wit_bindgen;

wit_bindgen::generate!({
inline: r"
package test:test;

world test {
include wasi:filesystem/[email protected];
include wasi:cli/[email protected];
}
",
additional_derives: [PartialEq, Eq, Hash, Clone],
// Work around https://github.com/bytecodealliance/wasm-tools/issues/2285.
features:["clocks-timezone"],
generate_all
});

use wasi::filesystem::types::Descriptor;
use wasi::filesystem::types::{DescriptorFlags, ErrorCode, OpenFlags, PathFlags};
use wit_bindgen::StreamResult;

async fn pread(fd: &Descriptor, size: usize, offset: u64) -> Result<Vec<u8>, ErrorCode> {
let (mut rx, future) = fd.read_via_stream(offset);
let data = Vec::<u8>::with_capacity(size);
let mut bytes_read = 0;
let (mut result, mut data) = rx.read(data).await;
loop {
match result {
StreamResult::Complete(n) => {
assert!(n <= size - bytes_read);
bytes_read += n;
assert_eq!(data.len(), bytes_read);
if bytes_read == size {
break;
}
(result, data) = rx.read(data).await;
}
StreamResult::Dropped => {
assert_eq!(data.len(), bytes_read);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert may want to get deferred to the Ok return value of this function because a short-read might happen due to an error and the error may want to get returned instead of panicking here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand the subtleties but I am a bit lost. If I understand you correctly, Dropped can be accompanied by data, is that right? If so we should add an n to Dropped: bytecodealliance/wit-bindgen#1396. If not I think this assertion will never cause a failed pread, because bytes_read and data.len() are both incremented as part of Complete.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct that Dropped can be accompanied by data, yeah. The lack of n here is definitely an oversight and the assumption was that you'd look at the length of the buffer before/after to figure out the size (but then that's inconsistent with n on StreamResult::Complete, so n should be on Dropped and Cancelled as well for consistency)

I think I actually confused myself about this assertion and what it originally meant. I thought it was somehow taking the size parameter to the function into account but it isn't. Otherwise though you're also correct that this can panic in the case that Dropped communicates some data, which wasn't actually what I was thinking about but is true nonetheless!

break;
}
StreamResult::Cancelled => {
panic!("who cancelled the stream?");
}
}
};
drop(rx);
match future.await {
Ok(()) => Ok(data),
Err(err) => Err(err),
}
}

async fn pwrite(fd: &Descriptor, offset: u64, data: &[u8]) -> Result<usize, ErrorCode> {
let (mut tx, rx) = wit_stream::new();
let future = fd.write_via_stream(rx, offset);
let len = data.len();
let mut written: usize = 0;
let mut result: Result<(), ErrorCode> = Ok(());
join!{
async {
let (mut result, mut buf) = tx.write(data.to_vec()).await;
loop {
match result {
StreamResult::Complete(n) => {
assert!(n <= len - written);
written += n;
assert_eq!(buf.remaining(), len - written);
if buf.remaining() != 0 {
(result, buf) = tx.write_buf(buf).await;
} else {
break;
}
}
StreamResult::Dropped => {
panic!("receiver dropped the stream?");
}
StreamResult::Cancelled => {
break;
}
}
}
assert_eq!(buf.remaining(), len - written);
drop(tx);
Comment on lines +64 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be replacable with StreamWriter::write_all perhaps? (plus an assert that the result of write_all is empty)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW... I have been avoiding write_all because I wanted to understand the stream mechanics. But, this is a WASI test suite, not a component model test suite, and so perhaps write_all is the right thing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! Either way seems reasonable to me

},
async { result = future.await; }
};
match result {
Ok(()) => Ok(written),
Err(err) => Err(err),
}
}

async fn pappend(fd: &Descriptor, data: &[u8]) -> Result<usize, ErrorCode> {
let (mut tx, rx) = wit_stream::new();
let future = fd.append_via_stream(rx);
let initial_size = fd.stat().await.unwrap().size as usize;
let len = data.len();
let mut written: usize = 0;
let mut result: Result<(), ErrorCode> = Ok(());
join!{
async {
let (mut result, mut buf) = tx.write(data.to_vec()).await;
loop {
match result {
StreamResult::Complete(n) => {
assert!(n <= len - written);
written += n;
assert_eq!(buf.remaining(), len - written);
assert_eq!(fd.stat().await.unwrap().size as usize,
initial_size + written);
if buf.remaining() != 0 {
(result, buf) = tx.write_buf(buf).await;
} else {
break;
}
}
StreamResult::Dropped => {
panic!("receiver dropped the stream?");
}
StreamResult::Cancelled => {
break;
}
}
}
assert_eq!(buf.remaining(), len - written);
drop(tx);
},
async { result = future.await; }
};
match result {
Ok(()) => Ok(written),
Err(err) => Err(err),
}
}

async fn read_to_eof(fd: &Descriptor, offset: u64) -> Vec<u8> {
let (stream, success) = fd.read_via_stream(offset);
let ret = stream.collect().await;
success.await.unwrap();
ret
}

async fn test_io(dir: &Descriptor) {
let open = |path: &str, oflags: OpenFlags, fdflags: DescriptorFlags| -> _ {
dir.open_at(PathFlags::empty(), path.to_string(), oflags, fdflags)
};
let open_r = |path: &str| -> _ { open(path, OpenFlags::empty(), DescriptorFlags::READ) };
let creat = |path: &str| -> _ {
open(
path,
OpenFlags::CREATE | OpenFlags::EXCLUSIVE,
DescriptorFlags::READ | DescriptorFlags::WRITE,
)
};
let rm = |path: &str| dir.unlink_file_at(path.to_string());

let a = open_r("a.txt").await.unwrap();

pread(&a, 0, 0).await.unwrap();
pread(&a, 0, 1).await.unwrap();
pread(&a, 0, 6).await.unwrap();
pread(&a, 0, 7).await.unwrap();
pread(&a, 0, 17).await.unwrap();

assert_eq!(&pread(&a, 1, 0).await.unwrap(), b"t");
assert_eq!(&pread(&a, 1, 1).await.unwrap(), b"e");
assert_eq!(&pread(&a, 1, 6).await.unwrap(), b"\n");
assert_eq!(&pread(&a, 1, 7).await.unwrap(), b"");
assert_eq!(&pread(&a, 1, 17).await.unwrap(), b"");

assert_eq!(&read_to_eof(&a, 0).await, b"test-a\n");
assert_eq!(&read_to_eof(&a, 1).await, b"est-a\n");
assert_eq!(&read_to_eof(&a, 6).await, b"\n");
assert_eq!(&read_to_eof(&a, 7).await, b"");
assert_eq!(&read_to_eof(&a, 17).await, b"");

// No-op on read-only fds.
a.sync_data().await.unwrap();
a.sync().await.unwrap();

assert_eq!(pread(&a, 1, u64::MAX).await, Err(ErrorCode::Invalid));

let c = creat("c.cleanup").await.unwrap();
assert_eq!(&read_to_eof(&c, 0).await, b"");
assert_eq!(pwrite(&c, 0, b"hello!").await, Ok(b"hello!".len()));
assert_eq!(&read_to_eof(&c, 0).await, b"hello!");
assert_eq!(pwrite(&c, 0, b"byeee").await, Ok(b"byeee".len()));
assert_eq!(&read_to_eof(&c, 0).await, b"byeee!");
assert_eq!(pappend(&c, b" laters!!").await, Ok(b" laters!!".len()));
assert_eq!(&read_to_eof(&c, 0).await, b"byeee! laters!!");
c.sync_data().await.unwrap();
assert_eq!(&read_to_eof(&open_r("c.cleanup").await.unwrap(), 0).await,
b"byeee! laters!!");
c.sync().await.unwrap();

rm("c.cleanup").await.unwrap();
}

struct Component;
export!(Component);
impl exports::wasi::cli::run::Guest for Component {
async fn run() -> Result<(), ()> {
match &wasi::filesystem::preopens::get_directories()[..] {
[(dir, dirname)] if dirname == "fs-tests.dir" => {
test_io(dir).await;
}
[..] => {
eprintln!("usage: run with one open dir named 'fs-tests.dir'");
process::exit(1)
}
};
Ok(())
}
}

fn main() {
unreachable!("main is a stub");
}
Loading