diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b926ac7..e2131409 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,13 @@ jobs: curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \ "wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack" wasm-pack --version + # Not a workspace member (it only builds for wasm32), so the `test` job's + # workspace-wide fmt and clippy do not reach it. + - name: Lint the harness crate + working-directory: harness/opfs-contract + run: | + cargo fmt --all -- --check + cargo clippy --target wasm32-unknown-unknown --all-targets -- -D warnings - name: Install the harness working-directory: harness run: npm ci diff --git a/harness/opfs-contract/src/lib.rs b/harness/opfs-contract/src/lib.rs index ea49d613..542e338e 100644 --- a/harness/opfs-contract/src/lib.rs +++ b/harness/opfs-contract/src/lib.rs @@ -62,12 +62,18 @@ impl Report { } } -/// Remove every entry in the origin root, so a run does not inherit the last -/// one's leftovers. +/// Remove every *document* in the origin root, so a run does not inherit the +/// last one's leftovers. /// /// Uses only the backend's own vocabulary — `refresh`, `delete`, /// `commit_deletes` — rather than reaching for `removeEntry` directly, so a /// broken `delete` shows up here as well as in the contract. +/// +/// It therefore cannot clear a zero-byte entry, because `list` no longer +/// names one. That is deliberate and costs nothing: `attach` recreates and +/// `write` fills whatever a previous run left behind, and a leftover empty +/// entry is invisible to every assertion here for the same reason it is +/// invisible to a document picker. async fn wipe(fs: &OpfsFs) -> Result { fs.refresh().await?; let names = fs.list(Path::new(""))?; @@ -99,12 +105,15 @@ pub async fn run_checks() -> Result { .map_err(|e| JsError::new(&format!("wipe: {e:#}")))?; report - .run("open_and_refresh_see_an_empty_origin_root", async || { - fs.refresh().await?; - let listed = fs.list(Path::new(""))?; - ensure!(listed.is_empty(), "a wiped root listed {listed:?}"); - Ok("empty".into()) - }) + .run( + "open_and_refresh_see_an_origin_root_holding_no_documents", + async || { + fs.refresh().await?; + let listed = fs.list(Path::new(""))?; + ensure!(listed.is_empty(), "a wiped root listed {listed:?}"); + Ok("empty".into()) + }, + ) .await; // `attach` must create the file to obtain a sync access handle for a @@ -114,28 +123,37 @@ pub async fn run_checks() -> Result { // the improvise-9x6 browser failure (the seed was skipped as "already // there" and `load_from` got an empty buffer). report - .run("an_attached_but_unwritten_entry_is_not_a_document", async || { - fs.attach(Path::new("attached.improv")).await?; - let listed = fs.list(Path::new(""))?; - ensure!(listed.is_empty(), "attach listed {listed:?} before anything was written"); + .run( + "an_attached_but_unwritten_entry_is_not_a_document", + async || { + fs.attach(Path::new("attached.improv")).await?; + let listed = fs.list(Path::new(""))?; + ensure!( + listed.is_empty(), + "attach listed {listed:?} before anything was written" + ); - // ...and a second instance, which shares no index, must not see - // it either: the filter has to be in `refresh`, not only in the - // omission from `attach`. - let other = OpfsFs::open().await?; - other.refresh().await?; - let seen = other.list(Path::new(""))?; - ensure!(seen.is_empty(), "refresh() listed the empty entry: {seen:?}"); + // ...and a second instance, which shares no index, must not see + // it either: the filter has to be in `refresh`, not only in the + // omission from `attach`. + let other = OpfsFs::open().await?; + other.refresh().await?; + let seen = other.list(Path::new(""))?; + ensure!( + seen.is_empty(), + "refresh() listed the empty entry: {seen:?}" + ); - // The handle is real, though: writing through it works, and then - // it IS a document. - fs.write(Path::new("attached.improv"), b"now it is one")?; - ensure!( - fs.list(Path::new(""))? == vec![std::path::PathBuf::from("attached.improv")], - "a written document was not listed" - ); - Ok("created, unlisted until written".into()) - }) + // The handle is real, though: writing through it works, and then + // it IS a document. + fs.write(Path::new("attached.improv"), b"now it is one")?; + ensure!( + fs.list(Path::new(""))? == vec![std::path::PathBuf::from("attached.improv")], + "a written document was not listed" + ); + Ok("created, unlisted until written".into()) + }, + ) .await; // The hand-rolled `keys()` async-iterator walk in `refresh` is the least @@ -168,7 +186,10 @@ pub async fn run_checks() -> Result { let p = Path::new("offsets.improv"); fs.attach(p).await?; fs.write(p, b"0123456789ABCDEF")?; - ensure!(fs.read(p)? == b"0123456789ABCDEF", "long write did not round-trip"); + ensure!( + fs.read(p)? == b"0123456789ABCDEF", + "long write did not round-trip" + ); fs.write(p, b"xy")?; let short = fs.read(p)?; ensure!( @@ -200,32 +221,38 @@ pub async fn run_checks() -> Result { // OPFS at the end of the batch. Both halves, because either one alone // would pass against a backend that got the timing exactly backwards. report - .run("a_delete_is_visible_at_once_and_lands_on_commit", async || { - wipe(&fs).await?; - let p = Path::new("doomed.improv"); - fs.attach(p).await?; - fs.write(p, b"bytes")?; + .run( + "a_delete_is_visible_at_once_and_lands_on_commit", + async || { + wipe(&fs).await?; + let p = Path::new("doomed.improv"); + fs.attach(p).await?; + fs.write(p, b"bytes")?; - fs.delete(p)?; - ensure!(fs.read(p).is_err(), "a deleted document was still readable"); - ensure!(fs.list(Path::new(""))?.is_empty(), "a deleted document was still listed"); + fs.delete(p)?; + ensure!(fs.read(p).is_err(), "a deleted document was still readable"); + ensure!( + fs.list(Path::new(""))?.is_empty(), + "a deleted document was still listed" + ); - let before = OpfsFs::open().await?; - before.refresh().await?; - ensure!( - before.list(Path::new(""))?.iter().any(|q| q == p), - "the entry left OPFS before commit_deletes — the queue is not a queue" - ); + let before = OpfsFs::open().await?; + before.refresh().await?; + ensure!( + before.list(Path::new(""))?.iter().any(|q| q == p), + "the entry left OPFS before commit_deletes — the queue is not a queue" + ); - fs.commit_deletes().await?; - let after = OpfsFs::open().await?; - after.refresh().await?; - ensure!( - !after.list(Path::new(""))?.iter().any(|q| q == p), - "commit_deletes did not remove the entry" - ); - Ok("queued then removed".into()) - }) + fs.commit_deletes().await?; + let after = OpfsFs::open().await?; + after.refresh().await?; + ensure!( + !after.list(Path::new(""))?.iter().any(|q| q == p), + "commit_deletes did not remove the entry" + ); + Ok("queued then removed".into()) + }, + ) .await; Ok(report.finish()) diff --git a/harness/tests/opfs.spec.js b/harness/tests/opfs.spec.js index f8506931..f5719bab 100644 --- a/harness/tests/opfs.spec.js +++ b/harness/tests/opfs.spec.js @@ -21,7 +21,7 @@ async function run(page, cmd) { * Rust on purpose: a check deleted on one side has to be deleted on both, * which is what stops the battery quietly shrinking. */ const EXPECTED = [ - "open_and_refresh_see_an_empty_origin_root", + "open_and_refresh_see_an_origin_root_holding_no_documents", "an_attached_but_unwritten_entry_is_not_a_document", "refresh_walks_every_key_and_sorts_them", "read_and_write_are_absolute_at_offset_zero",