|
| 1 | +#!/usr/bin/env bash |
| 2 | +# requires: unix-shell |
| 3 | +# 330_runner_hosted_targets.sh — `[target.<triple>].runner` on a hosted target, |
| 4 | +# and what `mcpp run` / `mcpp test` say when this host cannot execute an |
| 5 | +# artifact (#544). |
| 6 | +# |
| 7 | +# Design: .agents/docs/2026-09-02-runner-beyond-baremetal-design.md, §11 lists |
| 8 | +# the criteria this file implements. Two properties of the setup carry the |
| 9 | +# whole test: |
| 10 | +# |
| 11 | +# 1. No emulator is needed and none is used. The runner is a shell script |
| 12 | +# that records its argv and then executes it, so "the artifact went |
| 13 | +# through the runner" is asserted on the exact argv, not on a message. |
| 14 | +# 2. The "artifact this host cannot execute" is a real host binary whose ELF |
| 15 | +# e_machine is patched to 0xffff AFTER the build. No binfmt_misc entry |
| 16 | +# and no native loader accepts it, so posix_spawnp answers ENOEXEC on |
| 17 | +# every Linux host — one with qemu-user registered included — and the |
| 18 | +# criterion does not depend on which machine runs it. |
| 19 | +# |
| 20 | +# `requires: unix-shell` and nothing else, on purpose: a `requires: gcc` or |
| 21 | +# `requires: llvm` guard skips on both CI shards, and a skip exits 0. |
| 22 | +# The ELF half is Linux-only (macOS artifacts are Mach-O); the runner half |
| 23 | +# runs on every POSIX host. |
| 24 | +set -uo pipefail |
| 25 | + |
| 26 | +TMP=$(mktemp -d) |
| 27 | +trap "rm -rf $TMP" EXIT |
| 28 | +cd "$TMP" |
| 29 | + |
| 30 | +fail() { echo "FAIL: $*"; exit 1; } |
| 31 | +MCPP="${MCPP:?set MCPP to the mcpp binary under test}" |
| 32 | + |
| 33 | +"$MCPP" new app >/dev/null 2>&1 || fail "mcpp new" |
| 34 | +cd app |
| 35 | +mkdir -p tests |
| 36 | +rm -f tests/*.cpp # the scaffold's own smoke test; the counts below are exact |
| 37 | +cat > src/main.cpp <<'EOF' |
| 38 | +#include <cstdio> |
| 39 | +int main() { std::puts("ARTIFACT-RAN"); return 0; } |
| 40 | +EOF |
| 41 | +cat > tests/one.cpp <<'EOF' |
| 42 | +int main() { return 0; } |
| 43 | +EOF |
| 44 | + |
| 45 | +# The host triple as the manifest spells it. Read from the engine rather than |
| 46 | +# guessed: `[target.<triple>]` is matched against the resolved target. |
| 47 | +out=$("$MCPP" build 2>&1) || fail "initial build: $out" |
| 48 | +HOST=$(sed -n 's/.*Target \([^ ]*\) → .*/\1/p' <<<"$out" | head -1) |
| 49 | +[[ -n "$HOST" ]] || HOST=$(ls target | head -1) |
| 50 | +[[ -n "$HOST" ]] || fail "could not determine the host triple from: $out" |
| 51 | + |
| 52 | +cat > "$TMP/runner.sh" <<'EOF' |
| 53 | +#!/bin/sh |
| 54 | +printf '%s\n' "$@" >> "$RUNNER_LOG" |
| 55 | +exec "$@" |
| 56 | +EOF |
| 57 | +chmod +x "$TMP/runner.sh" |
| 58 | +export RUNNER_LOG="$TMP/runner.log" |
| 59 | + |
| 60 | +# ── 1. a declared runner is used on a hosted target, on both `run` doors ──── |
| 61 | +printf '\n[target.%s]\nrunner = ["%s"]\n' "$HOST" "$TMP/runner.sh" >> mcpp.toml |
| 62 | +out=$("$MCPP" run 2>&1) || fail "run through runner: $out" |
| 63 | +grep -q "ARTIFACT-RAN" <<<"$out" || fail "artifact output missing: $out" |
| 64 | +[[ -s "$RUNNER_LOG" ]] || fail "runner was not invoked (declared under [target.$HOST]): $out" |
| 65 | +exe=$(head -1 "$RUNNER_LOG") |
| 66 | +[[ "$exe" == */bin/app ]] || fail "runner argv[0] is not the artifact: $exe" |
| 67 | +: > "$RUNNER_LOG" |
| 68 | +out=$("$MCPP" run 2>&1) || fail "second run (fast path): $out" |
| 69 | +[[ -s "$RUNNER_LOG" ]] || fail "the run fast path bypassed the declared runner: $out" |
| 70 | + |
| 71 | +# ── 2. --no-runner executes directly and says so ─────────────────────────── |
| 72 | +: > "$RUNNER_LOG" |
| 73 | +out=$("$MCPP" run --no-runner 2>&1) || fail "--no-runner: $out" |
| 74 | +grep -q "ARTIFACT-RAN" <<<"$out" || fail "--no-runner lost the program output: $out" |
| 75 | +grep -q "no-runner" <<<"$out" || fail "--no-runner printed no note: $out" |
| 76 | +[[ ! -s "$RUNNER_LOG" ]] || fail "--no-runner still invoked the runner" |
| 77 | + |
| 78 | +# ── 3. runner not found: names the program and the search list, exit 2 ───── |
| 79 | +sed -i.bak "s|^runner = .*|runner = [\"mcpp-e2e-no-such-runner\"]|" mcpp.toml |
| 80 | +out=$("$MCPP" run 2>&1); rc=$? |
| 81 | +[[ $rc -eq 2 ]] || fail "missing runner: exit $rc, want 2: $out" |
| 82 | +grep -q "runner 'mcpp-e2e-no-such-runner' for '$HOST' was not found" <<<"$out" \ |
| 83 | + || fail "missing runner not named: $out" |
| 84 | +grep -q "Searched:" <<<"$out" || fail "search list missing: $out" |
| 85 | +grep -q "ARTIFACT-RAN" <<<"$out" && fail "artifact ran without its runner: $out" |
| 86 | +# `mcpp test`, one test: every test is not-run with that reason, exit 2. |
| 87 | +out=$("$MCPP" test 2>&1); rc=$? |
| 88 | +[[ $rc -eq 2 ]] || fail "test with missing runner: exit $rc, want 2: $out" |
| 89 | +grep -qE "NOT RUN\. 0 passed; 0 failed; 1 not run \(runner 'mcpp-e2e-no-such-runner'" <<<"$out" \ |
| 90 | + || fail "test summary with missing runner: $out" |
| 91 | +grep -q "one ... not run" <<<"$out" || fail "per-test not-run line missing: $out" |
| 92 | +# --no-runner on `test` runs it directly. |
| 93 | +out=$("$MCPP" test --no-runner 2>&1) || fail "test --no-runner: $out" |
| 94 | +grep -qE "ok\. 1 passed; 0 failed; finished" <<<"$out" || fail "test --no-runner summary: $out" |
| 95 | + |
| 96 | +# ── 4. no runner declared, artifact this host cannot load (ELF only) ─────── |
| 97 | +if [[ "$(uname -s)" == Linux ]]; then |
| 98 | + sed -i.bak "/^\[target\.$HOST\]/,/^runner = /d" mcpp.toml |
| 99 | + grep -q "runner" mcpp.toml && fail "runner key still present after removal" |
| 100 | + "$MCPP" build >/dev/null 2>&1 || fail "rebuild without runner" |
| 101 | + bin=$(ls target/*/*/bin/app | head -1) |
| 102 | + [[ -f "$bin" ]] || fail "no artifact at target/*/*/bin/app" |
| 103 | + printf '\xff\xff' | dd of="$bin" bs=1 seek=18 conv=notrunc status=none |
| 104 | + out=$("$MCPP" run 2>&1); rc=$? |
| 105 | + [[ $rc -eq 2 ]] || fail "unrunnable artifact: exit $rc, want 2: $out" |
| 106 | + grep -q "this host cannot execute" <<<"$out" || fail "unrunnable message missing: $out" |
| 107 | + grep -q "Exec format error" <<<"$out" || fail "the kernel's answer is missing: $out" |
| 108 | + grep -q "\[target.$HOST\]" <<<"$out" || fail "paste-able key missing: $out" |
| 109 | + grep -q 'runner = \["qemu-aarch64-static"\]' <<<"$out" || fail "runner example missing: $out" |
| 110 | + grep -q -- "--no-runner" <<<"$out" || fail "escape hatch not mentioned: $out" |
| 111 | + # The criterion measured the patched artifact, not a rebuilt one. |
| 112 | + [[ "$(od -An -tx1 -j18 -N2 "$bin" | tr -d ' ')" == "ffff" ]] \ |
| 113 | + || fail "the artifact was rebuilt under the run; the criterion measured nothing" |
| 114 | + |
| 115 | + # `mcpp test`: one test (streaming path) and two tests (capturing path). |
| 116 | + # Today they report a spawn failure differently; the assertion is the same. |
| 117 | + for n in 1 2; do |
| 118 | + if [[ $n -eq 2 ]]; then |
| 119 | + cat > tests/two.cpp <<'EOF' |
| 120 | +int main() { return 0; } |
| 121 | +EOF |
| 122 | + fi |
| 123 | + # A patched binary is newer than its source, so ninja keeps it; the |
| 124 | + # sources are touched so this iteration measures freshly built binaries. |
| 125 | + touch tests/*.cpp |
| 126 | + out=$("$MCPP" test 2>&1) || fail "test build+run before patching ($n): $out" |
| 127 | + grep -qE "ok\. $n passed; 0 failed" <<<"$out" || fail "sanity: tests should pass unpatched ($n): $out" |
| 128 | + tbins=$(find target -type f -perm -u+x \( -name one -o -name two \)) |
| 129 | + [[ $(wc -l <<<"$tbins") -eq $n ]] || fail "expected $n test binaries, found: $tbins" |
| 130 | + for t in $tbins; do |
| 131 | + printf '\xff\xff' | dd of="$t" bs=1 seek=18 conv=notrunc status=none |
| 132 | + done |
| 133 | + out=$("$MCPP" test 2>&1); rc=$? |
| 134 | + [[ $rc -eq 2 ]] || fail "test unrunnable ($n): exit $rc, want 2: $out" |
| 135 | + grep -qE "NOT RUN\. 0 passed; 0 failed; $n not run \(this host cannot execute $HOST artifacts: Exec format error" <<<"$out" \ |
| 136 | + || fail "test summary ($n): $out" |
| 137 | + [[ $(grep -c " \.\.\. not run" <<<"$out") -eq $n ]] || fail "per-test not-run lines ($n): $out" |
| 138 | + # The reason is printed once when established, and once in the summary. |
| 139 | + [[ $(grep -c "cannot execute $HOST artifacts" <<<"$out") -eq 2 ]] \ |
| 140 | + || fail "reason printed other than once + summary ($n): $out" |
| 141 | + for t in $tbins; do |
| 142 | + [[ "$(od -An -tx1 -j18 -N2 "$t" | tr -d ' ')" == "ffff" ]] \ |
| 143 | + || fail "test binary was rebuilt under the run ($n)" |
| 144 | + done |
| 145 | + jout=$("$MCPP" test --message-format json 2>/dev/null); jrc=$? |
| 146 | + [[ $jrc -eq 2 ]] || fail "json exit ($n): $jrc, want 2" |
| 147 | + [[ $(grep -c '"status":"not_run"' <<<"$jout") -eq $n ]] || fail "json not_run records ($n): $jout" |
| 148 | + grep -q "\"not_run\":$n," <<<"$jout" || fail "json summary not_run ($n): $jout" |
| 149 | + grep -q '"not_run_reason":"this host cannot execute' <<<"$jout" || fail "json summary reason ($n): $jout" |
| 150 | + done |
| 151 | +fi |
| 152 | + |
| 153 | +# ── 5. an array-valued typo is reported, and `runner` is in the list ─────── |
| 154 | +printf '\n[target.%s]\nrunnerX = ["x"]\n' "$HOST" >> mcpp.toml |
| 155 | +out=$("$MCPP" build 2>&1) |
| 156 | +grep -q "unsupported key 'runnerX'" <<<"$out" || fail "array typo not reported: $out" |
| 157 | +grep -q "Supported keys: cxx_runtime, linkage, runner, sysroot, toolchain" <<<"$out" \ |
| 158 | + || fail "runner missing from the supported-keys list: $out" |
| 159 | + |
| 160 | +echo "PASS: 330_runner_hosted_targets" |
0 commit comments