test: declarative test.toml + target-derived runners (issue #8) #11

Merged
jorijnvdgraaf merged 3 commits from claude/issue-8 into master 2026-05-27 18:10:08 +02:00

Closes issue #8.

Implements the redesign agreed on in issue #8 comment-131 / comment-136:
test discovery splits into two distinct shapes, runner is derived from the
target triple, and the env-var-driven transport runners are retired.

Two-bucket model

  • Declarative (test.toml + main.cpp) — for downstream test authors.
    Pick a target, declare preconditions, done. No project.cpp boilerplate
    per test.
  • Outer-driver (project.cpp) — for tests that exercise the build
    engine API in-process (Incremental, Libraries, Cuda, Shader,
    ShaderDep, BuildError, RunnerClassification, CrossProjectModule,
    Diamond, UnitLib).

project.cpp and test.toml are mutually exclusive per fixture — a
discovery error flags the conflict.

test.toml schema

```toml
target = "aarch64-linux-gnu" # optional; default = host
march = "armv8-a"
mtune = "generic"
sysroot = "/opt/aarch64-rootfs" # propagated to QEMU_LD_PREFIX
timeout = 30 # seconds; default 60
args = ["--seed=42"]
requires = [
"tool:qemu-aarch64", # which/where probes PATH
"file:/opt/aarch64-rootfs/usr/share/libc++/v1/std.cppm",
"env:CRAFTER_TEST_TOKEN",
]

[defines]
CRAFTER_TEST_FOO = "42"
```

Runner derivation (TestRunner::ForTarget)

target runner
host triple Local
`-w64-mingw32` / windows- Wine (or Local on Windows host)
`wasm32-wasip1` `wasmtime`
non-host `-linux-` triple `qemu-` + `QEMU_LD_PREFIX=`

`CRAFTER_BUILD_RUNNER_` survives as a power-user override (parsed
via the narrowed FromSpec: `local` | `cmd:`).

Hard-fail-unless-declared

When the derived runner depends on a tool (qemu/wine/wasmtime) and that
tool isn't on PATH, the test fails unless the test declared
`tool:` in `requires`. The intent: surface broken cross-arch CI
config instead of letting it masquerade as a silent Skip.

Multi-target sweep

Bare `crafter-build test` (no --target=) now sweeps every distinct target
declared across the suite plus host. Tests run at their declared target
only, so cross-arch fixtures don't drag the rest of the suite into a
cross-build. `--target=X` bypasses the sweep.

Migrations

Test Before After
CrossArchAarch64 outer driver + inner fixture main.cpp + test.toml
Wasi outer driver + inner fixture main.cpp + test.toml
Defines project.cpp with cfg.defines test.toml `[defines]`
WindowsViaSsh outer driver, env-gated replaced by WindowsViaWine
SshRunner, QemuUser tested the env-var plumbing deleted — plumbing is gone
Diamond, CrossProjectModule --target=-reading speculation hardcoded HostTarget()

WindowsViaWine is new: declarative target=x86_64-w64-mingw32, requires
`tool:wine` + `tool:x86_64-w64-mingw32-g++`. Replaces the SSH-to-Windows
test, which needed a reachable Windows VM and a hand-rolled
ssh+scp+cmd.exe shell chain.

The defense-in-depth artifact checks (CrossArchAarch64's `file ... | grep
'ARM aarch64'` and Wasi's WASM magic bytes) are dropped on purpose: qemu
refuses to run wrong-arch ELFs and wasmtime refuses non-WASM input, so a
silent host-arch fallback would still fail the run.

Deletions

  • TestRunner::{Ssh, SshWin, Wsl} factories + the copy/exec/cleanup
    template machinery.
  • TestRunner::Shell enum (Host/Sh/Cmd), ShellQuoteSh, and the per-shell
    switch in JoinAndQuoteArgs.
  • TestRunner::{copy, cleanup, remoteDir, argsShell} fields.
  • WindowsPathToWsl + the transport branch in RunSingleTest
    ({remote_bundle}, {bin_win}, {bundle_wsl} placeholder substitution).
  • ParseRunnerSpec narrowed from {local, cmd, ssh, sshwin, wsl} to
    {local, cmd}.

Vendors toml++ v3.4.0 as `lib/toml.hpp` (single-header, MIT, json.hpp
style). Added to the GMF of Crafter.Build-Test.cpp only so it doesn't
flow into PCM consumers.

Verification

`crafter-build test` on this host (no `wasmtime` installed):

```
=== target: aarch64-linux-gnu === CrossArchAarch64 (cmd:qemu-aarch64)
=== target: wasm32-wasip1 === ⏭ Wasi (cmd:wasmtime) skipped: tool 'wasmtime' not on PATH
=== target: x86_64-pc-linux-gnu === 13 passed (all outer-driver + arch-agnostic)
=== target: x86_64-w64-mingw32 === WindowsViaWine (wine)
```

Also verified single-target mode (`--target=x86_64-pc-linux-gnu`,
`--target=aarch64-linux-gnu`) works as before.

Test plan

  • CI: full sweep on Linux host (qemu-user + wine present, wasmtime missing → Wasi politely skips)
  • CI: single-target mode `--target=x86_64-pc-linux-gnu` runs only host tests
  • Windows: native build via build.cmd; sweep over host triple (no Wine on Windows)

🤖 Generated with Claude Code

Closes issue #8. Implements the redesign agreed on in issue #8 comment-131 / comment-136: test discovery splits into two distinct shapes, runner is derived from the target triple, and the env-var-driven transport runners are retired. ## Two-bucket model - **Declarative (`test.toml` + `main.cpp`)** — for downstream test authors. Pick a target, declare preconditions, done. No `project.cpp` boilerplate per test. - **Outer-driver (`project.cpp`)** — for tests that exercise the build engine API in-process (`Incremental`, `Libraries`, `Cuda`, `Shader`, `ShaderDep`, `BuildError`, `RunnerClassification`, `CrossProjectModule`, `Diamond`, `UnitLib`). `project.cpp` and `test.toml` are mutually exclusive per fixture — a discovery error flags the conflict. ## test.toml schema \`\`\`toml target = \"aarch64-linux-gnu\" # optional; default = host march = \"armv8-a\" mtune = \"generic\" sysroot = \"/opt/aarch64-rootfs\" # propagated to QEMU_LD_PREFIX timeout = 30 # seconds; default 60 args = [\"--seed=42\"] requires = [ \"tool:qemu-aarch64\", # which/where probes PATH \"file:/opt/aarch64-rootfs/usr/share/libc++/v1/std.cppm\", \"env:CRAFTER_TEST_TOKEN\", ] [defines] CRAFTER_TEST_FOO = \"42\" \`\`\` ## Runner derivation (TestRunner::ForTarget) | target | runner | |------------------------------|--------------------------------------------------| | host triple | Local | | \`*-w64-mingw32\` / windows-* | Wine (or Local on Windows host) | | \`wasm32-wasip1\` | \`wasmtime\` | | non-host \`-linux-\` triple | \`qemu-<arch>\` + \`QEMU_LD_PREFIX=<sysroot>\` | \`CRAFTER_BUILD_RUNNER_<target>\` survives as a power-user override (parsed via the narrowed FromSpec: \`local\` | \`cmd:<binary>\`). ## Hard-fail-unless-declared When the derived runner depends on a tool (qemu/wine/wasmtime) and that tool isn't on PATH, the test fails unless the test declared \`tool:<that>\` in \`requires\`. The intent: surface broken cross-arch CI config instead of letting it masquerade as a silent Skip. ## Multi-target sweep Bare \`crafter-build test\` (no --target=) now sweeps every distinct target declared across the suite plus host. Tests run at their declared target only, so cross-arch fixtures don't drag the rest of the suite into a cross-build. \`--target=X\` bypasses the sweep. ## Migrations | Test | Before | After | |---------------------|---------------------------------|-----------------------------| | CrossArchAarch64 | outer driver + inner fixture | main.cpp + test.toml | | Wasi | outer driver + inner fixture | main.cpp + test.toml | | Defines | project.cpp with cfg.defines | test.toml \`[defines]\` | | WindowsViaSsh | outer driver, env-gated | replaced by WindowsViaWine | | SshRunner, QemuUser | tested the env-var plumbing | deleted — plumbing is gone | | Diamond, CrossProjectModule | --target=-reading speculation | hardcoded HostTarget() | WindowsViaWine is new: declarative target=x86_64-w64-mingw32, requires \`tool:wine\` + \`tool:x86_64-w64-mingw32-g++\`. Replaces the SSH-to-Windows test, which needed a reachable Windows VM and a hand-rolled ssh+scp+cmd.exe shell chain. The defense-in-depth artifact checks (CrossArchAarch64's \`file ... | grep 'ARM aarch64'\` and Wasi's WASM magic bytes) are dropped on purpose: qemu refuses to run wrong-arch ELFs and wasmtime refuses non-WASM input, so a silent host-arch fallback would still fail the run. ## Deletions - TestRunner::{Ssh, SshWin, Wsl} factories + the copy/exec/cleanup template machinery. - TestRunner::Shell enum (Host/Sh/Cmd), ShellQuoteSh, and the per-shell switch in JoinAndQuoteArgs. - TestRunner::{copy, cleanup, remoteDir, argsShell} fields. - WindowsPathToWsl + the transport branch in RunSingleTest ({remote_bundle}, {bin_win}, {bundle_wsl} placeholder substitution). - ParseRunnerSpec narrowed from {local, cmd, ssh, sshwin, wsl} to {local, cmd}. Vendors toml++ v3.4.0 as \`lib/toml.hpp\` (single-header, MIT, json.hpp style). Added to the GMF of Crafter.Build-Test.cpp only so it doesn't flow into PCM consumers. ## Verification \`crafter-build test\` on this host (no \`wasmtime\` installed): \`\`\` === target: aarch64-linux-gnu === ✅ CrossArchAarch64 (cmd:qemu-aarch64) === target: wasm32-wasip1 === ⏭ Wasi (cmd:wasmtime) skipped: tool 'wasmtime' not on PATH === target: x86_64-pc-linux-gnu === 13 passed (all outer-driver + arch-agnostic) === target: x86_64-w64-mingw32 === ✅ WindowsViaWine (wine) \`\`\` Also verified single-target mode (\`--target=x86_64-pc-linux-gnu\`, \`--target=aarch64-linux-gnu\`) works as before. ## Test plan - [ ] CI: full sweep on Linux host (qemu-user + wine present, wasmtime missing → Wasi politely skips) - [ ] CI: single-target mode \`--target=x86_64-pc-linux-gnu\` runs only host tests - [ ] Windows: native build via build.cmd; sweep over host triple (no Wine on Windows) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Vendors toml++ v3.4.0 as lib/toml.hpp and wires it into Crafter.Build-Test
to parse a declarative test.toml manifest (target/march/mtune/sysroot/
requires/timeout/args/defines). Test discovery now treats project.cpp and
test.toml as mutually exclusive: project.cpp stays the escape hatch for
outer-driver tests, test.toml gives downstream test authors a no-boilerplate
path.

Adds:
- TestRunner::Wine() and TestRunner::ForTarget(cfg) — runner is now derived
  from cfg.target (Local for host, Wine for Windows-on-Linux, wasmtime for
  WASI, qemu-<arch> with QEMU_LD_PREFIX for non-host Linux). The env-var
  override CRAFTER_BUILD_RUNNER_<target> still wins as a power-user escape
  hatch via FromEnv.
- Declarative preconditions: tool:<name>, file:<path>, env:<VAR> are
  evaluated before the build; missing preconditions Skip without paying
  the compile cost.
- Hard-fail-unless-declared: when a derived runner's tool is missing AND
  the test didn't declare 'tool:<that>' in requires, the missing runner
  is a Fail instead of a silent Skip. Surfaces broken cross-arch CI
  config that previously hid as "skipped".
- Multi-target sweep: bare `crafter-build test` (no --target=) now
  iterates every distinct test.toml-declared target plus the host, so
  cross-arch tests run by default without the user needing to know which
  targets exist. `--target=X` bypasses the sweep.

Test struct gains a `requires_` vector so project.cpp users can declare
preconditions too (matching what test.toml writes there).

Existing tests, factories (Ssh/SshWin/Wsl/Cmd), and CRAFTER_BUILD_RUNNER_*
machinery remain intact — this commit only adds; migration and deletion
follow in subsequent commits.

Refs issue #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapses three outer-driver tests into declarative top-level fixtures:

  - CrossArchAarch64: outer + inner pair becomes main.cpp + test.toml. The
    'is this really an ARM aarch64 ELF?' artifact-introspection check is
    dropped — qemu-aarch64 refuses to run wrong-arch ELFs anyway, so a
    silent host-arch fallback would still fail the run.
  - Wasi: outer + inner pair becomes main.cpp + test.toml. The WASM
    magic-byte check is dropped on the same logic (wasmtime refuses
    non-WASM input).
  - Defines: simple defines-propagation smoke test becomes test.toml with
    [defines] CRAFTER_TEST_FOO = "42".

Adds WindowsViaWine to replace the (forthcoming-deletion) WindowsViaSsh:
declarative target=x86_64-w64-mingw32 + requires=[tool:wine,
tool:x86_64-w64-mingw32-g++]. Exercises the new Wine runner and the
ForTarget derivation end-to-end.

Diamond and CrossProjectModule had speculative --target= reading that
made them attempt cross-compilation under the multi-target sweep. They
have no sysroot/toolchain plumbing, so those cross-builds always fail.
Hardcoded them to HostTarget(); cross-arch tests live in their own
test.toml from now on.

Refs issue #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test: drop transport runners (ssh/sshwin/wsl) and the Shell-quoting enum
All checks were successful
CI / build-test-release (pull_request) Successful in 9m56s
8de93aaf06
With test.toml + ForTarget covering the cross-arch + Windows-on-Linux
cases, the env-var-driven transport runners are dead weight. This commit
removes them and the retired tests that exercised the env-var plumbing:

  - TestRunner::Ssh / SshWin / Wsl factories and their copy/exec/cleanup
    template machinery.
  - TestRunner::Shell enum (Host/Sh/Cmd) and the ShellQuoteSh helper —
    only Host shell quoting is needed once the remote shells are gone.
  - TestRunner::copy / cleanup / remoteDir / argsShell fields.
  - WindowsPathToWsl and the {remote_bundle}/{bin_win}/{bundle_wsl}
    placeholder substitution in RunSingleTest's transport branch.
  - ParseRunnerSpec narrowed from {local, cmd, ssh, sshwin, wsl} to
    {local, cmd} — the override hatch is preserved, just simpler.
  - tests/SshRunner, tests/WindowsViaSsh, tests/QemuUser: these tested
    the CRAFTER_BUILD_RUNNER_<target> → runner plumbing that has been
    replaced by ForTarget. The runner derivation is exercised every
    time CrossArchAarch64 / Wasi / WindowsViaWine runs.
  - tests/UnitLib: ssh/sshwin spec assertions become "throws on bogus
    spec" assertions.

Refs issue #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jorijnvdgraaf deleted branch claude/issue-8 2026-05-27 18:10:08 +02:00
Sign in to join this conversation.
No description provided.