diff --git a/Makefile b/Makefile index f8c87de..25f956e 100644 --- a/Makefile +++ b/Makefile @@ -530,6 +530,26 @@ wasm-clean: ## Remove WASM build artefacts (pkg/) $(call warn,Removing $(WASM_CRATE)/pkg/ ...) @rm -rf $(WASM_CRATE)/pkg/ +# ══════════════════════════════════════════════════════════════════════════════ +## WASI / RISC-V Integration Tests (Docker-based) +# ══════════════════════════════════════════════════════════════════════════════ + +WASI_SCRIPT := tests/wasm-runtimes/wasm-test.sh + +wasi-build: ## Build all WASM runtime + RISC-V Docker test images + $(call log,Building WASM/RISC-V integration test images…) + @$(WASI_SCRIPT) build all + +wasi-test: ## Run WASM/RISC-V integration tests across all runtimes + $(call log,Running WASM/RISC-V integration tests…) + @$(WASI_SCRIPT) test all + +wasi-status: ## Show Docker image / container status for WASI tests + @$(WASI_SCRIPT) status + +wasi-clean: ## Remove all WASI test Docker images and artefacts + @$(WASI_SCRIPT) clean + # ══════════════════════════════════════════════════════════════════════════════ ## Clean # ══════════════════════════════════════════════════════════════════════════════ diff --git a/crates/edgeparse-cli/Cargo.toml b/crates/edgeparse-cli/Cargo.toml index 925c49b..664ce4b 100644 --- a/crates/edgeparse-cli/Cargo.toml +++ b/crates/edgeparse-cli/Cargo.toml @@ -15,12 +15,16 @@ documentation = "https://docs.rs/edgeparse-cli" name = "edgeparse" path = "src/main.rs" +[features] +default = ["native"] +native = ["rayon", "edgeparse-core/native"] + [dependencies] -edgeparse-core = { path = "../edgeparse-core", version = "0.2.0" } +edgeparse-core = { path = "../edgeparse-core", version = "0.2.0", default-features = false } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } log = { workspace = true } env_logger = { workspace = true } -rayon = { workspace = true } +rayon = { workspace = true, optional = true } diff --git a/crates/edgeparse-cli/src/main.rs b/crates/edgeparse-cli/src/main.rs index 3a93f05..01d86af 100644 --- a/crates/edgeparse-cli/src/main.rs +++ b/crates/edgeparse-cli/src/main.rs @@ -6,6 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use clap::Parser; use edgeparse_core::api::config::OutputFormat; +#[cfg(feature = "native")] use rayon::prelude::*; /// EdgeParse: High-performance PDF-to-structured-data extraction @@ -124,10 +125,12 @@ fn main() { // Build processing config let config = build_config(&cli); - // Process each input file in parallel + // Process each input file (parallel when native feature is enabled) let has_errors = AtomicBool::new(false); - cli.input.par_iter().for_each(|input_path| { - match edgeparse_core::convert(input_path, &config) { + + let process_file = |input_path: &PathBuf| { + let result = convert_file(input_path, &config); + match result { Ok(doc) => { log::info!( "Processed {} ({} pages)", @@ -144,7 +147,13 @@ fn main() { has_errors.store(true, Ordering::Relaxed); } } - }); + }; + + #[cfg(feature = "native")] + cli.input.par_iter().for_each(process_file); + + #[cfg(not(feature = "native"))] + cli.input.iter().for_each(process_file); if has_errors.load(Ordering::Relaxed) { process::exit(1); @@ -202,6 +211,30 @@ fn write_outputs( Ok(()) } +/// Convert a PDF file using the appropriate backend. +/// +/// On native builds, uses `edgeparse_core::convert()` which supports raster +/// table OCR via external tools. On WASI/non-native builds, reads the file +/// into memory and uses `convert_bytes()` instead (no external tool support). +fn convert_file( + input_path: &std::path::Path, + config: &edgeparse_core::api::config::ProcessingConfig, +) -> Result { + #[cfg(feature = "native")] + { + edgeparse_core::convert(input_path, config) + } + #[cfg(not(feature = "native"))] + { + let data = std::fs::read(input_path)?; + let file_name = input_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown.pdf"); + edgeparse_core::convert_bytes(&data, file_name, config) + } +} + fn build_config(cli: &Cli) -> edgeparse_core::api::config::ProcessingConfig { use edgeparse_core::api::config::*; use edgeparse_core::api::filter::FilterConfig; diff --git a/tests/wasm-runtimes/.gitignore b/tests/wasm-runtimes/.gitignore new file mode 100644 index 0000000..120d20a --- /dev/null +++ b/tests/wasm-runtimes/.gitignore @@ -0,0 +1,2 @@ +# Build artifacts — extracted binaries from Docker builds +.build/ diff --git a/tests/wasm-runtimes/Dockerfile.build.riscv b/tests/wasm-runtimes/Dockerfile.build.riscv new file mode 100644 index 0000000..cd2fbb3 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.build.riscv @@ -0,0 +1,68 @@ +# ─── Reproducible RISC-V Cross-Compilation ────────────────────────────────── +# Greg's AI coding buddy: +# Cross-compiles edgeparse for riscv64gc-unknown-linux-gnu. +# Produces TWO binaries: +# 1. Dynamic-linked → runs under QEMU user-mode (with sysroot) +# 2. Static-linked → runs on Spike/libriscv/RVVM/CKB-VM (no sysroot) +# +# riscv64gc = RV64IMAFDC — the "general-purpose computing" profile +# that Linux distros target (Debian, Ubuntu, Fedora all ship riscv64gc). +# +# Usage: +# docker build -f tests/wasm-runtimes/Dockerfile.build.riscv \ +# -t edgeparse-riscv-build . +# ───────────────────────────────────────────────────────────────────────────── + +FROM rust:1-slim-bookworm AS builder + +# Cross-compilation toolchain for RISC-V 64-bit +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc-riscv64-linux-gnu \ + libc6-dev-riscv64-cross \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN rustup target add riscv64gc-unknown-linux-gnu + +# Tell cargo which linker to use for the RISC-V target +ENV CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc + +WORKDIR /build + +# ── Cache cargo registry: copy manifests first ────────────────────────────── +COPY Cargo.toml Cargo.lock ./ +COPY crates/edgeparse-core/Cargo.toml crates/edgeparse-core/ +COPY crates/edgeparse-cli/Cargo.toml crates/edgeparse-cli/ +COPY crates/pdf-cos/Cargo.toml crates/pdf-cos/ + +# Strip workspace members that need native platform SDKs +RUN sed -E -i \ + '/"crates\/(edgeparse-python|edgeparse-node|edgeparse-wasm)"/d' \ + Cargo.toml && \ + mkdir -p crates/edgeparse-core/src crates/edgeparse-cli/src crates/pdf-cos/src && \ + echo "fn main() {}" > crates/edgeparse-cli/src/main.rs && \ + touch crates/edgeparse-core/src/lib.rs && \ + touch crates/pdf-cos/src/lib.rs && \ + cargo build --target riscv64gc-unknown-linux-gnu --release \ + -p edgeparse-cli 2>/dev/null || true + +# ── Copy real source and build (dynamic) ──────────────────────────────────── +COPY crates/ crates/ +RUN find crates/ -name '*.rs' -exec touch {} + && \ + cargo build --target riscv64gc-unknown-linux-gnu --release \ + -p edgeparse-cli + +# Save dynamic binary before static build overwrites it +RUN cp target/riscv64gc-unknown-linux-gnu/release/edgeparse /tmp/edgeparse-dynamic + +# ── Build statically-linked variant for Spike/libriscv/RVVM/CKB-VM ───────── +# These VMs have minimal/no dynamic linker — static linking is essential. +RUN RUSTFLAGS="-C target-feature=+crt-static" \ + cargo build --target riscv64gc-unknown-linux-gnu --release \ + -p edgeparse-cli && \ + cp target/riscv64gc-unknown-linux-gnu/release/edgeparse /tmp/edgeparse-static + +# ── Output stage ──────────────────────────────────────────────────────────── +FROM scratch AS output +COPY --from=builder /tmp/edgeparse-dynamic /out/edgeparse +COPY --from=builder /tmp/edgeparse-static /out/edgeparse-static diff --git a/tests/wasm-runtimes/Dockerfile.build.wasix b/tests/wasm-runtimes/Dockerfile.build.wasix new file mode 100644 index 0000000..a1b2edc --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.build.wasix @@ -0,0 +1,47 @@ +# ─── Reproducible WASIX Build ─────────────────────────────────────────────── +# Greg's AI coding buddy: +# Builds edgeparse as a WASIX binary (.wasm) using cargo-wasix. +# WASIX = WASI + POSIX superpowers (threads, sockets, fork/exec). +# Only runs on Wasmer — the one runtime to rule them all (for WASIX). +# +# The target triple is wasm32-wasmer-wasi — a custom Wasmer target +# that extends wasm32-wasi with the full POSIX syscall surface. +# +# Usage: +# docker build -f tests/wasm-runtimes/Dockerfile.build.wasix \ +# -t edgeparse-wasix-build . +# ───────────────────────────────────────────────────────────────────────────── + +FROM rust:1-slim-bookworm AS builder + +# cargo-wasix installs its own rustup toolchain + wasm32-wasmer-wasi target +RUN cargo install cargo-wasix + +WORKDIR /build + +# ── Cache cargo registry: copy manifests first ────────────────────────────── +COPY Cargo.toml Cargo.lock ./ +COPY crates/edgeparse-core/Cargo.toml crates/edgeparse-core/ +COPY crates/edgeparse-cli/Cargo.toml crates/edgeparse-cli/ +COPY crates/pdf-cos/Cargo.toml crates/pdf-cos/ + +# Strip workspace members that don't compile for WASIX +RUN sed -E -i \ + '/"crates\/(edgeparse-python|edgeparse-node|edgeparse-wasm)"/d' \ + Cargo.toml && \ + mkdir -p crates/edgeparse-core/src crates/edgeparse-cli/src crates/pdf-cos/src && \ + echo "fn main() {}" > crates/edgeparse-cli/src/main.rs && \ + touch crates/edgeparse-core/src/lib.rs && \ + touch crates/pdf-cos/src/lib.rs && \ + cargo wasix build --release -p edgeparse-cli --no-default-features 2>/dev/null || true + +# ── Copy real source and build ────────────────────────────────────────────── +COPY crates/ crates/ +RUN find crates/ -name '*.rs' -exec touch {} + && \ + cargo wasix build --release -p edgeparse-cli --no-default-features + +# ── Output stage ──────────────────────────────────────────────────────────── +FROM scratch AS output +COPY --from=builder \ + /build/target/wasm32-wasmer-wasi/release/edgeparse.wasm \ + /out/edgeparse-wasix.wasm diff --git a/tests/wasm-runtimes/Dockerfile.build.wasm b/tests/wasm-runtimes/Dockerfile.build.wasm new file mode 100644 index 0000000..cbe1ffb --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.build.wasm @@ -0,0 +1,54 @@ +# ─── Reproducible WASM (WASI) Build ───────────────────────────────────────── +# Greg's AI coding buddy: +# Builds edgeparse as a WASI Preview 1 binary (.wasm) that runs on +# any conformant runtime (wasmtime, wasmer, wasmedge, wamr). +# +# The wasm32-wasip1 target compiles edgeparse-core without rayon/image/zip +# (those need native threads or JS glue). Pure PDF parsing still works — +# you just lose parallelism and image extraction. +# +# Usage: +# docker build -f tests/wasm-runtimes/Dockerfile.build.wasm \ +# -t edgeparse-wasi-build . +# # Extract the binary: +# id=$(docker create edgeparse-wasi-build) && \ +# docker cp "$id":/out/edgeparse.wasm tests/wasm-runtimes/.build/ && \ +# docker rm "$id" +# ───────────────────────────────────────────────────────────────────────────── + +FROM rust:1-slim-bookworm AS builder + +RUN rustup target add wasm32-wasip1 + +WORKDIR /build + +# ── Cache cargo registry: copy manifests first ────────────────────────────── +COPY Cargo.toml Cargo.lock ./ +COPY crates/edgeparse-core/Cargo.toml crates/edgeparse-core/ +COPY crates/edgeparse-cli/Cargo.toml crates/edgeparse-cli/ +COPY crates/pdf-cos/Cargo.toml crates/pdf-cos/ + +# Strip workspace members that don't compile for wasm32-wasip1 +# (pyo3, napi-rs, wasm-bindgen are browser-only / native-only) +RUN sed -E -i \ + '/"crates\/(edgeparse-python|edgeparse-node|edgeparse-wasm)"/d' \ + Cargo.toml && \ + mkdir -p crates/edgeparse-core/src crates/edgeparse-cli/src crates/pdf-cos/src && \ + echo "fn main() {}" > crates/edgeparse-cli/src/main.rs && \ + touch crates/edgeparse-core/src/lib.rs && \ + touch crates/pdf-cos/src/lib.rs && \ + cargo build --target wasm32-wasip1 --release \ + -p edgeparse-cli --no-default-features 2>/dev/null || true + # ^^ dummy build warms the dep cache; may fail on empty libs — that's fine + +# ── Copy real source and build ────────────────────────────────────────────── +COPY crates/ crates/ +RUN find crates/ -name '*.rs' -exec touch {} + && \ + cargo build --target wasm32-wasip1 --release \ + -p edgeparse-cli --no-default-features + +# ── Output stage — just the binary ────────────────────────────────────────── +FROM scratch AS output +COPY --from=builder \ + /build/target/wasm32-wasip1/release/edgeparse.wasm \ + /out/edgeparse.wasm diff --git a/tests/wasm-runtimes/Dockerfile.runner.base b/tests/wasm-runtimes/Dockerfile.runner.base new file mode 100644 index 0000000..686f1a7 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.base @@ -0,0 +1,31 @@ +# ─── Shared Runner Base ───────────────────────────────────────────────────── +# Greg's AI coding buddy: +# Common base layer for all WASM runtime test containers. +# Build this FIRST — subsequent runtime images inherit its cached layers. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.base \ +# -t edgeparse-wasi-base . +# ───────────────────────────────────────────────────────────────────────────── + +FROM ubuntu:24.04 + +# Shared dependencies — curl for runtime installers, ca-certs for HTTPS, +# xz-utils for compressed release tarballs, file for MIME sniffing +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + xz-utils \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /test + +# Copy test fixtures and scripts +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# The .wasm binary is expected at /test/edgeparse.wasm +# It gets copied in by each runtime Dockerfile or mounted at runtime. + +ENTRYPOINT ["/test/run-tests.sh"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.ckb-vm b/tests/wasm-runtimes/Dockerfile.runner.ckb-vm new file mode 100644 index 0000000..77bf617 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.ckb-vm @@ -0,0 +1,46 @@ +# ─── CKB-VM Runner ────────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# CKB-VM — Production blockchain VM from Nervos Network. +# rv64imc ISA, W^X memory protection, gas metering, JIT compilation. +# 2.5x faster than Wasmer Singlepass. Deployed on CKB mainnet. +# +# Uses ckb-debugger to execute RISC-V ELF binaries with +# Linux syscall emulation. Limited POSIX support — designed for +# deterministic computation, not general-purpose Linux apps. +# +# EXPERIMENTAL: CKB-VM has limited syscall support. edgeparse's +# file I/O may not be fully supported. Tests may partially fail. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.ckb-vm \ +# -t edgeparse-riscv-ckb-vm . +# ───────────────────────────────────────────────────────────────────────────── + +FROM rust:1-slim-bookworm AS builder + +# Build ckb-standalone-debugger which can run RISC-V ELF binaries +RUN cargo install ckb-debugger 2>/dev/null || \ + (apt-get update && apt-get install -y --no-install-recommends git pkg-config libssl-dev && \ + cargo install --git https://github.com/nervosnetwork/ckb-standalone-debugger ckb-debugger) + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + file \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/cargo/bin/ckb-debugger /usr/local/bin/ + +# Verify +RUN ckb-debugger --version 2>&1 || echo "CKB debugger installed" + +WORKDIR /test + +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# Copy pre-built RISC-V binary (statically linked) +COPY tests/wasm-runtimes/.build/edgeparse-riscv64-static /test/edgeparse-riscv64 + +ENTRYPOINT ["/test/run-tests.sh"] +CMD ["ckb-vm"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.libriscv b/tests/wasm-runtimes/Dockerfile.runner.libriscv new file mode 100644 index 0000000..f61c45b --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.libriscv @@ -0,0 +1,45 @@ +# ─── libriscv Runner ──────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# libriscv — "the fastest RISC-V sandbox" (768+ stars). +# 3-4ns function call overhead, binary translation, used in Godot. +# C++20 implementation with Linux syscall emulation built in. +# +# The 'rvlinux' CLI tool loads an RV64GC ELF and emulates it +# with a lightweight Linux ABI layer — no kernel needed. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.libriscv \ +# -t edgeparse-riscv-libriscv . +# ───────────────────────────────────────────────────────────────────────────── + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake git ca-certificates file \ + && rm -rf /var/lib/apt/lists/* + +# Build libriscv + rvlinux CLI from source (requires C++20) +RUN git clone --depth 1 https://github.com/libriscv/libriscv /tmp/libriscv && \ + cd /tmp/libriscv/emulator && \ + cmake -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DRISCV_EXT_A=ON \ + -DRISCV_EXT_C=ON \ + -DRISCV_BINARY_TRANSLATION=OFF && \ + cmake --build build --parallel "$(nproc)" --target rvlinux && \ + cp build/rvlinux /usr/local/bin/ && \ + rm -rf /tmp/libriscv + +# Verify +RUN rvlinux --help 2>&1 | head -3 || true + +WORKDIR /test + +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# Copy pre-built RISC-V binary (statically linked for sandbox compat) +COPY tests/wasm-runtimes/.build/edgeparse-riscv64-static /test/edgeparse-riscv64 + +ENTRYPOINT ["/test/run-tests.sh"] +CMD ["libriscv"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.riscv-qemu b/tests/wasm-runtimes/Dockerfile.runner.riscv-qemu new file mode 100644 index 0000000..ffb65c0 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.riscv-qemu @@ -0,0 +1,38 @@ +# ─── RISC-V QEMU Runner ───────────────────────────────────────────────────── +# Greg's AI coding buddy: +# Runs the RISC-V cross-compiled edgeparse binary under QEMU user-mode +# emulation. No real RISC-V hardware needed — qemu-riscv64 translates +# RV64GC instructions to your host ISA on the fly. +# +# This is the same approach Debian/Fedora use for their riscv64 ports: +# binfmt_misc + qemu-user-static. We just do it explicitly. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.riscv-qemu \ +# -t edgeparse-riscv-qemu . +# ───────────────────────────────────────────────────────────────────────────── + +FROM ubuntu:24.04 + +# QEMU user-mode for RISC-V + the riscv64 sysroot for dynamic linking +RUN apt-get update && apt-get install -y --no-install-recommends \ + qemu-user \ + libc6-riscv64-cross \ + libgcc-s1-riscv64-cross \ + file \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /test + +# Copy test fixtures and test script +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# Copy pre-built RISC-V binary (built by Dockerfile.build.riscv) +COPY tests/wasm-runtimes/.build/edgeparse-riscv64 /test/edgeparse-riscv64 + +# QEMU needs to know where the riscv64 dynamic linker lives +ENV QEMU_LD_PREFIX=/usr/riscv64-linux-gnu + +ENTRYPOINT ["/test/run-tests.sh"] +CMD ["riscv-qemu"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.rvvm b/tests/wasm-runtimes/Dockerfile.runner.rvvm new file mode 100644 index 0000000..e000c7a --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.rvvm @@ -0,0 +1,53 @@ +# ─── RVVM Runner ──────────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# RVVM — RISC-V Virtual Machine (1.1k+ stars). +# Tracing JIT with claims of outperforming QEMU TCG. +# Full system emulator with multi-core + device support. +# +# RVVM is a FULL SYSTEM EMULATOR ONLY — it boots firmware/kernels +# (OpenSBI → Linux), NOT user-mode binaries. There is no userland +# emulation mode like QEMU's qemu-riscv64 or libriscv's rvlinux. +# +# STATUS: INCOMPATIBLE — kept as documentation for future reference. +# If RVVM adds userland mode, this Dockerfile is ready to use. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.rvvm \ +# -t edgeparse-riscv-rvvm . +# ───────────────────────────────────────────────────────────────────────────── + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git ca-certificates file \ + && rm -rf /var/lib/apt/lists/* + +# Build RVVM from source +# NOTE: RVVM binary naming varies by version/platform. Recent versions produce +# rvvm_x86_64 (or rvvm_) instead of rvvm_userland or rvvm. +RUN git clone --depth 1 https://github.com/LekKit/RVVM /tmp/rvvm && \ + cd /tmp/rvvm && \ + make all -j"$(nproc)" && \ + echo "=== Build artifacts ===" && ls -la release.*/ && \ + ( cp release.linux.x86_64/rvvm_userland /usr/local/bin/rvvm-userland 2>/dev/null || \ + cp release.*/rvvm_userland /usr/local/bin/rvvm-userland 2>/dev/null || \ + cp release.linux.x86_64/rvvm /usr/local/bin/rvvm-userland 2>/dev/null || \ + cp release.*/rvvm /usr/local/bin/rvvm-userland 2>/dev/null || \ + cp release.linux.x86_64/rvvm_x86_64 /usr/local/bin/rvvm-userland 2>/dev/null || \ + cp release.*/rvvm_* /usr/local/bin/rvvm-userland 2>/dev/null || \ + { echo "ERROR: No RVVM binary found"; exit 1; } ) && \ + rm -rf /tmp/rvvm + +# Verify +RUN rvvm-userland --help 2>&1 | head -3 || echo "RVVM installed (may need different invocation)" + +WORKDIR /test + +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# Copy pre-built RISC-V binary (statically linked) +COPY tests/wasm-runtimes/.build/edgeparse-riscv64-static /test/edgeparse-riscv64 + +ENTRYPOINT ["/test/run-tests.sh"] +CMD ["rvvm"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.spike b/tests/wasm-runtimes/Dockerfile.runner.spike new file mode 100644 index 0000000..b3d7bcc --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.spike @@ -0,0 +1,69 @@ +# ─── Spike (riscv-isa-sim) Runner ─────────────────────────────────────────── +# Greg's AI coding buddy: +# Spike is the OFFICIAL RISC-V ISA reference simulator from riscv.org. +# When you want spec-compliance validation, Spike is the gold standard. +# We pair it with 'pk' (proxy kernel) which proxies Linux syscalls +# from the simulated RISC-V core to the host OS. +# +# spike pk — runs user-mode RISC-V programs. +# +# NOTE: The binary must be statically linked for pk to handle it. +# We build from source because distro packages are often stale. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.spike \ +# -t edgeparse-riscv-spike . +# ───────────────────────────────────────────────────────────────────────────── + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git ca-certificates autoconf automake libtool \ + device-tree-compiler pkg-config file \ + && rm -rf /var/lib/apt/lists/* + +# Build Spike (riscv-isa-sim) +RUN git clone --depth 1 https://github.com/riscv-software-src/riscv-isa-sim /tmp/spike && \ + cd /tmp/spike && \ + mkdir build && cd build && \ + ../configure --prefix=/usr/local && \ + make -j"$(nproc)" && \ + make install && \ + rm -rf /tmp/spike + +# Build pk (proxy kernel) — needs the riscv64 cross-compiler +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libc6-dev-riscv64-cross \ + && rm -rf /var/lib/apt/lists/* + +# pk needs gnu/stubs-lp64.h but the cross-libc only ships lp64d (double-float). +# Symlink to satisfy the include, then build with matching ABI. +RUN ln -sf /usr/riscv64-linux-gnu/include/gnu/stubs-lp64d.h \ + /usr/riscv64-linux-gnu/include/gnu/stubs-lp64.h + +RUN git clone --depth 1 https://github.com/riscv-software-src/riscv-pk /tmp/pk && \ + cd /tmp/pk && \ + mkdir build && cd build && \ + ../configure --prefix=/usr/local --host=riscv64-linux-gnu \ + --with-arch=rv64gc_zifencei && \ + make -j"$(nproc)" && \ + make install && \ + rm -rf /tmp/pk + +# Symlink pk where spike expects it (riscv64-unknown-elf vs riscv64-linux-gnu) +RUN mkdir -p /usr/local/riscv64-unknown-elf/bin && \ + ln -sf /usr/local/riscv64-linux-gnu/bin/pk /usr/local/riscv64-unknown-elf/bin/pk + +# Verify +RUN spike --help 2>&1 | head -1 || true + +WORKDIR /test + +COPY tests/fixtures/sample.pdf /test/fixtures/ +COPY tests/wasm-runtimes/run-tests.sh /test/ +RUN chmod +x /test/run-tests.sh + +# Copy pre-built RISC-V binary (statically linked) +COPY tests/wasm-runtimes/.build/edgeparse-riscv64-static /test/edgeparse-riscv64 + +ENTRYPOINT ["/test/run-tests.sh"] +CMD ["spike"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.wamr b/tests/wasm-runtimes/Dockerfile.runner.wamr new file mode 100644 index 0000000..ee9c1f1 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.wamr @@ -0,0 +1,35 @@ +# ─── WAMR (iwasm) Runner ──────────────────────────────────────────────────── +# Greg's AI coding buddy: +# WAMR — WebAssembly Micro Runtime from Bytecode Alliance (Intel origin). +# The embedded systems champion: 6 execution modes, ~100KB footprint, +# runs on everything from ESP32 to SGX enclaves. +# Built from source because there's no binary release — real hacker style. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.wamr \ +# -t edgeparse-wasi-wamr . +# ───────────────────────────────────────────────────────────────────────────── + +FROM edgeparse-wasi-base + +# Build iwasm from source (it's small and fast to compile) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake git \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/bytecodealliance/wasm-micro-runtime /tmp/wamr && \ + cd /tmp/wamr/product-mini/platforms/linux && \ + mkdir build && cd build && \ + cmake .. \ + -DWAMR_BUILD_LIBC_WASI=1 \ + -DWAMR_BUILD_FAST_INTERP=1 && \ + make -j"$(nproc)" && \ + cp iwasm /usr/local/bin/ && \ + rm -rf /tmp/wamr + +# Verify installation +RUN iwasm --version + +# Copy pre-built WASM binary +COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm + +CMD ["wamr"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.wasix b/tests/wasm-runtimes/Dockerfile.runner.wasix new file mode 100644 index 0000000..61847d2 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.wasix @@ -0,0 +1,31 @@ +# ─── WASIX on Wasmer Runner ───────────────────────────────────────────────── +# Greg's AI coding buddy: +# WASIX = WASI on steroids. Full POSIX process model: threads, sockets, +# fork/exec, chdir, TTY — everything WASI p1 left on the table. +# Only Wasmer speaks WASIX, so this is a Wasmer-exclusive party. +# +# We test two things here: +# 1. That a standard wasm32-wasip1 binary runs correctly on Wasmer's +# WASIX runtime (backward compatibility) +# 2. That WASIX-specific flags (--net, etc.) don't break anything +# +# A full WASIX build (cargo wasix / wasm32-wasmer-wasi target) would +# unlock threads + sockets, but edgeparse's PDF parsing doesn't need +# those — so we validate runtime compatibility instead. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.wasix \ +# -t edgeparse-wasi-wasix . +# ───────────────────────────────────────────────────────────────────────────── + +FROM edgeparse-wasi-base + +RUN curl https://get.wasmer.io -sSfL | sh +ENV PATH="/root/.wasmer/bin:${PATH}" + +# Verify installation +RUN wasmer --version + +# Copy standard WASI p1 binary (backward compatible with WASIX runtime) +COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm + +CMD ["wasix"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.wasmedge b/tests/wasm-runtimes/Dockerfile.runner.wasmedge new file mode 100644 index 0000000..b1bb536 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.wasmedge @@ -0,0 +1,30 @@ +# ─── WasmEdge Runner ──────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# WasmEdge — CNCF Sandbox project, the cloud-native WASM runtime. +# C++ core, AOT compilation support, smallest footprint among JIT runtimes. +# If you're into Kubernetes + WASM, this is your jam. +# +# We download the release tarball directly instead of using the installer +# script (which drags in python + git — overkill for a test container). +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.wasmedge \ +# -t edgeparse-wasi-wasmedge . +# ───────────────────────────────────────────────────────────────────────────── + +FROM edgeparse-wasi-base + +# Pin version for reproducibility; update when needed +ARG WASMEDGE_VERSION=0.14.1 + +RUN ARCH="$(uname -m)" && \ + curl -sSL "https://github.com/WasmEdge/WasmEdge/releases/download/${WASMEDGE_VERSION}/WasmEdge-${WASMEDGE_VERSION}-ubuntu20.04_${ARCH}.tar.gz" \ + | tar xz --strip-components=1 -C /usr/local && \ + ldconfig + +# Verify installation +RUN wasmedge --version + +# Copy pre-built WASM binary +COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm + +CMD ["wasmedge"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.wasmer b/tests/wasm-runtimes/Dockerfile.runner.wasmer new file mode 100644 index 0000000..11396c5 --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.wasmer @@ -0,0 +1,22 @@ +# ─── Wasmer Runner ────────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# Wasmer — the runtime with WASIX superpowers. +# Cranelift/LLVM/Singlepass backends, WASIX for full POSIX compat, +# and the only runtime that speaks the WASIX dialect. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.wasmer \ +# -t edgeparse-wasi-wasmer . +# ───────────────────────────────────────────────────────────────────────────── + +FROM edgeparse-wasi-base + +RUN curl https://get.wasmer.io -sSfL | sh +ENV PATH="/root/.wasmer/bin:${PATH}" + +# Verify installation +RUN wasmer --version + +# Copy pre-built WASM binary +COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm + +CMD ["wasmer"] diff --git a/tests/wasm-runtimes/Dockerfile.runner.wasmtime b/tests/wasm-runtimes/Dockerfile.runner.wasmtime new file mode 100644 index 0000000..7e4978a --- /dev/null +++ b/tests/wasm-runtimes/Dockerfile.runner.wasmtime @@ -0,0 +1,22 @@ +# ─── Wasmtime Runner ──────────────────────────────────────────────────────── +# Greg's AI coding buddy: +# Wasmtime — the Bytecode Alliance reference runtime. +# Cranelift JIT, full WASI Preview1 + Preview2, production-grade. +# The runtime that sets the standard. If it works here, it works anywhere. +# +# docker build -f tests/wasm-runtimes/Dockerfile.runner.wasmtime \ +# -t edgeparse-wasi-wasmtime . +# ───────────────────────────────────────────────────────────────────────────── + +FROM edgeparse-wasi-base + +RUN curl -sSf https://wasmtime.dev/install.sh | bash +ENV PATH="/root/.wasmtime/bin:${PATH}" + +# Verify installation +RUN wasmtime --version + +# Copy pre-built WASM binary (built by Dockerfile.build.wasm) +COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm + +CMD ["wasmtime"] diff --git a/tests/wasm-runtimes/README.md b/tests/wasm-runtimes/README.md new file mode 100644 index 0000000..7f08f44 --- /dev/null +++ b/tests/wasm-runtimes/README.md @@ -0,0 +1,154 @@ +# EdgeParse WASM/RISC-V Integration Tests + +> Greg's AI coding buddy: Testing edgeparse across every WASM runtime +> and CPU architecture we can get our hands on — all inside Docker, +> because reproducibility is not optional. + +## Architecture + +``` +tests/wasm-runtimes/ +├── wasm-test.sh # Management script (build/test/status/clean) +├── run-tests.sh # Test runner (executes inside containers) +│ +├── Dockerfile.build.wasm # Builds edgeparse → wasm32-wasip1 +├── Dockerfile.build.wasix # Builds edgeparse → wasm32-wasmer-wasi (WASIX) +├── Dockerfile.build.riscv # Cross-compiles edgeparse → riscv64gc (dynamic + static) +│ +├── Dockerfile.runner.base # Shared Ubuntu base for WASM runners +├── Dockerfile.runner.wasmtime # Wasmtime (Bytecode Alliance reference) +├── Dockerfile.runner.wasmer # Wasmer (WASIX superpowers) +├── Dockerfile.runner.wasmedge # WasmEdge (CNCF, cloud-native) +├── Dockerfile.runner.wamr # WAMR/iwasm (embedded, 6 exec modes) +├── Dockerfile.runner.wasix # WASIX on Wasmer (POSIX extensions) +│ +├── Dockerfile.runner.riscv-qemu # RISC-V QEMU user-mode emulation +├── Dockerfile.runner.spike # Spike — official RISC-V ISA reference sim +├── Dockerfile.runner.libriscv # libriscv — fastest RISC-V sandbox (3ns calls) +├── Dockerfile.runner.rvvm # RVVM — tracing JIT (experimental) +├── Dockerfile.runner.ckb-vm # CKB-VM — blockchain RISC-V VM (experimental) +│ +└── .build/ # Build artifacts (gitignored) + ├── edgeparse.wasm # WASI Preview 1 binary + ├── edgeparse-riscv64 # RISC-V ELF (dynamic) + └── edgeparse-riscv64-static # RISC-V ELF (static, for VM sandboxes) +``` + +## Quick Start + +```bash +# Build everything and run all tests +./tests/wasm-runtimes/wasm-test.sh build all +./tests/wasm-runtimes/wasm-test.sh test all + +# Or test a single runtime +./tests/wasm-runtimes/wasm-test.sh build wasmtime +./tests/wasm-runtimes/wasm-test.sh test wasmtime + +# Check what's built +./tests/wasm-runtimes/wasm-test.sh status + +# Interactive debugging shell inside a runtime container +./tests/wasm-runtimes/wasm-test.sh run wasmer + +# Clean up everything +./tests/wasm-runtimes/wasm-test.sh clean +``` + +## How It Works + +### Build Phase + +1. **`Dockerfile.build.wasm`** compiles edgeparse-cli for `wasm32-wasip1` using + `--no-default-features` (disables rayon/image/zip — those need native threads). + The result is a ~2-4 MB `.wasm` binary that any WASI Preview 1 runtime can execute. + +2. **`Dockerfile.build.riscv`** cross-compiles for `riscv64gc-unknown-linux-gnu` using + the Debian cross-toolchain (`gcc-riscv64-linux-gnu`). The result is a standard + ELF binary targeting the RV64GC ISA. + +Both build Dockerfiles use the same layer-caching strategy as the production +`docker/Dockerfile`: copy Cargo manifests first, warm the dep cache with a dummy +build, then copy real source. + +### Runner Phase + +All WASM runner Dockerfiles inherit from `edgeparse-wasi-base` (Ubuntu 24.04 with +curl, ca-certificates, test fixtures, and the test script). Each adds its runtime: + +| Runtime | Install Method | Target | Notes | +|------------|----------------------|----------------------|--------------------------------| +| Wasmtime | Official installer | WASI p1 + p2 | Bytecode Alliance reference | +| Wasmer | Official installer | WASI p1 + WASIX | Only runtime with WASIX | +| WasmEdge | Release tarball | WASI p1 (+ P2 WIP) | CNCF, AOT support | +| WAMR | Built from source | WASI p1 | ~100KB, 6 execution modes | +| WASIX | Wasmer (WASIX mode) | WASI p1 (compat) | Tests POSIX runtime compat | +| QEMU | apt package | riscv64gc ELF | User-mode emulation | +| Spike | Built from source | riscv64gc ELF | Official RISC-V ISA reference | +| libriscv | Built from source | riscv64gc ELF | Fastest sandbox (~3ns calls) | +| RVVM | Built from source | riscv64gc ELF | Tracing JIT (experimental) | +| CKB-VM | cargo install | riscv64gc ELF | Blockchain VM (experimental) | + +### Test Phase + +`run-tests.sh` runs inside each container and exercises: + +1. `--help` flag works +2. `--version` returns semver +3. PDF → JSON conversion +4. PDF → Markdown conversion +5. PDF → Text conversion (with content sanity check) +6. PDF → HTML conversion +7. Error handling for non-existent files + +Each runtime has slightly different CLI syntax for preopening directories; +`run-tests.sh` normalizes this via a `build_run_cmd()` function. + +## Docker Image Naming + +All images are prefixed with `edgeparse` (configurable via `EDGEPARSE_PREFIX`): + +* `edgeparse-wasi-build` — WASM build environment +* `edgeparse-riscv-build` — RISC-V cross-compilation environment +* `edgeparse-wasi-base` — Shared runner base +* `edgeparse-wasi-wasmtime` — Wasmtime runner +* `edgeparse-wasi-wasmer` — Wasmer runner +* `edgeparse-wasi-wasmedge` — WasmEdge runner +* `edgeparse-wasi-wamr` — WAMR runner +* `edgeparse-riscv-qemu` — RISC-V QEMU runner + +## CI/CD Integration + +The management script is CI-friendly — no interactive prompts, proper exit codes, +and the `test` command returns non-zero if any runtime fails. Add to GitHub Actions: + +```yaml +- name: WASM Runtime Integration Tests + run: | + ./tests/wasm-runtimes/wasm-test.sh build all + ./tests/wasm-runtimes/wasm-test.sh test all +``` + +## Extending + +To add a new WASM runtime: + +1. Create `Dockerfile.runner.` — inherit `FROM edgeparse-wasi-base` +2. Install the runtime and add it to `PATH` +3. `COPY tests/wasm-runtimes/.build/edgeparse.wasm /test/edgeparse.wasm` +4. Set `CMD [""]` +5. Add a case to `build_run_cmd()` in `run-tests.sh` +6. Add `` to `ALL_RUNTIMES` in `wasm-test.sh` + +## WASI vs WASIX vs Native + +| Feature | WASI (wasip1) | WASIX | Native CLI | +|---------------|---------------|-------------|------------| +| File I/O | Preopened | Full POSIX | Full | +| Parallelism | No (no rayon) | Threads | Full rayon | +| Image extract | No | Possible | Full | +| PDF parsing | Full | Full | Full | +| Binary size | ~2-4 MB | ~4-6 MB | ~15 MB | + +The WASI build disables `native` features (rayon, image, zip) since WASI Preview 1 +doesn't support threads. Core PDF parsing and text extraction work identically. diff --git a/tests/wasm-runtimes/run-tests.sh b/tests/wasm-runtimes/run-tests.sh new file mode 100755 index 0000000..6060e53 --- /dev/null +++ b/tests/wasm-runtimes/run-tests.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# ─── EdgeParse WASM/RISC-V Integration Test Runner ───────────────────────── +# Greg's AI coding buddy: +# This script runs INSIDE the Docker container. It exercises edgeparse +# against test PDFs using whichever runtime the container provides. +# +# Usage (automatic via ENTRYPOINT): +# run-tests.sh wasmtime # test with wasmtime runtime +# run-tests.sh wasmer # test with wasmer runtime +# run-tests.sh wasmedge # test with wasmedge runtime +# run-tests.sh wamr # test with wamr/iwasm runtime +# run-tests.sh riscv-qemu # test with QEMU RISC-V emulation +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +# ── Terminal colours (because we're civilized) ────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +RUNTIME="${1:-unknown}" +PASS=0 +FAIL=0 +SKIP=0 +TESTS=0 +ASSERTIONS=0 + +log_header() { + echo -e "\n${BOLD}${CYAN}═══════════════════════════════════════════════════════════${RESET}" + echo -e "${BOLD}${CYAN} EdgeParse Integration Tests — ${RUNTIME}${RESET}" + echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════════${RESET}\n" +} + +log_test() { + TESTS=$((TESTS + 1)) + echo -e "${DIM}[${TESTS}]${RESET} ${BOLD}$1${RESET}" +} + +log_pass() { + PASS=$((PASS + 1)) + ASSERTIONS=$((ASSERTIONS + 1)) + echo -e " ${GREEN}PASS${RESET} $1" +} + +log_fail() { + FAIL=$((FAIL + 1)) + ASSERTIONS=$((ASSERTIONS + 1)) + echo -e " ${RED}FAIL${RESET} $1" +} + +log_skip() { + SKIP=$((SKIP + 1)) + ASSERTIONS=$((ASSERTIONS + 1)) + echo -e " ${YELLOW}SKIP${RESET} $1" +} + +log_summary() { + echo -e "\n${BOLD}───────────────────────────────────────────────────────────${RESET}" + echo -e "${BOLD}Results:${RESET} ${GREEN}${PASS} passed${RESET}, ${RED}${FAIL} failed${RESET}, ${YELLOW}${SKIP} skipped${RESET} (${ASSERTIONS} assertions across ${TESTS} tests)" + echo -e "${BOLD}Runtime:${RESET} ${RUNTIME}" + echo -e "${BOLD}───────────────────────────────────────────────────────────${RESET}" +} + +# ── Build the runtime command ─────────────────────────────────────────────── +# Each runtime has slightly different CLI syntax. We normalize here. +build_run_cmd() { + local wasm_or_bin="$1" + shift + # NOTE: "--" handling is runtime-specific: + # - wasmtime/wasmedge/wamr: do NOT use "--" (they forward it into argv, + # causing clap to treat subsequent args as positional values) + # - wasmer/wasix: MUST use "--" (wasmer intercepts flags like -f/-h) + # - libriscv: MUST use "--" (rvlinux intercepts -f for fuel, -h for help) + # - ckb-vm: MUST use "--" (ckb-debugger has its own flags) + case "${RUNTIME}" in + wasmtime) + # wasmtime: trailing args after .wasm are passed to the program; + # do NOT use "--" as wasmtime forwards it into argv + echo "wasmtime run --dir / ${wasm_or_bin} $*" + ;; + wasmer) + # wasmer v7+: uses --volume (--dir is deprecated); + # requires "--" to separate wasmer flags from wasm args + echo "wasmer run --volume /test:/test ${wasm_or_bin} -- $*" + ;; + wasmedge) + # wasmedge: --dir guest_path:host_path + echo "wasmedge --dir /:/ ${wasm_or_bin} $*" + ;; + wamr) + # iwasm: --dir=path preopens a directory + echo "iwasm --dir=/ ${wasm_or_bin} $*" + ;; + wasix) + # WASIX on Wasmer: same as wasmer but with WASIX binary + echo "wasmer run --volume /test:/test ${wasm_or_bin} -- $*" + ;; + riscv-qemu) + # RISC-V: direct execution under QEMU user-mode (native binary) + echo "qemu-riscv64 ${wasm_or_bin} $*" + ;; + spike) + # Spike + pk: RISC-V ISA reference simulator with proxy kernel + echo "spike pk ${wasm_or_bin} $*" + ;; + libriscv) + # libriscv rvlinux: fastest RISC-V sandbox, Linux syscall emulation + # rvlinux intercepts -f (fuel) and -h (help), so use -- to separate + echo "rvlinux ${wasm_or_bin} -- $*" + ;; + rvvm) + # RVVM: tracing JIT RISC-V emulator, userland mode + echo "rvvm-userland ${wasm_or_bin} $*" + ;; + ckb-vm) + # CKB-VM: blockchain RISC-V VM (limited syscall support) + echo "ckb-debugger --bin ${wasm_or_bin} -- $*" + ;; + *) + echo "echo 'Unknown runtime: ${RUNTIME}' && false" + ;; + esac +} + +# ── Determine binary path ────────────────────────────────────────────────── +case "${RUNTIME}" in + riscv-qemu|spike|libriscv|rvvm|ckb-vm) + BINARY="/test/edgeparse-riscv64" + ;; + *) + BINARY="/test/edgeparse.wasm" + ;; +esac + +# ── Pre-flight checks ────────────────────────────────────────────────────── +log_header + +echo -e "${DIM}Binary: ${BINARY}${RESET}" +echo -e "${DIM}Size: $(du -h "${BINARY}" 2>/dev/null | cut -f1 || echo 'N/A')${RESET}" +echo -e "${DIM}Type: $(file "${BINARY}" 2>/dev/null || echo 'N/A')${RESET}" + +if [ ! -f "${BINARY}" ]; then + echo -e "\n${RED}ERROR: Binary not found at ${BINARY}${RESET}" + exit 1 +fi + +if [ ! -f "/test/fixtures/sample.pdf" ]; then + echo -e "\n${RED}ERROR: Test fixture not found at /test/fixtures/sample.pdf${RESET}" + exit 1 +fi + +mkdir -p /test/output + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 1: --help flag works +# ───────────────────────────────────────────────────────────────────────────── +log_test "CLI --help flag" +run_cmd=$(build_run_cmd "${BINARY}" "--help") +if eval "${run_cmd}" > /test/output/help.txt 2>&1; then + if grep -qi "edgeparse" /test/output/help.txt; then + log_pass "Help output contains 'edgeparse'" + else + log_fail "Help output doesn't mention 'edgeparse'" + fi +else + log_fail "Exit code non-zero: ${run_cmd}" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 2: --version flag works +# ───────────────────────────────────────────────────────────────────────────── +log_test "CLI --version flag" +run_cmd=$(build_run_cmd "${BINARY}" "--version") +if eval "${run_cmd}" > /test/output/version.txt 2>&1; then + if grep -qE '[0-9]+\.[0-9]+\.[0-9]+' /test/output/version.txt; then + version=$(cat /test/output/version.txt) + log_pass "Version: ${version}" + else + log_fail "Version output doesn't match semver pattern" + fi +else + log_fail "Exit code non-zero" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 3: Convert sample PDF to JSON +# ───────────────────────────────────────────────────────────────────────────── +log_test "Convert sample.pdf → JSON" +rm -f /test/output/sample.json +run_cmd=$(build_run_cmd "${BINARY}" "-f json -o /test/output -q /test/fixtures/sample.pdf") +if eval "${run_cmd}" > /test/output/json_stdout.txt 2>&1; then + if [ -f "/test/output/sample.json" ]; then + json_size=$(wc -c < /test/output/sample.json) + if [ "${json_size}" -gt 10 ]; then + log_pass "JSON output: ${json_size} bytes" + else + log_fail "JSON output too small: ${json_size} bytes" + fi + else + log_fail "Expected /test/output/sample.json not created" + fi +else + log_fail "Conversion failed (exit=$?)" + cat /test/output/json_stdout.txt 2>/dev/null || true +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 4: Convert sample PDF to Markdown +# ───────────────────────────────────────────────────────────────────────────── +log_test "Convert sample.pdf → Markdown" +# Clean previous output +rm -f /test/output/sample.md +run_cmd=$(build_run_cmd "${BINARY}" "-f markdown -o /test/output -q /test/fixtures/sample.pdf") +if eval "${run_cmd}" > /test/output/md_stdout.txt 2>&1; then + if [ -f "/test/output/sample.md" ]; then + md_size=$(wc -c < /test/output/sample.md) + if [ "${md_size}" -gt 5 ]; then + log_pass "Markdown output: ${md_size} bytes" + else + log_fail "Markdown output too small: ${md_size} bytes" + fi + else + log_fail "Expected /test/output/sample.md not created" + fi +else + log_fail "Conversion failed (exit=$?)" + cat /test/output/md_stdout.txt 2>/dev/null || true +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 5: Convert sample PDF to plain text +# ───────────────────────────────────────────────────────────────────────────── +log_test "Convert sample.pdf → Text" +rm -f /test/output/sample.txt +run_cmd=$(build_run_cmd "${BINARY}" "-f text -o /test/output -q /test/fixtures/sample.pdf") +if eval "${run_cmd}" > /test/output/txt_stdout.txt 2>&1; then + if [ -f "/test/output/sample.txt" ]; then + txt_size=$(wc -c < /test/output/sample.txt) + if [ "${txt_size}" -gt 5 ]; then + log_pass "Text output: ${txt_size} bytes" + # Bonus: check content looks like our test PDF + if grep -qi "Hello\|EdgePDF\|test" /test/output/sample.txt 2>/dev/null; then + log_pass "Content sanity check — found expected text" + else + log_fail "Content sanity check — expected text not found" + fi + else + log_fail "Text output too small: ${txt_size} bytes" + fi + else + log_fail "Expected /test/output/sample.txt not created" + fi +else + log_fail "Conversion failed (exit=$?)" + cat /test/output/txt_stdout.txt 2>/dev/null || true +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 6: Convert to HTML +# ───────────────────────────────────────────────────────────────────────────── +log_test "Convert sample.pdf → HTML" +rm -f /test/output/sample.html +run_cmd=$(build_run_cmd "${BINARY}" "-f html -o /test/output -q /test/fixtures/sample.pdf") +if eval "${run_cmd}" > /test/output/html_stdout.txt 2>&1; then + if [ -f "/test/output/sample.html" ]; then + html_size=$(wc -c < /test/output/sample.html) + log_pass "HTML output: ${html_size} bytes" + else + log_fail "Expected /test/output/sample.html not created" + fi +else + log_fail "Conversion failed (exit=$?)" +fi + +# ───────────────────────────────────────────────────────────────────────────── +# TEST 7: Bad input (non-existent file) returns error +# ───────────────────────────────────────────────────────────────────────────── +log_test "Error handling: non-existent file" +run_cmd=$(build_run_cmd "${BINARY}" "-q /test/fixtures/does_not_exist.pdf") +if eval "${run_cmd}" > /test/output/err_stdout.txt 2>&1; then + log_fail "Should have returned non-zero exit code for missing file" +else + log_pass "Correctly returned error for non-existent file" +fi + +# ── Summary ───────────────────────────────────────────────────────────────── +log_summary + +if [ "${FAIL}" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/tests/wasm-runtimes/wasm-test.sh b/tests/wasm-runtimes/wasm-test.sh new file mode 100755 index 0000000..8e1c9af --- /dev/null +++ b/tests/wasm-runtimes/wasm-test.sh @@ -0,0 +1,464 @@ +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════════════════════════════ +# EdgeParse WASM/RISC-V Integration Test Manager +# ═══════════════════════════════════════════════════════════════════════════════ +# Greg's AI coding buddy: +# One script to rule them all. Builds, runs, and manages Docker-based +# integration tests for edgeparse across multiple WASM runtimes and RISC-V. +# +# All Docker artifacts are prefixed with EDGEPARSE_PREFIX (default: "edgeparse") +# to avoid collisions. Override via environment: +# EDGEPARSE_PREFIX=myproject ./wasm-test.sh build all +# +# Usage: +# ./wasm-test.sh build [all|wasm|riscv|base|wasmtime|wasmer|wasmedge|wamr|wasix|riscv-qemu|spike|libriscv|rvvm|ckb-vm] +# ./wasm-test.sh test [all|experimental|wasmtime|wasmer|wasmedge|wamr|wasix|riscv-qemu|spike|libriscv|rvvm|ckb-vm] +# ./wasm-test.sh status +# ./wasm-test.sh run +# ./wasm-test.sh log +# ./wasm-test.sh rmi [all|] +# ./wasm-test.sh clean +# ./wasm-test.sh help +# ═══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +# ── Config ──────────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BUILD_DIR="${SCRIPT_DIR}/.build" + +# Docker artifact prefix — override with EDGEPARSE_PREFIX env var +PREFIX="${EDGEPARSE_PREFIX:-edgeparse}" + +# Image names — WASM +IMG_BUILD_WASM="${PREFIX}-wasi-build" +IMG_BUILD_WASIX="${PREFIX}-wasix-build" +IMG_BASE="${PREFIX}-wasi-base" +IMG_WASMTIME="${PREFIX}-wasi-wasmtime" +IMG_WASMER="${PREFIX}-wasi-wasmer" +IMG_WASMEDGE="${PREFIX}-wasi-wasmedge" +IMG_WAMR="${PREFIX}-wasi-wamr" +IMG_WASIX="${PREFIX}-wasi-wasix" + +# Image names — RISC-V +IMG_BUILD_RISCV="${PREFIX}-riscv-build" +IMG_RISCV_QEMU="${PREFIX}-riscv-qemu" +IMG_SPIKE="${PREFIX}-riscv-spike" +IMG_LIBRISCV="${PREFIX}-riscv-libriscv" +IMG_RVVM="${PREFIX}-riscv-rvvm" +IMG_CKB_VM="${PREFIX}-riscv-ckb-vm" + +# Stable runtimes — all tests pass (8/8 green) +STABLE_WASM_RUNTIMES="wasmtime wasmer wasmedge wamr wasix" +STABLE_RISCV_RUNTIMES="riscv-qemu" +STABLE_RUNNERS="${STABLE_WASM_RUNTIMES} ${STABLE_RISCV_RUNTIMES}" + +# Experimental runtimes — partial/no test pass (WIP, incompatible, or upstream broken) +EXPERIMENTAL_RUNTIMES="spike libriscv rvvm ckb-vm" + +# Combined (used for build/rmi/status, NOT for default test) +ALL_RUNNERS="${STABLE_RUNNERS} ${EXPERIMENTAL_RUNTIMES}" +ALL_IMAGES="${IMG_BUILD_WASM} ${IMG_BUILD_WASIX} ${IMG_BUILD_RISCV} ${IMG_BASE} ${IMG_WASMTIME} ${IMG_WASMER} ${IMG_WASMEDGE} ${IMG_WAMR} ${IMG_WASIX} ${IMG_RISCV_QEMU} ${IMG_SPIKE} ${IMG_LIBRISCV} ${IMG_RVVM} ${IMG_CKB_VM}" + +# ── Colours ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +log() { echo -e "${BOLD}${GREEN} >>>${RESET} $*"; } +warn() { echo -e "${BOLD}${YELLOW} !!${RESET} $*"; } +err() { echo -e "${BOLD}${RED} ✖${RESET} $*" >&2; } +dim() { echo -e "${DIM}$*${RESET}"; } + +# ── Helpers ─────────────────────────────────────────────────────────────────── +image_name_for() { + case "$1" in + wasm) echo "${IMG_BUILD_WASM}" ;; + wasix) echo "${IMG_WASIX}" ;; + wasix-build) echo "${IMG_BUILD_WASIX}" ;; + riscv) echo "${IMG_BUILD_RISCV}" ;; + base) echo "${IMG_BASE}" ;; + wasmtime) echo "${IMG_WASMTIME}" ;; + wasmer) echo "${IMG_WASMER}" ;; + wasmedge) echo "${IMG_WASMEDGE}" ;; + wamr) echo "${IMG_WAMR}" ;; + riscv-qemu) echo "${IMG_RISCV_QEMU}" ;; + spike) echo "${IMG_SPIKE}" ;; + libriscv) echo "${IMG_LIBRISCV}" ;; + rvvm) echo "${IMG_RVVM}" ;; + ckb-vm) echo "${IMG_CKB_VM}" ;; + *) err "Unknown target: $1"; exit 1 ;; + esac +} + +image_exists() { + docker image inspect "$1" &>/dev/null +} + +ensure_build_dir() { + mkdir -p "${BUILD_DIR}" +} + +# ── Build Commands ──────────────────────────────────────────────────────────── +build_wasm() { + log "Building WASM binary (wasm32-wasip1)..." + ensure_build_dir + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.build.wasm" \ + -t "${IMG_BUILD_WASM}" \ + "${REPO_ROOT}" + + log "Extracting edgeparse.wasm..." + local cid + cid=$(docker create "${IMG_BUILD_WASM}" /bin/true 2>/dev/null || docker create "${IMG_BUILD_WASM}" true) + docker cp "${cid}:/out/edgeparse.wasm" "${BUILD_DIR}/edgeparse.wasm" + docker rm "${cid}" > /dev/null + log "WASM binary: ${BUILD_DIR}/edgeparse.wasm ($(du -h "${BUILD_DIR}/edgeparse.wasm" | cut -f1))" +} + +build_riscv() { + log "Building RISC-V binaries (riscv64gc-unknown-linux-gnu)..." + ensure_build_dir + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.build.riscv" \ + -t "${IMG_BUILD_RISCV}" \ + "${REPO_ROOT}" + + log "Extracting RISC-V binaries (dynamic + static)..." + local cid + cid=$(docker create "${IMG_BUILD_RISCV}" /bin/true 2>/dev/null || docker create "${IMG_BUILD_RISCV}" true) + docker cp "${cid}:/out/edgeparse" "${BUILD_DIR}/edgeparse-riscv64" + docker cp "${cid}:/out/edgeparse-static" "${BUILD_DIR}/edgeparse-riscv64-static" + docker rm "${cid}" > /dev/null + log "RISC-V dynamic: ${BUILD_DIR}/edgeparse-riscv64 ($(du -h "${BUILD_DIR}/edgeparse-riscv64" | cut -f1))" + log "RISC-V static: ${BUILD_DIR}/edgeparse-riscv64-static ($(du -h "${BUILD_DIR}/edgeparse-riscv64-static" | cut -f1))" +} + +build_wasix() { + log "Building WASIX binary (wasm32-wasmer-wasi)..." + ensure_build_dir + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.build.wasix" \ + -t "${IMG_BUILD_WASIX}" \ + "${REPO_ROOT}" + + log "Extracting edgeparse-wasix.wasm..." + local cid + cid=$(docker create "${IMG_BUILD_WASIX}" /bin/true 2>/dev/null || docker create "${IMG_BUILD_WASIX}" true) + docker cp "${cid}:/out/edgeparse-wasix.wasm" "${BUILD_DIR}/edgeparse-wasix.wasm" + docker rm "${cid}" > /dev/null + log "WASIX binary: ${BUILD_DIR}/edgeparse-wasix.wasm ($(du -h "${BUILD_DIR}/edgeparse-wasix.wasm" | cut -f1))" +} + +build_base() { + log "Building shared runner base image..." + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.runner.base" \ + -t "${IMG_BASE}" \ + "${REPO_ROOT}" +} + +build_runner() { + local runtime="$1" + local img + img=$(image_name_for "${runtime}") + + # Ensure prerequisites based on runtime type + case "${runtime}" in + wasmtime|wasmer|wasmedge|wamr) + [ -f "${BUILD_DIR}/edgeparse.wasm" ] || build_wasm + image_exists "${IMG_BASE}" || build_base + ;; + wasix) + # WASIX runner uses standard WASI binary (backward compat) + [ -f "${BUILD_DIR}/edgeparse.wasm" ] || build_wasm + image_exists "${IMG_BASE}" || build_base + ;; + riscv-qemu) + [ -f "${BUILD_DIR}/edgeparse-riscv64" ] || build_riscv + ;; + spike|libriscv|rvvm|ckb-vm) + [ -f "${BUILD_DIR}/edgeparse-riscv64-static" ] || build_riscv + ;; + esac + + log "Building ${runtime} runner image..." + docker build \ + -f "${SCRIPT_DIR}/Dockerfile.runner.${runtime}" \ + -t "${img}" \ + "${REPO_ROOT}" +} + +cmd_build() { + local target="${1:-all}" + case "${target}" in + all) + build_wasm + build_riscv + build_base + for rt in ${ALL_RUNNERS}; do + build_runner "${rt}" + done + ;; + wasm) build_wasm ;; + wasix-build) build_wasix ;; + riscv) build_riscv ;; + base) build_base ;; + wasmtime|wasmer|wasmedge|wamr|wasix|riscv-qemu|spike|libriscv|rvvm|ckb-vm) + build_runner "${target}" + ;; + *) + err "Unknown build target: ${target}" + echo "Valid: all wasm wasix-build riscv base wasmtime wasmer wasmedge wamr wasix riscv-qemu spike libriscv rvvm ckb-vm" + exit 1 + ;; + esac +} + +# ── Test Commands ───────────────────────────────────────────────────────────── +run_test() { + local runtime="$1" + local img + img=$(image_name_for "${runtime}") + + if ! image_exists "${img}"; then + warn "Image ${img} not found, building first..." + build_runner "${runtime}" + fi + + log "Testing with ${runtime}..." + echo "" + # Capture exit code without triggering set -e (which would abort the + # entire script on first container failure, preventing test aggregation) + local exit_code=0 + if docker run --rm \ + --name "${PREFIX}-test-${runtime}" \ + "${img}" \ + "${runtime}"; then + exit_code=0 + else + exit_code=$? + fi + echo "" + return ${exit_code} +} + +cmd_test() { + local target="${1:-all}" + local failed=0 + local passed=0 + local targets + + case "${target}" in + all) targets="${STABLE_RUNNERS}" ;; + experimental) targets="${EXPERIMENTAL_RUNTIMES}" ;; + *) targets="${target}" ;; + esac + + for rt in ${targets}; do + if run_test "${rt}"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + warn "${rt}: SOME TESTS FAILED" + fi + done + + echo "" + echo -e "${BOLD}═══════════════════════════════════════════════════════════${RESET}" + echo -e "${BOLD} Overall: ${GREEN}${passed} runtimes passed${RESET}, ${RED}${failed} runtimes failed${RESET}" + echo -e "${BOLD}═══════════════════════════════════════════════════════════${RESET}" + + [ "${failed}" -eq 0 ] +} + +# ── Status ──────────────────────────────────────────────────────────────────── +cmd_status() { + echo -e "${BOLD}EdgeParse WASM/RISC-V Test Infrastructure${RESET}\n" + + echo -e "${BOLD}Docker Images:${RESET}" + for img in ${ALL_IMAGES}; do + if image_exists "${img}"; then + local size + size=$(docker image inspect "${img}" --format='{{.Size}}' 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "?") + echo -e " ${GREEN}●${RESET} ${img} (${size})" + else + echo -e " ${DIM}○ ${img} (not built)${RESET}" + fi + done + + echo -e "\n${BOLD}Build Artifacts:${RESET}" + if [ -f "${BUILD_DIR}/edgeparse.wasm" ]; then + echo -e " ${GREEN}●${RESET} edgeparse.wasm ($(du -h "${BUILD_DIR}/edgeparse.wasm" | cut -f1))" + else + echo -e " ${DIM}○ edgeparse.wasm (not built)${RESET}" + fi + if [ -f "${BUILD_DIR}/edgeparse-riscv64" ]; then + echo -e " ${GREEN}●${RESET} edgeparse-riscv64 ($(du -h "${BUILD_DIR}/edgeparse-riscv64" | cut -f1))" + else + echo -e " ${DIM}○ edgeparse-riscv64 (not built)${RESET}" + fi + + echo -e "\n${BOLD}Running Containers:${RESET}" + local running + running=$(docker ps --filter "name=${PREFIX}-test-" --format '{{.Names}} ({{.Status}})' 2>/dev/null) + if [ -n "${running}" ]; then + echo "${running}" | while read -r line; do + echo -e " ${CYAN}▶${RESET} ${line}" + done + else + echo -e " ${DIM}(none)${RESET}" + fi +} + +# ── Log ─────────────────────────────────────────────────────────────────────── +cmd_log() { + local runtime="${1:?Usage: wasm-test.sh log }" + local container="${PREFIX}-test-${runtime}" + docker logs "${container}" 2>&1 || err "No logs for ${container}" +} + +# ── Cleanup ─────────────────────────────────────────────────────────────────── +cmd_rmi() { + local target="${1:-all}" + if [ "${target}" = "all" ]; then + log "Removing all edgeparse test images..." + for img in ${ALL_IMAGES}; do + docker rmi -f "${img}" 2>/dev/null && dim " removed ${img}" || true + done + else + local img + img=$(image_name_for "${target}") + docker rmi -f "${img}" 2>/dev/null && dim " removed ${img}" || warn "Image ${img} not found" + fi +} + +cmd_clean() { + log "Cleaning build artifacts and images..." + + # Stop and remove any running test containers + # Note: avoid xargs -r (GNU-only, fails on macOS/BSD) + local containers + containers=$(docker ps -q --filter "name=${PREFIX}-test-" 2>/dev/null) + [ -n "${containers}" ] && docker rm -f ${containers} 2>/dev/null || true + + # Remove images + cmd_rmi all + + # Remove build directory + rm -rf "${BUILD_DIR}" + + log "Clean complete." +} + +# ── Run (interactive shell in container) ────────────────────────────────────── +cmd_run() { + local runtime="${1:?Usage: wasm-test.sh run }" + local img + img=$(image_name_for "${runtime}") + + if ! image_exists "${img}"; then + warn "Image ${img} not found, building first..." + build_runner "${runtime}" + fi + + log "Launching interactive shell in ${runtime} container..." + docker run --rm -it \ + --name "${PREFIX}-run-${runtime}" \ + --entrypoint /bin/bash \ + "${img}" +} + +# ── Help ────────────────────────────────────────────────────────────────────── +cmd_help() { + cat <<'BANNER' + ┌─────────────────────────────────────────────────────────┐ + │ EdgeParse WASM/RISC-V Integration Test Manager │ + │ Greg's AI coding buddy reporting for duty! o7 │ + └─────────────────────────────────────────────────────────┘ +BANNER + echo "" + echo -e "${BOLD}Usage:${RESET} $(basename "$0") [target]" + echo "" + echo -e "${BOLD}Commands:${RESET}" + echo " build [target] Build Docker images and binaries" + echo " test [target] Run integration tests" + echo " status Show image/container status" + echo " run Launch interactive shell in container" + echo " log Show container logs" + echo " rmi [target] Remove Docker images" + echo " clean Remove everything (images + artifacts)" + echo " help This help screen" + echo "" + echo -e "${BOLD}Build Targets:${RESET}" + echo " all Build everything (default)" + echo " wasm Build WASM binary only (wasm32-wasip1)" + echo " wasix-build Build WASIX binary (wasm32-wasmer-wasi)" + echo " riscv Build RISC-V binary only (riscv64gc)" + echo " base Build shared runner base image" + echo " wasmtime Build Wasmtime runner" + echo " wasmer Build Wasmer runner" + echo " wasmedge Build WasmEdge runner" + echo " wamr Build WAMR/iwasm runner" + echo " wasix Build WASIX runner" + echo " riscv-qemu Build RISC-V QEMU runner" + echo " spike Build Spike + pk runner" + echo " libriscv Build libriscv runner" + echo " rvvm Build RVVM runner (experimental)" + echo " ckb-vm Build CKB-VM runner (experimental)" + echo "" + echo -e "${BOLD}Test Targets:${RESET}" + echo " all Test stable runtimes only (default)" + echo " experimental Test experimental/WIP runtimes" + echo " wasmtime Test with Wasmtime" + echo " wasmer Test with Wasmer" + echo " wasmedge Test with WasmEdge" + echo " wamr Test with WAMR/iwasm" + echo " wasix Test with WASIX on Wasmer" + echo " riscv-qemu Test with RISC-V QEMU" + echo " spike Test with Spike + pk (WIP)" + echo " libriscv Test with libriscv (WIP)" + echo " rvvm Test with RVVM (incompatible)" + echo " ckb-vm Test with CKB-VM (upstream broken)" + echo "" + echo -e "${BOLD}Environment:${RESET}" + echo " EDGEPARSE_PREFIX Docker artifact prefix (default: edgeparse)" + echo "" + echo -e "${BOLD}Examples:${RESET}" + echo " $(basename "$0") build all # build everything" + echo " $(basename "$0") test wasmtime # test with wasmtime only" + echo " $(basename "$0") test all # test all runtimes" + echo " $(basename "$0") run wasmer # interactive shell in wasmer" + echo " $(basename "$0") status # check what's built" + echo " $(basename "$0") clean # nuke everything" +} + +# ── Main Dispatch ───────────────────────────────────────────────────────────── +main() { + local cmd="${1:-help}" + shift || true + + case "${cmd}" in + build) cmd_build "$@" ;; + test) cmd_test "$@" ;; + status) cmd_status ;; + run) cmd_run "$@" ;; + log) cmd_log "$@" ;; + rmi) cmd_rmi "$@" ;; + clean) cmd_clean ;; + help|-h|--help) + cmd_help + ;; + *) + err "Unknown command: ${cmd}" + cmd_help + exit 1 + ;; + esac +} + +main "$@"