8000 ci: test and lint the wasm32 targets again; fix browser panics and dropped pages by adityamukho · Pull Request #469 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
85 changes: 85 additions & 0 deletions .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: WASM

# Core's own tests and lints on the two wasm32 targets. The browser (`BrowserDb`,
# `browser` feature) and WASI builds share the engine with native, but their
# cfg-gated code only compiles for wasm32, so native CI never sees it. These jobs
# ran here before the repo split (#231) moved packaging to minigraf-wasm, which
# only builds; without them the wasm32 test build broke unnoticed for months.

on:
push:
branches: [ "main", "v[0-9]*" ]
pull_request:
branches: [ "main", "v[0-9]*" ]

env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
browser:
name: Browser tests (headless ${{ matrix.browser }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Both browsers and their WebDriver binaries are preinstalled on
# ubuntu-latest; wasm-pack finds them on PATH.
browser: [firefox, chrome]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- 8000 name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: wasm-bindgen tests (lib, incl. BrowserDb and IndexedDB)
run: wasm-pack test --headless --${{ matrix.browser }} --features browser

wasi:
name: WASI tests (Wasmtime)
runs-on: ubuntu-latest
env:
WASMTIME_VERSION: v49.0.2
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- name: Install Wasmtime
run: |
curl -sSfL \
"https://github.com/bytecodealliance/wasmtime/releases/download/${WASMTIME_VERSION}/wasmtime-${WASMTIME_VERSION}-x86_64-linux.tar.xz" \
-o /tmp/wasmtime.tar.xz
tar -xf /tmp/wasmtime.tar.xz -C /tmp
echo "/tmp/wasmtime-${WASMTIME_VERSION}-x86_64-linux" >> "$GITHUB_PATH"
- name: Unit and integration tests
# `--dir .` preopens the package root, where tests read fixtures
# such as tests/grammar/. File-backed tests are cfg'd off wasm32.
env:
CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime run --dir .
run: cargo test --target wasm32-wasip1 --lib --tests
- name: Build the REPL binary
run: cargo build --target wasm32-wasip1 --release --bin minigraf

clippy:
name: Clippy (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: wasm32-unknown-unknown
features: --features browser
- target: wasm32-wasip1
features: ""
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
components: clippy
- name: Clippy (strict — fails on warnings)
run: cargo clippy --target ${{ matrix.target }} ${{ matrix.features }} -- -D warnings
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **The wasm32 builds are tested again, and the browser layer no longer panics or drops pages.** The repo split (#231) moved the browser and WASI CI jobs to minigraf-wasm, which only builds the package, so core's own wasm tests stopped running and the wasm32 test build broke unnoticed. A new `wasm.yml` workflow runs the `wasm-bindgen` tests in headless Firefox and Chrome, runs the unit and integration tests on `wasm32-wasip1` under Wasmtime, and lints both targets with `clippy -D warnings`. Fixes found on the way: `BrowserDb` skipped any dirty page it could not read when flushing to IndexedDB and still reported success, and now returns the error. The IndexedDB upgrade callback, the IndexedDB backend handle and the page-buffer lock no longer `unwrap()` (on wasm32 a panic aborts the whole instance). IndexedDB keys that are not whole page numbers are rejected instead of truncated. The nine `BrowserBufferBackend` tests used `#[test]`, which the wasm test runner never runs, and now run. File-only modules (`wal`, `storage::verify`, `storage::dir_sync`) are no longer compiled for wasm32, so the wasm32 builds have no warnings.
- Two values of the same attribute written in one transaction are both returned by every query path. Previously only one came back, and entity-bound and attribute-bound queries could return different ones (#371, #287).
- A retract that removes several values of one attribute in one call now hides all of them (#371).
- Queries no longer lose or duplicate facts when more than 65,535 facts are waiting for a checkpoint (#445; also fixed in v2.0.3).
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ cargo run < demos/demo_negation.txt
- `src/fact_log.rs`: `FactLog` / `FactFilter` / `FactRecord` / `FactOrder` — every fact version streamed by `Minigraf::fact_log` (#430): tx order (multi-pass, bounded window) or storage order, key-level filters; an open log pins the committed generation, deferring checkpoints (`API-013`)
- `src/log_writer.rs`: `LogWriter` — builds a new file from `FactRecord`s with their tx and valid-time kept (#431): tx-ordered appends with holes (`API-015`/`API-016`), batched copy-on-write checkpoints at tx boundaries, no WAL, built at `<path>.partial` and renamed by `finish` (`STG-043` if the target exists)

7. **`src/wal.rs`** — Fact-level sidecar WAL, CRC32-protected entries, crash recovery
7. **`src/wal.rs`** — Fact-level sidecar WAL, CRC32-protected entries, crash recovery. Like `storage::verify` and `storage::dir_sync`, not compiled for wasm32 (no file backend there)

8. **`src/error.rs`** — Structured error codes: `MinigrafError`, `ErrorCategory`, `ErrorCode` registry (PRS/QRY/STG/WAL/API/INT codes); must match `docs/ERROR_REFERENCE.md`. `MinigrafError::invalid_argument` (API-017) and `MinigrafError::closed` (API-018) exist for language bindings

9. **`src/browser/`** — Browser WASM backend (`browser` feature): `buffer.rs` (`BrowserBufferBackend`, in-memory pages with dirty tracking), `indexeddb.rs` (IndexedDB persistence); `BrowserDb.query` returns a `BrowserCursor` (JSON row batches)
9. **`src/browser/`** — Browser WASM backend (`browser` feature): `buffer.rs` (`BrowserBufferBackend`, in-memory pages with dirty tracking), `indexeddb.rs` (IndexedDB persistence); `BrowserDb.query` returns a `BrowserCursor` (JSON row batches). Tests here must be `#[wasm_bindgen_test]` (the module only builds for wasm32); `.github/workflows/wasm.yml` runs them in headless browsers, runs the WASI tests under Wasmtime, and lints both wasm targets. New file-backed integration test files need `#![cfg(not(target_arch = "wasm32"))]`

### Data Model

Expand Down
2 changes: 1 addition & 1 deletion docs/TEST_COVERAGE.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions examples/pid_ns_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
//! release by the library, nothing but whatever the kernel does on its own.
//! That is the exact state #317 is about.

#[cfg(target_arch = "wasm32")]
fn main() {}

#[cfg(not(target_arch = "wasm32"))]
fn main() {
let args: Vec<String> = std::env::args().collect();
let path = &args[1];
Expand Down
21 changes: 12 additions & 9 deletions src/browser/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,37 +101,40 @@ impl StorageBackend for BrowserBufferBackend {

#[cfg(test)]
mod tests {
// The browser module only builds for wasm32, where the test runner runs
// `#[wasm_bindgen_test]` functions, not `#[test]` ones.
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;

fn page(byte: u8) -> Vec<u8> {
vec![byte; PAGE_SIZE]
}

#[test]
#[wasm_bindgen_test]
fn write_marks_dirty() {
let mut buf = BrowserBufferBackend::new();
buf.write_page(0, &page(1)).unwrap();
let dirty = buf.take_dirty();
assert!(dirty.contains(&0));
}

#[test]
#[wasm_bindgen_test]
fn take_dirty_clears_set() {
let mut buf = BrowserBufferBackend::new();
buf.write_page(0, &page(1)).unwrap();
let _ = buf.take_dirty();
assert!(buf.take_dirty().is_empty());
}

#[test]
#[wasm_bindgen_test]
fn read_after_write_returns_same_bytes() {
let mut buf = BrowserBufferBackend::new();
let p = page(42);
buf.write_page(3, &p).unwrap();
assert_eq!(buf.read_page(3).unwrap(), p);
}

#[test]
#[wasm_bindgen_test]
fn page_count_is_high_water_mark() {
let mut buf = BrowserBufferBackend::new();
buf.write_page(0, &page(0)).unwrap();
Expand All @@ -142,14 +145,14 @@ mod tests {
assert_eq!(buf.page_count().unwrap(), 6);
}

#[test]
#[wasm_bindgen_test]
fn load_pages_starts_with_no_dirty() {
let pages = HashMap::from([(0u64, page(0)), (1u64, page(1))]);
let mut buf = BrowserBufferBackend::load_pages(pages);
assert!(buf.take_dirty().is_empty());
}

#[test]
#[wasm_bindgen_test]
fn load_pages_all_dirty_marks_all() {
let pages = HashMap::from([(0u64, page(0)), (1u64, page(1))]);
let mut buf = BrowserBufferBackend::load_pages_all_dirty(pages);
Expand All @@ -158,18 +161,18 @@ mod tests {
assert!(dirty.contains(&1));
}

#[test]
#[wasm_bindgen_test]
fn empty_buffer_has_no_pages() {
assert_eq!(BrowserBufferBackend::new().page_count().unwrap(), 0);
}

#[test]
#[wasm_bindgen_test]
fn wrong_page_size_errors() {
let mut buf = BrowserBufferBackend::new();
assert!(buf.write_page(0, &[0u8; 100]).is_err());
}

#[test]
#[wasm_bindgen_test]
fn read_missing_page_errors() {
let buf = BrowserBufferBackend::new();
assert!(buf.read_page(99).is_err());
Expand Down
68 changes: 60 additions & 8 deletions src/browser/indexeddb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,11 @@ impl IndexedDbBackend {
// Create the object store if this is a fresh database (version upgrade).
let on_upgrade: Closure<dyn FnMut(web_sys::Event)> =
Closure::once(move |event: web_sys::Event| {
let target = event.target().unwrap();
let request: web_sys::IdbOpenDbRequest = target.dyn_into().unwrap();
let db: IdbDatabase = request.result().unwrap().dyn_into().unwrap();
if !db.object_store_names().contains(&store_name_upgrade) {
db.create_object_store(&store_name_upgrade).unwrap();
}
// A callback cannot return the error. If the store is not
// created, the first transaction on it fails instead, and
// that error reaches the caller; a panic would abort the
// whole wasm instance.
let _ = create_store(&event, &store_name_upgrade);
});
open_request.set_onupgradeneeded(Some(on_upgrade.as_ref().unchecked_ref()));
on_upgrade.forget();
Expand Down Expand Up @@ -126,8 +125,8 @@ impl IndexedDbBackend {
let key = keys_arr.get(i);
let page_id = key
.as_f64()
.ok_or_else(|| JsValue::from_str("page_id is not a number"))?
as u64;
.and_then(page_id_from_f64)
.ok_or_else(|| JsValue::from_str("page_id is not a valid page number"))?;
let val = vals_arr.get(i);
let arr: Uint8Array = val.dyn_into()?;
pages.insert(page_id, arr.to_vec());
Expand Down Expand Up @@ -173,3 +172,56 @@ impl IndexedDbBackend {
Ok(())
}
}

/// Create the object store `name` in the database an `upgradeneeded` event
/// opens, unless it exists.
fn create_store(event: &web_sys::Event, name: &str) -> Result<(), JsValue> {
let target = event
.target()
.ok_or_else(|| JsValue::from_str("upgradeneeded event has no target"))?;
let request: web_sys::IdbOpenDbRequest = target.dyn_into()?;
let db: IdbDatabase = request.result()?.dyn_into()?;
if !db.object_store_names().contains(name) {
db.create_object_store(name)?;
}
Ok(())
}

/// A page id stored as an IndexedDB key (a JS number): a whole, non-negative
/// number that a JS number holds exactly (at most 2^53).
fn page_id_from_f64(key: f64) -> Option<u64> {
const MAX_SAFE: f64 = 9_007_199_254_740_992.0; // 2^53
if key.fract() != 0.0 || !(0.0..=MAX_SAFE).contains(&key) {
return None;
}
// Checked above: whole and in range, so the cast is exact.
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Some(key as u64)
}

#[cfg(test)]
mod tests {
use super::page_id_from_f64;
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn page_id_accepts_whole_non_negative_numbers() {
assert_eq!(page_id_from_f64(0.0), Some(0));
assert_eq!(page_id_from_f64(42.0), Some(42));
assert_eq!(page_id_from_f64(9_007_199_254_740_992.0), Some(1 << 53));
}

#[wasm_bindgen_test]
fn page_id_rejects_fractions_negatives_and_non_finite() {
for bad in [
-1.0,
0.5,
9_007_199_254_740_994.0,
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
] {
assert_eq!(page_id_from_f64(bad), None);
}
}
}
Loading
Loading
0