diff --git a/AGENTS.md b/AGENTS.md index b5fcab9..7298af6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,84 +6,86 @@ Wasm file (with embedded WIT custom section) and generates Go bindings targeting ## Build & Run +Every routine operation is a `just` recipe; `just --list` shows them all. + ### Prerequisites -- Rust toolchain (version pinned to what's in in `rust-toolchain.toml`) -- `wasm32-unknown-unknown` and `wasm32-wasip1` targets (configured in - `rust-toolchain.toml`) +- [just](https://just.systems/) +- Rust toolchain (version pinned in `rust-toolchain.toml`). + `just install` installs it with the `wasm32-unknown-unknown` and + `wasm32-wasip1` targets the file configures. - Go (for running example tests) +The recipes ignore an inherited `RUSTUP_TOOLCHAIN` or `CARGO_TARGET_DIR`, so +they use the toolchain pinned in `rust-toolchain.toml` and the `target/` +directory that `examples/generate.go` reads from. + ### Building ```sh -cargo build +just build ``` ### Running Gravity takes a Core Wasm file and produces Go source + an optimized `.wasm` -file: +file. `just run` passes its arguments to gravity: ```sh -cargo run --bin gravity -- --world --output +just run --world --output ``` ## Testing +`just check` runs the same steps as `.github/workflows/ci.yml`, in the same +order: format check, build, unit tests, the Go example tests and the CLI +snapshot tests. Each step is also its own recipe, described below. + ### Unit Tests Run all unit tests (does NOT include snapshot/CLI tests): ```sh -cargo test +just test ``` ### Snapshot / CLI Tests -The CLI tests use `trycmd` for snapshot testing and require the example Wasm -files to be built first. They are excluded from the default `cargo test` run. +The CLI tests use `trycmd` for snapshot testing and read the example Wasm +files, so `just test-cli` builds every example first. They are excluded from +`just test`. ```sh -# 1. Build example Wasm files -cargo build -p example-basic --target wasm32-unknown-unknown --release -cargo build -p example-iface-method-returns-string --target wasm32-unknown-unknown --release -cargo build -p example-instructions --target wasm32-unknown-unknown --release -cargo build -p example-regressions --target wasm32-unknown-unknown --release - -# 2. Run CLI snapshot tests -cargo test --test cli +just test-cli ``` -To update snapshot expectations when output changes intentionally: +To update snapshot expectations when output changes intentionally, then read +the diff before committing it: ```sh -TRYCMD=overwrite cargo test --test cli +just update-snapshots ``` Snapshot files live in `cmd/gravity/tests/cmd/` (`.toml` for config, `.stdout` and `.stderr` for expected output). **Important**: The `trycmd` dependency has the `filesystem` feature disabled. -This means `TRYCMD=overwrite` can only update _existing_ `.stdout`/`.stderr` -files — it cannot create new ones. When adding a new snapshot test, you must -manually create the `.stdout` and `.stderr` files first (e.g. by redirecting -gravity's output), then `TRYCMD=overwrite` can keep them in sync afterward. +This means `just update-snapshots` can only update _existing_ +`.stdout`/`.stderr` files — it cannot create new ones. `just new-snapshot +` creates all three files for a new example from gravity's current +output; after that, `just update-snapshots` keeps them in sync. ### Example Go Tests The examples include Go test files that verify the generated bindings work -end-to-end with wazero. After building the example Wasm files and running -Gravity to regenerate bindings: +end-to-end with wazero. `examples/generate.go` has `//go:generate` directives +that build each example's Wasm and run Gravity on it. `just generate` runs +them, and `just test-go` runs them and then the Go tests: ```sh -cd examples -go generate ./... -go test ./... +just test-go ``` -Or use `go generate` from the repo root (the `examples/generate.go` file has -`//go:generate` directives that build Wasm and run Gravity). - ## Project Structure ```text @@ -120,13 +122,18 @@ examples/ iface-method-returns-string/ # Interface method returning a string instructions/ # Tests various canonical ABI instructions regressions/ # Regression tests for import codegen edge cases + +justfile # Recipes for building, testing and the CI gate ``` **Note on generated files**: `examples/.gitignore` ignores all `*/*.go` files (except `*/*_test.go`) and all `*/*.wasm` files. This means the generated Go bindings and Wasm binaries are not committed — only the test files, WIT -definitions, and Rust source are tracked. You must run `go generate ./...` from -the `examples/` directory before the Go tests will compile. +definitions, and Rust source are tracked. The Go tests compile only after +`just generate` has run, which `just test-go` does first. After switching to a +branch without an example, its directory holds only these generated files, and +the `examples/*` workspace glob then fails every cargo command with "failed to +load manifest"; `just clean-examples` removes them. ## Architecture @@ -200,12 +207,12 @@ Bump every example's pins with them. ## Style & Conventions - Use conventional commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, etc. -- Format code before committing with `cargo fmt` -- Lint with `cargo clippy` +- Format code before committing with `just format` +- Lint with `just clippy` (CI does not run clippy) - When adding new instruction handlers in `func.rs`, add corresponding entries in the `instructions` example and update snapshot tests -- When changing codegen output, update snapshot `.stdout` files (or use - `TRYCMD=overwrite`) +- When changing codegen output, update snapshot `.stdout` files with + `just update-snapshots` ## Adding a New Example @@ -216,9 +223,11 @@ Bump every example's pins with them. 3. Add `//go:generate` directives to `examples/generate.go` (one for `cargo build`, one for `cargo run --bin gravity`) 4. Write `examples//_test.go` (this file IS committed) -5. Create snapshot test files in `cmd/gravity/tests/cmd/`: +5. Create the snapshot test files with `just new-snapshot `, or + `just new-snapshot ` when the world is not named for the + example. It builds the example and writes three files to + `cmd/gravity/tests/cmd/`: - `.toml` — trycmd config (`bin.name = "gravity"`, `args = "..."`) - - `.stdout` — capture with - `cargo run --bin gravity -- --world > ` - - `.stderr` — typically empty (`touch `) -6. Verify: `cargo test && cargo test --test cli && (cd examples && go generate ./... && go test ./...)` + - `.stdout` — gravity's output for the example + - `.stderr` — empty +6. Verify: `just check` diff --git a/justfile b/justfile new file mode 100644 index 0000000..7fd2465 --- /dev/null +++ b/justfile @@ -0,0 +1,158 @@ +# Gravity command runner. +# Run `just --list` to see available recipes, or `just ` to run one. +# +# `just check` runs the same steps as .github/workflows/ci.yml, in the same +# order. Change both together. + +# show available recipes (default when no recipe is given) +[private] +default: + @just --list + +# rust-toolchain.toml pins the toolchain CI uses; an inherited override would +# replace it. +unexport RUSTUP_TOOLCHAIN + +# examples/generate.go builds into and reads from ../target, so a relocated +# target directory makes `go generate` fail with "unable to read file". +unexport CARGO_TARGET_DIR + +# styled header: bold cyan ✦, bold white label +h := BOLD + CYAN + "✦" + WHITE + " " +n := NORMAL + +# ─── Root commands ─────────────────────────────────────────────────────────────── + +# run the full CI gate: format check, build, unit tests, Go example tests, CLI +# snapshot tests +[doc('run the full CI gate')] +[group('ci')] +check: lint build test test-go test-cli + +# install the toolchain, components and targets pinned in rust-toolchain.toml +[group('setup')] +install: + @echo '{{ h }}install: rustup toolchain{{ n }}' + @rustup toolchain install --no-self-update + +# auto-format Rust code and this justfile +[group('format')] +format: + @echo '{{ h }}format: cargo fmt{{ n }}' + @cargo fmt --all + @echo '{{ h }}format: justfile{{ n }}' + @just --fmt + +# check formatting of Rust code and this justfile +[group('ci')] +[group('lint')] +lint: + @echo '{{ h }}lint: cargo fmt{{ n }}' + @cargo fmt --all --check + @echo '{{ h }}lint: justfile{{ n }}' + @just --fmt --check + +# lint with clippy. Not part of `check`, because CI does not run clippy. +[doc('lint with clippy (not part of the CI gate)')] +[group('lint')] +clippy: + @echo '{{ h }}lint: cargo clippy{{ n }}' + @cargo clippy --workspace --all-targets --locked + +# ─── Build and test ────────────────────────────────────────────────────────────── + +# build the workspace +[group('build')] +[group('ci')] +build: + @echo '{{ h }}build: cargo build{{ n }}' + @cargo build --locked + +# run the Rust unit tests. The CLI snapshot tests are excluded; see `test-cli`. +[doc('run the Rust unit tests')] +[group('ci')] +[group('test')] +test: + @echo '{{ h }}test: cargo test{{ n }}' + @cargo test --locked + +# run gravity with the given arguments, e.g. +# `just run --world basic --output basic.go basic.wasm` +[doc('run gravity with the given arguments')] +[group('build')] +[positional-arguments] +run *args: + @cargo run --quiet --locked --bin gravity -- "$@" + +# build the example wasm and generate their Go bindings +[group('build')] +generate: + @echo '{{ h }}generate: example wasm and Go bindings{{ n }}' + @go generate ./... + +# run the Go example tests against freshly generated bindings +[group('ci')] +[group('test')] +test-go: generate + @echo '{{ h }}test: go test{{ n }}' + @go test ./... + +# run the trycmd CLI snapshot tests. They read the example wasm that +# `generate` builds. +[doc('run the trycmd CLI snapshot tests')] +[group('ci')] +[group('test')] +test-cli: generate + @echo '{{ h }}test: cargo test --test cli{{ n }}' + @cargo test --locked --test cli + +# rewrite the CLI snapshot .stdout and .stderr files from current output. Read +# the diff before committing it. trycmd cannot create a snapshot file, so a new +# test's files must exist first; see AGENTS.md. +[doc('rewrite existing CLI snapshot files from current output')] +[group('test')] +update-snapshots: generate + @echo '{{ h }}test: cargo test --test cli (overwrite){{ n }}' + @TRYCMD=overwrite cargo test --locked --test cli + +# create the CLI snapshot files for the example in examples/: a .toml +# that runs gravity on the example's wasm, the .stdout gravity prints for it, +# and an empty .stderr. trycmd cannot create these files, only update them. +# `world` defaults to the example's name. Existing files are left alone. +[doc('create the CLI snapshot files for a new example')] +[group('test')] +[script('bash')] +new-snapshot name world=name: + set -euo pipefail + name={{ quote(name) }} + world={{ quote(world) }} + base="cmd/gravity/tests/cmd/$name" + for ext in toml stdout stderr; do + if [[ -e "$base.$ext" ]]; then + echo "{{ style("error") }}error:{{ n }} $base.$ext already exists" >&2 + exit 1 + fi + done + wasm="target/wasm32-unknown-unknown/release/example_${name//-/_}.wasm" + echo "{{ h }}build: example-$name (wasm){{ n }}" + cargo build --locked -p "example-$name" --target wasm32-unknown-unknown --release + echo "{{ h }}snapshot: $name{{ n }}" + # Capture first, so a failing gravity run leaves no partial snapshot behind. + stdout="$(mktemp)" + trap 'rm -f "$stdout"' EXIT + cargo run --quiet --locked --bin gravity -- --world "$world" "$wasm" > "$stdout" + # trycmd runs gravity from cmd/gravity, so the .toml's path is relative to it. + printf 'bin.name = "gravity"\nargs = "--world %s ../../%s"\n' "$world" "$wasm" > "$base.toml" + cp "$stdout" "$base.stdout" + : > "$base.stderr" + +# ─── Cleanup ───────────────────────────────────────────────────────────────────── + +# remove gitignored example output. After switching to a branch without an +# example, its directory holds only generated files, and the examples/* +# workspace glob then fails every cargo command with "failed to load manifest". +[doc('remove gitignored example output (generated Go and wasm)')] +[group('setup')] +clean-examples: + @echo '{{ h }}clean: examples{{ n }}' + @git clean -f -X -- examples/