test(harness): a headless browser, and the first execution of OpfsFs
improvise-vvl3, absorbing improvise-xf1. The condition the user set when
deferring browser testing — "WE HAVE TO HAVE SOMETHING TO TEST" — has been
met: after improvise-9x6, djm, hwpi and the demo model there is a complete
self-contained application whose entire untested surface is what a harness
covers.
`python3 -m http.server` plus one Playwright project, in `harness/`:
harness/opfs-contract/ a wasm crate that runs
`improvise_io::storage::contract::storage_contract`
against `OpfsFs` — the shared function, not a
transcription of it
harness/www/opfs/ the page and the dedicated Web Worker it runs in;
`createSyncAccessHandle` exists nowhere else
harness/tests/ two specs
harness/build.sh site + harness bundle -> harness/serve
It lives at the repo root rather than under `web/`, because `web/` is the
*source of what ships* and every file in it is copied to the site. The
harness serves the **assembled** tree (`web/assemble.sh`, extracted from
the CI workflow in this commit so there is one answer to "how is the site
arranged" rather than two) plus its own `/opfs/` — a harness that runs
against a different arrangement of files is testing something nobody
deploys. `harness/opfs-contract` is deliberately not a workspace member: it
only builds for wasm32, so a member would break `cargo test --workspace`.
What this runs that nothing has ever run:
- `storage_contract` against `OpfsFs`, at `""` **and** at `"docs"`. Two
bugs fell straight out — improvise-vhcz's `list` path shape, whose fix
the `"docs"` run guards (reverting it fails with `lists the one
document`), and the zero-byte entry fix in the previous commit, which
this found by failing on `starts empty`.
- The `keys()` async-iterator walk in `refresh`, hand-rolled through
`js_sys::Reflect` and named by xf1 as the least certain code in the file.
Three entries written out of order, so one passing by accident cannot
carry it.
- `FileSystemReadWriteOptions`' `at` on read and write: a long write then a
short one, which is where an append or a non-zero start shows up.
- `commit_deletes`, both halves — queued but still in OPFS before, gone
after — and **across a reload**, which is the only way to ask whether
OPFS persisted. That reload is also improvise-bck's persistence criterion
at the storage layer.
- improvise-hwpi's three one-assertion shell tests, which that ticket said
it could not write: status after a bad frame, status with no peer, one
repaint on commit. Plus the first-frame watchdog, which was found by
reading and only because someone went looking.
Check the checker, throughout, because a harness that passes against a stub
— or that silently skips because the worker never started — is exactly this
repo's characteristic defect wearing a green tick:
- `run_canary` deliberately fails, through the *same* reporting path every
real check uses, and the spec asserts it is reported as a failure with its
message intact.
- The spec asserts the report contains exactly the expected check names, in
order, so a battery that quietly shrinks is a failure.
- Every shell assertion was injected and watched to fail: removing the
bad-frame catch, the watchdog's `setTimeout`, the socket `error` listener
and the `transport.send(frame)` on commit each fails its own test and
only its own.
- The commit test types `20000` and asserts `20,000`, so a page that echoed
what was typed instead of drawing what the host returned would fail.
One live bug is pinned rather than fixed. `a peer that cannot be reached`
asserts the *history* of `#status`, because improvise-m18f is real and
reproduced here: the socket's `close` listener overwrites the precise
"cannot reach" with "disconnected". That is filed and deliberately not the
same fix as the watchdog's (gotcha 48), so the test asserts both halves and
will speak up whichever one moves.
CI gets a `browser` job that can fail the build, and `pages-deploy` now
needs it. Playwright's Chromium download is ~80 MB and well under a minute
on ubuntu-latest, next to the two wasm-pack builds the job already needs —
so "impractical in this CI" does not arise. A harness nobody runs is worse
than none, because it reads as coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+64
-44
@@ -74,15 +74,68 @@ jobs:
|
||||
nix shell nixpkgs#trealla --command tpl prolog-sessions/kb.pl \
|
||||
-g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The browser harness (improvise-vvl3, absorbing improvise-xf1).
|
||||
#
|
||||
# This is the only job that RUNS any of the browser half. `OpfsFs`,
|
||||
# `web/app.js` and `web/worker.js` were previously compile-checked,
|
||||
# syntax-checked and read — never executed — so every bug in them was found
|
||||
# by reading or by a person pressing keys, neither of which is repeatable.
|
||||
#
|
||||
# It can fail the build, deliberately: a harness nobody runs is worse than
|
||||
# none, because it reads as coverage. Playwright's browser download is ~80 MB
|
||||
# and takes well under a minute on ubuntu-latest, which is affordable next to
|
||||
# the two wasm-pack builds this job already needs.
|
||||
browser:
|
||||
name: Browser harness (OPFS contract + shell)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install wasm-pack
|
||||
env:
|
||||
WASM_PACK_VERSION: v0.15.0
|
||||
run: |
|
||||
set -eu
|
||||
url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
||||
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
|
||||
- name: Install the harness
|
||||
working-directory: harness
|
||||
run: npm ci
|
||||
- name: Install Chromium
|
||||
working-directory: harness
|
||||
run: npx playwright install --with-deps chromium
|
||||
# Builds both site bundles, the harness's own wasm bundle, and assembles
|
||||
# `harness/serve` — the same tree `pages-build` publishes, plus /opfs/.
|
||||
- name: Build the harness
|
||||
run: ./harness/build.sh
|
||||
- name: Run the harness
|
||||
working-directory: harness
|
||||
run: npx playwright test
|
||||
- if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: harness/test-results
|
||||
retention-days: 7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GitHub Pages delivery (improvise-d31).
|
||||
#
|
||||
# Two jobs on purpose. `pages-build` runs on every push and pull request, so
|
||||
# a change that breaks `web/build.sh` or the bundle fails on the PR that made
|
||||
# it — the build is a gate. `pages-deploy` runs only on a push to main and
|
||||
# `needs` the two check jobs above, so nothing that failed `cargo test` or the
|
||||
# wasm32 gate can reach the published site. The deploy depends on those checks
|
||||
# rather than repeating them (improvise-d31 AC 2).
|
||||
# `needs` the check jobs above, so nothing that failed `cargo test`, the
|
||||
# wasm32 gate or the browser harness can reach the published site. The deploy
|
||||
# depends on those checks rather than repeating them (improvise-d31 AC 2).
|
||||
#
|
||||
# Not path-filtered, deliberately. The bundles are `improvise-wasm-client` and
|
||||
# `improvise-worker-server` plus, between them, most of the workspace — the
|
||||
@@ -123,47 +176,14 @@ jobs:
|
||||
- name: Build the wasm bundle
|
||||
run: ./web/build.sh
|
||||
|
||||
# `web/` is the source directory: it also holds build.sh and .gitignore,
|
||||
# which have no business being served. Assemble what ships.
|
||||
# `web/assemble.sh`, not an inline copy of it: the browser harness job
|
||||
# below assembles the same tree, and a harness that runs against a
|
||||
# different arrangement of files is testing something nobody deploys.
|
||||
- name: Assemble the site
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p site
|
||||
cp web/index.html web/app.js web/style.css web/worker.js site/
|
||||
# The document the page opens on a browser that has never seen
|
||||
# one. web/worker.js fetches it by the ?seed= URL and hands the
|
||||
# bytes to Rust, which writes them through the VFS — so leaving
|
||||
# it out is an empty grid, not a 404 the page could report.
|
||||
cp web/demo.improv site/
|
||||
cp -rf web/pkg site/pkg
|
||||
cp -rf web/worker-pkg site/worker-pkg
|
||||
rm -f site/pkg/.gitignore site/pkg/package.json
|
||||
rm -f site/worker-pkg/.gitignore site/worker-pkg/package.json
|
||||
# Cache-bust the two generated bundles. Pages serves everything
|
||||
# `max-age=600` and the wasm-pack filenames carry no content hash,
|
||||
# so for ten minutes after a deploy a returning visitor runs the
|
||||
# PREVIOUS build -- which during improvise-9x6 debugging made a
|
||||
# shipped fix look like it had not shipped, twice. The specifiers
|
||||
# are static import strings, so stamping the copies in site/ is
|
||||
# enough; the tracked sources are left alone and a local
|
||||
# `http.server` is unaffected.
|
||||
# Both halves, because a fresh glue file does NOT pull a fresh
|
||||
# .wasm: the glue locates it with
|
||||
# `new URL('..._bg.wasm', import.meta.url)`, and a URL constructed
|
||||
# against a base does not inherit the base's query string. Stamping
|
||||
# only the import specifier would refresh the wrapper and keep the
|
||||
# stale binary -- the failure mode this is here to end.
|
||||
sed -i "s|\./pkg/improvise_wasm_client\.js|&?v=${GITHUB_SHA}|" site/app.js
|
||||
sed -i "s|\./worker-pkg/improvise_worker_server\.js|&?v=${GITHUB_SHA}|" site/worker.js
|
||||
sed -i "s|'improvise_wasm_client_bg\.wasm'|'improvise_wasm_client_bg.wasm?v=${GITHUB_SHA}'|" \
|
||||
site/pkg/improvise_wasm_client.js
|
||||
sed -i "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${GITHUB_SHA}'|" \
|
||||
site/worker-pkg/improvise_worker_server.js
|
||||
grep -n "?v=" site/app.js site/worker.js \
|
||||
site/pkg/improvise_wasm_client.js \
|
||||
site/worker-pkg/improvise_worker_server.js
|
||||
find site -type f | sort
|
||||
du -sh site
|
||||
run: ./web/assemble.sh site "$GITHUB_SHA"
|
||||
|
||||
- run: find site -type f | sort
|
||||
- run: du -sh site
|
||||
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
@@ -171,7 +191,7 @@ jobs:
|
||||
|
||||
pages-deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: [test, wasm, pages-build]
|
||||
needs: [test, wasm, browser, pages-build]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Built by build.sh; never committed.
|
||||
serve/
|
||||
www/opfs/pkg/
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# Build everything the browser harness serves, into harness/serve/.
|
||||
#
|
||||
# harness/serve/ the assembled site, exactly as `web/assemble.sh`
|
||||
# produces it for GitHub Pages
|
||||
# harness/serve/opfs/ this harness's own page, worker and wasm bundle
|
||||
#
|
||||
# One server root, so Playwright runs one `python3 -m http.server`. The site
|
||||
# half is the *deployed* arrangement rather than `web/` itself, because a
|
||||
# harness that runs against a different tree from the one that ships is
|
||||
# testing something nobody deploys (improvise-vvl3).
|
||||
set -eu
|
||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
serve="$root/harness/serve"
|
||||
|
||||
"$root/web/build.sh" >/dev/null
|
||||
"$root/web/assemble.sh" "$serve" harness >/dev/null
|
||||
|
||||
wasm-pack build "$root/harness/opfs-contract" \
|
||||
--target web --out-dir "$root/harness/www/opfs/pkg"
|
||||
|
||||
mkdir -p "$serve/opfs"
|
||||
cp -f "$root/harness/www/opfs/index.html" "$root/harness/www/opfs/worker.js" "$serve/opfs/"
|
||||
cp -rf "$root/harness/www/opfs/pkg" "$serve/opfs/pkg"
|
||||
rm -f "$serve/opfs/pkg/.gitignore" "$serve/opfs/pkg/package.json"
|
||||
|
||||
echo "harness/serve ready"
|
||||
Generated
+621
@@ -0,0 +1,621 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "improvise-core"
|
||||
version = "0.1.0-rc2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"improvise-formula",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "improvise-formula"
|
||||
version = "0.1.0-rc2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "improvise-io"
|
||||
version = "0.1.0-rc2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"csv",
|
||||
"flate2",
|
||||
"improvise-core",
|
||||
"improvise-formula",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "opfs-contract"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"improvise-io",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "opfs-contract"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
description = "Runs improvise-io's storage_contract against OpfsFs in a real browser"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
# Deliberately NOT a workspace member. It builds only for wasm32 — it names
|
||||
# `improvise_io::opfs`, which is `cfg(target_arch = "wasm32")` — so a member
|
||||
# would break `cargo test --workspace` and `cargo clippy --workspace`. An empty
|
||||
# `[workspace]` table makes it its own root, which is what keeps it out without
|
||||
# editing the root manifest (and without a Cargo.toml conflict with every other
|
||||
# agent).
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
improvise-io = { path = "../../crates/improvise-io" }
|
||||
anyhow = "1"
|
||||
js-sys = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-O", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"]
|
||||
@@ -0,0 +1,307 @@
|
||||
//! Run `improvise_io::storage::contract::storage_contract` against `OpfsFs`,
|
||||
//! inside a dedicated Web Worker, in a real browser.
|
||||
//!
|
||||
//! `OpfsFs` has been compile-checked and never run (gotcha 45). This is the
|
||||
//! instrument that changes that (improvise-vvl3, absorbing improvise-xf1). It
|
||||
//! is a *harness* crate, not part of the application: it ships in no bundle
|
||||
//! and is not a workspace member.
|
||||
//!
|
||||
//! # Why a worker
|
||||
//!
|
||||
//! `FileSystemFileHandle.createSyncAccessHandle()` exists only in a dedicated
|
||||
//! Web Worker, and it is the whole reason `Storage` could stay synchronous
|
||||
//! (gotcha 28). A check that ran on the main thread would fail for a reason
|
||||
//! that has nothing to do with `OpfsFs`.
|
||||
//!
|
||||
//! # Why a report rather than a panic
|
||||
//!
|
||||
//! Every check runs and its outcome is reported, so a failure names *which*
|
||||
//! step and the rest still run. More importantly it is what lets the spec
|
||||
//! check the checker: Playwright asserts the report contains exactly the
|
||||
//! checks it expects, so a worker that never started, or a build that dropped
|
||||
//! a check, is a failure rather than a silent pass.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow, ensure};
|
||||
use improvise_io::opfs::OpfsFs;
|
||||
use improvise_io::storage::Storage;
|
||||
use improvise_io::storage::contract::storage_contract;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// One named check and what happened to it.
|
||||
#[derive(Serialize)]
|
||||
struct Check {
|
||||
name: String,
|
||||
ok: bool,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Report(Vec<Check>);
|
||||
|
||||
impl Report {
|
||||
async fn run<F>(&mut self, name: &str, f: F)
|
||||
where
|
||||
F: AsyncFnOnce() -> Result<String>,
|
||||
{
|
||||
let (ok, detail) = match f().await {
|
||||
Ok(detail) => (true, detail),
|
||||
Err(e) => (false, format!("{e:#}")),
|
||||
};
|
||||
self.0.push(Check {
|
||||
name: name.to_string(),
|
||||
ok,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
serde_json::to_string(&self.0).expect("a Vec<Check> serializes")
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove every entry 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.
|
||||
async fn wipe(fs: &OpfsFs) -> Result<usize> {
|
||||
fs.refresh().await?;
|
||||
let names = fs.list(Path::new(""))?;
|
||||
for name in &names {
|
||||
fs.delete(name)?;
|
||||
}
|
||||
fs.commit_deletes().await?;
|
||||
fs.refresh().await?;
|
||||
ensure!(
|
||||
fs.list(Path::new(""))?.is_empty(),
|
||||
"wipe left {:?} behind",
|
||||
fs.list(Path::new(""))?
|
||||
);
|
||||
Ok(names.len())
|
||||
}
|
||||
|
||||
/// The whole battery. Returns a JSON array of `Check`.
|
||||
#[wasm_bindgen]
|
||||
pub async fn run_checks() -> Result<String, JsError> {
|
||||
let mut report = Report::default();
|
||||
|
||||
// `open()` is the first thing that can fail, and everything after it needs
|
||||
// the handle, so it is not a `Check` — a failure here is a thrown error.
|
||||
let fs = OpfsFs::open()
|
||||
.await
|
||||
.map_err(|e| JsError::new(&format!("OpfsFs::open(): {e:#}")))?;
|
||||
wipe(&fs)
|
||||
.await
|
||||
.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())
|
||||
})
|
||||
.await;
|
||||
|
||||
// `attach` must create the file to obtain a sync access handle for a
|
||||
// document that does not exist yet — and that created file is zero bytes,
|
||||
// which is NOT a document. `Storage::list` promises document names, so an
|
||||
// attached-but-unwritten name must not be listed. Indexing it is exactly
|
||||
// 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");
|
||||
|
||||
// ...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())
|
||||
})
|
||||
.await;
|
||||
|
||||
// The hand-rolled `keys()` async-iterator walk in `refresh` is the least
|
||||
// certain code in opfs.rs (improvise-xf1 named it). One entry could pass
|
||||
// by accident; several, out of order, cannot.
|
||||
report
|
||||
.run("refresh_walks_every_key_and_sorts_them", async || {
|
||||
for name in ["c.improv", "a.improv", "b.improv"] {
|
||||
fs.attach(Path::new(name)).await?;
|
||||
fs.write(Path::new(name), name.as_bytes())?;
|
||||
}
|
||||
let fresh = OpfsFs::open().await?;
|
||||
fresh.refresh().await?;
|
||||
let listed = fresh.list(Path::new(""))?;
|
||||
let names: Vec<String> = listed.iter().map(|p| p.display().to_string()).collect();
|
||||
ensure!(
|
||||
names == ["a.improv", "attached.improv", "b.improv", "c.improv"],
|
||||
"refresh() saw {names:?}"
|
||||
);
|
||||
Ok(format!("{names:?}"))
|
||||
})
|
||||
.await;
|
||||
|
||||
// `FileSystemReadWriteOptions`' `at` handling, the other thing xf1 called
|
||||
// out. A `write` that appended instead of overwriting, or a `read` that
|
||||
// started anywhere but 0, is invisible on a single short write and obvious
|
||||
// across a long one followed by a short one.
|
||||
report
|
||||
.run("read_and_write_are_absolute_at_offset_zero", async || {
|
||||
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");
|
||||
fs.write(p, b"xy")?;
|
||||
let short = fs.read(p)?;
|
||||
ensure!(
|
||||
short == b"xy",
|
||||
"a shorter write left {:?} behind — truncate or `at` is wrong",
|
||||
String::from_utf8_lossy(&short)
|
||||
);
|
||||
Ok("absolute".into())
|
||||
})
|
||||
.await;
|
||||
|
||||
// The point of the whole exercise. `attach` is hoisted out because
|
||||
// acquiring a handle is the async half and `Storage::write` cannot await;
|
||||
// the contract itself is the shared function, not a transcription of it.
|
||||
for dir in ["", "docs"] {
|
||||
report
|
||||
.run(&format!("storage_contract_at_{:?}", dir), async || {
|
||||
wipe(&fs).await?;
|
||||
let d = Path::new(dir);
|
||||
fs.attach(&d.join("a.improv")).await?;
|
||||
fs.attach(&d.join("b.improv")).await?;
|
||||
storage_contract(&fs, d)?;
|
||||
Ok("passed".into())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// Gotcha 28: a delete is visible to `read` and `list` at once and lands in
|
||||
// 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")?;
|
||||
|
||||
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"
|
||||
);
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// Write the state the reload check expects, then hand control back.
|
||||
///
|
||||
/// Split from [`after_reload`] because the one thing no in-page check can
|
||||
/// establish is that OPFS *persisted* — and a reload is the only way to ask.
|
||||
#[wasm_bindgen]
|
||||
pub async fn before_reload() -> Result<String, JsError> {
|
||||
inner_before().await.map_err(js)
|
||||
}
|
||||
|
||||
async fn inner_before() -> Result<String> {
|
||||
let fs = OpfsFs::open().await?;
|
||||
wipe(&fs).await?;
|
||||
|
||||
let survivor = Path::new("survivor.improv");
|
||||
fs.attach(survivor).await?;
|
||||
fs.write(survivor, b"persisted across a reload")?;
|
||||
|
||||
let doomed = Path::new("doomed.improv");
|
||||
fs.attach(doomed).await?;
|
||||
fs.write(doomed, b"should not survive")?;
|
||||
fs.delete(doomed)?;
|
||||
fs.commit_deletes().await?;
|
||||
|
||||
Ok("staged".into())
|
||||
}
|
||||
|
||||
/// Assert what [`before_reload`] left is still there, in a fresh page.
|
||||
#[wasm_bindgen]
|
||||
pub async fn after_reload() -> Result<String, JsError> {
|
||||
inner_after().await.map_err(js)
|
||||
}
|
||||
|
||||
async fn inner_after() -> Result<String> {
|
||||
let fs = OpfsFs::open().await?;
|
||||
fs.refresh().await?;
|
||||
let listed = fs.list(Path::new(""))?;
|
||||
let names: Vec<String> = listed.iter().map(|p| p.display().to_string()).collect();
|
||||
ensure!(
|
||||
names == ["survivor.improv"],
|
||||
"after a reload the origin held {names:?}"
|
||||
);
|
||||
|
||||
let survivor = Path::new("survivor.improv");
|
||||
fs.attach(survivor).await?;
|
||||
let bytes = fs.read(survivor)?;
|
||||
ensure!(
|
||||
bytes == b"persisted across a reload",
|
||||
"the surviving document read back as {:?}",
|
||||
String::from_utf8_lossy(&bytes)
|
||||
);
|
||||
Ok("survived".into())
|
||||
}
|
||||
|
||||
fn js(e: anyhow::Error) -> JsError {
|
||||
JsError::new(&format!("{e:#}"))
|
||||
}
|
||||
|
||||
/// Deliberately fails, so the harness can prove it can see a failure.
|
||||
///
|
||||
/// A green run against a stub, or one that silently skipped because the worker
|
||||
/// never started, is exactly the check-the-checker failure this repo has a
|
||||
/// doctrine about. The spec runs this and asserts it is reported as a failure
|
||||
/// with its message intact, which exercises the *same* reporting path every
|
||||
/// real check uses.
|
||||
#[wasm_bindgen]
|
||||
pub async fn run_canary() -> String {
|
||||
let mut report = Report::default();
|
||||
report
|
||||
.run("the_canary_must_fail", async || {
|
||||
Err(anyhow!("the harness can see a failure"))
|
||||
})
|
||||
.await;
|
||||
report.finish()
|
||||
}
|
||||
Generated
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "improvise-harness",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "improvise-harness",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "improvise-harness",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "The first instrument pointed at the browser half of improvise (improvise-vvl3)",
|
||||
"scripts": {
|
||||
"build": "./build.sh",
|
||||
"test": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
// `python3 -m http.server` and one spec directory. Deliberately small
|
||||
// (improvise-vvl3): the point is to be able to run `storage_contract` against
|
||||
// `OpfsFs` and to read `#status` out of a real page, not to grow a second
|
||||
// test framework beside `cargo test`.
|
||||
//
|
||||
// The server root is `harness/serve`, built by `build.sh` — the assembled
|
||||
// site plus this harness's own page. A module script will not load from
|
||||
// `file://`, so an http server is not optional.
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
reporter: process.env.CI ? [["github"], ["list"]] : "list",
|
||||
timeout: 60_000,
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:8099",
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
|
||||
webServer: {
|
||||
command: "python3 -m http.server 8099 --bind 127.0.0.1 --directory serve",
|
||||
url: "http://127.0.0.1:8099/index.html",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 30_000,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
// `storage_contract` against `OpfsFs`, in a browser (improvise-vvl3/xf1).
|
||||
//
|
||||
// `OpfsFs` has been compile-checked and never run. Everything found in a
|
||||
// browser so far was found by a person pressing keys, which is not a
|
||||
// repeatable check. This is.
|
||||
//
|
||||
// The checks themselves are in Rust (harness/opfs-contract) because that is
|
||||
// where `OpfsFs` is and where the contract is; this file drives them and reads
|
||||
// the report. Assertions here are about *the report*, deliberately, so a
|
||||
// worker that never started or a build that dropped a check fails rather than
|
||||
// passing quietly.
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
/** Run one entry point in the harness worker. */
|
||||
async function run(page, cmd) {
|
||||
return JSON.parse(await page.evaluate((c) => window.opfs(c), cmd));
|
||||
}
|
||||
|
||||
/** The checks the report must contain, in order. Named here as well as in
|
||||
* 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",
|
||||
"an_attached_but_unwritten_entry_is_not_a_document",
|
||||
"refresh_walks_every_key_and_sorts_them",
|
||||
"read_and_write_are_absolute_at_offset_zero",
|
||||
'storage_contract_at_""',
|
||||
'storage_contract_at_"docs"',
|
||||
"a_delete_is_visible_at_once_and_lands_on_commit",
|
||||
];
|
||||
|
||||
test.describe("OpfsFs in a real browser", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/opfs/index.html");
|
||||
});
|
||||
|
||||
// Check the checker, and do it first: a green run against a worker that
|
||||
// never started is the failure this repo has a doctrine about. This drives
|
||||
// the *same* reporting path every real check uses.
|
||||
test("the harness can see a failure", async ({ page }) => {
|
||||
const report = await run(page, "run_canary");
|
||||
expect(report).toHaveLength(1);
|
||||
expect(report[0].name).toBe("the_canary_must_fail");
|
||||
expect(report[0].ok).toBe(false);
|
||||
expect(report[0].detail).toContain("the harness can see a failure");
|
||||
});
|
||||
|
||||
test("every check in the battery runs and passes", async ({ page }) => {
|
||||
const report = await run(page, "run_checks");
|
||||
|
||||
// The battery is complete before any of it is believed.
|
||||
expect(report.map((c) => c.name)).toEqual(EXPECTED);
|
||||
|
||||
const failed = report.filter((c) => !c.ok);
|
||||
expect(
|
||||
failed,
|
||||
`failing checks:\n${failed.map((c) => ` ${c.name}: ${c.detail}`).join("\n")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
// The one thing no in-page check can establish. gotcha 28: a delete is
|
||||
// visible immediately and lands in OPFS at the end of the batch — so
|
||||
// "commit_deletes actually removed it" is only answered by a fresh page.
|
||||
// This is also the storage half of improvise-bck's persistence criterion.
|
||||
test("what OPFS held survives a reload, and a committed delete does not", async ({
|
||||
page,
|
||||
}) => {
|
||||
expect(await page.evaluate(() => window.opfs("before_reload"))).toBe("staged");
|
||||
await page.reload();
|
||||
expect(await page.evaluate(() => window.opfs("after_reload"))).toBe("survived");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
// The browser shell: the three assertions improvise-hwpi could not write.
|
||||
//
|
||||
// That ticket fixed four things and said of itself that the fixes were "as
|
||||
// unverified as the bugs", because three of them are one-assertion tests that
|
||||
// needed a browser. These are those three, plus the watchdog — which was found
|
||||
// by reading, and only because someone went looking.
|
||||
//
|
||||
// `web/app.js` and `web/worker.js` have never been rendered or run (gotcha 48).
|
||||
// Everything below is the first execution of either.
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const status = (page) => page.locator("#status");
|
||||
|
||||
test.describe("the shell reports what its peer is doing", () => {
|
||||
// improvise-hwpi: `on_frame` returns a Result, which wasm-bindgen throws.
|
||||
// Thrown in a bare listener it reached the devtools console and nothing
|
||||
// else: the grid simply stopped updating with nothing on screen saying why.
|
||||
//
|
||||
// The fault is injected at the *peer*, not in the page — a mocked socket
|
||||
// that answers the handshake with a frame the client cannot fold — so this
|
||||
// exercises the real `on_frame` and the real catch.
|
||||
test("a frame the client cannot fold says so on screen", async ({ page }) => {
|
||||
await page.routeWebSocket("ws://127.0.0.1:8787/", (ws) => {
|
||||
ws.onMessage(() => ws.send("{\"NotAFrame\":{}}"));
|
||||
});
|
||||
await page.goto("/index.html?ws=127.0.0.1:8787");
|
||||
await expect(status(page)).toContainText("bad frame", { timeout: 15_000 });
|
||||
});
|
||||
|
||||
// improvise-hwpi: without an `error` listener, a connection that never
|
||||
// opened left #status blank, and "the server is not running" read exactly
|
||||
// like "still connecting". Port 9 (discard) refuses immediately.
|
||||
//
|
||||
// It is asserted over the *history* of #status rather than its final value,
|
||||
// because of improvise-m18f, which this reproduces: the socket's `close`
|
||||
// listener fires after `error` and overwrites the precise message with
|
||||
// "disconnected". That one is filed and deliberately not fixed here —
|
||||
// suppressing `close` could hide a genuine mid-session disconnect, so it is
|
||||
// not the same fix as the watchdog's (gotcha 48). Asserting both halves
|
||||
// pins hwpi's fix AND m18f's bug, so whichever moves, this speaks up.
|
||||
test("a peer that cannot be reached is named, not left blank", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.__statusLog = [];
|
||||
new MutationObserver(() => {
|
||||
const el = document.getElementById("status");
|
||||
const text = el?.textContent ?? "";
|
||||
if (text && window.__statusLog.at(-1) !== text) window.__statusLog.push(text);
|
||||
}).observe(document, { childList: true, subtree: true, characterData: true });
|
||||
});
|
||||
await page.goto("/index.html?ws=127.0.0.1:9");
|
||||
await expect(status(page)).toContainText("disconnected", { timeout: 15_000 });
|
||||
|
||||
const log = await page.evaluate(() => window.__statusLog);
|
||||
expect(log.some((t) => t.includes("cannot reach")), `#status said: ${JSON.stringify(log)}`)
|
||||
.toBe(true);
|
||||
// improvise-m18f, as it stands today. Delete this line when it is fixed —
|
||||
// it is here so the fix cannot land silently.
|
||||
expect(log.at(-1)).toContain("disconnected");
|
||||
});
|
||||
|
||||
// The first-frame watchdog (gotcha 46), found by reading. The condition is
|
||||
// one no per-transport check would catch: the peer accepted the connection
|
||||
// and then said nothing. The mock accepts and never answers.
|
||||
test("a peer that accepts and then says nothing trips the watchdog", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeWebSocket("ws://127.0.0.1:8788/", () => {});
|
||||
await page.goto("/index.html?ws=127.0.0.1:8788");
|
||||
await expect(status(page)).toContainText("sent no document", { timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("the standalone deployment", () => {
|
||||
// No `?ws=`, so `web/app.js` spawns the in-page worker over OPFS: the whole
|
||||
// application, with no server process. If this draws a grid, then
|
||||
// worker.js, bindings.rs, OpfsFs, vfs::open and the seed fetch all ran.
|
||||
test("the page draws the demo document out of the worker", async ({ page }) => {
|
||||
await page.goto("/index.html");
|
||||
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
|
||||
await expect(status(page)).toContainText("in-page worker");
|
||||
// web/demo.improv: Product on row, _Measure on column (gotcha 46).
|
||||
await expect(page.locator("#grid th", { hasText: "Revenue" })).toBeVisible();
|
||||
await expect(page.locator("#grid td.editable").first()).toBeVisible();
|
||||
});
|
||||
|
||||
// improvise-hwpi's third fix: Enter removes the focused <input>, which
|
||||
// fires `blur`, which called `close` a second time — harmless before, but
|
||||
// only by accident. The observable is that the commit takes effect exactly
|
||||
// once and the value on screen is the SERVER's, not the string typed.
|
||||
//
|
||||
// `20000` is typed and `20,000` is asserted: the number format is applied
|
||||
// by the host and relayed back, so a page that simply echoed what was typed
|
||||
// would show `20000` and fail.
|
||||
test("committing an edit repaints once, with the server's value", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/index.html?doc=commit-test.improv");
|
||||
await expect(page.locator("#grid table")).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
const cell = page.locator("#grid td.editable").first();
|
||||
const before = await cell.textContent();
|
||||
await cell.click();
|
||||
const input = cell.locator("input");
|
||||
await expect(input).toBeFocused();
|
||||
await input.fill("20000");
|
||||
await input.press("Enter");
|
||||
|
||||
await expect(cell).toHaveText("20,000", { timeout: 10_000 });
|
||||
expect(before).not.toBe("20,000");
|
||||
// One editor, closed once: a second `close` would reopen nothing, but a
|
||||
// stray input left in the DOM is what the guard exists to prevent.
|
||||
await expect(page.locator("#grid input")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>improvise — OPFS contract harness</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
Not part of the application. This page exists so Playwright can drive
|
||||
`improvise_io::storage::contract::storage_contract` against `OpfsFs`
|
||||
inside a dedicated Web Worker, which is the only context where OPFS
|
||||
sync access handles exist (improvise-vvl3).
|
||||
-->
|
||||
<h1>OPFS contract harness</h1>
|
||||
<pre id="out">idle</pre>
|
||||
<script type="module">
|
||||
const worker = new Worker("./worker.js", { type: "module" });
|
||||
let next = 0;
|
||||
const pending = new Map();
|
||||
|
||||
worker.addEventListener("message", (event) => {
|
||||
const { id, ok, value, error } = event.data;
|
||||
const settle = pending.get(id);
|
||||
pending.delete(id);
|
||||
if (!settle) return;
|
||||
ok ? settle.resolve(value) : settle.reject(new Error(error));
|
||||
});
|
||||
worker.addEventListener("error", (event) => {
|
||||
for (const { reject } of pending.values()) {
|
||||
reject(new Error(`worker error: ${event.message ?? "see console"}`));
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
// The page's whole API. Returns whatever the Rust entry point returned,
|
||||
// and rejects — loudly, with the message intact — if it threw.
|
||||
window.opfs = (cmd) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const id = next++;
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, cmd });
|
||||
}).then((value) => {
|
||||
document.getElementById("out").textContent = value;
|
||||
return value;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
// The dedicated Web Worker the OPFS contract runs inside.
|
||||
//
|
||||
// `createSyncAccessHandle` exists only here — that is the whole reason
|
||||
// `Storage` could stay synchronous (gotcha 28), and the reason this file
|
||||
// exists rather than the page calling the bundle directly.
|
||||
//
|
||||
// Every decision is in Rust (harness/opfs-contract). This moves strings.
|
||||
|
||||
import init, {
|
||||
run_checks,
|
||||
run_canary,
|
||||
before_reload,
|
||||
after_reload,
|
||||
} from "./pkg/opfs_contract.js";
|
||||
|
||||
const ready = init();
|
||||
|
||||
const ENTRY = { run_checks, run_canary, before_reload, after_reload };
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const { id, cmd } = event.data;
|
||||
try {
|
||||
await ready;
|
||||
const fn = ENTRY[cmd];
|
||||
if (!fn) throw new Error(`no such entry point: ${cmd}`);
|
||||
self.postMessage({ id, ok: true, value: await fn() });
|
||||
} catch (err) {
|
||||
self.postMessage({ id, ok: false, error: String(err?.message ?? err) });
|
||||
}
|
||||
};
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
# Assemble the static site from `web/` into `site/`.
|
||||
#
|
||||
# `web/` is the *source* directory: it also holds build.sh, assemble.sh and
|
||||
# .gitignore, which have no business being served. This copies what ships.
|
||||
#
|
||||
# Extracted from the CI workflow so that the Pages job and the browser harness
|
||||
# assemble the same tree — a harness that runs against a different arrangement
|
||||
# of files is testing something nobody deploys (improvise-vvl3). Same reason
|
||||
# `web/build.sh` exists rather than two copies of the wasm-pack invocation.
|
||||
#
|
||||
# Usage: web/assemble.sh [OUT_DIR] [CACHE_BUST_TOKEN]
|
||||
#
|
||||
# The token cache-busts the two generated bundles. Pages serves everything
|
||||
# `max-age=600` and the wasm-pack filenames carry no content hash, so for ten
|
||||
# minutes after a deploy a returning visitor runs the PREVIOUS build — which
|
||||
# during improvise-9x6 debugging made a shipped fix look like it had not
|
||||
# shipped, twice. CI passes the commit SHA; a local run defaults to `dev`.
|
||||
#
|
||||
# Both halves are stamped, because a fresh glue file does NOT pull a fresh
|
||||
# .wasm: the glue locates it with `new URL('..._bg.wasm', import.meta.url)`,
|
||||
# and a URL constructed against a base does not inherit the base's query
|
||||
# string. Stamping only the import specifier would refresh the wrapper and
|
||||
# keep the stale binary — the failure mode this is here to end.
|
||||
set -eu
|
||||
|
||||
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||
out=${1:-"$root/site"}
|
||||
token=${2:-dev}
|
||||
|
||||
test -d "$root/web/pkg" || { echo "web/pkg missing — run web/build.sh first" >&2; exit 1; }
|
||||
test -d "$root/web/worker-pkg" || { echo "web/worker-pkg missing — run web/build.sh first" >&2; exit 1; }
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
cp -f "$root/web/index.html" "$root/web/app.js" "$root/web/style.css" "$root/web/worker.js" "$out/"
|
||||
# The document the page opens on a browser that has never seen one.
|
||||
# web/worker.js fetches it by the ?seed= URL and hands the bytes to Rust,
|
||||
# which writes them through the VFS — so leaving it out is an empty grid, not
|
||||
# a 404 the page could report.
|
||||
cp -f "$root/web/demo.improv" "$out/"
|
||||
cp -rf "$root/web/pkg" "$out/pkg"
|
||||
cp -rf "$root/web/worker-pkg" "$out/worker-pkg"
|
||||
rm -f "$out/pkg/.gitignore" "$out/pkg/package.json"
|
||||
rm -f "$out/worker-pkg/.gitignore" "$out/worker-pkg/package.json"
|
||||
|
||||
# BSD sed (macOS) needs an argument to -i; GNU sed must not be given one.
|
||||
sedi() { sed "$1" "$2" > "$2.tmp" && mv -f "$2.tmp" "$2"; }
|
||||
|
||||
sedi "s|\./pkg/improvise_wasm_client\.js|&?v=${token}|" "$out/app.js"
|
||||
sedi "s|\./worker-pkg/improvise_worker_server\.js|&?v=${token}|" "$out/worker.js"
|
||||
sedi "s|'improvise_wasm_client_bg\.wasm'|'improvise_wasm_client_bg.wasm?v=${token}'|" \
|
||||
"$out/pkg/improvise_wasm_client.js"
|
||||
sedi "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${token}'|" \
|
||||
"$out/worker-pkg/improvise_worker_server.js"
|
||||
|
||||
grep -n "?v=" "$out/app.js" "$out/worker.js" \
|
||||
"$out/pkg/improvise_wasm_client.js" \
|
||||
"$out/worker-pkg/improvise_worker_server.js"
|
||||
Reference in New Issue
Block a user