From c858bc4f843369795e26735ff7558aa12dc2d30c Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Tue, 11 Aug 2026 11:05:46 -0400 Subject: [PATCH] CLI: migrate to clap; add wizen, compose-runner, run subcommands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composition/execution half of the CLI's charter (#13) plus #85's wizen productization, in one move because all three need the same dependency step change (the CLI now embeds wasmtime). clap 4 derive replaces the hand-rolled parser: lock/fold/aggregate/ emit/pins keep their flags, semantics, and verdict exit codes; usage errors now exit 2 with clap's messages (previously 1 via bail). pins keeps its check-vs-bump split (args_conflicts_with_subcommands + subcommand_negates_reqs); every -o gains a --output long form. wizen (alias wizer): wizer pre-initialization as a first-class subcommand (#85). The mechanism moves into component-test-runner as wizen/wizen_with (feature `wizer`), mirroring Runner::new/with_data so SUT-importing suites reuse the linker setup they already have — instantiation is eager, so the shipped driver alone cannot wizen them. The wizer-preinit bin is subsumed and deleted. The docs carry the non-negotiable caveats: the snapshot freezes what init observed (env, entropy, clocks), and the wizened artifact must be used everywhere downstream. compose-runner: the examples/compose topology as a library (wac-graph 0.10) — one shared provider instance feeding the suite's test-context and the runner core's factory/test-context, which is exactly what wac plug cannot express. Inputs classify by exports: bare suites get the provider; bundles (tests + test-context + factory re-exported) skip it; partial bundles and already-composed components are rejected by name. Defaults are size-optimized `embed`-profile builds of runner-cli and provider committed under crates/component-test-cli/embedded/ (118KB + 68KB), overridable via --runner/--provider; regenerate with `just embed-update`. Freshness is gated behaviorally — verify-cli diffs their output against the Path-2 goldens; a byte-compare gate would violate #44's no-cross-environment-reproducibility rule. run: compose-runner + execution in one step. A minimal wasmtime embedding equivalent to `wasmtime run -W component-model-async -S p3`: WASI p2 (suite/provider std imports) + p3 (the runner core's stdout and run entry point), Command::instantiate_async + run_concurrent, I32Exit downcast, exit code from the guest. Env is explicit like wasmtime run's; --jsonl sets COMPONENT_TEST_JSONL=1 for the wire format. verify-cli joins `just all` (so CI gates it): compose-runner's output under the wasmtime CLI, run's human + JSONL legs against the existing Path-2 goldens byte-for-byte, and wizen legs proving finding #22 stays true through the productized path — the wizened sample suite passes lock --check, reproduces the host-embed golden, and composes + runs identically. New wasm-gated integration tests cover the same from cargo (`just test-wasm`). Verified: just check, just all (full matrix incl. verify-cli), just test-wasm; manual legs for bundle input, pre-composed input, wizer alias, overrides, and rejection messages. Also verified along the way: a wizened suite composes and runs under wac-graph composition — wizer output stays a plain component (the "nested components" limit applies to wizer inputs, not outputs). --- .gitignore | 3 + AGENTS.md | 8 +- Cargo.lock | 264 +++++++++- Cargo.toml | 18 + README.md | 4 +- components/sample-suite/README.md | 23 + crates/component-test-cli/Cargo.toml | 10 + .../component-test-cli/embedded/provider.wasm | Bin 0 -> 69163 bytes .../embedded/runner-cli.wasm | Bin 0 -> 118058 bytes crates/component-test-cli/src/compose.rs | 229 +++++++++ crates/component-test-cli/src/main.rs | 462 +++++++++++------- crates/component-test-cli/src/pins.rs | 127 ++--- crates/component-test-cli/src/run.rs | 81 +++ crates/component-test-cli/tests/cli.rs | 142 +++++- crates/component-test-runner/Cargo.toml | 14 +- .../src/bin/wizer-preinit.rs | 118 ----- crates/component-test-runner/src/lib.rs | 7 + crates/component-test-runner/src/wizen.rs | 138 ++++++ docs/findings.md | 4 +- examples/compose/README.md | 7 + justfile | 51 +- 21 files changed, 1342 insertions(+), 368 deletions(-) create mode 100644 crates/component-test-cli/embedded/provider.wasm create mode 100644 crates/component-test-cli/embedded/runner-cli.wasm create mode 100644 crates/component-test-cli/src/compose.rs create mode 100644 crates/component-test-cli/src/run.rs delete mode 100644 crates/component-test-runner/src/bin/wizer-preinit.rs create mode 100644 crates/component-test-runner/src/wizen.rs diff --git a/.gitignore b/.gitignore index 5db6562..d03acac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ target/ *.wasm +# The components baked into the CLI are committed (regenerate with +# `just embed-update`; `just verify-cli` gates their behavior). +!crates/component-test-cli/embedded/*.wasm node_modules/ # Prebuilt CLI/runner binaries for downstream consumers (their build diff --git a/AGENTS.md b/AGENTS.md index 6923360..2e1895f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,13 @@ crates/ host-side Rust (tested natively at root) component-test-results canonical results schema + fold (guest-linkable) component-test-sdk guest SDK (registry, prelude, verdict ergonomics) component-test-sdk-macro #[suite] attribute (case discovery, tags section) - component-test-cli `component-test` bin: lock, fold, aggregate + component-test-cli `component-test` bin: lock, fold, aggregate, + pins, wizen, compose-runner, run (the last + three embed wasmtime/wac-graph plus + size-optimized runner-cli + provider builds + from embedded/ — regenerate those with + `just embed-update`, commit the diff; + `just verify-cli` gates their behavior) component-test-runner wasmtime host-embed runner (`ct-runner` bin) components/ guest components (build with --target wasm32-wasip2) provider reference context provider diff --git a/Cargo.lock b/Cargo.lock index c16fa79..54faba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "allocator-api2" version = "0.2.21" @@ -23,6 +29,56 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -55,6 +111,18 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "auditable-serde" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d026218ae25ba5c72834245412dd1338f6d270d2c5109ee03a4badec288d4056" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + [[package]] name = "base64" version = "0.22.1" @@ -175,6 +243,46 @@ dependencies = [ "rand_core", ] +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cobs" version = "0.3.0" @@ -184,15 +292,26 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "component-test-cli" version = "0.1.0" dependencies = [ "anyhow", + "clap", "component-test-core", "component-test-formats", + "component-test-runner", "serde_json", "toml 0.9.12+spec-1.1.0", + "wac-graph", + "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -610,6 +729,16 @@ dependencies = [ "component-test-sdk", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -990,6 +1119,12 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1174,6 +1309,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -1203,6 +1348,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1522,6 +1673,12 @@ 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 = "slab" version = "0.4.12" @@ -1547,12 +1704,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spdx" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081670c233dfbed55690cc0cd38424e0e24ac1b2673d0b408b3f7b684738dfa9" +dependencies = [ + "smallvec", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.119" @@ -1747,6 +1919,12 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tracing" version = "0.1.44" @@ -1829,6 +2007,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.24.0" @@ -1855,6 +2039,39 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wac-graph" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc8c5ffe944e002a24ba9d0fa67f51d4a761c1fc97718a17d1d7379753a6f8e" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "petgraph", + "semver", + "thiserror 1.0.69", + "wac-types", + "wasm-encoder 0.247.0", + "wasm-metadata 0.247.0", + "wasmparser 0.247.0", +] + +[[package]] +name = "wac-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b90654c08294d879e85f0b8b2c5a7efc3a7a10df3009dd1578d98902d1ec8a2" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "semver", + "wasm-encoder 0.247.0", + "wasmparser 0.247.0", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1923,6 +2140,16 @@ dependencies = [ "wat", ] +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser 0.247.0", +] + [[package]] name = "wasm-encoder" version = "0.252.0" @@ -1953,6 +2180,25 @@ dependencies = [ "wasmparser 0.255.0", ] +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder 0.247.0", + "wasmparser 0.247.0", +] + [[package]] name = "wasm-metadata" version = "0.254.0" @@ -1990,6 +2236,19 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + [[package]] name = "wasmparser" version = "0.252.0" @@ -2047,6 +2306,7 @@ dependencies = [ "async-trait", "bitflags", "bumpalo", + "bytes", "cc", "cfg-if", "encoding_rs", @@ -2649,7 +2909,7 @@ dependencies = [ "indexmap", "prettyplease", "syn 2.0.119", - "wasm-metadata", + "wasm-metadata 0.254.0", "wit-bindgen-core", "wit-component", ] @@ -2684,7 +2944,7 @@ dependencies = [ "serde_derive", "serde_json", "wasm-encoder 0.254.0", - "wasm-metadata", + "wasm-metadata 0.254.0", "wasmparser 0.254.0", "wit-parser 0.254.0", ] diff --git a/Cargo.toml b/Cargo.toml index 15eb1a7..da9d7d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ component-test-formats = { path = "crates/component-test-formats" } component-test-results = { path = "crates/component-test-results" } component-test-sdk = { path = "crates/component-test-sdk" } component-test-sdk-macro = { path = "crates/component-test-sdk-macro" } +component-test-runner = { path = "crates/component-test-runner" } wit-bindgen = "0.60" futures = "0.3" serde = { version = "1", features = ["derive"] } @@ -53,7 +54,24 @@ toml = "0.9" tokio = { version = "1", default-features = false } anyhow = "1" trybuild = "1" +clap = { version = "4", features = ["derive"] } +# One wasmtime everywhere: the host-embed runner and the CLI's `run` +# embedding must agree (and stay in step with the wasmtime CLI the +# verify recipes drive). +wasmtime = { version = "47", features = ["component-model-async"] } +wasmtime-wasi = { version = "47", features = ["p3"] } +# Composition as a library (the CLI's compose-runner/run): same +# release line as the wac CLI the compose examples document. +wac-graph = "0.10" [profile.release] opt-level = "s" strip = true + +# The components baked into the CLI (`just embed-update`): size over +# speed — they ship inside every `component-test` binary. +[profile.embed] +inherits = "release" +opt-level = "z" +lto = true +codegen-units = 1 diff --git a/README.md b/README.md index 1a7a3bb..4ed0242 100644 --- a/README.md +++ b/README.md @@ -269,4 +269,6 @@ WPT, `go test`, LLVM lit) is tracked in the issues. viewer. - Interop emitters: JUnit XML, TAP, GitHub Actions annotations. - Reusable GitHub Actions workflows. -- A `component-test` CLI wrapping composition, execution, and aggregation. +- A `component-test` CLI wrapping composition, execution, and aggregation + (`compose-runner`/`run` embed the reference provider + wasi:cli runner + core), plus wizer pre-initialization for large suites (`wizen`). diff --git a/components/sample-suite/README.md b/components/sample-suite/README.md index bbcb54b..7c3564d 100644 --- a/components/sample-suite/README.md +++ b/components/sample-suite/README.md @@ -63,3 +63,26 @@ ct-runner target/wasm32-wasip2/release/my_suite.wasm # or --jsonl Commit `tests.lock` and regenerate it (`lock ... -o`) after any case change: the diff is the review surface, and runners/aggregation cross-check the inventory against what the suite actually enumerates. + +## Large suites + +Enumeration builds the whole registry per fresh instance, so +instance-per-case isolation gets expensive at scale (10k cases ≈ 3ms +per instance). Instead of relaxing isolation, pre-initialize the +artifact once after building: + +```sh +component-test wizen target/wasm32-wasip2/release/my_suite.wasm -o my_suite.wasm +``` + +The suite needs no changes (the contract's own `all()` is the init +function), inventory and tag scheduling survive, and every fresh +instance is born with the registry built — a 10k-case synthetic's +K=1 run went 30.8s → 7.1s sequential, 1.14s at `--jobs 8` +(findings.md 22–24; JS legs benefit far less — no copy-on-write +memory images). Two caveats: the +snapshot freezes whatever init observed (env, entropy, clocks), and +the wizened artifact must be the one used everywhere downstream +(runners, `lock --check`). Suites that import a system-under-test +drive `component_test_runner::wizen::wizen_with` with their own +linker instead. diff --git a/crates/component-test-cli/Cargo.toml b/crates/component-test-cli/Cargo.toml index a9d1860..1f2307a 100644 --- a/crates/component-test-cli/Cargo.toml +++ b/crates/component-test-cli/Cargo.toml @@ -13,6 +13,16 @@ path = "src/main.rs" [dependencies] component-test-core = { workspace = true } component-test-formats = { workspace = true } +# The wizen entry points (feature `wizer`); the CLI is the plain-suite +# driver, SUT-importing suites call the library directly (#85). +component-test-runner = { workspace = true, features = ["wizer"] } anyhow = { workspace = true } +clap = { workspace = true } serde_json = { workspace = true } toml = { workspace = true } +# compose-runner/run: composition as a library (the shared-provider +# topology of examples/compose, without shelling out to wac). +wac-graph = { workspace = true } +# run: the minimal embedding executing composed wasi:cli components. +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } diff --git a/crates/component-test-cli/embedded/provider.wasm b/crates/component-test-cli/embedded/provider.wasm new file mode 100644 index 0000000000000000000000000000000000000000..eaddae4dd8ce9387f1afb995c71b81e742f360d1 GIT binary patch literal 69163 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 APyhe` literal 0 HcmV?d00001 diff --git a/crates/component-test-cli/embedded/runner-cli.wasm b/crates/component-test-cli/embedded/runner-cli.wasm new file mode 100644 index 0000000000000000000000000000000000000000..fd94f48289058a4d420ba8b18e1781601c9cffbe GIT binary patch literal 118058 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
e(}_aya%EVFzR5K)33 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 E0d6QL*8l(j literal 0 HcmV?d00001 diff --git a/crates/component-test-cli/src/compose.rs b/crates/component-test-cli/src/compose.rs new file mode 100644 index 0000000..7738605 --- /dev/null +++ b/crates/component-test-cli/src/compose.rs @@ -0,0 +1,229 @@ +//! `compose-runner`: compose a suite with a context provider and the +//! wasi:cli runner core, as a library (wac-graph) instead of shelled-out +//! `wac` — the same graph as `examples/compose/compose.wac`. +//! +//! The topology needs a *shared* provider instance — the suite's +//! `test-context` import and the runner core's `factory`/`test-context` +//! imports must resolve to the same instance so the `context` resource +//! type is one type — which is exactly what `wac plug` cannot express +//! (see examples/compose/README.md). Pre-composed *bundles* (a suite +//! already `wac compose`d with its own providers, re-exporting `tests` +//! plus `test-context` plus `factory`) skip the provider and plug +//! straight into the runner core. +//! +//! The default provider and runner core are baked in at build time from +//! `embedded/` (size-optimized `embed`-profile builds of +//! `components/provider` and `components/runner-cli`; regenerate with +//! `just embed-update` and commit the diff — `just verify-cli` gates +//! their behavior against the same goldens as the wac-composed path). +//! Both are overridable per invocation. +//! +//! Composition strips custom sections (findings #14): the result is +//! execute-everything, its envelope says `scheduling: none`, and +//! lockfiles must be generated from the suite artifact, not from +//! anything this module produces. + +use anyhow::{bail, Context as _, Result}; +use wac_graph::types::Package; +use wac_graph::{CompositionGraph, EncodeOptions, NodeId, PackageId}; + +/// The wasi:cli runner core (`components/runner-cli`), `embed` profile. +pub const EMBEDDED_RUNNER: &[u8] = include_bytes!("../embedded/runner-cli.wasm"); +/// The reference context provider (`components/provider`), `embed` profile. +pub const EMBEDDED_PROVIDER: &[u8] = include_bytes!("../embedded/provider.wasm"); + +/// The frozen contract interfaces (wit/tests.wit; L1) and the provider +/// interface the reference runner core consumes. +const TESTS: &str = "polymorph:test/tests@0.1.0"; +const CONTEXT: &str = "polymorph:test/test-context@0.1.0"; +const FACTORY: &str = "polymorph:test-provider/factory@0.1.0"; +/// The composed entry point (version-prefix matched: the runner core +/// override decides the exact wasi:cli version). +const RUN_PREFIX: &str = "wasi:cli/run@"; + +/// What a composition input turned out to be, by its exports. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Input { + /// Exports `tests` only: needs a provider and the runner core. + Suite, + /// Exports `tests` + `test-context` + `factory` (a suite already + /// bundled with its providers): needs only the runner core. + Bundle, + /// Exports `wasi:cli/run`: already composed, nothing to do. + Composed, +} + +/// Read a component from disk, with the same crisp non-wasm error the +/// other subcommands give. +pub fn read_component(path: &str) -> Result> { + let wasm = std::fs::read(path).with_context(|| format!("reading {path}"))?; + if !wasm.starts_with(b"\0asm") { + bail!("{path} is not a WebAssembly binary (bad magic)"); + } + Ok(wasm) +} + +/// Classify `input` by its exports (see [`Input`]). +pub fn classify(input: &[u8]) -> Result { + let mut graph = CompositionGraph::new(); + let pkg = register(&mut graph, "input", input)?; + Ok(classify_registered(&graph, pkg)) +} + +fn classify_registered(graph: &CompositionGraph, pkg: PackageId) -> Input { + let exports = &graph.types()[graph[pkg].ty()].exports; + if exports.keys().any(|n| n.starts_with(RUN_PREFIX)) { + Input::Composed + } else if exports.contains_key(FACTORY) && exports.contains_key(CONTEXT) { + Input::Bundle + } else { + Input::Suite + } +} + +fn register(graph: &mut CompositionGraph, name: &str, bytes: &[u8]) -> Result { + let package = Package::from_bytes(name, None, bytes.to_vec(), graph.types_mut()) + .with_context(|| format!("parsing the {name} component"))?; + graph + .register_package(package) + .with_context(|| format!("registering the {name} component")) +} + +/// Wire every import of `to` that `from`'s instance exports by exact +/// name. Unwired imports (WASI) stay imports of the composition. +/// `aliases` caches export aliases so one exported instance feeds any +/// number of importers (the shared-provider requirement). +fn wire( + graph: &mut CompositionGraph, + aliases: &mut std::collections::HashMap<(NodeId, String), NodeId>, + from: (NodeId, PackageId, &str), + to: (NodeId, PackageId, &str), +) -> Result<()> { + let (from_inst, from_pkg, from_name) = from; + let (to_inst, to_pkg, to_name) = to; + let exports: Vec = graph.types()[graph[from_pkg].ty()] + .exports + .keys() + .cloned() + .collect(); + for name in exports { + if !graph.types()[graph[to_pkg].ty()] + .imports + .contains_key(&name) + { + continue; + } + let alias = match aliases.entry((from_inst, name.clone())) { + std::collections::hash_map::Entry::Occupied(e) => *e.get(), + std::collections::hash_map::Entry::Vacant(e) => *e.insert( + graph + .alias_instance_export(from_inst, &name) + .with_context(|| format!("aliasing `{name}` from the {from_name}"))?, + ), + }; + graph + .set_instantiation_argument(to_inst, &name, alias) + .with_context(|| { + format!("plugging the {to_name}'s `{name}` import from the {from_name}") + })?; + } + Ok(()) +} + +/// Compose `input` (a suite or a bundle; see [`Input`]) with `provider` +/// and the `runner` core into a runnable wasi:cli component. +pub fn compose(input: &[u8], provider: &[u8], runner: &[u8]) -> Result> { + let mut graph = CompositionGraph::new(); + let input_pkg = register(&mut graph, "input", input)?; + let runner_pkg = register(&mut graph, "runner-cli", runner)?; + + let input_kind = classify_registered(&graph, input_pkg); + if input_kind == Input::Composed { + bail!( + "input already exports wasi:cli/run — it is already composed \ + (`component-test run` executes it directly)" + ); + } + if !graph.types()[graph[input_pkg].ty()] + .exports + .contains_key(TESTS) + { + bail!("input does not export `{TESTS}`: not a test suite (or bundle of one)"); + } + // A partial bundle would wire ambiguously (whose context feeds the + // runner core?); a real bundle exports both or neither. + if input_kind == Input::Suite + && (graph.types()[graph[input_pkg].ty()] + .exports + .contains_key(CONTEXT) + || graph.types()[graph[input_pkg].ty()] + .exports + .contains_key(FACTORY)) + { + bail!( + "input exports one of `{CONTEXT}`/`{FACTORY}` but not both: \ + a bundle must re-export both (see examples/compose/bundle.wac)" + ); + } + + let mut aliases = std::collections::HashMap::new(); + let runner_inst = graph.instantiate(runner_pkg); + match input_kind { + Input::Composed => unreachable!("rejected above: composed inputs do not export tests"), + // Bundle: everything the runner core imports re-exports from + // the one bundle instance (whose innards already share their + // provider). + Input::Bundle => { + let bundle_inst = graph.instantiate(input_pkg); + wire( + &mut graph, + &mut aliases, + (bundle_inst, input_pkg, "bundle"), + (runner_inst, runner_pkg, "runner core"), + )?; + } + // Bare suite: one shared provider instance feeds the suite's + // `test-context` import and the runner core's `factory` + + // `test-context` imports (examples/compose/compose.wac). + Input::Suite => { + let provider_pkg = register(&mut graph, "provider", provider)?; + let provider_inst = graph.instantiate(provider_pkg); + let suite_inst = graph.instantiate(input_pkg); + wire( + &mut graph, + &mut aliases, + (provider_inst, provider_pkg, "provider"), + (suite_inst, input_pkg, "suite"), + )?; + wire( + &mut graph, + &mut aliases, + (provider_inst, provider_pkg, "provider"), + (runner_inst, runner_pkg, "runner core"), + )?; + wire( + &mut graph, + &mut aliases, + (suite_inst, input_pkg, "suite"), + (runner_inst, runner_pkg, "runner core"), + )?; + } + } + + let run_name = graph.types()[graph[runner_pkg].ty()] + .exports + .keys() + .find(|n| n.starts_with(RUN_PREFIX)) + .cloned() + .with_context(|| format!("runner core exports no `{RUN_PREFIX}...` — not a runner core"))?; + let run = graph + .alias_instance_export(runner_inst, &run_name) + .context("aliasing the runner core's run export")?; + graph + .export(run, &run_name) + .context("exporting wasi:cli/run")?; + + graph + .encode(EncodeOptions::default()) + .context("encoding the composition") +} diff --git a/crates/component-test-cli/src/main.rs b/crates/component-test-cli/src/main.rs index cc1afc1..5357ff9 100644 --- a/crates/component-test-cli/src/main.rs +++ b/crates/component-test-cli/src/main.rs @@ -1,34 +1,34 @@ //! `component-test`: tooling entry point. //! -//! Subcommands (v0): -//! lock [-o tests.lock] [--check existing.lock] [--leaves names.txt] -//! Execution-free inventory from the suite's tags section. -//! `--leaves` (a runner's `--enumerate` output) additionally pins -//! each generated row's leaves, making coverage exact for them. -//! fold [tests.lock] < results.jsonl -//! Fold a JSONL results stream into the document form + summary. -//! aggregate --lock tests.lock --manifest targets.toml -//! [--results target=path.jsonl]... [-o matrix.md] -//! Cross-target validation + markdown matrix (#30). -//! emit junit [--lock tests.lock] [--results target=path.jsonl]... -//! [-o results.xml] -//! Convert results streams to JUnit XML for CI test UIs (#11). -//! A converter, not a gate: always exits 0 after writing. -//! pins --cargo-lock Cargo.lock [--js-lock lockfile]... [--expect rev] -//! One-rev-everywhere gate for downstream pin sets (#54): the -//! component-test-* crates in Cargo.lock and the JS facade's -//! npm/pnpm pin must agree on a single commit. -//! pins bump [--cargo-toml Cargo.toml]... -//! [--package-json package.json]... [--workflow ci.yml]... -//! The write half of the gate (#60): rewrite the declared pins -//! in place and print the lockfile-regeneration follow-ups. +//! Subcommands: +//! lock Execution-free inventory from the suite's tags +//! section; `--check` is the drift gate, `--leaves` +//! pins generated rows from a runner enumeration. +//! fold Fold a JSONL results stream into the document form +//! + summary (the acceptance gate for one stream). +//! aggregate Cross-target validation + markdown matrix (#30). +//! emit Convert results streams for foreign consumers +//! (JUnit XML, #11). A converter, not a gate: always +//! exits 0 after writing. +//! pins One-rev-everywhere gate for downstream pin sets +//! (#54); `pins bump` is the write half (#60). +//! wizen Wizer pre-initialization of a suite artifact (#25, +//! #85): snapshot after the contract's own `all()`. +//! compose-runner Compose a suite (or bundle) with a context +//! provider and the wasi:cli runner core (#13, #85); +//! embedded defaults, overridable. +//! run compose-runner + execute under an embedded +//! wasmtime; the exit code is the guest's verdict. +mod compose; mod junit; mod pins; +mod run; use std::io::{IsTerminal, Read}; use anyhow::{bail, Context as _}; +use clap::{Args, Parser, Subcommand}; use component_test_formats::{ aggregate, inventory, lockfile::{Lockfile, SuiteRef}, @@ -36,72 +36,203 @@ use component_test_formats::{ matrix, results, sha256_hex, }; -fn usage() -> String { - "usage: component-test lock [-o tests.lock] [--check tests.lock] [--leaves names.txt]\n \ - component-test fold [tests.lock] < results.jsonl\n \ - component-test aggregate --lock tests.lock --manifest targets.toml \ - [--results target=path.jsonl]... [-o matrix.md]\n \ - component-test emit junit [--lock tests.lock] \ - [--results target=path.jsonl]... [-o results.xml]\n \ - component-test pins --cargo-lock Cargo.lock \ - [--js-lock lockfile]... [--expect rev]\n \ - component-test pins bump [--cargo-toml Cargo.toml]... \ - [--package-json package.json]... [--workflow ci.yml]..." - .into() +#[derive(Parser)] +#[command( + name = "component-test", + version, + about = "polymorph:test tooling: composition, execution, inventory, and aggregation" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Execution-free inventory from the suite's tags section + Lock(LockArgs), + /// Fold a JSONL results stream (stdin) into the document form + summary + Fold(FoldArgs), + /// Cross-target validation + markdown matrix + Aggregate(AggregateArgs), + /// Convert results streams for foreign consumers (a converter, not a gate) + Emit(EmitArgs), + /// One-rev-everywhere gate for downstream pin sets + Pins(pins::PinsArgs), + /// Pre-initialize a suite with wizer: its own `all()` runs at build + /// time, so every fresh instance is born with the registry built + #[command(alias = "wizer")] + Wizen(WizenArgs), + /// Compose a suite with a context provider and the wasi:cli runner + /// core into a runnable component + ComposeRunner(ComposeRunnerArgs), + /// Compose (unless already composed) and execute under an embedded + /// wasmtime; the exit code is the guest's verdict + Run(RunArgs), +} + +#[derive(Args)] +struct LockArgs { + /// Suite artifact (the suite itself, not a composed bundle: + /// composition strips the tags section) + #[arg(value_name = "suite.wasm")] + suite: String, + /// Write the lockfile here (default: stdout) + #[arg(short, long, value_name = "tests.lock")] + output: Option, + /// Check an existing lockfile for drift instead of writing + #[arg(long, value_name = "tests.lock")] + check: Option, + /// A runner's --enumerate output (one case name per line): pins + /// each generated row's leaves, making coverage exact for them + #[arg(long, value_name = "names.txt")] + leaves: Option, +} + +#[derive(Args)] +struct FoldArgs { + /// With a lockfile, coverage is part of acceptance: every case + /// reported exactly once, generated leaves under their prefix + #[arg(value_name = "tests.lock")] + lock: Option, +} + +#[derive(Args)] +struct AggregateArgs { + /// The suite inventory (coverage + applicability baseline) + #[arg(long, value_name = "tests.lock")] + lock: String, + /// The targets manifest (features, targets, expected failures) + #[arg(long, value_name = "targets.toml")] + manifest: String, + /// A target's results stream (repeatable) + #[arg( + long = "results", + value_name = "target=path.jsonl", + value_parser = parse_target_results + )] + results: Vec<(String, String)>, + /// Write the markdown matrix here (default: stdout) + #[arg(short, long, value_name = "matrix.md")] + output: Option, +} + +#[derive(Args)] +struct EmitArgs { + /// Output format + #[arg(value_parser = ["junit"])] + format: String, + /// Fold each stream against this lockfile (not-reached synthesis + /// for dropped cases); without it, the stream speaks for itself + #[arg(long, value_name = "tests.lock")] + lock: Option, + /// A target's results stream (repeatable, at least one) + #[arg( + long = "results", + value_name = "target=path.jsonl", + value_parser = parse_target_results, + required = true + )] + results: Vec<(String, String)>, + /// Write here (default: stdout) + #[arg(short, long, value_name = "results.xml")] + output: Option, +} + +#[derive(Args)] +struct WizenArgs { + /// Suite artifact to pre-initialize (plain suites — WASI + + /// test-context imports only; SUT-importing suites drive + /// component_test_runner::wizen::wizen_with with their own linker) + #[arg(value_name = "suite.wasm")] + suite: String, + /// Where to write the pre-initialized artifact (run it everywhere + /// downstream — runners, lockfile checks — instead of mixing + /// artifacts) + #[arg(short, long, value_name = "out.wasm")] + output: String, + /// NAME=VAL visible during initialization, baked into the snapshot + /// (as are entropy and clocks: init must not leave the registry + /// parameterized on any of them; repeatable) + #[arg(long = "env", value_name = "NAME=VAL", value_parser = parse_env)] + env: Vec<(String, String)>, +} + +#[derive(Args)] +struct ComposeRunnerArgs { + /// Suite artifact, or a bundle re-exporting tests + test-context + + /// factory (a suite pre-composed with its own providers) + #[arg(value_name = "suite.wasm")] + input: String, + /// Where to write the composed component + #[arg(short, long, value_name = "composed.wasm")] + output: String, + /// Context provider component (default: the embedded reference + /// provider; unused when the input is a bundle) + #[arg(long, value_name = "provider.wasm")] + provider: Option, + /// wasi:cli runner core component (default: the embedded + /// components/runner-cli build) + #[arg(long, value_name = "runner.wasm")] + runner: Option, +} + +#[derive(Args)] +struct RunArgs { + /// Suite, bundle, or already-composed component (anything exporting + /// wasi:cli/run is executed as-is) + #[arg(value_name = "suite.wasm")] + input: String, + /// Context provider component (default: the embedded reference + /// provider) + #[arg(long, value_name = "provider.wasm")] + provider: Option, + /// wasi:cli runner core component (default: the embedded + /// components/runner-cli build) + #[arg(long, value_name = "runner.wasm")] + runner: Option, + /// Emit the JSONL results wire format (sets COMPONENT_TEST_JSONL=1 + /// in the guest; pipe into `component-test fold`) + #[arg(long)] + jsonl: bool, + /// NAME=VAL in the guest environment (the host environment is not + /// inherited, matching `wasmtime run`; repeatable) + #[arg(long = "env", value_name = "NAME=VAL", value_parser = parse_env)] + env: Vec<(String, String)>, +} + +fn parse_target_results(s: &str) -> Result<(String, String), String> { + s.split_once('=') + .map(|(t, p)| (t.to_string(), p.to_string())) + .ok_or_else(|| "must be target=path.jsonl".to_string()) +} + +fn parse_env(s: &str) -> Result<(String, String), String> { + s.split_once('=') + .map(|(k, v)| (k.to_string(), v.to_string())) + .ok_or_else(|| "must be NAME=VAL".to_string()) } fn main() -> anyhow::Result<()> { - let args: Vec = std::env::args().skip(1).collect(); - match args.first().map(|s| s.as_str()) { - Some("lock") => lock(&args[1..]), - Some("fold") => fold(&args[1..]), - Some("aggregate") => aggregate_cmd(&args[1..]), - Some("emit") => emit_cmd(&args[1..]), - Some("pins") => pins::pins_cmd(&args[1..]), - Some("-h" | "--help" | "help") => { - println!("{}", usage()); - Ok(()) - } - Some("-V" | "--version") => { - println!("component-test {}", env!("CARGO_PKG_VERSION")); - Ok(()) - } - _ => { - eprintln!("{}", usage()); - std::process::exit(2); - } + match Cli::parse().command { + Command::Lock(args) => lock(&args), + Command::Fold(args) => fold(&args), + Command::Aggregate(args) => aggregate_cmd(&args), + Command::Emit(args) => emit_cmd(&args), + Command::Pins(args) => pins::pins_cmd(&args), + Command::Wizen(args) => wizen_cmd(&args), + Command::ComposeRunner(args) => compose_runner_cmd(&args), + Command::Run(args) => run_cmd(&args), } } -fn lock(args: &[String]) -> anyhow::Result<()> { - let mut suite_path = None; - let mut out_path = None; - let mut check_path = None; - let mut leaves_path = None; - let mut it = args.iter(); - while let Some(arg) = it.next() { - match arg.as_str() { - "-o" => out_path = Some(it.next().context("-o needs a path")?.clone()), - "--check" => check_path = Some(it.next().context("--check needs a path")?.clone()), - "--leaves" => leaves_path = Some(it.next().context("--leaves needs a path")?.clone()), - "-h" | "--help" => { - println!("{}", usage()); - return Ok(()); - } - s if s.starts_with('-') => bail!("unknown flag `{s}`\n{}", usage()), - _ if suite_path.is_none() => suite_path = Some(arg.clone()), - other => bail!("unexpected argument `{other}`"), - } - } - let suite_path = suite_path.context("missing suite.wasm path")?; - let wasm = std::fs::read(&suite_path).with_context(|| format!("reading {suite_path}"))?; - if !wasm.starts_with(b"\0asm") { - bail!("{suite_path} is not a WebAssembly binary (bad magic)"); - } +fn lock(args: &LockArgs) -> anyhow::Result<()> { + let suite_path = &args.suite; + let wasm = compose::read_component(suite_path)?; let inv = inventory::inventory(&wasm)?; let artifact_sha256 = sha256_hex(&wasm); - let name = std::path::Path::new(&suite_path) + let name = std::path::Path::new(suite_path) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("suite") @@ -118,7 +249,7 @@ fn lock(args: &[String]) -> anyhow::Result<()> { // case name per line) pins each generated row's leaves. Exact // static cases pass through; a name matching neither is inventory // drift, as is a generated row the enumeration never touched. - if let Some(path) = &leaves_path { + if let Some(path) = &args.leaves { let text = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; let static_names: std::collections::BTreeSet> = lf.case.iter().map(|c| c.name.as_str()).collect(); @@ -155,9 +286,9 @@ fn lock(args: &[String]) -> anyhow::Result<()> { lf.validate()?; let toml = lf.to_toml()?; - if let Some(check) = check_path { + if let Some(check) = &args.check { let existing = Lockfile::from_toml( - &std::fs::read_to_string(&check).with_context(|| format!("reading {check}"))?, + &std::fs::read_to_string(check).with_context(|| format!("reading {check}"))?, )?; existing.validate()?; // Inventory equality: names + tags (artifact hash may differ), @@ -170,7 +301,7 @@ fn lock(args: &[String]) -> anyhow::Result<()> { .iter() .zip(&lf.generated) .all(|(a, b)| a.prefix == b.prefix && a.tags == b.tags); - let leaves_ok = leaves_path.is_none() + let leaves_ok = args.leaves.is_none() || existing .generated .iter() @@ -182,7 +313,7 @@ fn lock(args: &[String]) -> anyhow::Result<()> { (regenerate with `component-test lock` and review the diff)" ); } - if leaves_path.is_none() && existing.generated.iter().any(|g| !g.cases.is_empty()) { + if args.leaves.is_none() && existing.generated.iter().any(|g| !g.cases.is_empty()) { println!( "note: {check} pins generated leaves; static check only (pass --leaves \ for the full comparison)" @@ -192,9 +323,9 @@ fn lock(args: &[String]) -> anyhow::Result<()> { return Ok(()); } - match out_path { + match &args.output { Some(path) => { - std::fs::write(&path, toml)?; + std::fs::write(path, toml)?; println!("wrote {} cases to {path}", lf.case.len()); } None => print!("{toml}"), @@ -202,16 +333,7 @@ fn lock(args: &[String]) -> anyhow::Result<()> { Ok(()) } -fn fold(args: &[String]) -> anyhow::Result<()> { - match args.first().map(|s| s.as_str()) { - Some("-h" | "--help") => { - println!("{}", usage()); - return Ok(()); - } - Some(s) if s.starts_with('-') => bail!("unknown flag `{s}`\n{}", usage()), - _ => {} - } - +fn fold(args: &FoldArgs) -> anyhow::Result<()> { // Reading stdin to EOF is the first thing that happens; without a // pipe it blocks forever, so say so. if std::io::stdin().is_terminal() { @@ -223,7 +345,7 @@ fn fold(args: &[String]) -> anyhow::Result<()> { let mut stream = String::new(); std::io::stdin().read_to_string(&mut stream)?; - let lockfile = match args.first() { + let lockfile = match &args.lock { Some(path) => { let lf = Lockfile::from_toml( &std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?, @@ -304,46 +426,20 @@ fn fold(args: &[String]) -> anyhow::Result<()> { std::process::exit(if failed { 1 } else { 0 }); } -fn aggregate_cmd(args: &[String]) -> anyhow::Result<()> { - let mut lock_path = None; - let mut manifest_path = None; - let mut result_args: Vec<(String, String)> = Vec::new(); - let mut out_path = None; - let mut it = args.iter(); - while let Some(arg) = it.next() { - match arg.as_str() { - "--lock" => lock_path = Some(it.next().context("--lock needs a path")?.clone()), - "--manifest" => { - manifest_path = Some(it.next().context("--manifest needs a path")?.clone()) - } - "--results" => { - let spec = it.next().context("--results needs target=path.jsonl")?; - let (target, path) = spec - .split_once('=') - .context("--results argument must be target=path.jsonl")?; - result_args.push((target.to_string(), path.to_string())); - } - "-o" => out_path = Some(it.next().context("-o needs a path")?.clone()), - "-h" | "--help" => { - println!("{}", usage()); - return Ok(()); - } - other => bail!("unexpected argument `{other}`\n{}", usage()), - } - } - let lock_path = lock_path.context("missing --lock")?; - let manifest_path = manifest_path.context("missing --manifest")?; +fn aggregate_cmd(args: &AggregateArgs) -> anyhow::Result<()> { + let lock_path = &args.lock; + let manifest_path = &args.manifest; let lf = Lockfile::from_toml( - &std::fs::read_to_string(&lock_path).with_context(|| format!("reading {lock_path}"))?, + &std::fs::read_to_string(lock_path).with_context(|| format!("reading {lock_path}"))?, )?; let manifest = Manifest::from_toml( - &std::fs::read_to_string(&manifest_path) + &std::fs::read_to_string(manifest_path) .with_context(|| format!("reading {manifest_path}"))?, )?; let mut docs = Vec::new(); - for (target, path) in &result_args { + for (target, path) in &args.results { let stream = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; let doc = results::fold_jsonl( &stream, @@ -355,7 +451,7 @@ fn aggregate_cmd(args: &[String]) -> anyhow::Result<()> { let agg = aggregate::aggregate(&lf, &manifest, &docs); let md = matrix::render(&agg); - match &out_path { + match &args.output { Some(path) => { std::fs::write(path, &md).with_context(|| format!("writing {path}"))?; } @@ -388,7 +484,7 @@ fn aggregate_cmd(args: &[String]) -> anyhow::Result<()> { "{} targets, {total} results, {failures} failing{expected}, {} validation error(s){}", agg.targets.len(), agg.errors.len(), - out_path + args.output .as_deref() .map(|p| format!("; wrote {p}")) .unwrap_or_default() @@ -403,47 +499,15 @@ fn aggregate_cmd(args: &[String]) -> anyhow::Result<()> { /// *in* the output where the consuming UI wants them, and the exit /// code only reflects I/O; `fold`/`aggregate` remain the verdict /// authorities. -fn emit_cmd(args: &[String]) -> anyhow::Result<()> { - let format = match args.first().map(|s| s.as_str()) { - Some("junit") => "junit", - other => bail!( - "emit: unknown or missing format {other:?} (supported: junit)\n{}", - usage() - ), - }; - let mut lock_path = None; - let mut result_args: Vec<(String, String)> = Vec::new(); - let mut out_path = None; - let mut it = args[1..].iter(); - while let Some(arg) = it.next() { - match arg.as_str() { - "--lock" => lock_path = Some(it.next().context("--lock needs a path")?.clone()), - "--results" => { - let spec = it.next().context("--results needs target=path.jsonl")?; - let (target, path) = spec - .split_once('=') - .context("--results argument must be target=path.jsonl")?; - result_args.push((target.to_string(), path.to_string())); - } - "-o" => out_path = Some(it.next().context("-o needs a path")?.clone()), - "-h" | "--help" => { - println!("{}", usage()); - return Ok(()); - } - other => bail!("unexpected argument `{other}`\n{}", usage()), - } - } - if result_args.is_empty() { - bail!("emit {format}: at least one --results target=path.jsonl required"); - } - let lockfile = match &lock_path { +fn emit_cmd(args: &EmitArgs) -> anyhow::Result<()> { + let lockfile = match &args.lock { Some(p) => Some(Lockfile::from_toml( &std::fs::read_to_string(p).with_context(|| format!("reading {p}"))?, )?), None => None, }; let mut docs = Vec::new(); - for (target, path) in &result_args { + for (target, path) in &args.results { let stream = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; let doc = results::fold_jsonl( &stream, @@ -453,9 +517,73 @@ fn emit_cmd(args: &[String]) -> anyhow::Result<()> { docs.push((target.clone(), doc)); } let xml = junit::junit(&docs); - match &out_path { + match &args.output { Some(path) => std::fs::write(path, &xml).with_context(|| format!("writing {path}"))?, None => print!("{xml}"), } Ok(()) } + +/// `wizen`: pre-initialize a plain suite (#25/#85). The library entry +/// points live in component-test-runner (`wizen`/`wizen_with`) so +/// SUT-importing embedders reuse their own linker setup; this is the +/// convenience driver for suites with no imports beyond WASI + +/// test-context. +fn wizen_cmd(args: &WizenArgs) -> anyhow::Result<()> { + let wasm = compose::read_component(&args.suite)?; + let wizened = component_test_runner::wizen::wizen(&wasm, &args.env) + .map_err(anyhow::Error::from) + .with_context(|| format!("wizening {}", args.suite))?; + std::fs::write(&args.output, &wizened).with_context(|| format!("writing {}", args.output))?; + println!( + "wizened: {} -> {} bytes ({})", + wasm.len(), + wizened.len(), + args.output + ); + Ok(()) +} + +fn compose_runner_cmd(args: &ComposeRunnerArgs) -> anyhow::Result<()> { + let input = compose::read_component(&args.input)?; + let composed = compose_from_args(&input, args.provider.as_deref(), args.runner.as_deref())?; + std::fs::write(&args.output, &composed).with_context(|| format!("writing {}", args.output))?; + println!( + "wrote composed component to {} ({} bytes)", + args.output, + composed.len() + ); + Ok(()) +} + +fn run_cmd(args: &RunArgs) -> anyhow::Result<()> { + let input = compose::read_component(&args.input)?; + let composed = match compose::classify(&input)? { + compose::Input::Composed => input, + _ => compose_from_args(&input, args.provider.as_deref(), args.runner.as_deref())?, + }; + let mut env = args.env.clone(); + if args.jsonl { + env.push(("COMPONENT_TEST_JSONL".into(), "1".into())); + } + let code = run::execute(&composed, &env)?; + std::process::exit(code); +} + +/// Shared compose step: resolve the provider/runner overrides (embedded +/// defaults otherwise) and compose. +fn compose_from_args( + input: &[u8], + provider: Option<&str>, + runner: Option<&str>, +) -> anyhow::Result> { + let provider = match provider { + Some(path) => compose::read_component(path)?, + None => compose::EMBEDDED_PROVIDER.to_vec(), + }; + let runner = match runner { + Some(path) => compose::read_component(path)?, + None => compose::EMBEDDED_RUNNER.to_vec(), + }; + compose::compose(input, &provider, &runner) +} diff --git a/crates/component-test-cli/src/pins.rs b/crates/component-test-cli/src/pins.rs index a90661a..cef85e3 100644 --- a/crates/component-test-cli/src/pins.rs +++ b/crates/component-test-cli/src/pins.rs @@ -10,41 +10,76 @@ //! which broke every downstream grep-based gate at once — names do not. use anyhow::{bail, Context as _}; +use clap::{Args, Subcommand}; /// This workspace's crates, as named in a consumer's Cargo.lock. const CRATE_PREFIX: &str = "component-test"; /// The JS runner-core facade packaged at the repository root. const JS_PACKAGE: &str = "@polymorph/component-test-js"; -pub(crate) fn pins_cmd(args: &[String]) -> anyhow::Result<()> { - if args.first().map(|s| s.as_str()) == Some("bump") { - return bump_cmd(&args[1..]); - } - let mut cargo_lock = None; - let mut js_locks: Vec = Vec::new(); - let mut expect = None; - let mut it = args.iter(); - while let Some(arg) = it.next() { - match arg.as_str() { - "--cargo-lock" => { - cargo_lock = Some(it.next().context("--cargo-lock needs a path")?.clone()) - } - "--js-lock" => js_locks.push(it.next().context("--js-lock needs a path")?.clone()), - "--expect" => expect = Some(it.next().context("--expect needs a rev")?.clone()), - "-h" | "--help" => { - println!("{}", crate::usage()); - return Ok(()); - } - other => bail!("unexpected argument `{other}`\n{}", crate::usage()), - } +#[derive(Args)] +#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)] +pub(crate) struct PinsArgs { + #[command(subcommand)] + command: Option, + /// Consumer Cargo.lock: every git-sourced component-test-* crate's + /// resolved commit is a pin + #[arg(long, required = true, value_name = "Cargo.lock")] + cargo_lock: Option, + /// npm/pnpm lockfile carrying the @polymorph/component-test-js pin + /// (repeatable) + #[arg(long = "js-lock", value_name = "lockfile")] + js_locks: Vec, + /// Additionally require the single rev to be exactly this + #[arg(long, value_name = "rev")] + expect: Option, +} + +#[derive(Subcommand)] +enum PinsCommand { + /// Rewrite the declared pins in place (the write half of the gate); + /// lockfile regeneration follow-ups are printed, not run + Bump(BumpArgs), +} + +#[derive(Args)] +struct BumpArgs { + /// The commit to pin (a full 40-hex hash) + #[arg(value_name = "rev", value_parser = parse_rev)] + rev: String, + /// Cargo.toml with git-pinned component-test-* dependency lines + /// (repeatable) + #[arg(long = "cargo-toml", value_name = "Cargo.toml")] + cargo_tomls: Vec, + /// package.json naming @polymorph/component-test-js (repeatable) + #[arg(long = "package-json", value_name = "package.json")] + package_jsons: Vec, + /// Workflow file with polymorph-test action refs (repeatable) + #[arg(long = "workflow", value_name = "ci.yml")] + workflows: Vec, +} + +fn parse_rev(s: &str) -> Result { + let rev = s.to_ascii_lowercase(); + if is_rev(&rev) { + Ok(rev) + } else { + Err("rev must be a full commit hash (40 hex)".to_string()) } - let cargo_lock = cargo_lock.context("missing --cargo-lock")?; +} + +pub(crate) fn pins_cmd(args: &PinsArgs) -> anyhow::Result<()> { + if let Some(PinsCommand::Bump(bump)) = &args.command { + return bump_cmd(bump); + } + // Required by clap whenever no subcommand was given. + let cargo_lock = args.cargo_lock.as_deref().expect("required by clap"); // Every place a pin was found: (source description, rev). let mut findings: Vec<(String, String)> = Vec::new(); let text = - std::fs::read_to_string(&cargo_lock).with_context(|| format!("reading {cargo_lock}"))?; + std::fs::read_to_string(cargo_lock).with_context(|| format!("reading {cargo_lock}"))?; let crates = cargo_lock_revs(&text)?; if crates.is_empty() { bail!( @@ -56,7 +91,7 @@ pub(crate) fn pins_cmd(args: &[String]) -> anyhow::Result<()> { findings.push((format!("{cargo_lock}: {name}"), rev)); } - for path in &js_locks { + for path in &args.js_locks { let text = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; let revs = js_lock_revs(&text); if revs.is_empty() { @@ -67,7 +102,7 @@ pub(crate) fn pins_cmd(args: &[String]) -> anyhow::Result<()> { } } - if let Some(rev) = expect { + if let Some(rev) = &args.expect { findings.push(("--expect".into(), rev.to_ascii_lowercase())); } @@ -94,41 +129,19 @@ pub(crate) fn pins_cmd(args: &[String]) -> anyhow::Result<()> { /// regenerating them belongs to cargo/npm/pnpm, so the follow-up /// commands are printed instead of run, and the check half verifies /// the result. -fn bump_cmd(args: &[String]) -> anyhow::Result<()> { - let mut rev = None; - let mut cargo_tomls: Vec = Vec::new(); - let mut package_jsons: Vec = Vec::new(); - let mut workflows: Vec = Vec::new(); - let mut it = args.iter(); - while let Some(arg) = it.next() { - match arg.as_str() { - "--cargo-toml" => { - cargo_tomls.push(it.next().context("--cargo-toml needs a path")?.clone()) - } - "--package-json" => { - package_jsons.push(it.next().context("--package-json needs a path")?.clone()) - } - "--workflow" => workflows.push(it.next().context("--workflow needs a path")?.clone()), - "-h" | "--help" => { - println!("{}", crate::usage()); - return Ok(()); - } - other if rev.is_none() && is_rev(&other.to_ascii_lowercase()) => { - rev = Some(other.to_ascii_lowercase()) - } - other => bail!("unexpected argument `{other}` (rev must be 40 lowercase hex)"), - } - } - let rev = rev.context("missing (40-hex commit)")?; +fn bump_cmd(args: &BumpArgs) -> anyhow::Result<()> { + let rev = &args.rev; + let (cargo_tomls, package_jsons, workflows) = + (&args.cargo_tomls, &args.package_jsons, &args.workflows); if cargo_tomls.is_empty() && package_jsons.is_empty() && workflows.is_empty() { bail!("nothing to bump: name at least one --cargo-toml/--package-json/--workflow"); } let mut cargo_crates: std::collections::BTreeSet = std::collections::BTreeSet::new(); let mut touched_js = false; - for path in &cargo_tomls { + for path in cargo_tomls { let text = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; - let (new, crates) = bump_cargo_toml(&text, &rev); + let (new, crates) = bump_cargo_toml(&text, rev); if crates.is_empty() { bail!("{path}: no git-pinned `{CRATE_PREFIX}-*` dependency lines found"); } @@ -136,9 +149,9 @@ fn bump_cmd(args: &[String]) -> anyhow::Result<()> { cargo_crates.extend(crates); std::fs::write(path, new).with_context(|| format!("writing {path}"))?; } - for path in &package_jsons { + for path in package_jsons { let text = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; - let (new, n) = bump_named_lines(&text, JS_PACKAGE, &rev); + let (new, n) = bump_named_lines(&text, JS_PACKAGE, rev); if n == 0 { bail!("{path}: no rev-pinned {JS_PACKAGE} line found"); } @@ -146,9 +159,9 @@ fn bump_cmd(args: &[String]) -> anyhow::Result<()> { touched_js = true; std::fs::write(path, new).with_context(|| format!("writing {path}"))?; } - for path in &workflows { + for path in workflows { let text = std::fs::read_to_string(path).with_context(|| format!("reading {path}"))?; - let (new, n) = bump_action_refs(&text, &rev); + let (new, n) = bump_action_refs(&text, rev); if n == 0 { bail!("{path}: no `{ACTION_PATH_ANCHOR}@` refs found"); } diff --git a/crates/component-test-cli/src/run.rs b/crates/component-test-cli/src/run.rs new file mode 100644 index 0000000..116015b --- /dev/null +++ b/crates/component-test-cli/src/run.rs @@ -0,0 +1,81 @@ +//! `run`: execute a composed wasi:cli component — the minimal embedding +//! equivalent of `wasmtime run -W component-model-async -S p3`, so +//! `component-test run` is compose-runner + wasmtime in one step with +//! no external tools. +//! +//! Scope is exactly "sufficient for the runner core": WASI p2 (the +//! suite's and provider's std imports) + WASI p3 (the runner core's +//! `wasi:cli/stdout@0.3.0` and its `wasi:cli/run@0.3.0` entry point), +//! stdio inherited, environment explicit (wasmtime run's behavior: the +//! host env is not inherited). Known divergences from `wasmtime run`, +//! irrelevant to the runner core: stdin is closed, and no argv is +//! passed. No epoch budgets, no pooling — hangs and +//! traps are the wasmtime CLI's problem too; the serious execution +//! policy lives in the host-embed runner. + +use anyhow::Result; +use wasmtime::component::{Component, Linker}; +use wasmtime::error::Context as _; +use wasmtime::{Config, Engine, Store}; +use wasmtime_wasi::p3::bindings::Command; +use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +struct Ctx { + wasi: WasiCtx, + table: ResourceTable, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +/// Execute `composed` (exporting `wasi:cli/run@0.3.0`) and return the +/// process exit code: the guest's `run` result (0/1), or its explicit +/// exit status. Traps and instantiation failures surface as errors. +pub fn execute(composed: &[u8], env: &[(String, String)]) -> Result { + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_async(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, composed).context("loading composed component")?; + + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; + wasmtime_wasi::p3::add_to_linker(&mut linker)?; + + let mut wasi = WasiCtxBuilder::new(); + wasi.inherit_stdout().inherit_stderr(); + for (k, v) in env { + wasi.env(k, v); + } + let mut store = Store::new( + &engine, + Ctx { + wasi: wasi.build(), + table: ResourceTable::new(), + }, + ); + + let result: wasmtime::Result> = wasmtime_wasi::runtime::in_tokio(async { + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + store + .run_concurrent(async move |store| command.wasi_cli_run().call_run(store).await) + .await? + }); + + match result { + Ok(Ok(())) => Ok(0), + Ok(Err(())) => Ok(1), + // An explicit guest exit unwinds as an error carrying the + // status (both the p2 and p3 exit interfaces). + Err(e) => match e.downcast_ref::() { + Some(exit) => Ok(exit.0), + None => Err(e.context("executing composed component").into()), + }, + } +} diff --git a/crates/component-test-cli/tests/cli.rs b/crates/component-test-cli/tests/cli.rs index e1c2b4a..5d664c7 100644 --- a/crates/component-test-cli/tests/cli.rs +++ b/crates/component-test-cli/tests/cli.rs @@ -90,7 +90,7 @@ fn fold_with(lock: &Path, jsonl: &str) -> Output { fn usage_and_help() { let out = run(&[], None); assert_eq!(out.code, 2); - assert!(out.stderr.contains("usage:"), "{}", out.stderr); + assert!(out.stderr.contains("Usage:"), "{}", out.stderr); for args in [ &["--help"][..], @@ -98,22 +98,26 @@ fn usage_and_help() { &["fold", "--help"], &["aggregate", "--help"], &["pins", "--help"], + &["wizen", "--help"], + &["compose-runner", "--help"], + &["run", "--help"], ] { let out = run(args, None); assert_eq!(out.code, 0, "{args:?}"); - assert!(out.stdout.contains("usage:"), "{args:?}"); + assert!(out.stdout.contains("Usage:"), "{args:?}"); } let out = run(&["frobnicate"], None); assert_eq!(out.code, 2); let out = run(&["lock", "--bogus"], None); - assert_eq!(out.code, 1); - assert!( - out.stderr.contains("unknown flag `--bogus`"), - "{}", - out.stderr - ); + assert_eq!(out.code, 2); + assert!(out.stderr.contains("--bogus"), "{}", out.stderr); + + // `wizer` is an alias for `wizen`. + let out = run(&["wizer", "--help"], None); + assert_eq!(out.code, 0); + assert!(out.stdout.contains("Usage:"), "{}", out.stdout); } #[test] @@ -517,9 +521,10 @@ fn aggregate_applies_applicability_for_unscheduled_streams() { #[test] fn aggregate_missing_args() { + // Required-argument enforcement is clap's: exit 2, naming the flag. let out = run(&["aggregate", "--manifest", "x.toml"], None); - assert_eq!(out.code, 1); - assert!(out.stderr.contains("missing --lock"), "{}", out.stderr); + assert_eq!(out.code, 2); + assert!(out.stderr.contains("--lock"), "{}", out.stderr); } const AGG_XFAIL_MANIFEST: &str = r#" @@ -735,6 +740,123 @@ fn lock_emits_inventory_to_stdout() { assert!(out.stdout.contains("artifact_sha256"), "{}", out.stdout); } +// ---------------------------------- wizen / compose-runner / run +// (need built wasm) + +fn target_tmp(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name) +} + +/// Wizening must leave the inventory intact: the tags section survives +/// the rewrite (finding #22), so the committed lockfile still checks +/// against the wizened artifact. +#[test] +#[ignore = "needs built components: run via `just test-wasm`"] +fn wizen_preserves_inventory() { + let wasm = suite_artifact("sample_suite"); + let wizened = target_tmp("sample-wizened.wasm"); + let out = run( + &[ + "wizen", + wasm.to_str().unwrap(), + "-o", + wizened.to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 0, "stderr: {}", out.stderr); + assert!(out.stdout.contains("wizened:"), "{}", out.stdout); + + let lock = workspace_root().join("components/sample-suite/tests.lock"); + let out = run( + &[ + "lock", + wizened.to_str().unwrap(), + "--check", + lock.to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 0, "stderr: {}", out.stderr); + assert!(out.stdout.contains("ok: 3 cases match"), "{}", out.stdout); +} + +/// compose-runner emits a component; run executes the same composition +/// in memory and reproduces the composed runner's JSONL golden +/// byte-for-byte (the same bytes `just verify-compose` pins for the +/// wac-composed path — one wire format, however composed). +#[test] +#[ignore = "needs built components: run via `just test-wasm`"] +fn compose_runner_and_run_reproduce_the_composed_golden() { + let wasm = suite_artifact("sample_suite"); + let composed = target_tmp("sample-composed.wasm"); + let out = run( + &[ + "compose-runner", + wasm.to_str().unwrap(), + "-o", + composed.to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 0, "stderr: {}", out.stderr); + let bytes = std::fs::read(&composed).unwrap(); + assert!(bytes.starts_with(b"\0asm"), "composed output is not wasm"); + + let golden = workspace_root().join("expected/verify-compose-sample.jsonl"); + let golden = std::fs::read_to_string(golden).unwrap(); + // The sample suite has a deliberate failure: exit 1, verdicts intact. + for input in [wasm.as_path(), composed.as_path()] { + let out = run(&["run", "--jsonl", input.to_str().unwrap()], None); + assert_eq!(out.code, 1, "stderr: {}", out.stderr); + assert_eq!(out.stdout, golden, "run {input:?} diverged from golden"); + } +} + +/// Inputs that cannot compose are rejected by name, not by wac +/// internals: an already-composed component, and a non-suite component. +#[test] +#[ignore = "needs built components: run via `just test-wasm`"] +fn compose_runner_rejects_non_suites() { + let wasm = suite_artifact("sample_suite"); + let composed = target_tmp("sample-composed-reject.wasm"); + let out = run( + &[ + "compose-runner", + wasm.to_str().unwrap(), + "-o", + composed.to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 0, "stderr: {}", out.stderr); + + let out = run( + &[ + "compose-runner", + composed.to_str().unwrap(), + "-o", + target_tmp("never-written.wasm").to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 1); + assert!(out.stderr.contains("already composed"), "{}", out.stderr); + + let provider = suite_artifact("provider"); + let out = run( + &[ + "compose-runner", + provider.to_str().unwrap(), + "-o", + target_tmp("never-written2.wasm").to_str().unwrap(), + ], + None, + ); + assert_eq!(out.code, 1); + assert!(out.stderr.contains("not a test suite"), "{}", out.stderr); +} + // ------------------------------------------------------ pins const PIN_REV: &str = "b80c13be7b9fecfed8ec10a91d23d8cf8349defe"; diff --git a/crates/component-test-runner/Cargo.toml b/crates/component-test-runner/Cargo.toml index edf087d..9c0069e 100644 --- a/crates/component-test-runner/Cargo.toml +++ b/crates/component-test-runner/Cargo.toml @@ -15,10 +15,11 @@ futures = { workspace = true } # on the wasmtime-wasi runtime (which powers wasi clock pollables, so # time is enabled there). tokio = { workspace = true, features = ["time"] } -wasmtime = { version = "47", features = ["component-model-async"] } -wasmtime-wasi = "47" -# For the feature-gated `wizer-preinit` bin only (component-level -# pre-initialization experiments, #25 / findings.md #22). +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } +# For the feature-gated `wizen` module (component-level +# pre-initialization, #25 / findings.md #22; `component-test wizen` +# is the plain-suite driver). wasmtime-wizer = { version = "47", features = ["wasmtime", "component-model"], optional = true } [features] @@ -32,10 +33,5 @@ path = "src/bin/ct-runner.rs" name = "bench-mint" path = "src/bin/bench-mint.rs" -[[bin]] -name = "wizer-preinit" -path = "src/bin/wizer-preinit.rs" -required-features = ["wizer"] - [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/component-test-runner/src/bin/wizer-preinit.rs b/crates/component-test-runner/src/bin/wizer-preinit.rs deleted file mode 100644 index 415dfe6..0000000 --- a/crates/component-test-runner/src/bin/wizer-preinit.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Component-level wizer pre-initialization driver (`wizer-preinit`, -//! feature `wizer`): snapshots a suite component after running its own -//! `tests.all()`, so every fresh instance is born with the case table -//! built and `all()` costs only mint+lift (#25). -//! -//! Works on any unmodified suite artifact: the init function is the -//! contract's own `all()` — named in the version-last invoke syntax -//! `polymorph:test/tests.all@0.1.0()` (the parenthesized wave-call -//! form; the bare item-name form requires a `[] -> []` signature). -//! The handles `all()` returns are per-call state; only the guest heap -//! (the built registry) lands in the snapshot, at no measurable size -//! cost over a dedicated no-op init export (findings.md #22). -//! -//! Exists because the `wasmtime wizer` CLI cannot wizen a *suite*: -//! the suite world imports `test-context`, whose `context` resource -//! cannot be synthesized by unknown-import stubbing ("resource -//! implementation is missing"), and composed bundles hit "nested -//! components with modules not currently supported". Driving -//! wasmtime-wizer as a library with our own linker — WASI plus a host -//! `context` resource whose methods init never calls — sidesteps both. -//! -//! cargo run --release -p component-test-runner --features wizer \ -//! --bin wizer-preinit -- [ENV=VAL ...] - -use wasmtime::component::{Linker, Resource, ResourceType}; -use wasmtime::error::format_err; -use wasmtime::{Config, Engine, Result, Store}; -use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; - -struct Ctx { - wasi: WasiCtx, - table: ResourceTable, -} - -impl WasiView for Ctx { - fn ctx(&mut self) -> WasiCtxView<'_> { - WasiCtxView { - ctx: &mut self.wasi, - table: &mut self.table, - } - } -} - -struct HostContext; - -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let usage = "usage: wizer-preinit [ENV=VAL ...]"; - let input = args.next().ok_or_else(|| format_err!("{usage}"))?; - let output = args.next().ok_or_else(|| format_err!("{usage}"))?; - let env: Vec<(String, String)> = args - .map(|kv| { - kv.split_once('=') - .map(|(k, v)| (k.to_string(), v.to_string())) - .ok_or_else(|| format_err!("bad env pair `{kv}` (want NAME=VAL)\n{usage}")) - }) - .collect::>()?; - - let wasm = std::fs::read(&input)?; - - let mut config = Config::new(); - config.wasm_component_model(true); - config.wasm_component_model_async(true); - let engine = Engine::new(&config)?; - - let mut linker: Linker = Linker::new(&engine); - wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; - let mut ctx_instance = linker.instance("polymorph:test/test-context@0.1.0")?; - ctx_instance.resource( - "context", - ResourceType::host::(), - |_, _| Ok(()), - )?; - // Present so the component type-checks; never called during init - // (registry builds are pure — anything else is a suite bug this - // driver would surface as a snapshot-time diagnostic call). - ctx_instance.func_wrap_concurrent( - "[method]context.diagnostic", - |_accessor, (_this, _msg): (Resource, String)| Box::pin(async move { Ok(()) }), - )?; - - let mut wasi = WasiCtxBuilder::new(); - wasi.inherit_stderr(); - for (k, v) in &env { - wasi.env(k, v); - } - let mut store = Store::new( - &engine, - Ctx { - wasi: wasi.build(), - table: ResourceTable::new(), - }, - ); - - let mut wizer = wasmtime_wizer::Wizer::new(); - // The contract's own enumeration is the init function (version-last - // invoke syntax; the parens select the wave-call path, which - // permits results). - wizer.init_func("polymorph:test/tests.all@0.1.0()"); - // Mandatory: "stripping" the init function would remove the - // `tests.all` export itself. (Stripping a dedicated init export is - // also currently broken upstream — dangling core-instance export - // reference, bytecodealliance/wasmtime#13168.) - wizer.keep_init_func(true); - let (wizened, _rets) = wasmtime_wasi::runtime::in_tokio(wizer.run_component( - &mut store, - &wasm, - async |store: &mut Store, component| linker.instantiate_async(store, component).await, - ))?; - - std::fs::write(&output, &wizened)?; - eprintln!( - "wizened: {} -> {} bytes ({output})", - wasm.len(), - wizened.len(), - ); - Ok(()) -} diff --git a/crates/component-test-runner/src/lib.rs b/crates/component-test-runner/src/lib.rs index df138bb..d9e4a42 100644 --- a/crates/component-test-runner/src/lib.rs +++ b/crates/component-test-runner/src/lib.rs @@ -12,6 +12,9 @@ use std::path::Path; +#[cfg(feature = "wizer")] +pub mod wizen; + use component_test_core::{Provenance, Tags}; use component_test_formats::results::{ CaseResult, Envelope, Event, RunInfo, Status, SuiteInfo, RESULTS_VERSION, TERMINATOR, @@ -590,6 +593,10 @@ impl Runner { /// unlimited (single instance for the whole run; cheap suites); /// K = fresh instance every K cases. A trap always abandons the /// current instance regardless. + /// For K=1 at scale, wizening the suite removes most of the + /// per-instance enumeration cost instead of relaxing isolation + /// ([`wizen`](crate::wizen), `component-test wizen`; findings + /// 22–24). /// `jobs`: worker parallelism. Workers share the compiled engine, /// own their stores, and run the modulo stripe /// `runnable-index % jobs == worker` (expensive cases cluster, so diff --git a/crates/component-test-runner/src/wizen.rs b/crates/component-test-runner/src/wizen.rs new file mode 100644 index 0000000..32ef569 --- /dev/null +++ b/crates/component-test-runner/src/wizen.rs @@ -0,0 +1,138 @@ +//! Component-level wizer pre-initialization (#25, findings.md 22–24): +//! snapshot a suite component after running its own `tests.all()`, so +//! every fresh instance is born with the case table built and `all()` +//! costs only mint+lift. +//! +//! Works on any unmodified suite artifact: the init function is the +//! contract's own `all()` — named in the version-last invoke syntax +//! `polymorph:test/tests.all@0.1.0()` (the parenthesized wave-call +//! form; the bare item-name form requires a `[] -> []` signature). +//! The handles `all()` returns are per-call state; only the guest heap +//! (the built registry) lands in the snapshot, at no measurable size +//! cost over a dedicated no-op init export (findings.md #22). Custom +//! sections survive, so tags scheduling, drift checks, and +//! `lock --check` all work unchanged on the wizened artifact. +//! +//! Exists because the `wasmtime wizer` CLI cannot wizen a *suite*: +//! the suite world imports `test-context`, whose `context` resource +//! cannot be synthesized by unknown-import stubbing ("resource +//! implementation is missing"), and composed bundles hit "nested +//! components with modules not currently supported". Driving +//! wasmtime-wizer as a library with our own linker — WASI plus a host +//! `context` resource whose methods init never calls — sidesteps both. +//! +//! Two entry points, mirroring [`Runner::new`]/[`Runner::with_data`]: +//! [`wizen`] for plain suites (WASI + `test-context` only), and +//! [`wizen_with`] for suites with SUT imports — component +//! instantiation is eager, so a SUT-importing suite fails to wizen +//! without its host module even though init never calls it; reuse the +//! same linker setup the embedding runner already has. +//! +//! Caveat the caller must own: the snapshot freezes whatever registry +//! construction observed (env, entropy, clocks — baked at wizen-time +//! values for every future instance). For contract-conforming suites +//! that is #25's determinism *feature*, but an env-parameterized +//! registry stops being parameterizable; run the wizened artifact +//! everywhere downstream instead of mixing artifacts. +//! +//! [`Runner::new`]: crate::Runner::new +//! [`Runner::with_data`]: crate::Runner::with_data + +use wasmtime::component::{Linker, Resource, ResourceType}; +use wasmtime::{Config, Engine, Result, Store}; +use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +use crate::CONTEXT_INSTANCE; + +/// The init function: the contract's own enumeration, in version-last +/// invoke syntax (the parens select the wave-call path, which permits +/// results — finding #22). +const INIT_FUNC: &str = "polymorph:test/tests.all@0.1.0()"; + +struct Ctx { + wasi: WasiCtx, + table: ResourceTable, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +struct HostContext; + +/// Wizen a plain suite (WASI + `test-context` imports only), returning +/// the pre-initialized component bytes. `env` is the environment +/// visible during initialization — and therefore baked into the +/// snapshot for every future instance. +pub fn wizen(wasm: &[u8], env: &[(String, String)]) -> Result> { + wizen_with( + wasm, + || { + let mut wasi = WasiCtxBuilder::new(); + wasi.inherit_stderr(); + for (k, v) in env { + wasi.env(k, v); + } + Ctx { + wasi: wasi.build(), + table: ResourceTable::new(), + } + }, + |_| Ok(()), + ) +} + +/// Wizen a suite with SUT imports: `make_data` builds the store data +/// for the single init store; `configure_linker` wires the SUT's +/// `add_to_linker` (instantiation is eager, so every import must be +/// satisfiable even though init never calls the SUT). WASI and the +/// host `context` resource are provided here, exactly as +/// [`Runner::with_data`](crate::Runner::with_data) does for execution. +pub fn wizen_with( + wasm: &[u8], + make_data: impl FnOnce() -> D, + configure_linker: impl FnOnce(&mut Linker) -> Result<()>, +) -> Result> { + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_async(true); + let engine = Engine::new(&config)?; + + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; + configure_linker(&mut linker)?; + let mut ctx_instance = linker.instance(CONTEXT_INSTANCE)?; + ctx_instance.resource( + "context", + ResourceType::host::(), + |_, _| Ok(()), + )?; + // Present so the component type-checks; never called during init + // (registry builds are pure — anything else is a suite bug this + // driver would surface as a snapshot-time diagnostic call). + ctx_instance.func_wrap_concurrent( + "[method]context.diagnostic", + |_accessor, (_this, _msg): (Resource, String)| Box::pin(async move { Ok(()) }), + )?; + + let mut store = Store::new(&engine, make_data()); + + let mut wizer = wasmtime_wizer::Wizer::new(); + wizer.init_func(INIT_FUNC); + // Mandatory: "stripping" the init function would remove the + // `tests.all` export itself. (Stripping a dedicated init export is + // also currently broken upstream — dangling core-instance export + // reference, bytecodealliance/wasmtime#13168.) + wizer.keep_init_func(true); + let (wizened, _rets) = wasmtime_wasi::runtime::in_tokio(wizer.run_component( + &mut store, + wasm, + async |store: &mut Store, component| linker.instantiate_async(store, component).await, + ))?; + Ok(wizened) +} diff --git a/docs/findings.md b/docs/findings.md index 18b7f67..f02011c 100644 --- a/docs/findings.md +++ b/docs/findings.md @@ -178,7 +178,9 @@ x86_64 Linux), wasmtime 47.0.3 / deltic pre-83fff30 / Node 24. Follow-up to the bench above: pre-build the registry at build time so fresh instances are born initialized. `wasmtime-wizer` 47 as a -library; drivers: `wizer-preinit` bin (feature `wizer`) over the +library; drivers: originally the `wizer-preinit` bin, since #85 the +`wizen` module in component-test-runner (feature `wizer`) behind +`component-test wizen`; measured over the bench-suite artifact built with its `wizer-init` feature. 22. **Component-level pre-init works today (wasmtime-wizer 47) — #25's diff --git a/examples/compose/README.md b/examples/compose/README.md index daeae6c..e0a1471 100644 --- a/examples/compose/README.md +++ b/examples/compose/README.md @@ -1,5 +1,12 @@ # Composition examples +`component-test compose-runner suite.wasm -o composed.wasm` (and +`component-test run suite.wasm`, which composes in memory and +executes) wrap this entire flow with an embedded reference provider +and runner core. The raw commands below remain the recipe for custom +topologies — SUT providers, negative composition gates, debugging — +and are what the wrapper mirrors. + Two-step (canonical): bundle the suite with the reference context provider, then plug the bundle into a runner core. diff --git a/justfile b/justfile index d6c92e4..f38e0c3 100644 --- a/justfile +++ b/justfile @@ -19,8 +19,7 @@ default: ci: (gha::host-checks) (gha::verify) # Everything: host tests, component builds, all verification paths. -all: build test test-wasm lock-check verify-embed verify-compose verify-deltic verify-pipeline verify-aggregate verify-viewer verify-imports verify-emit - +all: build test test-wasm lock-check verify-embed verify-compose verify-cli verify-deltic verify-pipeline verify-aggregate verify-viewer verify-imports verify-emit # The fast pre-commit checks: formatting, clippy, host tests, WIT # validation. The CI job of the same name runs the identical set # through gha::host-checks. @@ -99,6 +98,42 @@ verify-compose: build diff -u expected/verify-pipeline-sample-fold.txt "$tmp/fold.txt" echo "verify-compose: output matches expected/ (incl. JSONL + cross-runner fold)" +# Path 2b: the CLI's composition/execution subcommands (#85). +# compose-runner (embedded provider + runner core) must reproduce Path +# 2's goldens under the wasmtime CLI; run is the same composition under +# the embedded wasmtime (human + JSONL legs); wizen pre-initializes +# with inventory, scheduling, and runnability intact (findings 22–24). +# This is also the embedded artifacts' freshness gate: behavioral drift +# between components/{runner-cli,provider} and the committed +# crates/component-test-cli/embedded/ copies fails the diffs +# (byte-comparing builds across environments is off the table, #44) — +# after changing those components run `just embed-update` and commit. +verify-cli: build + #!/usr/bin/env bash + set -euo pipefail + cli() { cargo run -q -p component-test-cli -- "$@"; } + ct() { cargo run -q -p component-test-runner --bin ct-runner -- "$@"; } + tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT + cli compose-runner {{release_dir}}/sample_suite.wasm -o "$tmp/composed.wasm" + out=$(wasmtime run {{wasmtime_flags}} "$tmp/composed.wasm") && code=0 || code=$? + test "$code" -eq 1 + diff -u expected/verify-run-sample.txt <(printf '%s\n' "$out") + out=$(cli run {{release_dir}}/sample_suite.wasm) && code=0 || code=$? + test "$code" -eq 1 + diff -u expected/verify-run-sample.txt <(printf '%s\n' "$out") + jsonl=$(cli run --jsonl {{release_dir}}/sample_suite.wasm) && code=0 || code=$? + test "$code" -eq 1 + diff -u expected/verify-compose-sample.jsonl <(printf '%s\n' "$jsonl") + cli wizen {{release_dir}}/sample_suite.wasm -o "$tmp/wizened.wasm" + cli lock "$tmp/wizened.wasm" --check components/sample-suite/tests.lock + out=$(ct "$tmp/wizened.wasm") && code=0 || code=$? + test "$code" -eq 1 + diff -u expected/verify-embed-sample.txt <(printf '%s\n' "$out") + out=$(cli run "$tmp/wizened.wasm") && code=0 || code=$? + test "$code" -eq 1 + diff -u expected/verify-run-sample.txt <(printf '%s\n' "$out") + echo "verify-cli: output matches expected/ (compose-runner, run, wizen)" + # The one-version-everywhere gate for the deltic pin (successor to the # retired fetch script's `assertPinConsistency`): every jsr:@deltic/* # specifier in every deno.json, and every @deltic package the lock @@ -386,6 +421,18 @@ lock-update: build cargo run -q -p component-test-cli -- lock \ {{release_dir}}/fixture_suite.wasm -o components/fixture-suite/tests.lock +# Regenerate the components baked into the CLI (compose-runner/run +# defaults) after changing components/runner-cli or components/provider, +# and commit the diff. Size-optimized `embed` profile; freshness is +# gated behaviorally by verify-cli (no byte comparison — builds are not +# reproducible across environments, #44). +embed-update: + cargo build --target {{wasm_target}} --profile embed -p runner-cli -p provider + cp target/{{wasm_target}}/embed/runner_cli.wasm \ + crates/component-test-cli/embedded/runner-cli.wasm + cp target/{{wasm_target}}/embed/provider.wasm \ + crates/component-test-cli/embedded/provider.wasm + # --- WIT --------------------------------------------------------------- # Component WIT dirs are symlinks into the canonical copies (wit/ and