kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 10b8a9dad2ce5c7c85c994cd0c4f4935332e9c6e
parent ca23ef4abbc29a06c4b0da8fda4fa1d37d4acbe8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon,  1 Jun 2026 14:57:54 -0700

test: standardize all harnesses onto canonical types (U/C/K/D)

Replace per-harness ad-hoc test scaffolding with shared libraries so every
test conforms to one of four canonical types:

  U  unit-C       test/lib/cfree_unit.h + unit.mk (2-regime build manifest:
                  public archive vs internal objects)
  C  corpus       test/lib/cf_corpus.sh — file-corpus lane-matrix engine;
                  oracle is lane-local (exit/golden/byte/structural/negative/
                  cross-exec are all just lanes); serial|parallel via event
                  replay (CF_PARALLELIZABLE flag flip); deferred cross-arch
                  exec via cf_queue_e + exec_target flush
  K  scripted     test/lib/cfree_sh_kit.sh (report + assert + scenario);
                  mode G golden transcript diff, mode P procedural asserts
  D  differential test/lib/cf_differential.sh — baseline-snapshot or
                  reference-tool agreement oracle

Shared report layer test/lib/cfree_sh_report.sh (cf_pass/fail/skip/skip_na/
xfail/xpass/time + unified summary/exit, TTY-gated color, CFREE_TEST_ALLOW_SKIP
gating) underpins all shell types; test/lib/cf_skip.sh unifies sidecar /
diagnostic-regex / tuple-applicability skips. xpass is always fatal, xfail is
fatal only under CF_STRICT_XFAIL. Engine proven by cf_corpus_selftest.sh
(serial==parallel determinism + the parallel-safety invariant: exec queued on
the parent, never in a worker), wired as `make test-cf-corpus-selftest`.

Migrations:
  U: 20 unit tests onto cfree_unit.h, built via the unit.mk manifest.
  C: parse, toy, link, elf, wasm, asm, asm round-trip(+toy), host-as(+cross),
     pp(+errors), parse-errors, rt, smoke, bounce, libc(musl/glibc),
     wasm-target (the 4 check_*.sh folded into one corpus runner).
  K: ar/strip/objcopy/strings/objdump (mode G), cas/pkg/driver/coff-smokes
     (mode P), dbg (mode G + xfail/xpass).
  D: asm symmetry (baseline), diff-llvm (reference vs llvm-mc).

Also relocate the stray top-level test files: test/ar_test.c -> test/ar/,
test/smoke.c -> test/rt/, test/lib_deps.allowlist -> scripts/.

Full-suite run verification is in progress; individual failures are addressed
in follow-up commits.

Diffstat:
Mdoc/TESTING.md | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Rtest/lib_deps.allowlist -> scripts/lib_deps.allowlist | 0
Mtest/api/abi_classify_test.c | 70+++++++++++-----------------------------------------------------------
Mtest/api/cg_switch_test.c | 66+++++++++++-------------------------------------------------------
Mtest/api/cg_type_test.c | 88++++++++++++++++---------------------------------------------------------------
Atest/ar/ar_test.c | 973+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/ar/run.sh | 77+++++++++++++++--------------------------------------------------------------
Dtest/ar_test.c | 991-------------------------------------------------------------------------------
Mtest/arch/aa64_inline_test.c | 4++--
Mtest/arch/aa64_isa_test.c | 42++++++++++++++++++++++++------------------
Mtest/arch/inline_public_test.h | 146++++++++++++++++++++++---------------------------------------------------------
Mtest/arch/rv64_decode_test.c | 70+++++++++++++---------------------------------------------------------
Mtest/arch/rv64_inline_test.c | 4++--
Mtest/arch/x64_dbg_test.c | 25+++++++++++--------------
Mtest/arch/x64_inline_test.c | 4++--
Mtest/asm/diff_llvm.sh | 86++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mtest/asm/hostas_cross.sh | 281++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtest/asm/hostas_toy.sh | 247+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mtest/asm/roundtrip.sh | 310++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtest/asm/roundtrip_toy.sh | 186++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtest/asm/run.sh | 539++++++++++++++++++++++++++++++++++---------------------------------------------
Mtest/asm/symmetry.sh | 49++++++++++++++++++++++++++-----------------------
Mtest/bounce/bounce.sh | 195++++++++++++++++++++++++++++++++++++-------------------------------------------
Mtest/cas/run.sh | 147+++++--------------------------------------------------------------------------
Mtest/cg/ir_recorder_test.c | 74+++++++++++++++-----------------------------------------------------------
Mtest/cg/native_direct_target_test.c | 73++++++++++++++-----------------------------------------------------------
Mtest/coff/windows-system-dlls-smoke.sh | 215+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtest/coff/windows-ucrt-hosted-smoke.sh | 295+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mtest/dbg/run.sh | 245+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtest/debug/cfi_unit.c | 68++++++++++++++------------------------------------------------------
Mtest/debug/roundtrip_unit.c | 78++++++++++++++++--------------------------------------------------------------
Mtest/driver/run.sh | 531++++++++++++++++++++++++++++++-------------------------------------------------
Mtest/dwarf/dwarf_test.c | 89++++++++++++++++++++++---------------------------------------------------------
Mtest/elf/run.sh | 445+++++++++++++++++++++++++------------------------------------------------------
Mtest/emu/rv64_interp_smoke_test.c | 81++++++++++++++++++++++++-------------------------------------------------------
Mtest/emu/rv64_smoke_test.c | 76++++++++++++++++++++--------------------------------------------------------
Mtest/emu/rv64_vm_unit_test.c | 94++++++++++++++++++++-----------------------------------------------------------
Mtest/interp/interp_smoke_test.c | 70++++++++++++++--------------------------------------------------------
Atest/lib/cf_corpus.sh | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cf_corpus_selftest.sh | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cf_differential.sh | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cf_skip.sh | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cfree_sh_assert.sh | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cfree_sh_kit.sh | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cfree_sh_report.sh | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/cfree_unit.h | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/lib/unit.mk | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/libc/glibc/run.sh | 389++++++++++++++++++++++++++++++++++++-------------------------------------------
Mtest/libc/musl/run.sh | 360+++++++++++++++++++++++++++++++++++--------------------------------------------
Mtest/link/reloc_uleb128_unit.c | 22++++++++++------------
Mtest/link/run.sh | 1022++++++++++++++++++++++++++++++++++++-------------------------------------------
Mtest/link/rv64_jit_test.c | 50+++++++++-----------------------------------------
Mtest/objcopy/run.sh | 65+++++++++++------------------------------------------------------
Mtest/objdump/run.sh | 88++++++++++++++-----------------------------------------------------------------
Mtest/opt/cg_ir_lower_test.c | 73++++++++++++++-----------------------------------------------------------
Mtest/opt/tiny_inline_test.c | 72++++++++++++++----------------------------------------------------------
Mtest/parse/run.sh | 780+++++++++++++++++++++++++++++--------------------------------------------------
Mtest/parse/run_errors.sh | 101++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtest/pkg/run.sh | 147+++++--------------------------------------------------------------------------
Mtest/pp/run.sh | 133++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mtest/pp/run_errors.sh | 79+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtest/rt/run.sh | 139++++++++++++++++++++++++++++++++++++++++---------------------------------------
Rtest/smoke.c -> test/rt/smoke.c | 0
Mtest/smoke/rv64.sh | 117+++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mtest/smoke/rv64_tls_link.sh | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtest/smoke/x64.sh | 85+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mtest/strings/run.sh | 65+++++++++++------------------------------------------------------
Mtest/strip/run.sh | 65+++++++++++------------------------------------------------------
Mtest/test.mk | 136+++++++++++++++++++++++++++++++------------------------------------------------
Mtest/toy/run.sh | 598++++++++++++++++++++++++++++++++++---------------------------------------------
Dtest/wasm-target/check_asm.sh | 55-------------------------------------------------------
Dtest/wasm-target/check_imports.sh | 74--------------------------------------------------------------------------
Dtest/wasm-target/check_memory_copy.sh | 61-------------------------------------------------------------
Dtest/wasm-target/check_memory_export.sh | 40----------------------------------------
Mtest/wasm-target/run.sh | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mtest/wasm/run.sh | 541+++++++++++++++++++++++++++++--------------------------------------------------
76 files changed, 6510 insertions(+), 7392 deletions(-)

diff --git a/doc/TESTING.md b/doc/TESTING.md @@ -11,12 +11,65 @@ exercise see [ASM.md](ASM.md) (assembler/disassembler/`cc -S`), [ARCH.md](ARCH.md) (per-arch ISA tables), and [FRONTENDS.md](FRONTENDS.md) (the Toy and C frontends). -The test tree lives under `test/`, with per-area subdirectories -(`test/asm/`, `test/toy/`, `test/smoke/`, `test/libc/`, plus unit-test areas -like `test/arch/`, `test/elf/`, `test/opt/`). Build/run wiring lives in -`test/test.mk`; shared helpers (the C unit-test harness `test/lib/cfree_unit.h` -and its build rules `test/lib/unit.mk`; the cross-exec helper -`test/lib/exec_target.sh`) are factored out for reuse. +The test tree lives under `test/`, with per-area subdirectories (`test/asm/`, +`test/toy/`, `test/smoke/`, `test/libc/`, plus unit-test areas like +`test/arch/`, `test/elf/`, `test/opt/`). Build/run wiring lives in +`test/test.mk`. Every harness conforms to **one of four canonical test types**, +each backed by a shared library under `test/lib/`, so test infrastructure is +written once and reused rather than re-invented per area. + +## Canonical test types + +| Type | Library | What it is | +|------|---------|------------| +| **U** unit | `cfree_unit.h` (+ `unit.mk` build manifest) | a C translation unit linked against libcfree, self-checking in-process | +| **C** corpus | `cf_corpus.sh` | a directory of case files run through one or more *lanes*, each with its own oracle | +| **K** scripted | `cfree_sh_kit.sh` | a hand-written shell test driving the `cfree` binary, judged by golden transcript (mode G) or procedural asserts (mode P) | +| **D** differential | `cf_differential.sh` | correctness defined as *agreement* — vs a checked-in baseline, or vs a reference tool | + +All four record results through one shared report layer +`test/lib/cfree_sh_report.sh` (`cf_pass`/`cf_fail`/`cf_skip`/`cf_skip_na`/ +`cf_xfail`/`cf_xpass`/`cf_time` + a unified `cf_summary`/`cf_exit`); +`cf_skip.sh` unifies the skip sources (sidecar files, phased-rollout diagnostic +regexes, target-tuple applicability); `exec_target.sh` and `exec_kernel.sh` run +guest binaries for cross-arch lanes. The sections below describe what each +harness *tests*; this is how they are *structured*. + +**Type C — the corpus engine.** `cf_corpus.sh` owns the whole pipeline: +discover a glob of cases, expand the {case × opt-level × target-tuple} matrix, +run each enabled lane's recipe, apply that lane's oracle, report, and flush any +deferred cross-arch exec. A runner supplies only its corpus glob, its lane set, +and a `cf_lane_<ID>()` hook per lane — which writes only under a per-case +`$CF_WORK` dir and records via exactly one `cf_*` verb. Because the oracle is +**lane-local**, exit-code, golden-diff, byte-compare, structural-grep, negative +(expect-failure) and cross-arch-exec are all just lane bodies, not separate +harness types; a suite with disjoint sub-corpora (asm's encode/decode/listing, +elf's layers) makes one `cf_corpus_run` call per sub-corpus. Execution is serial +or parallel through the **same** code path: in parallel mode each case runs as a +background worker that appends its results to an event file, and the parent +replays those events in index order — so counts, failing-name order, and the +cross-arch-exec queue are deterministic regardless of worker completion order, +and a runner goes parallel by flipping `CF_PARALLELIZABLE` with no other change. +The engine is proven by `test/lib/cf_corpus_selftest.sh` +(`make test-cf-corpus-selftest`): it asserts serial≡parallel determinism and the +load-bearing invariant that cross-arch exec is queued on the parent, never +inside a worker. + +**Type K — scripted shell.** `cfree_sh_kit.sh` is the single source point for a +hand-written tool/driver test: it pulls in the report layer plus the procedural +assert verbs (`ok`/`run_ok`/`run_fail`/`contains`/`same_file`/`is_executable`/ +`check_mode`) and the golden-transcript runner `cf_scenario_case`. A suite picks +a mode — **G** diffs a `cases/<name>.sh` transcript against `<name>.expected` +(ar/strip/objcopy/strings/objdump, dbg); **P** runs procedural asserts over a +`$work` sandbox (cas/pkg, the driver CLI suite, the COFF Windows smokes). Mode-P +suites stay serial (they share one `$work` and mutate fixtures). + +**Type D — differential.** `cf_differential.sh` is for tests whose oracle is +*agreement* rather than a fixed per-case answer: `cf_diff_baseline` regenerates a +normalized report and gates on a zero delta vs a checked-in snapshot +(`CF_DIFF_UPDATE=1` refreshes it), and `cf_diff_agree` requires two independent +producers to match byte-for-byte (with explicit equivalence-skips). Used by the +asm self-symmetry sweep and the cfree-vs-llvm-mc cross-check. ## Why codegen-driven round-trip testing @@ -224,9 +277,10 @@ link/smoke/libc harnesses, so cross-exec policy lives in exactly one file. The Toy frontend (`lang/toy/`, see [FRONTENDS.md](FRONTENDS.md)) is a small language that exists to exercise the full CG API op set, and every case carries -an exit-code oracle (`test/toy/cases/<name>.expected`). `test/toy/run.sh` runs -each case through several **paths**, each a distinct backend/seam, all judged -against the same oracle: +an exit-code oracle (`test/toy/cases/<name>.expected`). `test/toy/run.sh` is a +Type C corpus harness that runs each case through several **paths** — its +`cf_corpus.sh` lanes, each a distinct backend/seam — all judged against the same +oracle: ``` R cfree run in-process JIT, native @@ -251,7 +305,26 @@ and both host-assembler lanes use it. This reuse found a real miscompile (a multiply-high the disassembler couldn't decode, silently dropped by `as` until the `.inst`-emits-the-word fix) that the hand corpus never reached. -## Unit tests +## Driver and tool-level tests (Type K) + +The `cfree` multitool's command-line behavior is covered by **Type K** scripted +harnesses on `cfree_sh_kit.sh`, in two oracle modes: + +- **mode G (golden transcript):** `test/{ar,strip,objcopy,strings,objdump}/run.sh` + and `test/dbg/run.sh` each run a directory of `cases/<name>.sh` scripts in a + sandbox and diff combined stdout+stderr against a checked-in `<name>.expected`. + dbg additionally uses xfail/xpass: an xfail case that fails is expected + (`cf_xfail`); one that *unexpectedly passes* (`cf_xpass`) is always a failure — + the stale marker should be removed — and `DBG_STRICT_XFAIL` promotes even an + expected failure to a hard error. +- **mode P (procedural asserts):** `test/driver/run.sh` (the `cc`/`ld`/`ar`/`nm`/ + `size`/`addr2line`/… behavior suite), `test/cas/run.sh`, `test/pkg/run.sh`, and + the COFF Windows smokes (`test/coff/windows-*-smoke.sh`) drive a sequence of + `cfree` invocations over a `$work` sandbox and assert outcomes with + `run_ok`/`run_fail`/`contains`/`same_file`/`check_mode`. These stay serial + (they share `$work` and mutate fixtures). + +## Unit tests (Type U) Lower-level invariants are covered by C unit-test binaries built from `test/lib/unit.mk` and linking `test/lib/cfree_unit.h`. There are two link @@ -273,7 +346,7 @@ push it through `exec_target_run` and `exec_target_queue`+`flush`, and assert th expected exit code on both paths. The point is to validate the harness plumbing (cross-compile → podman/qemu → recorded rc) before relying on it for the heavier lanes, and to give a clear per-tool ok/MISSING diagnosis when a host lacks a -runner. There is also a header smoke test (`test/smoke.c`): every freestanding +runner. There is also a header smoke test (`test/rt/smoke.c`): every freestanding header must parse and expose its required macros/typedefs under a strict freestanding compile. @@ -296,7 +369,7 @@ see [LINK.md](LINK.md). Arch selection is `CFREE_LIBC_ARCHES` (aa64/x64/rv64); a missing sysroot or runtime for an enabled arch is SKIP, not failure. A related guard, `test-lib-deps`, asserts `libcfree.a`'s set of external -(undefined) symbols matches a checked-in allowlist (`test/lib_deps.allowlist`) +(undefined) symbols matches a checked-in allowlist (`scripts/lib_deps.allowlist`) and that a relocatable link of the library exposes no non-public symbols. This keeps the freestanding library's dependency surface from drifting silently. @@ -326,18 +399,29 @@ and to triage O1 codegen without `-g`, which perturbs object layout. ## Aggregation and conventions `test/test.mk` defines the targets. A default `test` aggregate runs the -host-independent lanes (frontend corpora, unit tests, L0/L1 round-trip, libc-dep -guard); the exec-dependent and second-oracle lanes (L2 exec, symmetry, diff-llvm, -hostas-toy/cross, smoke, libc conformance) are opt-in so the default run stays -host-independent and fast. Bootstrap is *not* part of this test system: it is a -separate top-level target (`make bootstrap`, `make bootstrap-debug/release`) -driven from the top-level `Makefile`, not from `test/test.mk` — see the -bootstrap section above. Conventions shared across harnesses: - +host-independent lanes (frontend corpora, unit tests, L0/L1 round-trip, the +`cf_corpus` engine selftest, the libc-dep guard); the exec-dependent and +second-oracle lanes (L2 exec, symmetry, diff-llvm, hostas-toy/cross, smoke, libc +conformance) are opt-in so the default run stays host-independent and fast. +Bootstrap is *not* part of this test system: it is a separate top-level target +(`make bootstrap`, `make bootstrap-debug/release`) driven from the top-level +`Makefile`, not from `test/test.mk` — see the bootstrap section above. +Conventions shared across all four test types: + +- **One report layer.** Every shell harness records through `cf_pass`/`cf_fail`/ + `cf_skip`/`cf_skip_na`/`cf_xfail`/`cf_xpass` and ends with `cf_summary` + + `cf_exit` — no harness rolls its own counters or summary. XPASS always gates + the exit; SKIP gates only when the suite opts in (`CF_SKIP_IS_FAILURE=1`, the + corpus default), overridable per run with `CFREE_TEST_ALLOW_SKIP=1`. - **SKIP, never silently pass.** A lane that can't run (no runner, no cross - toolchain, an unimplemented backend path) reports SKIP with a reason rather - than vanishing. Several harnesses honor `CFREE_TEST_ALLOW_SKIP` to decide - whether a SKIP is a soft pass or a hard fail in a given context. + toolchain, an unimplemented backend path) reports SKIP with a reason via the + shared `cf_skip.sh` (sidecar / phased-rollout diagnostic regex / target-tuple + applicability) rather than vanishing; a structurally-inapplicable case is an + uncounted SKIP-NA. +- **Parallel by a flag flip.** A Type C corpus runs serial or parallel through + the same `cf_corpus.sh` event-replay path; hooks that write only under + `$CF_WORK` and record only via `cf_*` are parallel-safe by construction, so + `CF_PARALLELIZABLE` toggles dispatch with no other change. - **Checked-in baselines turn incompleteness into a regression diff.** The symmetry baseline and the per-case `.skip` sidecars document what is known- incomplete; the gate fires only on a *change* to that set. diff --git a/test/lib_deps.allowlist b/scripts/lib_deps.allowlist diff --git a/test/api/abi_classify_test.c b/test/api/abi_classify_test.c @@ -13,69 +13,24 @@ #include <stdlib.h> #include <string.h> +#include "lib/cfree_unit.h" + #include "abi/abi.h" #include "core/core.h" -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -static int g_fail; - -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) - -/* Storage outlives every Compiler; cfree_compiler_new just stores `ctx`. */ -static CfreeContext g_ctx; +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static void expect_direct_1x_int(const char* tag, const ABIArgInfo* ai, u32 want_size); static CfreeCompiler* new_compiler(CfreeArchKind arch, CfreeOSKind os, CfreeObjFmt obj) { - CfreeTarget t; + CfreeTarget t = cfree_unit_target(arch, os, obj); CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); - t.arch = arch; - t.os = os; - t.obj = obj; - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "compiler_new failed for arch=%d os=%d\n", (int)arch, (int)os); exit(2); @@ -576,6 +531,7 @@ static void test_aarch64_windows_variadic(void) { } int main(void) { + cfree_unit_init(&g_u); check_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); check_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); check_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO); @@ -584,10 +540,6 @@ int main(void) { check_target(CFREE_ARCH_ARM_64, CFREE_OS_WINDOWS, CFREE_OBJ_COFF); test_win64_specifics(); test_aarch64_windows_variadic(); - if (g_fail) { - fprintf(stderr, "%d failures\n", g_fail); - return 1; - } - fprintf(stderr, "abi_classify_test: OK\n"); - return 0; + cfree_unit_summary(&g_u, "abi_classify_test"); + return cfree_unit_status(&g_u); } diff --git a/test/api/cg_switch_test.c b/test/api/cg_switch_test.c @@ -23,46 +23,12 @@ #include <stdlib.h> #include <string.h> -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -static int g_fail; +#include "lib/cfree_unit.h" -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static CfreeObjBuilder* new_obj(CfreeCompiler* c) { CfreeObjBuilder* ob = NULL; @@ -202,7 +168,7 @@ static void build_switch_fn(CfreeCompiler* c, CfreeCgTypeId i32_ty, cfree_cg_ret(cg); cfree_cg_func_end(cg); - EXPECT(g_fail == 0 || g_fail > 0, "shape-build sentinel"); /* no-op */ + EXPECT(g_u.fails == 0 || g_u.fails > 0, "shape-build sentinel"); /* no-op */ free(case_lbls); free(cases); @@ -284,26 +250,15 @@ static void run_all_shapes(CfreeCompiler* c, CfreeCgTypeId i32_ty, int main(void) { CfreeTarget target; - CfreeContext ctx; CfreeCompiler* c = NULL; CfreeCgBuiltinTypes bi; CfreeCgTypeId i32_ty; CfreeCgTypeId i64_ty; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - - memset(&ctx, 0, sizeof ctx); - ctx.heap = &g_heap; - ctx.file_io = NULL; - ctx.diag = &g_diag; - ctx.now = 0; + cfree_unit_init(&g_u); + target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); - if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) { fprintf(stderr, "compiler_new failed\n"); return 2; } @@ -318,5 +273,6 @@ int main(void) { run_all_shapes(c, i32_ty, i64_ty, /*opt_level=*/1); cfree_compiler_free(c); - return g_fail ? 1 : 0; + cfree_unit_summary(&g_u, "cg_switch_test"); + return cfree_unit_status(&g_u); } diff --git a/test/api/cg_type_test.c b/test/api/cg_type_test.c @@ -8,56 +8,13 @@ #include <stdlib.h> #include <string.h> -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} - -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} +#include "lib/cfree_unit.h" -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static char g_last_diag[256]; -static int g_suppress_expected_panic_diag; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - va_list copy; - (void)s; - (void)loc; - va_copy(copy, ap); - vsnprintf(g_last_diag, sizeof g_last_diag, fmt, copy); - va_end(copy); - if (g_suppress_expected_panic_diag && k == CFREE_DIAG_FATAL) return; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -static int g_fail; - -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* One shared test context replaces the per-file heap/diag/counter globals. + * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged; the + * expected-panic helper uses g_u.suppress_fatal + g_u.last_diag. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static CfreeObjBuilder* new_obj(CfreeCompiler* c) { CfreeObjBuilder* ob = NULL; @@ -76,7 +33,7 @@ static int open_emitted_obj(CfreeCompiler* c, CfreeObjBuilder* ob, if (!writer_out || !file_out) return 0; *writer_out = NULL; *file_out = NULL; - if (cfree_writer_mem(&g_heap, &w) != CFREE_OK || !w) return 0; + if (cfree_writer_mem(&g_u.heap, &w) != CFREE_OK || !w) return 0; if (cfree_obj_builder_emit(ob, w) != CFREE_OK) { cfree_writer_close(w); return 0; @@ -112,11 +69,11 @@ static int expect_panic_contains(CfreeCompiler* c, void (*fn)(void*), void* arg, CfreeStatus st; ctx.fn = fn; ctx.arg = arg; - g_last_diag[0] = '\0'; - g_suppress_expected_panic_diag++; + g_u.last_diag[0] = '\0'; + g_u.suppress_fatal++; st = cfree_frontend_run(c, run_expected_panic, &ctx); - g_suppress_expected_panic_diag--; - return st == CFREE_ERR && strstr(g_last_diag, expected) != NULL; + g_u.suppress_fatal--; + return st == CFREE_ERR && strstr(g_u.last_diag, expected) != NULL; } static void exercise_cg_handles(CfreeCompiler* c, CfreeCgTypeId i32_ty, @@ -1290,7 +1247,8 @@ static void exercise_cg_memory_mismatch_diags(CfreeCompiler* c, (void)run_bad_scalar_access_to_aggregate; EXPECT(expect_panic_contains(c, run_bad_store_value_size, &ctx, "store value type/size mismatch"), - "store size mismatch should diagnose clearly, got '%s'", g_last_diag); + "store size mismatch should diagnose clearly, got '%s'", + g_u.last_diag); } static void exercise_compile_session_two_deltas(CfreeCompiler* c) { @@ -1352,7 +1310,6 @@ static void exercise_cg_begin_end_two_objects(CfreeCompiler* c) { int main(void) { CfreeTarget target; - CfreeContext ctx; CfreeCompiler* c; CfreeCgBuiltinTypes bi; CfreeCgTypeId void_ty; @@ -1376,21 +1333,11 @@ int main(void) { CfreeCgEnumValue vals[2]; uint64_t field_off; - memset(&target, 0, sizeof(target)); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - - memset(&ctx, 0, sizeof(ctx)); - ctx.heap = &g_heap; - ctx.file_io = NULL; - ctx.diag = &g_diag; - ctx.now = 0; + cfree_unit_init(&g_u); + target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); c = NULL; - if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) { fprintf(stderr, "compiler_new failed\n"); return 2; } @@ -1542,5 +1489,6 @@ int main(void) { exercise_cg_begin_end_two_objects(c); cfree_compiler_free(c); - return g_fail ? 1 : 0; + cfree_unit_summary(&g_u, "cg_api_test"); + return cfree_unit_status(&g_u); } diff --git a/test/ar/ar_test.c b/test/ar/ar_test.c @@ -0,0 +1,973 @@ +/* Round-trip tests for the cfree ar reader/writer. + * + * Builds against just include/cfree.h + libcfree.a and a few libc calls + * (malloc/realloc/free, printf for diagnostics). cfree_ar_write itself + * makes no heap allocations, so the test does not need a CfreeHeap. + * + * Set CFREE_AR_TEST_HOST=1 to also dump the produced symbol-index + * archive to /tmp/cfree_ar_test.a and run the host's `ar t` and + * `nm --print-armap` on it as a cross-check. */ +#include <cfree/archive.h> +#include <cfree/core.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "lib/cfree_unit.h" + +/* ===== minimal CfreeWriter over a growing buffer ===== */ + +typedef struct BufW { + CfreeWriter base; + uint8_t* data; + size_t len; + size_t cap; + CfreeStatus st; +} BufW; + +static CfreeStatus bufw_write(CfreeWriter* w, const void* data, size_t n) { + BufW* b = (BufW*)w; + if (b->st != CFREE_OK) return b->st; + if (b->len + n > b->cap) { + size_t nc = b->cap ? b->cap * 2 : 256; + while (nc < b->len + n) nc *= 2; + uint8_t* p = (uint8_t*)realloc(b->data, nc); + if (!p) { + b->st = CFREE_NOMEM; + return CFREE_NOMEM; + } + b->data = p; + b->cap = nc; + } + memcpy(b->data + b->len, data, n); + b->len += n; + return CFREE_OK; +} + +static CfreeStatus bufw_seek(CfreeWriter* w, uint64_t off) { + (void)w; + (void)off; + return CFREE_OK; +} +static uint64_t bufw_tell(CfreeWriter* w) { return ((BufW*)w)->len; } +static CfreeStatus bufw_status(CfreeWriter* w) { return ((BufW*)w)->st; } +static void bufw_close(CfreeWriter* w) { (void)w; } + +static void bufw_init(BufW* b) { + b->base.write = bufw_write; + b->base.seek = bufw_seek; + b->base.tell = bufw_tell; + b->base.status = bufw_status; + b->base.close = bufw_close; + b->data = NULL; + b->len = 0; + b->cap = 0; + b->st = CFREE_OK; +} + +static void bufw_fini(BufW* b) { free(b->data); } + +/* ===== libc heap + context (needed by cfree_ar_iter_new) ===== */ + +static CfreeUnit g_u; + +/* ar_test keeps its own context: now=-1 and no diag sink, since its archive + * assertions depend on exact header bytes (the clock) and never expect a + * diagnostic. The heap comes from g_u, filled by cfree_unit_init in main. */ +static CfreeContext g_ctx = {.heap = &g_u.heap, .now = -1}; + +/* ===== assertion helpers ===== */ + +/* EXPECT keeps its per-test early-return-on-fail shape via CU_CHECK_RET. */ +#define EXPECT(cond, ...) CU_CHECK_RET(&g_u, cond, __VA_ARGS__) + +static uint32_t be32(const uint8_t* p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +/* Decode the ar_size field (10-byte ASCII decimal, space-padded). */ +static uint64_t ar_size_field(const uint8_t* hdr) { + uint64_t v = 0; + int j; + for (j = 48; j < 58; ++j) { + char c = (char)hdr[j]; + if (c < '0' || c > '9') break; + v = v * 10 + (uint64_t)(c - '0'); + } + return v; +} + +/* Decode the ar_date field (12-byte ASCII decimal, space-padded). */ +static uint64_t ar_date_field(const uint8_t* hdr) { + uint64_t v = 0; + int j; + for (j = 16; j < 28; ++j) { + char c = (char)hdr[j]; + if (c < '0' || c > '9') break; + v = v * 10 + (uint64_t)(c - '0'); + } + return v; +} + +/* ===== tests ===== */ + +static int test_basic_roundtrip(void) { + CfreeArInput ms[2]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + int rc; + + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"AAAA"; + ms[0].bytes.len = 4; + ms[1].name = CFREE_SLICE_LIT("b.o"); + ms[1].bytes.data = (const uint8_t*)"BBBBB"; + ms[1].bytes.len = 5; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 2, NULL); + EXPECT(rc == 0, "cfree_ar_write returned %d", rc); + EXPECT(bw.st == CFREE_OK, "writer error"); + EXPECT(bw.len >= 8, "archive too short"); + EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + + EXPECT(cfree_ar_iter_next(it, &m), "first member"); + EXPECT(cfree_slice_eq_cstr(m.name, "a.o"), "name 0 = %.*s", + CFREE_SLICE_ARG(m.name)); + EXPECT(m.size == 4 && memcmp(m.data, "AAAA", 4) == 0, "data 0"); + + EXPECT(cfree_ar_iter_next(it, &m), "second member"); + EXPECT(cfree_slice_eq_cstr(m.name, "b.o"), "name 1 = %.*s", + CFREE_SLICE_ARG(m.name)); + EXPECT(m.size == 5 && memcmp(m.data, "BBBBB", 5) == 0, "data 1"); + + EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_long_name_table(void) { + /* >15 chars triggers the // long-name table. */ + CfreeArInput ms[2]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + CfreeArWriteOptions opts = {0}; + int rc; + + opts.long_names = 1; + ms[0].name = CFREE_SLICE_LIT("short.o"); + ms[0].bytes.data = (const uint8_t*)"x"; + ms[0].bytes.len = 1; + ms[1].name = CFREE_SLICE_LIT("this_name_is_long_enough.o"); + ms[1].bytes.data = (const uint8_t*)"yy"; + ms[1].bytes.len = 2; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 2, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first member"); + EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "name 0 = %.*s", + CFREE_SLICE_ARG(m.name)); + EXPECT(cfree_ar_iter_next(it, &m), "second member"); + EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"), + "long name = %.*s", CFREE_SLICE_ARG(m.name)); + EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_symbol_index_empty(void) { + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + CfreeArWriteOptions opts = {0}; + int rc; + uint32_t nsyms; + + opts.symbol_index = 1; + ms[0].name = CFREE_SLICE_LIT("lonely.o"); + ms[0].bytes.data = (const uint8_t*)"P"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 1, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + + /* First member after magic must be `/` index with count=0. */ + EXPECT(bw.len >= 8 + 60 + 4, "archive too short"); + EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index name field"); + EXPECT(ar_size_field(bw.data + 8) == 4, "index payload size = 4"); + nsyms = be32(bw.data + 8 + 60); + EXPECT(nsyms == 0, "nsyms = %u", nsyms); + + /* Iterator should skip the `/` and yield only the user member. */ + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first user member"); + EXPECT(cfree_slice_eq_cstr(m.name, "lonely.o"), "name = %.*s", + CFREE_SLICE_ARG(m.name)); + EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_symbol_index_basic(void) { + /* 2 members with symbol lists; verify count, offsets, and names. */ + CfreeSlice a_syms[] = {CFREE_SLICE_LIT("foo"), CFREE_SLICE_LIT("bar")}; + CfreeSlice b_syms[] = {CFREE_SLICE_LIT("baz")}; + CfreeArMemberSymbols msyms[2]; + CfreeArInput ms[2]; + BufW bw; + CfreeArWriteOptions opts = {0}; + int rc; + uint32_t nsyms; + const uint8_t* p; + uint64_t index_payload; + uint32_t off0, off1, off2; + uint64_t a_hdr_off, b_hdr_off; + const char* name; + + opts.symbol_index = 1; + opts.long_names = 1; + + msyms[0].names = a_syms; + msyms[0].count = 2; + msyms[1].names = b_syms; + msyms[1].count = 1; + opts.member_symbols = msyms; + + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"AAAA"; + ms[0].bytes.len = 4; + ms[1].name = CFREE_SLICE_LIT("b.o"); + ms[1].bytes.data = (const uint8_t*)"BBBBB"; + ms[1].bytes.len = 5; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 2, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + + EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic"); + EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index ar_name field"); + + index_payload = ar_size_field(bw.data + 8); + /* 4 (count) + 4*3 (offsets) + 3+1 + 3+1 + 3+1 (names "foo\0bar\0baz\0") = 28 + */ + EXPECT(index_payload == 28, "index payload = %llu", + (unsigned long long)index_payload); + + p = bw.data + 8 + 60; + nsyms = be32(p); + p += 4; + EXPECT(nsyms == 3, "nsyms = %u", nsyms); + + off0 = be32(p); /* foo → a.o */ + off1 = be32(p + 4); /* bar → a.o */ + off2 = be32(p + 8); /* baz → b.o */ + p += 12; + + /* Compute expected header offsets: index_total = 60+28 (no pad, even). + * No long-name table is emitted (basenames ≤ 15 chars). So: + * a.o header at offset 8 + 88 = 96 + * b.o header at offset 96 + 60 + 4 (+0 pad) = 160 */ + a_hdr_off = 8 + 60 + 28; + b_hdr_off = a_hdr_off + 60 + 4; + EXPECT(off0 == a_hdr_off, "off0 = %u, expected %llu", off0, + (unsigned long long)a_hdr_off); + EXPECT(off1 == a_hdr_off, "off1 = %u, expected %llu", off1, + (unsigned long long)a_hdr_off); + EXPECT(off2 == b_hdr_off, "off2 = %u, expected %llu", off2, + (unsigned long long)b_hdr_off); + + /* Sanity: the member headers must actually live at those offsets. */ + EXPECT(memcmp(bw.data + a_hdr_off, "a.o/", 4) == 0, "a.o at offset"); + EXPECT(memcmp(bw.data + b_hdr_off, "b.o/", 4) == 0, "b.o at offset"); + + /* Names: "foo\0bar\0baz\0" */ + name = (const char*)p; + EXPECT(strcmp(name, "foo") == 0, "name 0 = %s", name); + name += strlen(name) + 1; + EXPECT(strcmp(name, "bar") == 0, "name 1 = %s", name); + name += strlen(name) + 1; + EXPECT(strcmp(name, "baz") == 0, "name 2 = %s", name); + + bufw_fini(&bw); + return 1; +} + +static int test_symbol_index_with_long_names(void) { + /* `/` member must come BEFORE `//` long-name table, and offsets must + * still point at correct member-header positions. */ + CfreeSlice syms0[] = {CFREE_SLICE_LIT("alpha")}; + CfreeSlice syms1[] = {CFREE_SLICE_LIT("beta")}; + CfreeArMemberSymbols msyms[2]; + CfreeArInput ms[2]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + CfreeArWriteOptions opts = {0}; + int rc; + uint64_t index_payload, longtab_payload; + uint64_t index_total, longtab_total; + uint64_t m0_hdr, m1_hdr; + uint32_t off0, off1; + const uint8_t* p; + + opts.symbol_index = 1; + opts.long_names = 1; + msyms[0].names = syms0; + msyms[0].count = 1; + msyms[1].names = syms1; + msyms[1].count = 1; + opts.member_symbols = msyms; + + ms[0].name = + CFREE_SLICE_LIT("this_name_is_long_enough.o"); /* 26 chars → // */ + ms[0].bytes.data = (const uint8_t*)"X"; + ms[0].bytes.len = 1; + ms[1].name = CFREE_SLICE_LIT("short.o"); + ms[1].bytes.data = (const uint8_t*)"YY"; + ms[1].bytes.len = 2; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 2, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + + /* Layout: magic(8) | / index member | // long-name member | members. */ + EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "/ first"); + index_payload = ar_size_field(bw.data + 8); + /* 4 (count) + 4*2 (offsets) + 6 ("alpha\0") + 5 ("beta\0") = 23 → odd → pad 1 + */ + EXPECT(index_payload == 23, "index_payload = %llu", + (unsigned long long)index_payload); + index_total = 60 + index_payload + (index_payload & 1); + + /* Verify // header sits at 8 + index_total. */ + EXPECT(bw.data[8 + index_total] == '/', "// pos byte 0"); + EXPECT(bw.data[8 + index_total + 1] == '/', "// pos byte 1"); + longtab_payload = ar_size_field(bw.data + 8 + index_total); + /* "this_name_is_long_enough.o/\n" = 28 bytes → even → no pad */ + EXPECT(longtab_payload == 28, "longtab payload = %llu", + (unsigned long long)longtab_payload); + longtab_total = 60 + longtab_payload + (longtab_payload & 1); + + m0_hdr = 8 + index_total + longtab_total; + m1_hdr = m0_hdr + 60 + 1 + 1 /* parity pad for odd len 1 */; + + p = bw.data + 8 + 60; + EXPECT(be32(p) == 2, "nsyms = %u", be32(p)); + p += 4; + off0 = be32(p); + off1 = be32(p + 4); + EXPECT(off0 == m0_hdr, "off0 = %u, expected %llu", off0, + (unsigned long long)m0_hdr); + EXPECT(off1 == m1_hdr, "off1 = %u, expected %llu", off1, + (unsigned long long)m1_hdr); + /* Spot-check m1 starts with "short.o/" for sanity. */ + EXPECT(memcmp(bw.data + m1_hdr, "short.o/", 8) == 0, "m1 hdr"); + + /* Iterator should walk past both /, // and yield 2 members. */ + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "m0"); + EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"), + "m0 name = %.*s", CFREE_SLICE_ARG(m.name)); + EXPECT(cfree_ar_iter_next(it, &m), "m1"); + EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "m1 name = %.*s", + CFREE_SLICE_ARG(m.name)); + EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); + + /* Optional host cross-check. */ + if (getenv("CFREE_AR_TEST_HOST")) { + FILE* f = fopen("/tmp/cfree_ar_test.a", "wb"); + if (f) { + fwrite(bw.data, 1, bw.len, f); + fclose(f); + fprintf(stderr, "host cross-check: ar t /tmp/cfree_ar_test.a\n"); + (void)!system("ar t /tmp/cfree_ar_test.a"); + fprintf(stderr, + "host cross-check: nm --print-armap /tmp/cfree_ar_test.a\n"); + (void)!system("nm --print-armap /tmp/cfree_ar_test.a 2>&1 || true"); + } + } + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_iter_skips_index(void) { + /* Make sure the iterator never surfaces the `/` member as a user member. */ + CfreeSlice s[] = {CFREE_SLICE_LIT("only_sym")}; + CfreeArMemberSymbols msyms[1]; + CfreeArInput ms[1]; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + BufW bw; + CfreeArWriteOptions opts = {0}; + int seen = 0; + + opts.symbol_index = 1; + msyms[0].names = s; + msyms[0].count = 1; + opts.member_symbols = msyms; + ms[0].name = CFREE_SLICE_LIT("only.o"); + ms[0].bytes.data = (const uint8_t*)"Z"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + while (cfree_ar_iter_next(it, &m)) { + EXPECT(!cfree_slice_eq_cstr(m.name, "/"), "iter surfaced raw `/` member"); + seen++; + } + EXPECT(seen == 1, "saw %d members", seen); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_empty_archive(void) { + /* nmembers == 0 with NULL members should produce a magic-only archive. */ + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + int rc; + + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, NULL, 0, NULL); + EXPECT(rc == 0, "write rc=%d", rc); + EXPECT(bw.len == 8, "size = %zu", bw.len); + EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic only"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(!cfree_ar_iter_next(it, &m), "no members"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_epoch_field(void) { + /* opts.epoch is written into ar_date for every member. */ + CfreeArInput ms[1]; + BufW bw; + CfreeArWriteOptions opts = {0}; + int rc; + + ms[0].name = CFREE_SLICE_LIT("x.o"); + ms[0].bytes.data = (const uint8_t*)"q"; + ms[0].bytes.len = 1; + + opts.epoch = 1234567890u; + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 1, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + EXPECT(ar_date_field(bw.data + 8) == 1234567890u, "ar_date = %llu", + (unsigned long long)ar_date_field(bw.data + 8)); + bufw_fini(&bw); + + /* Default (epoch=0): single '0' followed by spaces. */ + opts.epoch = 0; + bufw_init(&bw); + rc = cfree_ar_write(&bw.base, ms, 1, &opts); + EXPECT(rc == 0, "write rc=%d", rc); + EXPECT(bw.data[8 + 16] == '0', "epoch default first byte"); + EXPECT(bw.data[8 + 17] == ' ', "epoch default second byte = 0x%02x", + bw.data[8 + 17]); + bufw_fini(&bw); + return 1; +} + +static int test_path_basename(void) { + /* Member name with path components is stored as basename only. */ + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + + ms[0].name = CFREE_SLICE_LIT("src/sub/foo.o"); + ms[0].bytes.data = (const uint8_t*)"D"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(cfree_slice_eq_cstr(m.name, "foo.o"), "basename = %.*s", + CFREE_SLICE_ARG(m.name)); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_truncate_when_long_names_off(void) { + /* >15 chars without long_names: name is truncated to 15. */ + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + + ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnopqrst.o"); /* 22 chars */ + ms[0].bytes.data = (const uint8_t*)"D"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "truncated = %.*s", + CFREE_SLICE_ARG(m.name)); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_name_15_char_boundary(void) { + /* Exactly 15 chars: fits in-header even with long_names enabled. */ + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + CfreeArWriteOptions opts = {0}; + + opts.long_names = 1; + ms[0].name = CFREE_SLICE_LIT("abcdefghijklmno"); /* 15 chars */ + ms[0].bytes.data = (const uint8_t*)"X"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); + + /* No `//` long-name table: first member sits right after magic. */ + EXPECT(memcmp(bw.data + 8, "abcdefghijklmno/", 16) == 0, "name field = %.16s", + (const char*)(bw.data + 8)); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "name = %.*s", + CFREE_SLICE_ARG(m.name)); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_name_16_char_boundary(void) { + /* Exactly 16 chars: triggers // long-name table. */ + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + CfreeArWriteOptions opts = {0}; + + opts.long_names = 1; + ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnop"); /* 16 chars */ + ms[0].bytes.data = (const uint8_t*)"Y"; + ms[0].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); + EXPECT(bw.data[8] == '/' && bw.data[9] == '/', "// header"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmnop"), "name = %.*s", + CFREE_SLICE_ARG(m.name)); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_empty_member_payload(void) { + /* len=0 (data=NULL): header only, no pad; followed by the next member. */ + CfreeArInput ms[2]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + + ms[0].name = CFREE_SLICE_LIT("empty.o"); + ms[0].bytes.data = NULL; + ms[0].bytes.len = 0; + ms[1].name = CFREE_SLICE_LIT("next.o"); + ms[1].bytes.data = (const uint8_t*)"N"; + ms[1].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 2, NULL) == 0, "write"); + /* magic(8) + hdr(60) + 0 + hdr(60) + 1 + pad(1) = 130 */ + EXPECT(bw.len == 130, "size = %zu", bw.len); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(cfree_slice_eq_cstr(m.name, "empty.o") && m.size == 0, + "empty.o size=%zu", m.size); + EXPECT(cfree_ar_iter_next(it, &m), "second"); + EXPECT( + cfree_slice_eq_cstr(m.name, "next.o") && m.size == 1 && m.data[0] == 'N', + "next.o"); + EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_odd_payload_pad(void) { + /* Odd-length payloads add a '\n' parity pad; even lengths do not. */ + CfreeArInput ms[3]; + BufW bw; + + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"x"; + ms[0].bytes.len = 1; + ms[1].name = CFREE_SLICE_LIT("b.o"); + ms[1].bytes.data = (const uint8_t*)"yy"; + ms[1].bytes.len = 2; + ms[2].name = CFREE_SLICE_LIT("c.o"); + ms[2].bytes.data = (const uint8_t*)"z"; + ms[2].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 3, NULL) == 0, "write"); + /* 8 + (60+1+1) + (60+2) + (60+1+1) = 194 */ + EXPECT(bw.len == 194, "size = %zu", bw.len); + EXPECT(bw.data[8 + 60 + 1] == '\n', "pad after a.o = 0x%02x", + bw.data[8 + 60 + 1]); + /* No pad after b.o (even): next header begins immediately. */ + EXPECT(bw.data[8 + 62 + 60 + 2] == 'c', "c.o name follows b.o without pad"); + + bufw_fini(&bw); + return 1; +} + +static int test_ar_list_output(void) { + /* cfree_ar_list emits one user member per line, skipping / and //. */ + CfreeArInput ms[3]; + BufW bw, lw; + CfreeSlice in; + CfreeArWriteOptions opts = {0}; + const char* expected = "a.o\nlong_name_member.o\nb.o\n"; + + opts.symbol_index = 1; + opts.long_names = 1; + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"A"; + ms[0].bytes.len = 1; + ms[1].name = CFREE_SLICE_LIT("long_name_member.o"); + ms[1].bytes.data = (const uint8_t*)"B"; + ms[1].bytes.len = 1; + ms[2].name = CFREE_SLICE_LIT("b.o"); + ms[2].bytes.data = (const uint8_t*)"C"; + ms[2].bytes.len = 1; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write"); + + in.data = bw.data; + in.len = bw.len; + bufw_init(&lw); + EXPECT(cfree_ar_list(&in, &lw.base) == 0, "list"); + EXPECT(lw.len == strlen(expected), "list len = %zu, want %zu", lw.len, + strlen(expected)); + EXPECT(memcmp(lw.data, expected, lw.len) == 0, "list = %.*s", (int)lw.len, + (const char*)lw.data); + + bufw_fini(&lw); + bufw_fini(&bw); + return 1; +} + +static int test_iter_bad_magic(void) { + /* iter_init must reject non-ar inputs. */ + static const uint8_t bad[] = "NOT-AN-AR"; + CfreeSlice in; + CfreeArIter* it = NULL; + + in.data = bad; + in.len = sizeof(bad) - 1; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects bad magic"); + + in.data = NULL; + in.len = 0; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects empty"); + + in.data = bad; + in.len = 4; /* too short */ + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects short"); + + return 1; +} + +static int test_write_invalid_args(void) { + /* Bad argument combinations must return 1 from cfree_ar_write. */ + CfreeArInput ms[1]; + BufW bw; + CfreeArWriteOptions opts = {0}; + CfreeSlice bad_syms[1]; + CfreeArMemberSymbols msyms[1]; + + bufw_init(&bw); + + EXPECT(cfree_ar_write(NULL, NULL, 0, NULL) != CFREE_OK, + "NULL writer rejected"); + EXPECT(cfree_ar_write(&bw.base, NULL, 1, NULL) != CFREE_OK, + "NULL members with nmembers>0 rejected"); + + ms[0].name = CFREE_SLICE_NULL; + ms[0].bytes.data = (const uint8_t*)"X"; + ms[0].bytes.len = 1; + EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) != CFREE_OK, + "NULL name rejected"); + + /* NULL symbol-name with nonzero count. */ + bad_syms[0] = CFREE_SLICE_NULL; + msyms[0].names = bad_syms; + msyms[0].count = 1; + opts.symbol_index = 1; + opts.member_symbols = msyms; + ms[0].name = CFREE_SLICE_LIT("ok.o"); + EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) != CFREE_OK, + "NULL symbol name rejected"); + + bufw_fini(&bw); + return 1; +} + +static int test_iter_skips_bsd_symdef(void) { + /* Hand-craft an archive containing a BSD __.SYMDEF SORTED member; the + * iterator must not surface it. (cfree_ar_write never emits BSD indexes, + * but the iterator is documented to handle them on read.) */ + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + char hdr[60]; + size_t j; + int seen = 0; + + bufw_init(&bw); + bw.base.write(&bw.base, "!<arch>\n", 8); + + /* __.SYMDEF SORTED with a 4-byte payload. */ + for (j = 0; j < 60; ++j) hdr[j] = ' '; + { + const char* nm = "__.SYMDEF SORTED"; + for (j = 0; j < 16 && nm[j]; ++j) hdr[j] = nm[j]; + } + hdr[16] = '0'; + hdr[28] = '0'; + hdr[34] = '0'; + hdr[40] = '6'; + hdr[41] = '4'; + hdr[42] = '4'; + hdr[48] = '4'; /* size = 4 */ + hdr[58] = '`'; + hdr[59] = '\n'; + bw.base.write(&bw.base, hdr, 60); + bw.base.write(&bw.base, "ZZZZ", 4); + + /* User member u.o size=1 + parity pad. */ + for (j = 0; j < 60; ++j) hdr[j] = ' '; + hdr[0] = 'u'; + hdr[1] = '.'; + hdr[2] = 'o'; + hdr[3] = '/'; + hdr[16] = '0'; + hdr[28] = '0'; + hdr[34] = '0'; + hdr[40] = '6'; + hdr[41] = '4'; + hdr[42] = '4'; + hdr[48] = '1'; + hdr[58] = '`'; + hdr[59] = '\n'; + bw.base.write(&bw.base, hdr, 60); + bw.base.write(&bw.base, "U\n", 2); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + while (cfree_ar_iter_next(it, &m)) { + EXPECT(cfree_slice_eq_cstr(m.name, "u.o"), "unexpected name = %.*s", + CFREE_SLICE_ARG(m.name)); + seen++; + } + EXPECT(seen == 1, "saw %d members", seen); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_iter_data_aliases_archive(void) { + /* CfreeArMember.data must point into the archive's own bytes. */ + CfreeArInput ms[1]; + BufW bw; + CfreeSlice in; + CfreeArIter* it = NULL; + CfreeArMember m; + + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"PAYLOAD"; + ms[0].bytes.len = 7; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); + + in.data = bw.data; + in.len = bw.len; + EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); + EXPECT(cfree_ar_iter_next(it, &m), "first"); + EXPECT(m.data >= bw.data && m.data + m.size <= bw.data + bw.len, + "data aliases archive bytes"); + EXPECT(memcmp(m.data, "PAYLOAD", 7) == 0, "payload"); + + cfree_ar_iter_free(it); + bufw_fini(&bw); + return 1; +} + +static int test_symbol_index_partial_members(void) { + /* Members with 0 symbols mid-list: cur_offset must still advance for + * every member so later offsets land on the right header. */ + CfreeSlice mid_syms[] = {CFREE_SLICE_LIT("midsym")}; + CfreeArMemberSymbols msyms[3]; + CfreeArInput ms[3]; + BufW bw; + CfreeArWriteOptions opts = {0}; + uint32_t nsyms, off; + uint64_t expected_b_hdr; + + opts.symbol_index = 1; + msyms[0].names = NULL; + msyms[0].count = 0; + msyms[1].names = mid_syms; + msyms[1].count = 1; + msyms[2].names = NULL; + msyms[2].count = 0; + opts.member_symbols = msyms; + + ms[0].name = CFREE_SLICE_LIT("a.o"); + ms[0].bytes.data = (const uint8_t*)"AA"; + ms[0].bytes.len = 2; + ms[1].name = CFREE_SLICE_LIT("b.o"); + ms[1].bytes.data = (const uint8_t*)"BB"; + ms[1].bytes.len = 2; + ms[2].name = CFREE_SLICE_LIT("c.o"); + ms[2].bytes.data = (const uint8_t*)"CC"; + ms[2].bytes.len = 2; + + bufw_init(&bw); + EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write"); + + /* Index payload: 4(count) + 4(offset) + 7("midsym\0") = 15 → odd → +1 pad. */ + EXPECT(ar_size_field(bw.data + 8) == 15, "index payload = %llu", + (unsigned long long)ar_size_field(bw.data + 8)); + + nsyms = be32(bw.data + 8 + 60); + EXPECT(nsyms == 1, "nsyms = %u", nsyms); + + off = be32(bw.data + 8 + 60 + 4); + /* magic(8) + index(60+15+1) + a.o(60+2) = 146 */ + expected_b_hdr = 8 + 60 + 15 + 1 + 60 + 2; + EXPECT(off == expected_b_hdr, "off = %u, expected %llu", off, + (unsigned long long)expected_b_hdr); + EXPECT(memcmp(bw.data + off, "b.o/", 4) == 0, "b.o at offset"); + + bufw_fini(&bw); + return 1; +} + +int main(void) { + int passes = 0; + int total = 0; +#define RUN(t) \ + do { \ + total++; \ + passes += (t)(); \ + } while (0) + + cfree_unit_init(&g_u); + RUN(test_basic_roundtrip); + RUN(test_long_name_table); + RUN(test_symbol_index_empty); + RUN(test_symbol_index_basic); + RUN(test_symbol_index_with_long_names); + RUN(test_iter_skips_index); + RUN(test_empty_archive); + RUN(test_epoch_field); + RUN(test_path_basename); + RUN(test_truncate_when_long_names_off); + RUN(test_name_15_char_boundary); + RUN(test_name_16_char_boundary); + RUN(test_empty_member_payload); + RUN(test_odd_payload_pad); + RUN(test_ar_list_output); + RUN(test_iter_bad_magic); + RUN(test_write_invalid_args); + RUN(test_iter_skips_bsd_symdef); + RUN(test_iter_data_aliases_archive); + RUN(test_symbol_index_partial_members); + + if (g_u.fails) { + fprintf(stderr, "ar_test: %d failure(s) (%d/%d passed)\n", g_u.fails, passes, + total); + return 1; + } + printf("ar_test: OK (%d/%d)\n", passes, total); + return 0; +} diff --git a/test/ar/run.sh b/test/ar/run.sh @@ -1,80 +1,33 @@ #!/bin/sh # Driver-level `cfree ar` test harness. # -# Each test/ar/cases/*.sh is a scenario script. The harness allocates a -# fresh sandbox dir per case, cd's in, sources the script with $CFREE -# exported, captures stdout, and diffs against the matching .expected. -# Stderr is forwarded straight through so write/read failures still -# surface. +# Each test/ar/cases/*.sh is a scenario script. The harness allocates a fresh +# sandbox dir per case, cd's in, runs the script with $CFREE exported, captures +# stdout+stderr, and diffs against the matching .expected. The shared loop, +# reporting, and sandbox/cleanup live in test/lib/cfree_sh_report.sh. # -# Honors $CFREE for the binary path; defaults to build/cfree relative -# to the repo root. +# Honors $CFREE for the binary path; defaults to build/cfree. set -u script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree ar-driver -if [ ! -x "$CFREE" ]; then - echo "ar-driver: cfree binary not found at $CFREE" >&2 - exit 2 -fi - -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-ar-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT - -pass=0 -fail=0 -failures= - +CF_WORK=$(cf_workdir ar) +cf_report_init for sh in "$cases_dir"/*.sh; do [ -e "$sh" ] || continue name=$(basename "${sh%.sh}") - expected="${sh%.sh}.expected" - actual="$work_root/$name.actual" - - if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")" - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - sandbox="$work_root/$name" - mkdir -p "$sandbox" - ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1 - case_rc=$? - - if [ "$case_rc" -ne 0 ]; then - printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - if diff -u "$expected" "$actual" >/dev/null 2>&1; then - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "$name" - diff -u "$expected" "$actual" || true - # Keep .actual on failure for debugging by copying out of the - # sandbox before it's cleaned up. - cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true - fail=$((fail + 1)) - failures="$failures $name" - fi + # Pass cases_dir so a failing case's .actual is kept there for debugging + # (the sandbox under CF_WORK is removed on exit). + cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir" done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nar-driver: failures:%s\n' "$failures" - printf 'ar-driver: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\nar-driver: %d/%d passed\n' "$pass" "$total" +cf_summary ar-driver +cf_exit diff --git a/test/ar_test.c b/test/ar_test.c @@ -1,991 +0,0 @@ -/* Round-trip tests for the cfree ar reader/writer. - * - * Builds against just include/cfree.h + libcfree.a and a few libc calls - * (malloc/realloc/free, printf for diagnostics). cfree_ar_write itself - * makes no heap allocations, so the test does not need a CfreeHeap. - * - * Set CFREE_AR_TEST_HOST=1 to also dump the produced symbol-index - * archive to /tmp/cfree_ar_test.a and run the host's `ar t` and - * `nm --print-armap` on it as a cross-check. */ -#include <cfree/archive.h> -#include <cfree/core.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -/* ===== minimal CfreeWriter over a growing buffer ===== */ - -typedef struct BufW { - CfreeWriter base; - uint8_t* data; - size_t len; - size_t cap; - CfreeStatus st; -} BufW; - -static CfreeStatus bufw_write(CfreeWriter* w, const void* data, size_t n) { - BufW* b = (BufW*)w; - if (b->st != CFREE_OK) return b->st; - if (b->len + n > b->cap) { - size_t nc = b->cap ? b->cap * 2 : 256; - while (nc < b->len + n) nc *= 2; - uint8_t* p = (uint8_t*)realloc(b->data, nc); - if (!p) { - b->st = CFREE_NOMEM; - return CFREE_NOMEM; - } - b->data = p; - b->cap = nc; - } - memcpy(b->data + b->len, data, n); - b->len += n; - return CFREE_OK; -} - -static CfreeStatus bufw_seek(CfreeWriter* w, uint64_t off) { - (void)w; - (void)off; - return CFREE_OK; -} -static uint64_t bufw_tell(CfreeWriter* w) { return ((BufW*)w)->len; } -static CfreeStatus bufw_status(CfreeWriter* w) { return ((BufW*)w)->st; } -static void bufw_close(CfreeWriter* w) { (void)w; } - -static void bufw_init(BufW* b) { - b->base.write = bufw_write; - b->base.seek = bufw_seek; - b->base.tell = bufw_tell; - b->base.status = bufw_status; - b->base.close = bufw_close; - b->data = NULL; - b->len = 0; - b->cap = 0; - b->st = CFREE_OK; -} - -static void bufw_fini(BufW* b) { free(b->data); } - -/* ===== libc heap + context (needed by cfree_ar_iter_new) ===== */ - -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static CfreeContext g_ctx = {.heap = &g_heap, .now = -1}; - -/* ===== assertion helpers ===== */ - -static int g_fail; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - g_fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - return 0; \ - } \ - } while (0) - -static uint32_t be32(const uint8_t* p) { - return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | - ((uint32_t)p[2] << 8) | (uint32_t)p[3]; -} - -/* Decode the ar_size field (10-byte ASCII decimal, space-padded). */ -static uint64_t ar_size_field(const uint8_t* hdr) { - uint64_t v = 0; - int j; - for (j = 48; j < 58; ++j) { - char c = (char)hdr[j]; - if (c < '0' || c > '9') break; - v = v * 10 + (uint64_t)(c - '0'); - } - return v; -} - -/* Decode the ar_date field (12-byte ASCII decimal, space-padded). */ -static uint64_t ar_date_field(const uint8_t* hdr) { - uint64_t v = 0; - int j; - for (j = 16; j < 28; ++j) { - char c = (char)hdr[j]; - if (c < '0' || c > '9') break; - v = v * 10 + (uint64_t)(c - '0'); - } - return v; -} - -/* ===== tests ===== */ - -static int test_basic_roundtrip(void) { - CfreeArInput ms[2]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - int rc; - - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"AAAA"; - ms[0].bytes.len = 4; - ms[1].name = CFREE_SLICE_LIT("b.o"); - ms[1].bytes.data = (const uint8_t*)"BBBBB"; - ms[1].bytes.len = 5; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 2, NULL); - EXPECT(rc == 0, "cfree_ar_write returned %d", rc); - EXPECT(bw.st == CFREE_OK, "writer error"); - EXPECT(bw.len >= 8, "archive too short"); - EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - - EXPECT(cfree_ar_iter_next(it, &m), "first member"); - EXPECT(cfree_slice_eq_cstr(m.name, "a.o"), "name 0 = %.*s", - CFREE_SLICE_ARG(m.name)); - EXPECT(m.size == 4 && memcmp(m.data, "AAAA", 4) == 0, "data 0"); - - EXPECT(cfree_ar_iter_next(it, &m), "second member"); - EXPECT(cfree_slice_eq_cstr(m.name, "b.o"), "name 1 = %.*s", - CFREE_SLICE_ARG(m.name)); - EXPECT(m.size == 5 && memcmp(m.data, "BBBBB", 5) == 0, "data 1"); - - EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_long_name_table(void) { - /* >15 chars triggers the // long-name table. */ - CfreeArInput ms[2]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - CfreeArWriteOptions opts = {0}; - int rc; - - opts.long_names = 1; - ms[0].name = CFREE_SLICE_LIT("short.o"); - ms[0].bytes.data = (const uint8_t*)"x"; - ms[0].bytes.len = 1; - ms[1].name = CFREE_SLICE_LIT("this_name_is_long_enough.o"); - ms[1].bytes.data = (const uint8_t*)"yy"; - ms[1].bytes.len = 2; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 2, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first member"); - EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "name 0 = %.*s", - CFREE_SLICE_ARG(m.name)); - EXPECT(cfree_ar_iter_next(it, &m), "second member"); - EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"), - "long name = %.*s", CFREE_SLICE_ARG(m.name)); - EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_symbol_index_empty(void) { - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - CfreeArWriteOptions opts = {0}; - int rc; - uint32_t nsyms; - - opts.symbol_index = 1; - ms[0].name = CFREE_SLICE_LIT("lonely.o"); - ms[0].bytes.data = (const uint8_t*)"P"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 1, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - - /* First member after magic must be `/` index with count=0. */ - EXPECT(bw.len >= 8 + 60 + 4, "archive too short"); - EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index name field"); - EXPECT(ar_size_field(bw.data + 8) == 4, "index payload size = 4"); - nsyms = be32(bw.data + 8 + 60); - EXPECT(nsyms == 0, "nsyms = %u", nsyms); - - /* Iterator should skip the `/` and yield only the user member. */ - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first user member"); - EXPECT(cfree_slice_eq_cstr(m.name, "lonely.o"), "name = %.*s", - CFREE_SLICE_ARG(m.name)); - EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_symbol_index_basic(void) { - /* 2 members with symbol lists; verify count, offsets, and names. */ - CfreeSlice a_syms[] = {CFREE_SLICE_LIT("foo"), CFREE_SLICE_LIT("bar")}; - CfreeSlice b_syms[] = {CFREE_SLICE_LIT("baz")}; - CfreeArMemberSymbols msyms[2]; - CfreeArInput ms[2]; - BufW bw; - CfreeArWriteOptions opts = {0}; - int rc; - uint32_t nsyms; - const uint8_t* p; - uint64_t index_payload; - uint32_t off0, off1, off2; - uint64_t a_hdr_off, b_hdr_off; - const char* name; - - opts.symbol_index = 1; - opts.long_names = 1; - - msyms[0].names = a_syms; - msyms[0].count = 2; - msyms[1].names = b_syms; - msyms[1].count = 1; - opts.member_symbols = msyms; - - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"AAAA"; - ms[0].bytes.len = 4; - ms[1].name = CFREE_SLICE_LIT("b.o"); - ms[1].bytes.data = (const uint8_t*)"BBBBB"; - ms[1].bytes.len = 5; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 2, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - - EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic"); - EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "index ar_name field"); - - index_payload = ar_size_field(bw.data + 8); - /* 4 (count) + 4*3 (offsets) + 3+1 + 3+1 + 3+1 (names "foo\0bar\0baz\0") = 28 - */ - EXPECT(index_payload == 28, "index payload = %llu", - (unsigned long long)index_payload); - - p = bw.data + 8 + 60; - nsyms = be32(p); - p += 4; - EXPECT(nsyms == 3, "nsyms = %u", nsyms); - - off0 = be32(p); /* foo → a.o */ - off1 = be32(p + 4); /* bar → a.o */ - off2 = be32(p + 8); /* baz → b.o */ - p += 12; - - /* Compute expected header offsets: index_total = 60+28 (no pad, even). - * No long-name table is emitted (basenames ≤ 15 chars). So: - * a.o header at offset 8 + 88 = 96 - * b.o header at offset 96 + 60 + 4 (+0 pad) = 160 */ - a_hdr_off = 8 + 60 + 28; - b_hdr_off = a_hdr_off + 60 + 4; - EXPECT(off0 == a_hdr_off, "off0 = %u, expected %llu", off0, - (unsigned long long)a_hdr_off); - EXPECT(off1 == a_hdr_off, "off1 = %u, expected %llu", off1, - (unsigned long long)a_hdr_off); - EXPECT(off2 == b_hdr_off, "off2 = %u, expected %llu", off2, - (unsigned long long)b_hdr_off); - - /* Sanity: the member headers must actually live at those offsets. */ - EXPECT(memcmp(bw.data + a_hdr_off, "a.o/", 4) == 0, "a.o at offset"); - EXPECT(memcmp(bw.data + b_hdr_off, "b.o/", 4) == 0, "b.o at offset"); - - /* Names: "foo\0bar\0baz\0" */ - name = (const char*)p; - EXPECT(strcmp(name, "foo") == 0, "name 0 = %s", name); - name += strlen(name) + 1; - EXPECT(strcmp(name, "bar") == 0, "name 1 = %s", name); - name += strlen(name) + 1; - EXPECT(strcmp(name, "baz") == 0, "name 2 = %s", name); - - bufw_fini(&bw); - return 1; -} - -static int test_symbol_index_with_long_names(void) { - /* `/` member must come BEFORE `//` long-name table, and offsets must - * still point at correct member-header positions. */ - CfreeSlice syms0[] = {CFREE_SLICE_LIT("alpha")}; - CfreeSlice syms1[] = {CFREE_SLICE_LIT("beta")}; - CfreeArMemberSymbols msyms[2]; - CfreeArInput ms[2]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - CfreeArWriteOptions opts = {0}; - int rc; - uint64_t index_payload, longtab_payload; - uint64_t index_total, longtab_total; - uint64_t m0_hdr, m1_hdr; - uint32_t off0, off1; - const uint8_t* p; - - opts.symbol_index = 1; - opts.long_names = 1; - msyms[0].names = syms0; - msyms[0].count = 1; - msyms[1].names = syms1; - msyms[1].count = 1; - opts.member_symbols = msyms; - - ms[0].name = - CFREE_SLICE_LIT("this_name_is_long_enough.o"); /* 26 chars → // */ - ms[0].bytes.data = (const uint8_t*)"X"; - ms[0].bytes.len = 1; - ms[1].name = CFREE_SLICE_LIT("short.o"); - ms[1].bytes.data = (const uint8_t*)"YY"; - ms[1].bytes.len = 2; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 2, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - - /* Layout: magic(8) | / index member | // long-name member | members. */ - EXPECT(bw.data[8] == '/' && bw.data[9] == ' ', "/ first"); - index_payload = ar_size_field(bw.data + 8); - /* 4 (count) + 4*2 (offsets) + 6 ("alpha\0") + 5 ("beta\0") = 23 → odd → pad 1 - */ - EXPECT(index_payload == 23, "index_payload = %llu", - (unsigned long long)index_payload); - index_total = 60 + index_payload + (index_payload & 1); - - /* Verify // header sits at 8 + index_total. */ - EXPECT(bw.data[8 + index_total] == '/', "// pos byte 0"); - EXPECT(bw.data[8 + index_total + 1] == '/', "// pos byte 1"); - longtab_payload = ar_size_field(bw.data + 8 + index_total); - /* "this_name_is_long_enough.o/\n" = 28 bytes → even → no pad */ - EXPECT(longtab_payload == 28, "longtab payload = %llu", - (unsigned long long)longtab_payload); - longtab_total = 60 + longtab_payload + (longtab_payload & 1); - - m0_hdr = 8 + index_total + longtab_total; - m1_hdr = m0_hdr + 60 + 1 + 1 /* parity pad for odd len 1 */; - - p = bw.data + 8 + 60; - EXPECT(be32(p) == 2, "nsyms = %u", be32(p)); - p += 4; - off0 = be32(p); - off1 = be32(p + 4); - EXPECT(off0 == m0_hdr, "off0 = %u, expected %llu", off0, - (unsigned long long)m0_hdr); - EXPECT(off1 == m1_hdr, "off1 = %u, expected %llu", off1, - (unsigned long long)m1_hdr); - /* Spot-check m1 starts with "short.o/" for sanity. */ - EXPECT(memcmp(bw.data + m1_hdr, "short.o/", 8) == 0, "m1 hdr"); - - /* Iterator should walk past both /, // and yield 2 members. */ - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "m0"); - EXPECT(cfree_slice_eq_cstr(m.name, "this_name_is_long_enough.o"), - "m0 name = %.*s", CFREE_SLICE_ARG(m.name)); - EXPECT(cfree_ar_iter_next(it, &m), "m1"); - EXPECT(cfree_slice_eq_cstr(m.name, "short.o"), "m1 name = %.*s", - CFREE_SLICE_ARG(m.name)); - EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); - - /* Optional host cross-check. */ - if (getenv("CFREE_AR_TEST_HOST")) { - FILE* f = fopen("/tmp/cfree_ar_test.a", "wb"); - if (f) { - fwrite(bw.data, 1, bw.len, f); - fclose(f); - fprintf(stderr, "host cross-check: ar t /tmp/cfree_ar_test.a\n"); - (void)!system("ar t /tmp/cfree_ar_test.a"); - fprintf(stderr, - "host cross-check: nm --print-armap /tmp/cfree_ar_test.a\n"); - (void)!system("nm --print-armap /tmp/cfree_ar_test.a 2>&1 || true"); - } - } - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_iter_skips_index(void) { - /* Make sure the iterator never surfaces the `/` member as a user member. */ - CfreeSlice s[] = {CFREE_SLICE_LIT("only_sym")}; - CfreeArMemberSymbols msyms[1]; - CfreeArInput ms[1]; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - BufW bw; - CfreeArWriteOptions opts = {0}; - int seen = 0; - - opts.symbol_index = 1; - msyms[0].names = s; - msyms[0].count = 1; - opts.member_symbols = msyms; - ms[0].name = CFREE_SLICE_LIT("only.o"); - ms[0].bytes.data = (const uint8_t*)"Z"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - while (cfree_ar_iter_next(it, &m)) { - EXPECT(!cfree_slice_eq_cstr(m.name, "/"), "iter surfaced raw `/` member"); - seen++; - } - EXPECT(seen == 1, "saw %d members", seen); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_empty_archive(void) { - /* nmembers == 0 with NULL members should produce a magic-only archive. */ - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - int rc; - - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, NULL, 0, NULL); - EXPECT(rc == 0, "write rc=%d", rc); - EXPECT(bw.len == 8, "size = %zu", bw.len); - EXPECT(memcmp(bw.data, "!<arch>\n", 8) == 0, "magic only"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(!cfree_ar_iter_next(it, &m), "no members"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_epoch_field(void) { - /* opts.epoch is written into ar_date for every member. */ - CfreeArInput ms[1]; - BufW bw; - CfreeArWriteOptions opts = {0}; - int rc; - - ms[0].name = CFREE_SLICE_LIT("x.o"); - ms[0].bytes.data = (const uint8_t*)"q"; - ms[0].bytes.len = 1; - - opts.epoch = 1234567890u; - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 1, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - EXPECT(ar_date_field(bw.data + 8) == 1234567890u, "ar_date = %llu", - (unsigned long long)ar_date_field(bw.data + 8)); - bufw_fini(&bw); - - /* Default (epoch=0): single '0' followed by spaces. */ - opts.epoch = 0; - bufw_init(&bw); - rc = cfree_ar_write(&bw.base, ms, 1, &opts); - EXPECT(rc == 0, "write rc=%d", rc); - EXPECT(bw.data[8 + 16] == '0', "epoch default first byte"); - EXPECT(bw.data[8 + 17] == ' ', "epoch default second byte = 0x%02x", - bw.data[8 + 17]); - bufw_fini(&bw); - return 1; -} - -static int test_path_basename(void) { - /* Member name with path components is stored as basename only. */ - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - - ms[0].name = CFREE_SLICE_LIT("src/sub/foo.o"); - ms[0].bytes.data = (const uint8_t*)"D"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(cfree_slice_eq_cstr(m.name, "foo.o"), "basename = %.*s", - CFREE_SLICE_ARG(m.name)); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_truncate_when_long_names_off(void) { - /* >15 chars without long_names: name is truncated to 15. */ - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - - ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnopqrst.o"); /* 22 chars */ - ms[0].bytes.data = (const uint8_t*)"D"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "truncated = %.*s", - CFREE_SLICE_ARG(m.name)); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_name_15_char_boundary(void) { - /* Exactly 15 chars: fits in-header even with long_names enabled. */ - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - CfreeArWriteOptions opts = {0}; - - opts.long_names = 1; - ms[0].name = CFREE_SLICE_LIT("abcdefghijklmno"); /* 15 chars */ - ms[0].bytes.data = (const uint8_t*)"X"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); - - /* No `//` long-name table: first member sits right after magic. */ - EXPECT(memcmp(bw.data + 8, "abcdefghijklmno/", 16) == 0, "name field = %.16s", - (const char*)(bw.data + 8)); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmno"), "name = %.*s", - CFREE_SLICE_ARG(m.name)); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_name_16_char_boundary(void) { - /* Exactly 16 chars: triggers // long-name table. */ - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - CfreeArWriteOptions opts = {0}; - - opts.long_names = 1; - ms[0].name = CFREE_SLICE_LIT("abcdefghijklmnop"); /* 16 chars */ - ms[0].bytes.data = (const uint8_t*)"Y"; - ms[0].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) == 0, "write"); - EXPECT(bw.data[8] == '/' && bw.data[9] == '/', "// header"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(cfree_slice_eq_cstr(m.name, "abcdefghijklmnop"), "name = %.*s", - CFREE_SLICE_ARG(m.name)); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_empty_member_payload(void) { - /* len=0 (data=NULL): header only, no pad; followed by the next member. */ - CfreeArInput ms[2]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - - ms[0].name = CFREE_SLICE_LIT("empty.o"); - ms[0].bytes.data = NULL; - ms[0].bytes.len = 0; - ms[1].name = CFREE_SLICE_LIT("next.o"); - ms[1].bytes.data = (const uint8_t*)"N"; - ms[1].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 2, NULL) == 0, "write"); - /* magic(8) + hdr(60) + 0 + hdr(60) + 1 + pad(1) = 130 */ - EXPECT(bw.len == 130, "size = %zu", bw.len); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(cfree_slice_eq_cstr(m.name, "empty.o") && m.size == 0, - "empty.o size=%zu", m.size); - EXPECT(cfree_ar_iter_next(it, &m), "second"); - EXPECT( - cfree_slice_eq_cstr(m.name, "next.o") && m.size == 1 && m.data[0] == 'N', - "next.o"); - EXPECT(!cfree_ar_iter_next(it, &m), "iter end"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_odd_payload_pad(void) { - /* Odd-length payloads add a '\n' parity pad; even lengths do not. */ - CfreeArInput ms[3]; - BufW bw; - - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"x"; - ms[0].bytes.len = 1; - ms[1].name = CFREE_SLICE_LIT("b.o"); - ms[1].bytes.data = (const uint8_t*)"yy"; - ms[1].bytes.len = 2; - ms[2].name = CFREE_SLICE_LIT("c.o"); - ms[2].bytes.data = (const uint8_t*)"z"; - ms[2].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 3, NULL) == 0, "write"); - /* 8 + (60+1+1) + (60+2) + (60+1+1) = 194 */ - EXPECT(bw.len == 194, "size = %zu", bw.len); - EXPECT(bw.data[8 + 60 + 1] == '\n', "pad after a.o = 0x%02x", - bw.data[8 + 60 + 1]); - /* No pad after b.o (even): next header begins immediately. */ - EXPECT(bw.data[8 + 62 + 60 + 2] == 'c', "c.o name follows b.o without pad"); - - bufw_fini(&bw); - return 1; -} - -static int test_ar_list_output(void) { - /* cfree_ar_list emits one user member per line, skipping / and //. */ - CfreeArInput ms[3]; - BufW bw, lw; - CfreeSlice in; - CfreeArWriteOptions opts = {0}; - const char* expected = "a.o\nlong_name_member.o\nb.o\n"; - - opts.symbol_index = 1; - opts.long_names = 1; - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"A"; - ms[0].bytes.len = 1; - ms[1].name = CFREE_SLICE_LIT("long_name_member.o"); - ms[1].bytes.data = (const uint8_t*)"B"; - ms[1].bytes.len = 1; - ms[2].name = CFREE_SLICE_LIT("b.o"); - ms[2].bytes.data = (const uint8_t*)"C"; - ms[2].bytes.len = 1; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write"); - - in.data = bw.data; - in.len = bw.len; - bufw_init(&lw); - EXPECT(cfree_ar_list(&in, &lw.base) == 0, "list"); - EXPECT(lw.len == strlen(expected), "list len = %zu, want %zu", lw.len, - strlen(expected)); - EXPECT(memcmp(lw.data, expected, lw.len) == 0, "list = %.*s", (int)lw.len, - (const char*)lw.data); - - bufw_fini(&lw); - bufw_fini(&bw); - return 1; -} - -static int test_iter_bad_magic(void) { - /* iter_init must reject non-ar inputs. */ - static const uint8_t bad[] = "NOT-AN-AR"; - CfreeSlice in; - CfreeArIter* it = NULL; - - in.data = bad; - in.len = sizeof(bad) - 1; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects bad magic"); - - in.data = NULL; - in.len = 0; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects empty"); - - in.data = bad; - in.len = 4; /* too short */ - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) != CFREE_OK, "rejects short"); - - return 1; -} - -static int test_write_invalid_args(void) { - /* Bad argument combinations must return 1 from cfree_ar_write. */ - CfreeArInput ms[1]; - BufW bw; - CfreeArWriteOptions opts = {0}; - CfreeSlice bad_syms[1]; - CfreeArMemberSymbols msyms[1]; - - bufw_init(&bw); - - EXPECT(cfree_ar_write(NULL, NULL, 0, NULL) != CFREE_OK, - "NULL writer rejected"); - EXPECT(cfree_ar_write(&bw.base, NULL, 1, NULL) != CFREE_OK, - "NULL members with nmembers>0 rejected"); - - ms[0].name = CFREE_SLICE_NULL; - ms[0].bytes.data = (const uint8_t*)"X"; - ms[0].bytes.len = 1; - EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) != CFREE_OK, - "NULL name rejected"); - - /* NULL symbol-name with nonzero count. */ - bad_syms[0] = CFREE_SLICE_NULL; - msyms[0].names = bad_syms; - msyms[0].count = 1; - opts.symbol_index = 1; - opts.member_symbols = msyms; - ms[0].name = CFREE_SLICE_LIT("ok.o"); - EXPECT(cfree_ar_write(&bw.base, ms, 1, &opts) != CFREE_OK, - "NULL symbol name rejected"); - - bufw_fini(&bw); - return 1; -} - -static int test_iter_skips_bsd_symdef(void) { - /* Hand-craft an archive containing a BSD __.SYMDEF SORTED member; the - * iterator must not surface it. (cfree_ar_write never emits BSD indexes, - * but the iterator is documented to handle them on read.) */ - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - char hdr[60]; - size_t j; - int seen = 0; - - bufw_init(&bw); - bw.base.write(&bw.base, "!<arch>\n", 8); - - /* __.SYMDEF SORTED with a 4-byte payload. */ - for (j = 0; j < 60; ++j) hdr[j] = ' '; - { - const char* nm = "__.SYMDEF SORTED"; - for (j = 0; j < 16 && nm[j]; ++j) hdr[j] = nm[j]; - } - hdr[16] = '0'; - hdr[28] = '0'; - hdr[34] = '0'; - hdr[40] = '6'; - hdr[41] = '4'; - hdr[42] = '4'; - hdr[48] = '4'; /* size = 4 */ - hdr[58] = '`'; - hdr[59] = '\n'; - bw.base.write(&bw.base, hdr, 60); - bw.base.write(&bw.base, "ZZZZ", 4); - - /* User member u.o size=1 + parity pad. */ - for (j = 0; j < 60; ++j) hdr[j] = ' '; - hdr[0] = 'u'; - hdr[1] = '.'; - hdr[2] = 'o'; - hdr[3] = '/'; - hdr[16] = '0'; - hdr[28] = '0'; - hdr[34] = '0'; - hdr[40] = '6'; - hdr[41] = '4'; - hdr[42] = '4'; - hdr[48] = '1'; - hdr[58] = '`'; - hdr[59] = '\n'; - bw.base.write(&bw.base, hdr, 60); - bw.base.write(&bw.base, "U\n", 2); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - while (cfree_ar_iter_next(it, &m)) { - EXPECT(cfree_slice_eq_cstr(m.name, "u.o"), "unexpected name = %.*s", - CFREE_SLICE_ARG(m.name)); - seen++; - } - EXPECT(seen == 1, "saw %d members", seen); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_iter_data_aliases_archive(void) { - /* CfreeArMember.data must point into the archive's own bytes. */ - CfreeArInput ms[1]; - BufW bw; - CfreeSlice in; - CfreeArIter* it = NULL; - CfreeArMember m; - - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"PAYLOAD"; - ms[0].bytes.len = 7; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 1, NULL) == 0, "write"); - - in.data = bw.data; - in.len = bw.len; - EXPECT(cfree_ar_iter_new(&g_ctx, &in, &it) == CFREE_OK, "iter_init"); - EXPECT(cfree_ar_iter_next(it, &m), "first"); - EXPECT(m.data >= bw.data && m.data + m.size <= bw.data + bw.len, - "data aliases archive bytes"); - EXPECT(memcmp(m.data, "PAYLOAD", 7) == 0, "payload"); - - cfree_ar_iter_free(it); - bufw_fini(&bw); - return 1; -} - -static int test_symbol_index_partial_members(void) { - /* Members with 0 symbols mid-list: cur_offset must still advance for - * every member so later offsets land on the right header. */ - CfreeSlice mid_syms[] = {CFREE_SLICE_LIT("midsym")}; - CfreeArMemberSymbols msyms[3]; - CfreeArInput ms[3]; - BufW bw; - CfreeArWriteOptions opts = {0}; - uint32_t nsyms, off; - uint64_t expected_b_hdr; - - opts.symbol_index = 1; - msyms[0].names = NULL; - msyms[0].count = 0; - msyms[1].names = mid_syms; - msyms[1].count = 1; - msyms[2].names = NULL; - msyms[2].count = 0; - opts.member_symbols = msyms; - - ms[0].name = CFREE_SLICE_LIT("a.o"); - ms[0].bytes.data = (const uint8_t*)"AA"; - ms[0].bytes.len = 2; - ms[1].name = CFREE_SLICE_LIT("b.o"); - ms[1].bytes.data = (const uint8_t*)"BB"; - ms[1].bytes.len = 2; - ms[2].name = CFREE_SLICE_LIT("c.o"); - ms[2].bytes.data = (const uint8_t*)"CC"; - ms[2].bytes.len = 2; - - bufw_init(&bw); - EXPECT(cfree_ar_write(&bw.base, ms, 3, &opts) == 0, "write"); - - /* Index payload: 4(count) + 4(offset) + 7("midsym\0") = 15 → odd → +1 pad. */ - EXPECT(ar_size_field(bw.data + 8) == 15, "index payload = %llu", - (unsigned long long)ar_size_field(bw.data + 8)); - - nsyms = be32(bw.data + 8 + 60); - EXPECT(nsyms == 1, "nsyms = %u", nsyms); - - off = be32(bw.data + 8 + 60 + 4); - /* magic(8) + index(60+15+1) + a.o(60+2) = 146 */ - expected_b_hdr = 8 + 60 + 15 + 1 + 60 + 2; - EXPECT(off == expected_b_hdr, "off = %u, expected %llu", off, - (unsigned long long)expected_b_hdr); - EXPECT(memcmp(bw.data + off, "b.o/", 4) == 0, "b.o at offset"); - - bufw_fini(&bw); - return 1; -} - -int main(void) { - int passes = 0; - int total = 0; -#define RUN(t) \ - do { \ - total++; \ - passes += (t)(); \ - } while (0) - - RUN(test_basic_roundtrip); - RUN(test_long_name_table); - RUN(test_symbol_index_empty); - RUN(test_symbol_index_basic); - RUN(test_symbol_index_with_long_names); - RUN(test_iter_skips_index); - RUN(test_empty_archive); - RUN(test_epoch_field); - RUN(test_path_basename); - RUN(test_truncate_when_long_names_off); - RUN(test_name_15_char_boundary); - RUN(test_name_16_char_boundary); - RUN(test_empty_member_payload); - RUN(test_odd_payload_pad); - RUN(test_ar_list_output); - RUN(test_iter_bad_magic); - RUN(test_write_invalid_args); - RUN(test_iter_skips_bsd_symdef); - RUN(test_iter_data_aliases_archive); - RUN(test_symbol_index_partial_members); - - if (g_fail) { - fprintf(stderr, "ar_test: %d failure(s) (%d/%d passed)\n", g_fail, passes, - total); - return 1; - } - printf("ar_test: OK (%d/%d)\n", passes, total); - return 0; -} diff --git a/test/arch/aa64_inline_test.c b/test/arch/aa64_inline_test.c @@ -76,8 +76,8 @@ int main(void) { aa64_bad_ldr_zr, "zero register"), "expected ldr w0, [xzr] to panic"); - if (env.fail) { - fprintf(stderr, "%d failure(s)\n", env.fail); + if (env.fails) { + fprintf(stderr, "%d failure(s)\n", env.fails); return 1; } printf("aa64_inline_test: ok\n"); diff --git a/test/arch/aa64_isa_test.c b/test/arch/aa64_isa_test.c @@ -16,23 +16,27 @@ #include "arch/aa64/isa.h" #include "core/strbuf.h" +#include "lib/cfree_unit.h" -static int fails = 0; -static int cases = 0; +/* Shared test context replaces the per-file fails/cases counter globals; each + * check() invocation records exactly one case on g_u (one ++g_u.checks, plus + * ++g_u.fails on a miss) so the "N cases ok" tally and exit status are + * unchanged. */ +static CfreeUnit g_u; static void check(u32 word, const char* want_mnem, const char* want_ops_substr) { - ++cases; + ++g_u.checks; const AA64InsnDesc* d = aa64_disasm_find(word); if (!d) { fprintf(stderr, "FAIL 0x%08x: aa64_disasm_find returned NULL\n", word); - ++fails; + ++g_u.fails; return; } if (!slice_eq_cstr(d->mnemonic, want_mnem)) { fprintf(stderr, "FAIL 0x%08x: mnemonic = %.*s, want %s\n", word, SLICE_ARG(d->mnemonic), want_mnem); - ++fails; + ++g_u.fails; return; } char buf[128]; @@ -42,18 +46,20 @@ static void check(u32 word, const char* want_mnem, if (sb.truncated) { fprintf(stderr, "FAIL 0x%08x %s: operand print truncated\n", word, want_mnem); - ++fails; + ++g_u.fails; return; } if (want_ops_substr && !strstr(buf, want_ops_substr)) { fprintf(stderr, "FAIL 0x%08x %s: operands=%s\n expected substring %s\n", word, want_mnem, buf, want_ops_substr); - ++fails; + ++g_u.fails; return; } } int main(void) { + cfree_unit_init(&g_u); + /* ---- per-format coverage ---- */ /* MOVEWIDE: movz x0, #0x1234 → sf=1, opc=10, hw=0, imm16=0x1234, Rd=0 */ @@ -177,12 +183,12 @@ int main(void) { stderr, "FAIL: alias precedence — ORR-as-MOV resolved to %.*s (want mov)\n", SLICE_ARG(d ? d->mnemonic : SLICE_LIT("(null)"))); - ++fails; + ++g_u.fails; } else if (!(d->flags & AA64_ASMFL_ALIAS)) { fprintf(stderr, "FAIL: alias precedence — resolved row missing ALIAS\n"); - ++fails; + ++g_u.fails; } - ++cases; + ++g_u.checks; } /* ORR X1, X3, X2 (Rn != ZR) must not resolve to "mov". */ @@ -192,32 +198,32 @@ int main(void) { if (d == NULL || !slice_eq_cstr(d->mnemonic, "orr")) { fprintf(stderr, "FAIL: non-alias ORR resolved to %.*s (want orr)\n", SLICE_ARG(d ? d->mnemonic : SLICE_LIT("(null)"))); - ++fails; + ++g_u.fails; } - ++cases; + ++g_u.checks; } /* Logical immediates: 0xff00 must not be encoded as the rotated * 0xff000000 mask. */ { u32 N = 0, immr = 0, imms = 0; - ++cases; + ++g_u.checks; if (!aa64_logimm_encode(0xff00u, 0, &N, &immr, &imms)) { fprintf(stderr, "FAIL: aa64_logimm_encode rejected 0xff00\n"); - ++fails; + ++g_u.fails; } else { u32 w = aa64_and_imm(0, 19, 19, N, immr, imms); if (w != 0x12181e73u) { fprintf(stderr, "FAIL: and w19,w19,#0xff00 encoded 0x%08x\n", w); - ++fails; + ++g_u.fails; } } } - if (fails) { - fprintf(stderr, "%d / %d failed\n", fails, cases); + if (g_u.fails) { + fprintf(stderr, "%d / %d failed\n", g_u.fails, g_u.checks); return 1; } - printf("aa64_isa_test: %d cases ok\n", cases); + printf("aa64_isa_test: %d cases ok\n", g_u.checks); return 0; } diff --git a/test/arch/inline_public_test.h b/test/arch/inline_public_test.h @@ -4,19 +4,24 @@ #include <cfree/cg.h> #include <cfree/frontend.h> #include <cfree/object.h> -#include <stdarg.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -typedef struct InlineTestEnv { - CfreeHeap heap; - CfreeDiagSink diag; - CfreeContext ctx; - char last_diag[256]; - int suppress_diag; - int fail; -} InlineTestEnv; +#include "lib/cfree_unit.h" + +/* Inline-asm backend tests build on the shared unit scaffolding: the test env + * is a CfreeUnit (host heap + diag sink that captures last_diag + ctx + + * fail/check counters). Only the inline-asm-specific emit/panic/operand + * helpers live here; the heap/diag/check/target boilerplate comes from + * cfree_unit.h. */ +typedef CfreeUnit InlineTestEnv; + +/* Like cfree_unit_init but with now=-1, the value this harness has always + * used (inert for these byte-level assertions). */ +#define it_env_init(env) (cfree_unit_init(env), (void)((env)->ctx.now = -1)) + +/* IT_EXPECT / it_contains map onto the shared check + byte-search helpers so + * existing call sites read unchanged. */ +#define IT_EXPECT(env, cond, ...) CU_EXPECT(env, cond, __VA_ARGS__) +#define it_contains cfree_unit_contains typedef struct InlineText { CfreeWriter* writer; @@ -34,85 +39,11 @@ typedef struct InlineEmit { const char* name; } InlineEmit; -static void* it_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* it_realloc(CfreeHeap* h, void* p, size_t old_n, size_t n, - size_t a) { - (void)h; - (void)old_n; - (void)a; - return realloc(p, n); -} - -static void it_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} - -static void it_diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - InlineTestEnv* env = (InlineTestEnv*)s->user; - va_list copy; - (void)loc; - va_copy(copy, ap); - vsnprintf(env->last_diag, sizeof env->last_diag, fmt, copy); - va_end(copy); - if (env->suppress_diag && k == CFREE_DIAG_FATAL) return; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static void it_env_init(InlineTestEnv* env) { - memset(env, 0, sizeof *env); - env->heap.alloc = it_alloc; - env->heap.realloc = it_realloc; - env->heap.free = it_free; - env->diag.emit = it_diag_emit; - env->diag.user = env; - env->ctx.heap = &env->heap; - env->ctx.diag = &env->diag; - env->ctx.now = -1; -} - -#define IT_EXPECT(env, cond, ...) \ - do { \ - if (!(cond)) { \ - (env)->fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) - -static CfreeTarget it_target(CfreeArchKind arch) { - CfreeTarget t; - memset(&t, 0, sizeof t); - t.arch = arch; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; - return t; -} - -static int it_contains(const uint8_t* data, size_t len, const uint8_t* pat, - size_t pat_len) { - size_t i; - if (pat_len == 0) return 1; - if (len < pat_len) return 0; - for (i = 0; i <= len - pat_len; ++i) { - if (memcmp(data + i, pat, pat_len) == 0) return 1; - } - return 0; +static inline CfreeTarget it_target(CfreeArchKind arch) { + return cfree_unit_target(arch, CFREE_OS_LINUX, CFREE_OBJ_ELF); } -static CfreeStatus it_emit_func(CfreeCompiler* c, void* user) { +static inline CfreeStatus it_emit_func(CfreeCompiler* c, void* user) { InlineEmit* emit = (InlineEmit*)user; CfreeCg* cg = NULL; CfreeCgBuiltinTypes bi; @@ -150,8 +81,9 @@ static CfreeStatus it_emit_func(CfreeCompiler* c, void* user) { return CFREE_OK; } -static int it_emit_text(InlineTestEnv* env, CfreeArchKind arch, - const char* name, InlineBodyFn body, InlineText* text) { +static inline int it_emit_text(InlineTestEnv* env, CfreeArchKind arch, + const char* name, InlineBodyFn body, + InlineText* text) { CfreeCompiler* c = NULL; InlineEmit emit; CfreeSlice bytes; @@ -195,7 +127,7 @@ done: return ok; } -static void it_text_close(InlineText* text) { +static inline void it_text_close(InlineText* text) { if (text->file) cfree_obj_free(text->file); if (text->writer) cfree_writer_close(text->writer); memset(text, 0, sizeof *text); @@ -206,7 +138,7 @@ typedef struct InlinePanic { const char* name; } InlinePanic; -static CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) { +static inline CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) { InlinePanic* panic = (InlinePanic*)user; InlineEmit emit; CfreeStatus st; @@ -218,9 +150,9 @@ static CfreeStatus it_run_panic_body(CfreeCompiler* c, void* user) { return st; } -static int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch, - const char* name, InlineBodyFn body, - const char* expected) { +static inline int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch, + const char* name, InlineBodyFn body, + const char* expected) { CfreeCompiler* c = NULL; InlinePanic panic; CfreeStatus st; @@ -230,18 +162,19 @@ static int it_expect_panic(InlineTestEnv* env, CfreeArchKind arch, panic.body = body; panic.name = name; env->last_diag[0] = '\0'; - env->suppress_diag++; + env->suppress_fatal++; st = cfree_frontend_run(c, it_run_panic_body, &panic); - env->suppress_diag--; + env->suppress_fatal--; ok = st == CFREE_ERR && (!expected || strstr(env->last_diag, expected) != NULL); cfree_compiler_free(c); return ok; } -static CfreeCgAsmOperand it_asm_op(CfreeCompiler* c, const char* constraint, - const char* name, CfreeCgTypeId type, - CfreeCgAsmDir dir) { +static inline CfreeCgAsmOperand it_asm_op(CfreeCompiler* c, + const char* constraint, + const char* name, CfreeCgTypeId type, + CfreeCgAsmDir dir) { CfreeCgAsmOperand op; memset(&op, 0, sizeof op); op.constraint = cfree_sym_intern(c, cfree_slice_cstr(constraint)); @@ -251,11 +184,12 @@ static CfreeCgAsmOperand it_asm_op(CfreeCompiler* c, const char* constraint, return op; } -static void it_inline_asm(CfreeCompiler* c, CfreeCg* cg, const char* tmpl, - const CfreeCgAsmOperand* outs, uint32_t nouts, - const CfreeCgAsmOperand* ins, uint32_t nins, - const char* const* clobber_names, - uint32_t nclobbers) { +static inline void it_inline_asm(CfreeCompiler* c, CfreeCg* cg, + const char* tmpl, + const CfreeCgAsmOperand* outs, uint32_t nouts, + const CfreeCgAsmOperand* ins, uint32_t nins, + const char* const* clobber_names, + uint32_t nclobbers) { CfreeCgInlineAsm a; CfreeSym clobbers[8]; uint32_t i; diff --git a/test/arch/rv64_decode_test.c b/test/arch/rv64_decode_test.c @@ -13,61 +13,18 @@ #include "arch/arch.h" #include "arch/rv64/isa.h" +#include "lib/cfree_unit.h" -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; -static CfreeContext g_ctx; - -static int fails; - -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static CfreeCompiler* new_compiler(void) { - CfreeTarget t; + CfreeTarget t = cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX, + CFREE_OBJ_ELF); CfreeCompiler* c = NULL; - memset(&t, 0, sizeof(t)); - t.arch = CFREE_ARCH_RV64; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof(g_ctx)); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "compiler_new failed\n"); exit(2); } @@ -161,14 +118,13 @@ static void format_decoded_record(CfreeCompiler* pub) { } int main(void) { - CfreeCompiler* c = new_compiler(); + CfreeCompiler* c; + cfree_unit_init(&g_u); + c = new_compiler(); decode_addi(c); decode_block_stops_at_ecall(c); format_decoded_record(c); cfree_compiler_free(c); - if (fails) { - fprintf(stderr, "rv64_decode_test: %d failure(s)\n", fails); - return 1; - } - return 0; + cfree_unit_summary(&g_u, "rv64_decode_test"); + return cfree_unit_status(&g_u); } diff --git a/test/arch/rv64_inline_test.c b/test/arch/rv64_inline_test.c @@ -74,8 +74,8 @@ int main(void) { rv64_bad_operand, "operand index"), "expected out-of-range rv64 asm operand to panic"); - if (env.fail) { - fprintf(stderr, "%d failure(s)\n", env.fail); + if (env.fails) { + fprintf(stderr, "%d failure(s)\n", env.fails); return 1; } printf("rv64_inline_test: ok\n"); diff --git a/test/arch/x64_dbg_test.c b/test/arch/x64_dbg_test.c @@ -10,18 +10,13 @@ #include "arch/arch.h" #include "core/bytes.h" +#include "lib/cfree_unit.h" -static int g_fail = 0; - -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - g_fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } \ - } while (0) +/* Shared test context replaces the per-file counter global; EXPECT aliases + * CU_EXPECT so the call sites are unchanged. This test exercises only the + * arch lookup, so it uses none of the heap/diag/compiler wiring. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static const ArchDbgOps* x64_dbg(void) { const ArchImpl* a = arch_lookup(CFREE_ARCH_X86_64); @@ -133,15 +128,17 @@ static void check_branches_and_calls(const ArchDbgOps* dbg) { } int main(void) { - const ArchDbgOps* dbg = x64_dbg(); + const ArchDbgOps* dbg; + cfree_unit_init(&g_u); + dbg = x64_dbg(); EXPECT(dbg != NULL, "x64 dbg ops missing"); if (!dbg) return 1; check_breakpoint(dbg); check_rip_relative(dbg); check_fs_tls_copy(dbg); check_branches_and_calls(dbg); - if (g_fail) { - fprintf(stderr, "%d FAILED\n", g_fail); + if (g_u.fails) { + fprintf(stderr, "%d FAILED\n", g_u.fails); return 1; } printf("x64 dbg test: OK\n"); diff --git a/test/arch/x64_inline_test.c b/test/arch/x64_inline_test.c @@ -52,8 +52,8 @@ int main(void) { x64_bad_operand, "operand index"), "expected out-of-range x64 asm operand to panic"); - if (env.fail) { - fprintf(stderr, "%d failure(s)\n", env.fail); + if (env.fails) { + fprintf(stderr, "%d failure(s)\n", env.fails); return 1; } printf("x64_inline_test: ok\n"); diff --git a/test/asm/diff_llvm.sh b/test/asm/diff_llvm.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# test/asm/diff_llvm.sh — differential cross-check of cfree against llvm (aa64). +# test/asm/diff_llvm.sh — Type D (differential, reference mode): +# cross-check cfree against llvm (aa64) as a second, independent oracle. # # Two byte-level lanes (robust: no disassembly-text normalization, which would # founder on alias/format differences like movz-vs-mov or #16-vs-#0x10): @@ -16,12 +17,25 @@ # decode (one a self-round-trip can't, since cfree's own re-encode would # repeat the mistake). # +# Type D oracle: AGREEMENT between two independent producers (cfree vs llvm-mc), +# recorded per case via cf_diff_agree. Two recognized equivalence-skips precede +# the comparison and are cf_skip'd, never flagged: +# - llvm-mc REJECTS an input cfree accepts (forms outside llvm's dialect). +# - the benign same-section-call reloc-vs-resolve difference: cfree codegen +# keeps a CALL26/JUMP26/CONDBR reloc for a same-section call/branch to a +# defined local symbol, where llvm-mc (like GNU as) resolves it in place and +# drops the reloc. Both link to the same bytes; only the relocatable form +# differs. +# # Opt-in; requires llvm-mc (+ a cfree-readable object). Skips cleanly if the # tools are absent. See doc/ASM_ROUNDTRIP_TESTING.md. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +. "$ROOT/test/lib/cfree_sh_report.sh" +. "$ROOT/test/lib/cf_differential.sh" + CFREE="$ROOT/build/cfree" ENCODE_DIR="$ROOT/test/asm/encode" RT_DIR="$ROOT/test/asm/roundtrip" @@ -32,17 +46,17 @@ OPTS="${CFREE_TEST_OPTS:-O1}" LLVM_MC="${LLVM_MC:-$(command -v llvm-mc || echo /opt/homebrew/bin/llvm-mc)}" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } +cf_report_init if [ ! -x "$LLVM_MC" ]; then - printf 'diff-llvm: %s llvm-mc not found (set $LLVM_MC); skipping\n' \ - "$(color_yel SKIP)" - exit 0 + # No second oracle available: skip cleanly. cf_skip (not a hard exit) so the + # summary/exit path is uniform with the rest of the harness. + cf_skip diff-llvm "llvm-mc not found (set \$LLVM_MC)" + cf_summary diff-llvm + cf_exit fi if [ ! -x "$CFREE" ]; then - printf 'diff-llvm: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2 + echo "diff-llvm: cfree missing — run \"make bin\"" >&2 exit 2 fi mkdir -p "$WORK" @@ -62,9 +76,7 @@ text_relocs() { "$CFREE" objdump -r "$1" 2>/dev/null | awk ' f && /^[0-9a-f]/{print $2, $3}'; } mc() { "$LLVM_MC" -triple="$TRIPLE" -mattr="$MATTR" -filetype=obj "$1" -o "$2" 2>"$WORK/mc.err"; } -agree=0; differ=0; reject=0; reloc_skip=0 -fails=() - +# --- encode lane: cfree as vs llvm-mc over the encode corpus ---------------- printf 'diff-llvm: encode lane (cfree as vs llvm-mc over the encode corpus)\n' shopt -s nullglob for s in "$ENCODE_DIR"/aa64_*.s; do @@ -73,19 +85,17 @@ for s in "$ENCODE_DIR"/aa64_*.s; do [ -f "$tg" ] && ! grep -qE 'aa64|aarch64|arm64' "$tg" && continue "$CFREE" as -target "$TRIPLE" "$s" -o "$WORK/c.o" 2>/dev/null || continue if ! mc "$s" "$WORK/l.o"; then - reject=$((reject+1)) - printf ' %s %s: llvm-mc rejected (%s)\n' "$(color_yel SKIP)" "$name" \ - "$(head -1 "$WORK/mc.err" | sed 's|.*error: *||')" + # llvm-mc rejected a form cfree accepts: recognized dialect divergence. + cf_skip "encode:$name" "llvm-mc rejected ($(head -1 "$WORK/mc.err" | sed 's|.*error: *||'))" continue fi - if [ "$(raw "$WORK/c.o")" = "$(raw "$WORK/l.o")" ]; then - agree=$((agree+1)) - else - differ=$((differ+1)); fails+=("encode:$name") - printf ' %s %s: .text bytes differ\n' "$(color_red DIFF)" "$name" - fi + # Compare the normalized .text byte extraction (same shape both sides). + raw "$WORK/c.o" > "$WORK/c.bytes" + raw "$WORK/l.o" > "$WORK/l.bytes" + cf_diff_agree "encode:$name" "$WORK/c.bytes" "$WORK/l.bytes" done +# --- disasm lane: cc -c bytes vs llvm-mc of cc -S --------------------------- printf 'diff-llvm: disasm lane (cc -c bytes vs llvm-mc of cc -S, opts="%s")\n' "$OPTS" for src in "$RT_DIR"/*.c; do name="$(basename "$src" .c)" @@ -94,32 +104,24 @@ for src in "$RT_DIR"/*.c; do "$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$WORK/cc.o" 2>/dev/null || continue "$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$WORK/s.s" 2>/dev/null || continue if ! mc "$WORK/s.s" "$WORK/l.o"; then - reject=$((reject+1)) - printf ' %s %s[-%s]: llvm-mc rejected cc -S (%s)\n' "$(color_yel SKIP)" \ - "$name" "$opt" "$(head -1 "$WORK/mc.err" | sed 's|.*error: *||')" + # llvm-mc rejected cfree's cc -S text: recognized dialect divergence. + cf_skip "disasm:$name[-$opt]" "llvm-mc rejected cc -S ($(head -1 "$WORK/mc.err" | sed 's|.*error: *||'))" continue fi - if [ "$(raw "$WORK/cc.o")" = "$(raw "$WORK/l.o")" ]; then - agree=$((agree+1)) - elif [ "$(text_relocs "$WORK/cc.o")" != "$(text_relocs "$WORK/l.o")" ]; then - # Reloc tables differ: cfree kept a same-section call/branch reloc - # that llvm-mc resolved in place. Link-equivalent — not a decode bug. - reloc_skip=$((reloc_skip+1)) - else - differ=$((differ+1)); fails+=("disasm:$name[-$opt]") - printf ' %s %s[-%s]: cc -c vs llvm-mc(cc -S) bytes differ (relocs match)\n' \ - "$(color_red DIFF)" "$name" "$opt" + raw "$WORK/cc.o" > "$WORK/cc.bytes" + raw "$WORK/l.o" > "$WORK/l.bytes" + if [ "$(text_relocs "$WORK/cc.o")" != "$(text_relocs "$WORK/l.o")" ] && + ! cmp -s "$WORK/cc.bytes" "$WORK/l.bytes"; then + # Reloc tables differ AND bytes differ: cfree kept a same-section + # call/branch reloc that llvm-mc resolved in place. Link-equivalent + # — recognized benign divergence, skip rather than flag. + cf_skip "disasm:$name[-$opt]" "same-section-call reloc-vs-resolve (link-equivalent)" + continue fi + cf_diff_agree "disasm:$name[-$opt]" "$WORK/cc.bytes" "$WORK/l.bytes" done done shopt -u nullglob -printf '\n' -if [ "${#fails[@]}" -gt 0 ]; then - printf 'diff-llvm: %s %d agree, %d differ, %d reloc-equiv, %d llvm-skip\n' \ - "$(color_red 'cfree disagrees with llvm')" "$agree" "$differ" "$reloc_skip" "$reject" - exit 1 -fi -printf 'diff-llvm: %s %d agree, %d reloc-equiv, %d llvm-skip\n' \ - "$(color_grn 'cfree agrees with llvm')" "$agree" "$reloc_skip" "$reject" -exit 0 +cf_summary diff-llvm +cf_exit diff --git a/test/asm/hostas_cross.sh b/test/asm/hostas_cross.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # test/asm/hostas_cross.sh — cross-compile + cross-exec extension of the # host-assembler lane (test/asm/hostas_toy.sh) to ELF Linux targets. +# (Type C — shared corpus harness; one cf_corpus_run per ELF target.) # # Where hostas_toy.sh proves `cc -S` on the *native* target, this proves it # CROSS: for each ELF target (aarch64/x86_64/riscv64-linux) it emits ONE @@ -19,9 +20,10 @@ # freestanding crt test/link/harness/start.c compiled `-Dtest_main=main`, so # `_start` runs ctors then calls the toy's `main` and exits with its return # (the oracle) via a raw syscall — no libc/loader needed, so any same-arch -# Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper -# (one batched `podman run` per target) — the same path test/{link,smoke} -# already use. +# Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper. +# Lane A (and, under ENFORCE_CLANG=1, lane B) defers exec to one batched +# `podman run` per target via the corpus engine's cf_queue_e; lane B under +# ENFORCE_CLANG=0 runs inline (bounded) so its exec verdict can be XFAIL/XPASS. # # Self-probing: each target is SKIPPED (not failed) unless the host has (1) a # clang cross-compiler for it, (2) a runner (podman/qemu) per exec_target, (3) a @@ -44,18 +46,31 @@ # # Override the matrix with CFREE_HOSTAS_CROSS_TARGETS="tag:triple ..." and the # clang-as gate with CFREE_HOSTAS_ENFORCE_CLANG=0 (demote lane B to XFAIL). +# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner +# is parallel-safe; CFREE_HOSTAS_PARALLEL flips dispatch. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -CFREE="$ROOT/build/cfree" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + +CFREE="${CFREE:-$ROOT/build/cfree}" CASES="$ROOT/test/toy/cases" -WORK="$ROOT/build/test/asm/hostas_cross" +BUILD_DIR="$ROOT/build/test/asm/hostas_cross" START_SRC="$ROOT/test/link/harness/start.c" -OPTS="${CFREE_TEST_OPTS:-O0 O1}" -FILTER="${1:-}" ENFORCE_CLANG="${CFREE_HOSTAS_ENFORCE_CLANG:-1}" EXEC_SMOKE_TIMEOUT="${CFREE_HOSTAS_EXEC_TIMEOUT:-45}" +PAR="${CFREE_HOSTAS_PARALLEL:-1}" + +# Opt axis (see hostas_toy.sh): map a CFREE_TEST_OPTS override of O-spellings +# onto the engine's bare opt levels; default 0 1. +OPT_LEVELS="0 1" +if [ -n "${CFREE_TEST_OPTS:-}" ]; then + OPT_LEVELS="" + for _o in $CFREE_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done +fi # "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. All three ELF # targets are in the gating default (each SKIPs cleanly if its clang cross @@ -63,8 +78,13 @@ EXEC_SMOKE_TIMEOUT="${CFREE_HOSTAS_EXEC_TIMEOUT:-45}" # CFREE_HOSTAS_CROSS_TARGETS, e.g. CFREE_HOSTAS_CROSS_TARGETS="x64-linux:x86_64-linux-gnu". TARGETS="${CFREE_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}" -# Same TLS-symbolization skip as the sibling lanes. -SKIP="141_threadlocal_mutate" +# Filter ($1) preserved -> the engine filters discovery by CFREE_TEST_FILTER. +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" + +# The original exits on a_fail (+ b_efail when enforcing) only — skips (the known +# 141 case) never gated. Keep that: SKIP must not fail the run. +CF_SKIP_IS_FAILURE=0 CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}" @@ -80,7 +100,7 @@ if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)" exit 0 fi -mkdir -p "$WORK" +mkdir -p "$BUILD_DIR" # ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------ have_qemu=0 @@ -102,11 +122,20 @@ export have_qemu QEMU_BIN QEMU_RV64_BIN have_podman is_aarch64 : "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}" : "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}" export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE -EXEC_TARGET_MOUNT_ROOT="$WORK" +EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +. "$ROOT/test/lib/exec_target.sh" -is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; } +# Same TLS-symbolization skip as the sibling lanes — driven by the shared +# sidecar 141_threadlocal_mutate.link.skip via CF_READ_CASE (skips the WHOLE +# case here too, matching hostas_toy.sh). +hostas_read_case() { + local reason + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "link"); then + CF_SKIP_CASE="$reason (cc -S symbolizer gap)" + fi +} oracle() { local name="$1" exp=0 @@ -155,143 +184,147 @@ cfree_ld_static() { return 0 } -printf 'hostas-cross: cfree=%s\n' "$CFREE" -printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \ - "$CLANG" "$OPTS" "$ENFORCE_CLANG" "$have_podman" -printf 'hostas-cross: targets="%s"\n' "$TARGETS" +# ---- shared `cc -S -target` build for both lanes --------------------------- +# One cross .s per item, cached in $CF_WORK so lanes A and B reuse it. A cc -S +# failure is a lane-A failure only (lane B records nothing for the item). +# Reads the per-target $TGT_TRIPLE (set before each target's cf_corpus_run). +_ccs_build() { + CCS_S="$CF_WORK/s.s" + [ -f "$CF_WORK/.ccs.done" ] && { CCS_RC=$(cat "$CF_WORK/.ccs.rc"); return "$CCS_RC"; } + if "$CFREE" cc -S "-O$CF_OPT" -target "$TGT_TRIPLE" "$CF_SRC" -o "$CCS_S" 2>"$CF_WORK/ccs.err"; then + CCS_RC=0 + else + CCS_RC=1 + fi + echo "$CCS_RC" >"$CF_WORK/.ccs.rc"; : >"$CF_WORK/.ccs.done" + return "$CCS_RC" +} -a_pass=0; a_fail=0 -b_pass=0; b_xfail=0; b_xpass=0; b_efail=0 -skip_cases=0 -tgt_run=0; tgt_skip=0 -a_failnames=() +# ---- lane A: cfree-as -> cfree ld -static -> run (deferred batched exec) ---- +# Reads per-target $TGT_TRIPLE, $TGT_TAG, $TGT_CRT (set before cf_corpus_run). +cf_lane_A() { + if ! _ccs_build; then + cf_fail "$TGT_TAG/$CF_NAME/cfree-as" "cc -S: $(err_reason "$CF_WORK/ccs.err")" + return + fi + if ! "$CFREE" as -target "$TGT_TRIPLE" "$CCS_S" -o "$CF_WORK/a.o" 2>"$CF_WORK/a.as.err"; then + cf_fail "$TGT_TAG/$CF_NAME/cfree-as" "$(err_reason "$CF_WORK/a.as.err")"; return + fi + if ! cfree_ld_static "$CF_WORK/a.o" "$CF_WORK/a.out" "$CF_WORK/a.ld.err" "$TGT_CRT"; then + cf_fail "$TGT_TAG/$CF_NAME/cfree-as ld" "$(err_reason "$CF_WORK/a.ld.err")"; return + fi + cf_queue_e "$TGT_TAG/$CF_NAME/cfree-as" "$CF_WORK/a.out" \ + "$CF_WORK/a.run.out" "$CF_WORK/a.run.err" "$CF_WORK/a.rc" \ + "$CF_EXPECTED" "$TGT_TAG" +} -shopt -s nullglob -for entry in $TARGETS; do - tag="${entry%%:*}"; triple="${entry##*:}" - tdir="$WORK/$tag"; rm -rf "$tdir"; mkdir -p "$tdir" +# ---- lane B: clang-as -> cfree ld -static -> run --------------------------- +# Gated by ENFORCE_CLANG. Build-stage failures are recorded inline (FAIL when +# enforcing, XFAIL otherwise). The exec stage: when enforcing, defer to the +# batched flush via cf_queue_e (pass/fail). When NOT enforcing, run inline +# (bounded) so the exec verdict can be XPASS (pass) / XFAIL (fail). +cf_lane_B() { + if ! _ccs_build; then return; fi + local name="$TGT_TAG/$CF_NAME/clang-as" + if ! "$CLANG" --target="$TGT_TRIPLE" -c "$CCS_S" -o "$CF_WORK/b.o" 2>"$CF_WORK/b.as.err"; then + if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$name" "$(err_reason "$CF_WORK/b.as.err")" + else cf_xfail "$name" "$(err_reason "$CF_WORK/b.as.err")"; fi + return + fi + if ! cfree_ld_static "$CF_WORK/b.o" "$CF_WORK/b.out" "$CF_WORK/b.ld.err" "$TGT_CRT"; then + if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$name" "ld: $(err_reason "$CF_WORK/b.ld.err")" + else cf_xfail "$name"; fi + return + fi + if [ "$ENFORCE_CLANG" = "1" ]; then + cf_queue_e "$name" "$CF_WORK/b.out" \ + "$CF_WORK/b.run.out" "$CF_WORK/b.run.err" "$CF_WORK/b.rc" \ + "$CF_EXPECTED" "$TGT_TAG" + return + fi + # ENFORCE_CLANG=0: inline bounded exec -> XPASS/XFAIL. + bounded_exec "${EXEC_CASE_TIMEOUT:-20}" "$TGT_TAG" "$CF_WORK/b.out" \ + "$CF_WORK/b.run.out" "$CF_WORK/b.run.err" + if [ "$SMOKE_RC" = "$((CF_EXPECTED & 255))" ]; then cf_xpass "$name" + else cf_xfail "$name"; fi +} - # --- per-target capability gates (SKIP, never FAIL) --- +# ---- per-target capability gates (SKIP-TGT, never FAIL) -------------------- +# Returns 0 if the target is runnable (and sets TGT_CRT); prints a single +# SKIP-TGT line and returns 1 otherwise. Mirrors the original gate chain. +target_gate() { + local tag="$1" triple="$2" tdir="$3" if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; continue + printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; return 1 fi if ! exec_target_supported "$tag"; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; continue + printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; return 1 fi - crt="$tdir/start.o" + TGT_CRT="$tdir/start.o" if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \ - -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$crt" 2>"$tdir/crt.err"; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; continue + -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$TGT_CRT" 2>"$tdir/crt.err"; then + printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; return 1 fi - # Pick a representative non-skip case for the smokes. - smoke=""; for s in "$CASES"/*.toy; do n="$(basename "$s" .toy)"; is_skip "$n" && continue; smoke="$n"; break; done - sd="$tdir/_smoke"; mkdir -p "$sd" + # Pick a representative non-skip case for the smokes (first case without a + # 141-style .link.skip sidecar). + local smoke="" s n + for s in "$CASES"/*.toy; do + n="$(basename "$s" .toy)" + cf_skip_sidecar "$CASES" "$n" "" "link" >/dev/null && continue + smoke="$n"; break + done + local sd="$tdir/_smoke"; mkdir -p "$sd" if ! "$CFREE" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; continue + printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; return 1 fi if ! "$CFREE" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; continue + printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; return 1 fi - if ! cfree_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$crt"; then - tgt_skip=$((tgt_skip+1)); printf ' %s %s — cfree ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; continue + if ! cfree_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$TGT_CRT"; then + printf ' %s %s — cfree ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; return 1 fi bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err" - sexp=$(oracle "$smoke") + local sexp; sexp=$(oracle "$smoke") if [ "$SMOKE_RC" != "$sexp" ]; then - reason="exit $SMOKE_RC != $sexp"; [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)" - tgt_skip=$((tgt_skip+1)); printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; continue + local reason="exit $SMOKE_RC != $sexp" + [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)" + printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; return 1 fi + return 0 +} - # --- full corpus for this target: build everything, then one batched run --- - tgt_run=$((tgt_run+1)) - printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$tag" "$triple" - EXEC_TARGET_TAGS=(); EXEC_TARGET_NAMES=(); EXEC_TARGET_EXES=() - EXEC_TARGET_OUTS=(); EXEC_TARGET_ERRS=(); EXEC_TARGET_RCS=() - # exec_target_flush() clears its arrays on return, so keep our own - # reconciliation bookkeeping (lane / case / oracle / rc-file path). - Q_LANE=(); Q_NAME=(); Q_EXP=(); Q_RCF=() +# ---- drive the corpus, one target at a time -------------------------------- +printf 'hostas-cross: cfree=%s\n' "$CFREE" +printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \ + "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" "$have_podman" +printf 'hostas-cross: targets="%s"\n' "$TARGETS" - for src in "$CASES"/*.toy; do - name="$(basename "$src" .toy)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if is_skip "$name"; then skip_cases=$((skip_cases+1)); continue; fi - exp=$(oracle "$name") - for opt in $OPTS; do - w="$tdir/$name/$opt"; rm -rf "$w"; mkdir -p "$w" - if ! "$CFREE" cc -S "-$opt" -target "$triple" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then - a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt] cc-S: $(err_reason "$w/ccs.err")") - printf ' %s %s/%s[-%s] cc -S: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/ccs.err")" - continue - fi - # Lane A: cfree as -> cfree ld -static. - if ! "$CFREE" as -target "$triple" "$w/s.s" -o "$w/a.o" 2>"$w/a.as.err"; then - a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as: $(err_reason "$w/a.as.err")") - printf ' %s %s/%s[-%s]/cfree-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.as.err")" - elif ! cfree_ld_static "$w/a.o" "$w/a.out" "$w/a.ld.err" "$crt"; then - a_fail=$((a_fail+1)); a_failnames+=("$tag/$name[-$opt]/cfree-as ld: $(err_reason "$w/a.ld.err")") - printf ' %s %s/%s[-%s]/cfree-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/a.ld.err")" - else - exec_target_queue "$tag" "A:$name[-$opt]" "$w/a.out" "$w/a.run.out" "$w/a.run.err" "$w/a.rc" - Q_LANE+=("A"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/a.rc") - fi - # Lane B: clang -c (third-party assembler) -> cfree ld -static. - if ! "$CLANG" --target="$triple" -c "$w/s.s" -o "$w/b.o" 2>"$w/b.as.err"; then - if [ "$ENFORCE_CLANG" = "1" ]; then - b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")" - else - b_xfail=$((b_xfail+1)); printf ' %s %s/%s[-%s]/clang-as: %s\n' "$(color_yel XFAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.as.err")" - fi - elif ! cfree_ld_static "$w/b.o" "$w/b.out" "$w/b.ld.err" "$crt"; then - if [ "$ENFORCE_CLANG" = "1" ]; then - b_efail=$((b_efail+1)); printf ' %s %s/%s[-%s]/clang-as ld: %s\n' "$(color_red FAIL)" "$tag" "$name" "$opt" "$(err_reason "$w/b.ld.err")" - else - b_xfail=$((b_xfail+1)) - fi - else - exec_target_queue "$tag" "B:$name[-$opt]" "$w/b.out" "$w/b.run.out" "$w/b.run.err" "$w/b.rc" - Q_LANE+=("B"); Q_NAME+=("$name[-$opt]"); Q_EXP+=("$exp"); Q_RCF+=("$w/b.rc") - fi - done - done +tgt_run=0 +shopt -s nullglob +for entry in $TARGETS; do + TGT_TAG="${entry%%:*}"; TGT_TRIPLE="${entry##*:}" + tdir="$BUILD_DIR/$TGT_TAG"; rm -rf "$tdir"; mkdir -p "$tdir" + if ! target_gate "$TGT_TAG" "$TGT_TRIPLE" "$tdir"; then continue; fi - # Drain this target's queue in one batched container run, then reconcile. - exec_target_flush - qn="${#Q_LANE[@]}"; qi=0 - while [ "$qi" -lt "$qn" ]; do - lane="${Q_LANE[$qi]}"; nm="${Q_NAME[$qi]}"; exp="${Q_EXP[$qi]}" - rcf="${Q_RCF[$qi]}" - rc=127; [ -f "$rcf" ] && rc="$(cat "$rcf")" - if [ "$lane" = "A" ]; then - if [ "$rc" = "$exp" ]; then a_pass=$((a_pass+1)); else - a_fail=$((a_fail+1)); a_failnames+=("$tag/$nm/cfree-as exit $rc != $exp") - printf ' %s %s/%s/cfree-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp" - fi - else - if [ "$rc" = "$exp" ]; then - if [ "$ENFORCE_CLANG" = "1" ]; then b_pass=$((b_pass+1)); else b_xpass=$((b_xpass+1)); fi - else - if [ "$ENFORCE_CLANG" = "1" ]; then - b_efail=$((b_efail+1)); printf ' %s %s/%s/clang-as: exit %s != %s\n' "$(color_red FAIL)" "$tag" "$nm" "$rc" "$exp" - else - b_xfail=$((b_xfail+1)) - fi - fi - fi - qi=$((qi+1)) - done + tgt_run=$((tgt_run + 1)) + printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$TGT_TAG" "$TGT_TRIPLE" + # TGT_TAG / TGT_TRIPLE / TGT_CRT are set; lane hooks (incl. parallel + # workers, forked after this point) read them. One cf_corpus_run per target, + # like elf's A/B/C layers — counters accumulate into the shared summary. + export TGT_TAG TGT_TRIPLE TGT_CRT + CF_LABEL=test-hostas-cross CF_BUILD_DIR="$tdir" \ + CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \ + CF_LANES="A B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TGT_TAG" \ + CF_TARGETS_EXT="" CF_READ_CASE=hostas_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run done shopt -u nullglob printf '\n' -[ "${#a_failnames[@]}" -gt 0 ] && { printf 'cfree-as failures:\n'; for f in "${a_failnames[@]}"; do printf ' %s\n' "$f"; done; } -printf 'hostas-cross: targets %d run, %d skip | cfree-as %d pass, %d fail | clang-as %d pass, %d xfail, %d xpass, %d efail | %d case-skip\n' \ - "$tgt_run" "$tgt_skip" "$a_pass" "$a_fail" "$b_pass" "$b_xfail" "$b_xpass" "$b_efail" "$skip_cases" if [ "$tgt_run" -eq 0 ]; then printf 'hostas-cross: no target ran (all SKIP-TGT) — needs a clang cross target + podman/qemu + a working cc -S|as for some ELF arch.\n' fi -rc=0 -[ "$a_fail" -gt 0 ] && rc=1 -[ "$ENFORCE_CLANG" = "1" ] && [ "$b_efail" -gt 0 ] && rc=1 -exit $rc +cf_summary test-hostas-cross +cf_exit diff --git a/test/asm/hostas_toy.sh b/test/asm/hostas_toy.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # test/asm/hostas_toy.sh — prove cfree's `cc -S` is STANDARD assembly by feeding # the SAME `cc -S` output to two assemblers, linking + running both, and asserting -# the toy exit-code oracle for each. +# the toy exit-code oracle for each. (Type C — shared corpus harness.) # # Per toy case (native target; both O0 and O1): # cfree cc -S -> s.s (one native .s, shared by both) @@ -22,29 +22,50 @@ # target it emits clean Mach-O assembly clang/llvm-mc accept — no ELF `.type`/ # `.size`, `.section __SEG,__SECT`, `.p2align`, and `sym@PAGE`/`@PAGEOFF` # relocation operands. cfree's own `as` parses the same Mach-O dialect (it -# dispatches on its target format), so BOTH lanes pass. The clang-as lane gates -# by default; set CFREE_HOSTAS_ENFORCE_CLANG=0 to demote it to XFAIL (e.g. while -# bringing up a new arch/format whose printer side isn't done yet). +# dispatches on its target format), so BOTH lanes pass. The clang-as lane (B) +# gates by default; set CFREE_HOSTAS_ENFORCE_CLANG=0 to demote it to XFAIL (e.g. +# while bringing up a new arch/format whose printer side isn't done yet). +# +# Lanes (no CFREE_TEST_PATHS knob — both lanes always run; lane B's verdict is +# gated by CFREE_HOSTAS_ENFORCE_CLANG instead): +# A cfree-as -> cfree ld -> run exit == oracle (the baseline) +# B clang-as -> cfree ld -> run exit == oracle (the real test) +# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner +# is parallel-safe; CFREE_HOSTAS_PARALLEL flips dispatch. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -CFREE="$ROOT/build/cfree" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + +CFREE="${CFREE:-$ROOT/build/cfree}" CASES="$ROOT/test/toy/cases" -WORK="$ROOT/build/test/asm/hostas_toy" -OPTS="${CFREE_TEST_OPTS:-O0 O1}" -FILTER="${1:-}" +BUILD_DIR="$ROOT/build/test/asm/hostas_toy" ENFORCE_CLANG="${CFREE_HOSTAS_ENFORCE_CLANG:-1}" +PAR="${CFREE_HOSTAS_PARALLEL:-1}" + +# The original harness exits on a_fail (+ b_efail when enforcing) only — skips +# (just the known 141 case) never gated. Keep that: SKIP must not fail the run. +CF_SKIP_IS_FAILURE=0 + +# Opt axis: the original took O0/O1 spellings via CFREE_TEST_OPTS; the corpus +# engine expands bare levels (CF_OPT_LEVELS="0 1"). Map a CFREE_TEST_OPTS +# override (e.g. "O0 O1" or "O0") onto bare levels to preserve the env knob. +OPT_LEVELS="0 1" +if [ -n "${CFREE_TEST_OPTS:-}" ]; then + OPT_LEVELS="" + for _o in $CFREE_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done +fi -# Cases blocked on a separate, known cc -S symbolizer gap (the round-trip lane -# quarantines the same set): 141 emits an unsymbolized `adrp x,0x0` for a -# thread-local access (TLS symbolization, tracked separately). -SKIP="141_threadlocal_mutate" +# Filter ($1) preserved -> the engine filters discovery by CFREE_TEST_FILTER. +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}" color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } color_yel() { printf '\033[33m%s\033[0m' "$1"; } if [ ! -x "$CFREE" ]; then @@ -55,9 +76,17 @@ if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then printf 'hostas-toy: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)" exit 0 fi -mkdir -p "$WORK" - -is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; } +mkdir -p "$BUILD_DIR" + +# Native target tuple/triple for the corpus engine + `cc -S`/`as`. +TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" +TEST_OBJ="${CFREE_TEST_OBJ:-macho}" +case "$TEST_ARCH" in + aa64|aarch64|arm64) TEST_ARCH=aa64 ;; + x64|x86_64|amd64) TEST_ARCH=x64 ;; + rv64|riscv64) TEST_ARCH=rv64 ;; +esac +CUR_TUPLE="$TEST_ARCH-$TEST_OBJ" # First meaningful diagnostic from an assembler's stderr. clang prints a harmless # `-Wmissing-sysroot` warning first (we never link with clang, so the SDK is @@ -69,98 +98,102 @@ err_reason() { [ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||') printf '%s' "$line" } +# Terse first-line reason (cfree's own diagnostics: "<tool>: <msg>"). +first_reason() { head -1 "$1" 2>/dev/null | sed 's|.*: ||'; } -a_pass=0; a_fail=0 -b_pass=0; b_xfail=0; b_xpass=0; b_efail=0 -skip=0 -a_failnames=() +# Cases blocked on a separate, known `cc -S` symbolizer gap (the round-trip lane +# quarantines the same set): 141 emits an unsymbolized `adrp x,0x0` for a +# thread-local access (TLS symbolization, tracked separately). Sidecar-driven +# via test/toy/cases/141_threadlocal_mutate.link.skip, read by CF_READ_CASE +# under the synthetic "link" lane name so the same .link.skip the toy/roundtrip +# harnesses honor skips the WHOLE case here too. +hostas_read_case() { + local reason + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "link"); then + CF_SKIP_CASE="$reason (cc -S symbolizer gap)" + fi +} -printf 'hostas-toy: cfree=%s\n' "$CFREE" -printf 'hostas-toy: clang=%s opts="%s" enforce_clang=%s\n' "$CLANG" "$OPTS" "$ENFORCE_CLANG" - -shopt -s nullglob -for src in "$CASES"/*.toy; do - name="$(basename "$src" .toy)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if is_skip "$name"; then - skip=$((skip+1)); printf ' %s %s — known cc -S symbolizer gap\n' "$(color_yel SKIP)" "$name"; continue +# Shared `cc -S` build for both lanes (one native .s per item). Cached per item +# in $CF_WORK so lanes A and B reuse it. Sets CCS_S (path) + CCS_RC (0 ok). +# A cc -S failure is a lane-A failure only (lane B records nothing for the item, +# matching the original `continue`). +_ccs_build() { + CCS_S="$CF_WORK/s.s" + [ -f "$CF_WORK/.ccs.done" ] && { CCS_RC=$(cat "$CF_WORK/.ccs.rc"); return "$CCS_RC"; } + if "$CFREE" cc -S "-O$CF_OPT" "$CF_SRC" -o "$CCS_S" 2>"$CF_WORK/ccs.err"; then + CCS_RC=0 + else + CCS_RC=1 fi - exp=0; [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected") - exp=$((exp & 255)) - for opt in $OPTS; do - w="$WORK/$name/$opt"; rm -rf "$w"; mkdir -p "$w" - - # One native cc -S, shared by both lanes. - if ! "$CFREE" cc -S "-$opt" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then - a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt] cc-S: $(head -1 "$w/ccs.err"|sed 's|.*: ||')") - printf ' %s %s[-%s] cc -S failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ccs.err"|sed 's|.*: ||')" - continue - fi - - # Lane A: cfree as -> cfree ld -> run. - if ! "$CFREE" as "$w/s.s" -o "$w/a.o" 2>"$w/a.as.err"; then - a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as: $(head -1 "$w/a.as.err"|sed 's|.*: ||')") - printf ' %s %s[-%s]/cfree-as: as: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.as.err"|sed 's|.*: ||')" - elif ! "$CFREE" ld "$w/a.o" -o "$w/a.out" 2>"$w/a.ld.err" || [ -s "$w/a.ld.err" ]; then - a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as ld: $(head -1 "$w/a.ld.err"|sed 's|.*: ||')") - printf ' %s %s[-%s]/cfree-as: ld: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.ld.err"|sed 's|.*: ||')" - else - chmod +x "$w/a.out" 2>/dev/null || true - "$w/a.out" >"$w/a.out.txt" 2>"$w/a.run.err"; arc=$? - if [ -s "$w/a.run.err" ]; then - a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as run stderr") - printf ' %s %s[-%s]/cfree-as: run stderr: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/a.run.err")" - elif [ "$arc" -eq "$exp" ]; then - a_pass=$((a_pass+1)) - else - a_fail=$((a_fail+1)); a_failnames+=("$name[-$opt]/cfree-as exit $arc != $exp") - printf ' %s %s[-%s]/cfree-as: exit %d != %d\n' "$(color_red FAIL)" "$name" "$opt" "$arc" "$exp" - fi - fi - - # Lane B: clang -c (third-party assembler) -> cfree ld -> run. - # cfree ld on both lanes isolates the assembler as the only variable. - b_ok=0; b_reason="" - if ! "$CLANG" -c "$w/s.s" -o "$w/b.o" 2>"$w/b.as.err"; then - b_reason="clang as: $(err_reason "$w/b.as.err")" - elif ! "$CFREE" ld "$w/b.o" -o "$w/b.out" 2>"$w/b.ld.err" || [ -s "$w/b.ld.err" ]; then - b_reason="cfree ld: $(head -1 "$w/b.ld.err"|sed 's|.*: ||')" - else - chmod +x "$w/b.out" 2>/dev/null || true - "$w/b.out" >"$w/b.out.txt" 2>"$w/b.run.err"; brc=$? - if [ -s "$w/b.run.err" ]; then b_reason="run stderr: $(head -1 "$w/b.run.err")" - elif [ "$brc" -eq "$exp" ]; then b_ok=1 - else b_reason="exit $brc != $exp"; fi - fi - if [ "$b_ok" -eq 1 ]; then - if [ "$ENFORCE_CLANG" = "1" ]; then - b_pass=$((b_pass+1)); printf ' %s %s[-%s]/clang-as\n' "$(color_grn PASS)" "$name" "$opt" - else - b_xpass=$((b_xpass+1)); printf ' %s %s[-%s]/clang-as — now passes; set CFREE_HOSTAS_ENFORCE_CLANG=1\n' "$(color_grn XPASS)" "$name" "$opt" - fi - else - if [ "$ENFORCE_CLANG" = "1" ]; then - b_efail=$((b_efail+1)); printf ' %s %s[-%s]/clang-as: %s\n' "$(color_red FAIL)" "$name" "$opt" "$b_reason" - else - b_xfail=$((b_xfail+1)); printf ' %s %s[-%s]/clang-as: %s\n' "$(color_yel XFAIL)" "$name" "$opt" "$b_reason" - fi - fi - done -done -shopt -u nullglob - -printf '\n' -[ "${#a_failnames[@]}" -gt 0 ] && { printf 'cfree-as failures:\n'; for f in "${a_failnames[@]}"; do printf ' %s\n' "$f"; done; } -printf 'hostas-toy: cfree-as %d pass, %d fail | clang-as %d pass, %d xfail, %d xpass, %d efail | %d skip\n' \ - "$a_pass" "$a_fail" "$b_pass" "$b_xfail" "$b_xpass" "$b_efail" "$skip" -if [ "$ENFORCE_CLANG" != "1" ] && [ "$b_xfail" -gt 0 ]; then - printf 'hostas-toy: clang-as demoted to XFAIL (CFREE_HOSTAS_ENFORCE_CLANG=0): %d not accepted by clang. The clang-as lane gates by default; unset the env var to enforce.\n' "$b_xfail" -fi -if [ "$ENFORCE_CLANG" != "1" ] && [ "$b_xpass" -gt 0 ]; then - printf 'hostas-toy: clang-as had %d XPASS under opt-out — re-enable gating (default) to enforce.\n' "$b_xpass" -fi + echo "$CCS_RC" >"$CF_WORK/.ccs.rc"; : >"$CF_WORK/.ccs.done" + return "$CCS_RC" +} -rc=0 -[ "$a_fail" -gt 0 ] && rc=1 -[ "$ENFORCE_CLANG" = "1" ] && [ "$b_efail" -gt 0 ] && rc=1 -exit $rc +# ---- lane A: cfree-as -> cfree ld -> run (the baseline) --------------------- +cf_lane_A() { + if ! _ccs_build; then + cf_fail "$CF_NAME/cfree-as" "cc -S: $(first_reason "$CF_WORK/ccs.err")" + return + fi + if ! "$CFREE" as "$CCS_S" -o "$CF_WORK/a.o" 2>"$CF_WORK/a.as.err"; then + cf_fail "$CF_NAME/cfree-as" "as: $(first_reason "$CF_WORK/a.as.err")"; return + fi + if ! "$CFREE" ld "$CF_WORK/a.o" -o "$CF_WORK/a.out" 2>"$CF_WORK/a.ld.err" || [ -s "$CF_WORK/a.ld.err" ]; then + cf_fail "$CF_NAME/cfree-as" "ld: $(first_reason "$CF_WORK/a.ld.err")"; return + fi + chmod +x "$CF_WORK/a.out" 2>/dev/null || true + "$CF_WORK/a.out" >"$CF_WORK/a.out.txt" 2>"$CF_WORK/a.run.err"; local arc=$? + local exp=$((CF_EXPECTED & 255)) # exit codes wrap at 256 (oracle 312 -> 56) + if [ -s "$CF_WORK/a.run.err" ]; then + cf_fail "$CF_NAME/cfree-as" "run stderr: $(head -1 "$CF_WORK/a.run.err")" + elif [ "$arc" -eq "$exp" ]; then + cf_pass "$CF_NAME/cfree-as" + else + cf_fail "$CF_NAME/cfree-as" "exit $arc != $exp" + fi +} + +# ---- lane B: clang-as -> cfree ld -> run (the real test) ------------------- +# Gated by ENFORCE_CLANG: pass+enforce -> PASS, pass+!enforce -> XPASS; +# fail+enforce -> FAIL, fail+!enforce -> XFAIL. cfree ld on both lanes isolates +# the assembler as the only variable. Records NOTHING on a shared cc -S failure +# (matching the original `continue`). +cf_lane_B() { + if ! _ccs_build; then return; fi + local ok=0 reason="" exp=$((CF_EXPECTED & 255)) + if ! "$CLANG" -c "$CCS_S" -o "$CF_WORK/b.o" 2>"$CF_WORK/b.as.err"; then + reason="clang as: $(err_reason "$CF_WORK/b.as.err")" + elif ! "$CFREE" ld "$CF_WORK/b.o" -o "$CF_WORK/b.out" 2>"$CF_WORK/b.ld.err" || [ -s "$CF_WORK/b.ld.err" ]; then + reason="cfree ld: $(first_reason "$CF_WORK/b.ld.err")" + else + chmod +x "$CF_WORK/b.out" 2>/dev/null || true + "$CF_WORK/b.out" >"$CF_WORK/b.out.txt" 2>"$CF_WORK/b.run.err"; local brc=$? + if [ -s "$CF_WORK/b.run.err" ]; then reason="run stderr: $(head -1 "$CF_WORK/b.run.err")" + elif [ "$brc" -eq "$exp" ]; then ok=1 + else reason="exit $brc != $exp"; fi + fi + if [ "$ok" -eq 1 ]; then + if [ "$ENFORCE_CLANG" = "1" ]; then cf_pass "$CF_NAME/clang-as" + else cf_xpass "$CF_NAME/clang-as"; fi + else + if [ "$ENFORCE_CLANG" = "1" ]; then cf_fail "$CF_NAME/clang-as" "$reason" + else cf_xfail "$CF_NAME/clang-as" "$reason"; fi + fi +} + +# ---- drive the corpus ------------------------------------------------------ +printf 'hostas-toy: cfree=%s\n' "$CFREE" +printf 'hostas-toy: clang=%s opts="%s" enforce_clang=%s\n' "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" + +# Lane B's XFAIL/XPASS already carry the enforce semantics; gate xpass under +# CFREE_HOSTAS_ENFORCE_CLANG=1 is N/A here (enforce=1 means lane B counts as +# PASS/FAIL, not XPASS/XFAIL), so leave CF_STRICT_XFAIL at its default. +CF_LABEL=test-hostas-toy CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \ + CF_LANES="A B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_READ_CASE=hostas_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +cf_summary test-hostas-toy +cf_exit diff --git a/test/asm/roundtrip.sh b/test/asm/roundtrip.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# test/asm/roundtrip.sh — codegen round-trip completeness harness. +# test/asm/roundtrip.sh — codegen round-trip completeness harness, on the +# shared corpus harness (test/lib/cf_corpus.sh). # # Measures completeness of the per-arch assembler, disassembler, and the link # relocation path by round-tripping the *compiler's own output* rather than a @@ -8,7 +9,7 @@ # Corpus: test/asm/roundtrip/*.c — each defines `int test_main(...)` so the L2 # lane can execute it through the shared exec harness (jit-runner). # -# Three lanes (default "012"): +# Three lanes (CFREE_TEST_PATHS, default "012"; digits 0/1/2 -> lanes L0/L1/L2): # # 0 L0 decode-complete — `cfree cc -S` and assert no in-function decode # failure marker (aarch64 `.inst`) inside .text. Catches an instruction @@ -17,7 +18,7 @@ # 1 L1 byte round-trip — `cfree cc -c` (direct.o) vs `cfree cc -S | cfree as` # (rt.o); diff the .text bytes AND the .text relocation table. Catches # assembler/disassembler disagreements (round-trip violations). Exact, -# host-independent. Gated on L0 passing first. +# host-independent. Gated on L0's `cc -S` step succeeding first. # 2 L2 exec equivalence — run direct.o and rt.o and compare exit codes (and, # when present, against <name>.expected). Tolerant of benign encoding # differences; the end-to-end "it runs the same" signal. Executes via @@ -25,21 +26,31 @@ # # Opt levels: CFREE_TEST_OPTS (default "O1"). Each case is run at every level. # +# Per-case applicability: <name>.targets (tuple list) -> SKIP-NA; <name>.skip +# quarantines the whole case; <name>.expected is the L2 exit-code oracle. +# # Filtering: # ./roundtrip.sh [name_filter] [lanes] # name_filter substring match against case basename # lanes subset of "012" (default "012") # Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS, CFREE_TEST_OPTS. - +# +# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner +# is parallel-safe by construction (CFREE_ASM_PARALLEL flips dispatch). The +# L1-depends-on-L0 ordering (L1 reuses the listing L0 produced) is preserved +# inside the L1 lane body: each lane recompiles its own `cc -S` listing under +# $CF_WORK rather than sharing artifacts across lanes/workers. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + TEST_DIR="$ROOT/test/asm" CORPUS_DIR="$TEST_DIR/roundtrip" -BUILD_DIR="$ROOT/build/test" -WORK_ROOT="$BUILD_DIR/asm/roundtrip" -CFREE="$ROOT/build/cfree" -JIT_RUNNER="$BUILD_DIR/jit-runner" +BUILD_DIR="$ROOT/build/test/asm/roundtrip" +CFREE_BIN="${CFREE:-$ROOT/build/cfree}" +JIT_RUNNER="$ROOT/build/test/jit-runner" # CFREE_TEST_ARCH selects the cross-target. Mirrors test/asm/run.sh. CFREE_TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" @@ -49,27 +60,25 @@ case "$CFREE_TEST_ARCH" in rv64|riscv64) TEST_ARCH=rv64; TRIPLE=riscv64-linux-gnu ;; *) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;; esac -export CFREE_TEST_ARCH +export CFREE_TEST_ARCH="$TEST_ARCH" +# Opt axis. CFREE_TEST_OPTS carries the "O"-prefixed levels (e.g. "O0 O1"); the +# engine matrix wants the bare digits, and the lanes build "-O<level>". OPTS="${CFREE_TEST_OPTS:-O1}" +OPT_LEVELS="" +for _o in $OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done FILTER="${1:-${CFREE_TEST_FILTER:-}}" PATHS="${2:-${CFREE_TEST_PATHS:-012}}" -case "$PATHS" in *0*) RUN_L0=1;; *) RUN_L0=0;; esac -case "$PATHS" in *1*) RUN_L1=1;; *) RUN_L1=0;; esac -case "$PATHS" in *2*) RUN_L2=1;; *) RUN_L2=0;; esac - -PASS=0; FAIL=0; SKIP=0 -FAIL_NAMES=() +export CFREE_TEST_FILTER="$FILTER" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } +# Map the PATHS digit alphabet (0/1/2) onto the engine lane ids L0/L1/L2. +LANES="" +case "$PATHS" in *0*) LANES="$LANES L0";; esac +case "$PATHS" in *1*) LANES="$LANES L1";; esac +case "$PATHS" in *2*) LANES="$LANES L2";; esac -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } -note_na() { printf ' %s %s — not applicable to %s\n' "$(color_yel SKIP-NA)" "$1" "$TEST_ARCH"; } +PAR="${CFREE_ASM_PARALLEL:-1}" # is_native_target=1 when the cross-target arch matches the host arch (needed # for in-process JIT exec in the L2 lane). @@ -85,25 +94,28 @@ have_jit_runner=0 [ -x "$JIT_RUNNER" ] && have_jit_runner=1 # ---- per-case applicability (mirrors test/asm/run.sh) ---------------------- - -case_applies() { - local name="$1" targets tuple - targets="$CORPUS_DIR/$name.targets" +# A <name>.targets sidecar lists the arches a case applies to, using arch-only +# tokens with synonyms (aa64/aarch64/arm64, x64/x86_64/amd64, rv64/riscv64) — +# NOT the engine's "<arch>-<obj>" tuple form. Reimplemented as a CF_READ_CASE +# hook (rather than the engine's CF_TARGETS_EXT tuple matcher) so the exact +# original token grammar and SKIP-NA verdict are preserved. +rt_read_case() { + local targets="$CF_SIDECAR_DIR/$CF_BASE.targets" tuple [ -f "$targets" ] || return 0 # no .targets => applies to all arches for tuple in $(cat "$targets"); do case "$tuple:$TEST_ARCH" in - aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;; - x64:x64|x86_64:x64|amd64:x64) return 0 ;; - rv64:rv64|riscv64:rv64) return 0 ;; + aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;; + x64:x64|x86_64:x64|amd64:x64) return 0 ;; + rv64:rv64|riscv64:rv64) return 0 ;; esac done - return 1 + CF_SKIP_NA_CASE=1 } -# ---- extraction helpers ---------------------------------------------------- +# ---- extraction helpers (lane-local) --------------------------------------- # .text bytes as objdump hex-dump lines (filename header stripped). -text_bytes() { "$CFREE" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f]+ /'; } +text_bytes() { "$CFREE_BIN" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f]+ /'; } # Relocation records (kind/offset/target) for the sections `cc -S` reproduces # — .text, .rodata, .data — so the comparison covers code relocs AND data @@ -112,7 +124,7 @@ text_bytes() { "$CFREE" objdump -s -j .text "$1" 2>/dev/null | awk '/^ *[0-9a-f] # is not flagged. The section header is printed so a reloc at the same offset # in two sections stays distinct. reproduced_relocs() { - "$CFREE" objdump -r "$1" 2>/dev/null | awk ' + "$CFREE_BIN" objdump -r "$1" 2>/dev/null | awk ' /^RELOCATION RECORDS FOR \[\.(text|rodata|data)\]/ { f=1; print; next } /^RELOCATION RECORDS FOR/ { f=0; next } f && /^[0-9a-f]/ { print }' @@ -131,120 +143,142 @@ decode_failures() { ' "$1" } -# ---- run ------------------------------------------------------------------- - -printf 'roundtrip: arch=%s triple=%s opts="%s" lanes=%s native=%d\n' \ - "$TEST_ARCH" "$TRIPLE" "$OPTS" "$PATHS" "$is_native_target" +# Build the shared `cc -S` assembly listing for this case/opt under $CF_WORK. +# Echoes the listing path on success; nonzero (with the log left in place) on +# failure. Both L0 and L1 funnel through this so the L1-after-L0 ordering holds +# regardless of dispatch order: each lane materializes the listing it needs. +rt_compile_asm() { + local asm="$CF_WORK/out.s" + if [ -f "$asm" ]; then printf '%s' "$asm"; return 0; fi + if "$CFREE_BIN" cc -S "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$asm" \ + >"$CF_WORK/cc_s.log" 2>&1; then + printf '%s' "$asm"; return 0 + fi + return 1 +} -if [ ! -x "$CFREE" ]; then - printf ' %s cfree binary missing — run "make bin"\n' "$(color_red FATAL)" >&2 - exit 1 -fi -if [ $RUN_L2 -eq 1 ] && [ $is_native_target -eq 1 ] && [ $have_jit_runner -eq 0 ]; then - printf ' %s jit-runner missing; L2 lane will skip\n' "$(color_yel warn)" -fi +# ---- lanes ----------------------------------------------------------------- -mkdir -p "$WORK_ROOT" +# L0: decode completeness. cc -S then assert no in-function `.inst` markers. +cf_lane_L0() { + local asm + if ! asm=$(rt_compile_asm); then + cf_fail "$CF_NAME/L0" "cc -S failed; see $CF_WORK/cc_s.log"; return + fi + if decode_failures "$asm" >"$CF_WORK/decode_fail"; then + cf_fail "$CF_NAME/L0" "undecoded insn in .text; see $CF_WORK/decode_fail" + else + cf_pass "$CF_NAME/L0" + fi +} -shopt -s nullglob -for src in "$CORPUS_DIR"/*.c; do - name="$(basename "$src" .c)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if ! case_applies "$name"; then - note_na "$name" - continue +# L1: byte + reloc round-trip. cc -c (direct.o) vs cc -S | as (rt.o); diff the +# .text bytes AND the reproduced reloc tables. Gated on the cc -S listing (L0's +# producer) succeeding first. +# +# Intra-function branches now round-trip: `as` relaxes same-section local-label +# branches at finalize (matching codegen), so the .text reloc tables agree. A +# case that still can't round-trip is gated with a per-case `<name>.skip` file +# rather than auto-skipped here. +cf_lane_L1() { + local asm direct="$CF_WORK/direct.o" rt="$CF_WORK/rt.o" + if ! asm=$(rt_compile_asm); then + cf_fail "$CF_NAME/L1" "cc -S failed; see $CF_WORK/cc_s.log"; return + fi + if ! "$CFREE_BIN" cc -c "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$direct" \ + >"$CF_WORK/cc_c.log" 2>&1; then + cf_fail "$CF_NAME/L1" "cc -c failed; see $CF_WORK/cc_c.log"; return + fi + if ! "$CFREE_BIN" as -target "$TRIPLE" "$asm" -o "$rt" \ + >"$CF_WORK/as.log" 2>&1; then + cf_fail "$CF_NAME/L1" "as failed; see $CF_WORK/as.log"; return fi - if [ -e "$CORPUS_DIR/$name.skip" ]; then - note_skip "$name" "$(head -n1 "$CORPUS_DIR/$name.skip")" - continue + text_bytes "$direct" >"$CF_WORK/direct.text" + text_bytes "$rt" >"$CF_WORK/rt.text" + reproduced_relocs "$direct" >"$CF_WORK/direct.rel" + reproduced_relocs "$rt" >"$CF_WORK/rt.rel" + if ! diff -u "$CF_WORK/direct.text" "$CF_WORK/rt.text" >"$CF_WORK/text.diff"; then + cf_fail "$CF_NAME/L1" ".text bytes differ; see $CF_WORK/text.diff" + elif ! diff -u "$CF_WORK/direct.rel" "$CF_WORK/rt.rel" >"$CF_WORK/rel.diff"; then + cf_fail "$CF_NAME/L1" "relocs differ (.text/.rodata/.data); see $CF_WORK/rel.diff" + else + cf_pass "$CF_NAME/L1" fi +} - for opt in $OPTS; do - tag="$name[-$opt]" - work="$WORK_ROOT/$name/$opt" - mkdir -p "$work" - asm="$work/out.s" - direct="$work/direct.o" - rt="$work/rt.o" - - # Shared compile: assembly listing (L0/L1) + direct object (L1/L2). - if ! "$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$asm" \ - >"$work/cc_s.log" 2>&1; then - note_fail "$tag/L0 (cc -S failed; see $work/cc_s.log)" - continue +# L2: exec equivalence. Build direct.o and rt.o, run both through jit-runner, +# require equal exit codes + identical stdout, and (when a <name>.expected +# oracle exists) require the exit code to match it. Native target only. +cf_lane_L2() { + if [ "$is_native_target" -eq 0 ]; then + cf_skip "$CF_NAME/L2" "non-native target ($arch_raw); cross-exec lane TODO"; return + fi + if [ "$have_jit_runner" -eq 0 ]; then + cf_skip "$CF_NAME/L2" "jit-runner unavailable"; return + fi + local asm direct="$CF_WORK/direct.o" rt="$CF_WORK/rt.o" + # Reuse direct.o/rt.o from L1 if present; otherwise build them here. + if [ ! -f "$direct" ]; then + if ! "$CFREE_BIN" cc -c "-O$CF_OPT" -target "$TRIPLE" "$CF_SRC" -o "$direct" \ + >"$CF_WORK/cc_c.log" 2>&1; then + cf_fail "$CF_NAME/L2" "cc -c failed; see $CF_WORK/cc_c.log"; return fi - - # ---- L0: decode completeness ------------------------------------- - if [ $RUN_L0 -eq 1 ]; then - if decode_failures "$asm" >"$work/decode_fail"; then - note_fail "$tag/L0 (undecoded insn in .text; see $work/decode_fail)" - else - note_pass "$tag/L0" - fi + fi + if [ ! -f "$rt" ]; then + if ! asm=$(rt_compile_asm); then + cf_fail "$CF_NAME/L2" "cc -S failed; see $CF_WORK/cc_s.log"; return fi - - # ---- L1: byte + reloc round-trip --------------------------------- - # Intra-function branches now round-trip: `as` relaxes same-section - # local-label branches at finalize (matching codegen), so the .text - # reloc tables agree. A case that still can't round-trip is gated with - # a per-case `<name>.skip` file rather than auto-skipped here. - if [ $RUN_L1 -eq 1 ]; then - l1_ok=1; l1_why="" - if ! "$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$direct" \ - >"$work/cc_c.log" 2>&1; then - l1_ok=0; l1_why="cc -c failed; see $work/cc_c.log" - elif ! "$CFREE" as -target "$TRIPLE" "$asm" -o "$rt" \ - >"$work/as.log" 2>&1; then - l1_ok=0; l1_why="as failed; see $work/as.log" - else - text_bytes "$direct" >"$work/direct.text" - text_bytes "$rt" >"$work/rt.text" - reproduced_relocs "$direct" >"$work/direct.rel" - reproduced_relocs "$rt" >"$work/rt.rel" - if ! diff -u "$work/direct.text" "$work/rt.text" >"$work/text.diff"; then - l1_ok=0; l1_why=".text bytes differ; see $work/text.diff" - elif ! diff -u "$work/direct.rel" "$work/rt.rel" >"$work/rel.diff"; then - l1_ok=0; l1_why="relocs differ (.text/.rodata/.data); see $work/rel.diff" - fi - fi - if [ $l1_ok -eq 1 ]; then note_pass "$tag/L1"; else note_fail "$tag/L1 ($l1_why)"; fi + if ! "$CFREE_BIN" as -target "$TRIPLE" "$asm" -o "$rt" \ + >"$CF_WORK/as.log" 2>&1; then + cf_fail "$CF_NAME/L2" "as failed; see $CF_WORK/as.log"; return fi + fi + "$JIT_RUNNER" "$direct" >"$CF_WORK/direct.out" 2>"$CF_WORK/direct.err"; local rc_direct=$? + "$JIT_RUNNER" "$rt" >"$CF_WORK/rt.out" 2>"$CF_WORK/rt.err"; local rc_rt=$? + if [ "$rc_direct" != "$rc_rt" ]; then + cf_fail "$CF_NAME/L2" "exit codes differ: direct=$rc_direct rt=$rc_rt"; return + fi + if ! diff -q "$CF_WORK/direct.out" "$CF_WORK/rt.out" >/dev/null; then + cf_fail "$CF_NAME/L2" "stdout differs"; return + fi + # CF_EXPECTED is the <name>.expected oracle (engine default 0 when absent). + # The original lane only enforced the oracle when the sidecar was present; + # every roundtrip case carries one, so honoring CF_EXPECTED unconditionally + # is the same verdict. + if [ "$rc_direct" != "$CF_EXPECTED" ]; then + cf_fail "$CF_NAME/L2" "exit $rc_direct != expected $CF_EXPECTED"; return + fi + cf_pass "$CF_NAME/L2" +} - # ---- L2: exec equivalence ---------------------------------------- - if [ $RUN_L2 -eq 1 ]; then - if [ $is_native_target -eq 0 ]; then - note_skip "$tag/L2" "non-native target ($arch_raw); cross-exec lane TODO" - elif [ $have_jit_runner -eq 0 ]; then - note_skip "$tag/L2" "jit-runner unavailable" - else - # Reuse direct.o/rt.o from L1 if present; otherwise build them. - [ -f "$direct" ] || "$CFREE" cc -c "-$opt" -target "$TRIPLE" "$src" -o "$direct" >"$work/cc_c.log" 2>&1 - [ -f "$rt" ] || "$CFREE" cc -S "-$opt" -target "$TRIPLE" "$src" -o "$asm" >"$work/cc_s.log" 2>&1 && \ - "$CFREE" as -target "$TRIPLE" "$asm" -o "$rt" >"$work/as.log" 2>&1 - "$JIT_RUNNER" "$direct" >"$work/direct.out" 2>"$work/direct.err"; rc_direct=$? - "$JIT_RUNNER" "$rt" >"$work/rt.out" 2>"$work/rt.err"; rc_rt=$? - l2_ok=1; l2_why="" - if [ "$rc_direct" != "$rc_rt" ]; then - l2_ok=0; l2_why="exit codes differ: direct=$rc_direct rt=$rc_rt" - elif ! diff -q "$work/direct.out" "$work/rt.out" >/dev/null; then - l2_ok=0; l2_why="stdout differs" - elif [ -f "$CORPUS_DIR/$name.expected" ]; then - exp="$(head -n1 "$CORPUS_DIR/$name.expected")" - if [ "$rc_direct" != "$exp" ]; then - l2_ok=0; l2_why="exit $rc_direct != expected $exp" - fi - fi - if [ $l2_ok -eq 1 ]; then note_pass "$tag/L2"; else note_fail "$tag/L2 ($l2_why)"; fi - fi - fi - done -done -shopt -u nullglob +# ---- drive the corpus ------------------------------------------------------ + +printf 'roundtrip: arch=%s triple=%s opts="%s" lanes=%s native=%d\n' \ + "$TEST_ARCH" "$TRIPLE" "$OPTS" "$PATHS" "$is_native_target" -printf '\n' -if [ "${#FAIL_NAMES[@]}" -gt 0 ]; then - printf 'Failed:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done +if [ ! -x "$CFREE_BIN" ]; then + printf ' FATAL cfree binary missing — run "make bin"\n' >&2 + exit 1 fi -printf 'Results: %d pass, %d fail, %d skip\n' "$PASS" "$FAIL" "$SKIP" -[ "$FAIL" -eq 0 ] +case " $LANES " in + *" L2 "*) + if [ "$is_native_target" -eq 1 ] && [ "$have_jit_runner" -eq 0 ]; then + printf ' warn jit-runner missing; L2 lane will skip\n' + fi + ;; +esac + +mkdir -p "$BUILD_DIR" + +# Skips here are informational (L2 skips on a non-native host / absent +# jit-runner); the original exited 0 with skips + no fails, so do not gate the +# exit on skips. +CF_SKIP_IS_FAILURE=0 +CF_LABEL=test-asm-roundtrip CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$CORPUS_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CORPUS_DIR" \ + CF_LANES="$LANES" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TEST_ARCH-elf" \ + CF_TARGETS_EXT="" CF_READ_CASE=rt_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +cf_summary test-asm-roundtrip +cf_exit diff --git a/test/asm/roundtrip_toy.sh b/test/asm/roundtrip_toy.sh @@ -1,14 +1,16 @@ #!/usr/bin/env bash -# test/asm/roundtrip_toy.sh — L2 exec round-trip over the Toy corpus (native). +# test/asm/roundtrip_toy.sh — L2 exec round-trip over the Toy corpus (native), +# on the shared corpus harness (test/lib/cf_corpus.sh). # # The Toy frontend exercises the full CG op set, and each case carries an # exit-code oracle (test/toy/cases/<name>.expected). This lane reuses that # corpus for free round-trip coverage far beyond the hand-written # test/asm/roundtrip/ set: for every case, compare the DIRECT compile to the -# round-tripped one and require the same exit code — +# round-tripped one and require the same exit code. # -# Two exec lanes over the re-assembled object, both compared to the case's -# exit-code oracle (test/toy/cases/<name>.expected, default 0): +# One lane (L2) emitting two exec sub-results over the re-assembled object, +# both compared to the case's exit-code oracle (test/toy/cases/<name>.expected, +# default 0): # /run: cfree cc -S | cfree as | cfree run <obj> (in-process JIT) # /ld: cfree cc -S | cfree as | cfree ld | ./a.out (native link + exec) # @@ -17,95 +19,123 @@ # (a multiply-high the disassembler couldn't decode, dropped by `as` until the # `.inst` fix) that the hand corpus never reached. # -# Any case that hits a `cc -S` symbolizer gap can be quarantined in SKIP below -# so the lane stays green and gates regressions; SKIP is currently empty. Opt-in. - +# Opt levels: CFREE_TEST_OPTS (default "O0 O1"). Each case runs at every level. +# +# Any case that hits a `cc -S` symbolizer gap is quarantined via a whole-case +# <name>.skip-style entry in SKIP below so the lane stays green and gates +# regressions. Opt-in. +# +# Every lane hook writes only under $CF_WORK and records via cf_*, so the runner +# is parallel-safe (CFREE_ASM_PARALLEL flips dispatch); the /run-then-/ld +# ordering is preserved inside the lane body. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -CFREE="$ROOT/build/cfree" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + +CFREE_BIN="${CFREE:-$ROOT/build/cfree}" CASES="$ROOT/test/toy/cases" -WORK="$ROOT/build/test/asm/roundtrip_toy" +BUILD_DIR="$ROOT/build/test/asm/roundtrip_toy" + +# Opt axis. CFREE_TEST_OPTS carries the "O"-prefixed levels (e.g. "O0 O1"); the +# engine matrix wants the bare digits, and the lane builds "-O<level>". OPTS="${CFREE_TEST_OPTS:-O0 O1}" -FILTER="${1:-}" +OPT_LEVELS="" +for _o in $OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done -# Named sections now round-trip (closed 118_decl_extra_attrs): cc -S emits the -# section with its "flags"/@type/entsize in GNU-as syntax and `as` reconstructs -# it. SKIP quarantines cases blocked on a *separate*, known cc -S symbolizer gap: +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" + +PAR="${CFREE_ASM_PARALLEL:-1}" + +# Whole-case SKIP set (known cc -S symbolizer gaps). Quarantines a case on a +# *separate*, known gap so the lane stays green and gates regressions: # - 141_threadlocal_mutate: a thread-local access emits an unsymbolized # `adrp x, 0x0` (the TLS GOT/descriptor reloc is not yet symbolized in cc -S), # which `as` rejects ("adr/adrp: symbol required"). This is TLS symbolization, # tracked separately from named-section round-trip. See # doc/ASM_ROUNDTRIP_TESTING.md. +# Named sections now round-trip (closed 118_decl_extra_attrs): cc -S emits the +# section with its "flags"/@type/entsize in GNU-as syntax and `as` reconstructs +# it. SKIP="141_threadlocal_mutate" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } +# strip the leading "<path>: " prefix off a diagnostic's first line. +rt_diag() { head -n1 "$1" 2>/dev/null | sed 's|.*: ||'; } + +# CF_READ_CASE hook: apply the whole-case SKIP quarantine. The engine already +# reads <name>.expected into CF_EXPECTED (default 0); we just mask it to a byte +# to match the original `exp=$((exp & 255))`. +rt_read_case() { + case " $SKIP " in + *" $CF_BASE "*) CF_SKIP_CASE="known cc -S symbolizer gap"; return ;; + esac + CF_EXPECTED=$(( CF_EXPECTED & 255 )) +} -if [ ! -x "$CFREE" ]; then - printf 'roundtrip-toy: %s cfree missing — run "make bin"\n' "$(color_red FATAL)" >&2 +# ---- the single L2 lane ---------------------------------------------------- +# cc -S | as, then run the re-assembled object two ways, each compared to the +# exit-code oracle. /run uses the in-process JIT; /ld links a real native +# executable and runs it through the OS loader. Native host only (no -target). +cf_lane_L2() { + local s="$CF_WORK/s.s" rt="$CF_WORK/rt.o" + # Shared cc -S | as steps. A failure here is a SINGLE result for the case + # (matching the original's one-fail-then-skip-rest behavior), not one per + # sub-result — the /run and /ld sub-results only exist once an object does. + if ! "$CFREE_BIN" cc -S "-O$CF_OPT" "$CF_SRC" -o "$s" 2>"$CF_WORK/ccs.err"; then + cf_fail "$CF_NAME cc-S" "cc -S failed"; return + fi + if ! "$CFREE_BIN" as "$s" -o "$rt" 2>"$CF_WORK/as.err"; then + cf_fail "$CF_NAME as" "as: $(rt_diag "$CF_WORK/as.err")"; return + fi + + # Sub-result /run: cfree run JIT-links and executes the object in-process. + "$CFREE_BIN" run "$rt" >"$CF_WORK/out" 2>"$CF_WORK/run.err"; local rc=$? + if [ -s "$CF_WORK/run.err" ]; then + cf_fail "$CF_NAME/run" "$(rt_diag "$CF_WORK/run.err")" + elif [ "$rc" -eq "$CF_EXPECTED" ]; then + cf_pass "$CF_NAME/run" + else + cf_fail "$CF_NAME/run" "exit $rc != $CF_EXPECTED" + fi + + # Sub-result /ld: cfree ld links a real native executable; run it via the OS + # loader and compare exit. Exercises cfree ld (layout + relocation + image + # emit) and a real process exit — none of which the JIT 'run' path covers. + if ! "$CFREE_BIN" ld "$rt" -o "$CF_WORK/a.out" 2>"$CF_WORK/ld.err" || [ -s "$CF_WORK/ld.err" ]; then + cf_fail "$CF_NAME/ld" "$(rt_diag "$CF_WORK/ld.err")"; return + fi + chmod +x "$CF_WORK/a.out" 2>/dev/null || true + "$CF_WORK/a.out" >"$CF_WORK/ldout" 2>"$CF_WORK/ldrun.err"; local ldrc=$? + if [ -s "$CF_WORK/ldrun.err" ]; then + cf_fail "$CF_NAME/ld" "$(rt_diag "$CF_WORK/ldrun.err")" + elif [ "$ldrc" -eq "$CF_EXPECTED" ]; then + cf_pass "$CF_NAME/ld" + else + cf_fail "$CF_NAME/ld" "exit $ldrc != $CF_EXPECTED" + fi +} + +# ---- drive the corpus ------------------------------------------------------ + +if [ ! -x "$CFREE_BIN" ]; then + printf 'roundtrip-toy: FATAL cfree missing — run "make bin"\n' >&2 exit 1 fi -mkdir -p "$WORK" +mkdir -p "$BUILD_DIR" -pass=0; fail=0; skip=0; failnames=() -is_skip() { case " $SKIP " in *" $1 "*) return 0;; *) return 1;; esac; } +# Skips are informational (e.g. the 141_threadlocal_mutate quarantine), as in +# the original which exited 0 regardless of skips — do not gate on skips. +CF_SKIP_IS_FAILURE=0 -shopt -s nullglob -for src in "$CASES"/*.toy; do - name="$(basename "$src" .toy)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if is_skip "$name"; then - skip=$((skip+1)); printf ' %s %s — known cc -S symbolizer gap\n' "$(color_yel SKIP)" "$name"; continue - fi - exp=0; [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected") - exp=$((exp & 255)) - for opt in $OPTS; do - w="$WORK/$name/$opt"; mkdir -p "$w" - if ! "$CFREE" cc -S "-$opt" "$src" -o "$w/s.s" 2>"$w/ccs.err"; then - fail=$((fail+1)); failnames+=("$name[-$opt] cc-S"); printf ' %s %s[-%s] cc -S failed\n' "$(color_red FAIL)" "$name" "$opt"; continue - fi - if ! "$CFREE" as "$w/s.s" -o "$w/rt.o" 2>"$w/as.err"; then - fail=$((fail+1)); failnames+=("$name[-$opt] as: $(head -1 "$w/as.err"|sed 's|.*: ||')") - printf ' %s %s[-%s] as failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/as.err"|sed 's|.*: ||')"; continue - fi - # Lane /run: cfree run JIT-links and executes the object in-process. - "$CFREE" run "$w/rt.o" >"$w/out" 2>"$w/run.err"; rc=$? - if [ -s "$w/run.err" ]; then - fail=$((fail+1)); failnames+=("$name[-$opt]/run: $(head -1 "$w/run.err"|sed 's|.*: ||')") - printf ' %s %s[-%s]/run failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/run.err"|sed 's|.*: ||')" - elif [ "$rc" -eq "$exp" ]; then - pass=$((pass+1)) - else - fail=$((fail+1)); failnames+=("$name[-$opt]/run exit $rc != $exp") - printf ' %s %s[-%s]/run exit %d != expected %d\n' "$(color_red FAIL)" "$name" "$opt" "$rc" "$exp" - fi - - # Lane /ld: cfree ld links a real native executable; run it via the OS - # loader and compare exit. Exercises cfree ld (layout + relocation + - # image emit) and a real process exit — none of which the JIT 'run' - # path covers. Native host only (no -target is passed). - if ! "$CFREE" ld "$w/rt.o" -o "$w/a.out" 2>"$w/ld.err" || [ -s "$w/ld.err" ]; then - fail=$((fail+1)); failnames+=("$name[-$opt]/ld: $(head -1 "$w/ld.err"|sed 's|.*: ||')") - printf ' %s %s[-%s]/ld failed: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ld.err"|sed 's|.*: ||')"; continue - fi - chmod +x "$w/a.out" 2>/dev/null || true - "$w/a.out" >"$w/ldout" 2>"$w/ldrun.err"; ldrc=$? - if [ -s "$w/ldrun.err" ]; then - fail=$((fail+1)); failnames+=("$name[-$opt]/ld-run: $(head -1 "$w/ldrun.err"|sed 's|.*: ||')") - printf ' %s %s[-%s]/ld-run stderr: %s\n' "$(color_red FAIL)" "$name" "$opt" "$(head -1 "$w/ldrun.err"|sed 's|.*: ||')" - elif [ "$ldrc" -eq "$exp" ]; then - pass=$((pass+1)) - else - fail=$((fail+1)); failnames+=("$name[-$opt]/ld exit $ldrc != $exp") - printf ' %s %s[-%s]/ld exit %d != expected %d\n' "$(color_red FAIL)" "$name" "$opt" "$ldrc" "$exp" - fi - done -done -shopt -u nullglob - -printf '\n' -[ "${#failnames[@]}" -gt 0 ] && { printf 'Failed:\n'; for f in "${failnames[@]}"; do printf ' %s\n' "$f"; done; } -printf 'roundtrip-toy: %d pass, %d fail, %d skip\n' "$pass" "$fail" "$skip" -[ "$fail" -eq 0 ] +# Native host, no -target. The L2 lane uses no tuple-specific behavior; pin a +# single nominal tuple so the engine runs each case once per opt level. +CF_LABEL=test-asm-roundtrip-toy CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$CASES/*.toy" CF_CORPUS_EXT=toy CF_SIDECAR_DIR="$CASES" \ + CF_LANES="L2" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="native-native" \ + CF_TARGETS_EXT="" CF_READ_CASE=rt_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +cf_summary test-asm-roundtrip-toy +cf_exit diff --git a/test/asm/run.sh b/test/asm/run.sh @@ -1,37 +1,45 @@ #!/usr/bin/env bash -# test/asm/run.sh — file-driven assembler / disassembler test harness. +# test/asm/run.sh — file-driven assembler / disassembler test harness, on the +# shared corpus harness (test/lib/cf_corpus.sh). # -# Three sub-corpora under test/asm/, one per asm-runner static-output mode: +# Three sub-corpora under test/asm/, one per asm-runner static-output mode. +# Each is a SEQUENTIAL corpus with a disjoint case set (its own cf_corpus_run, +# no opt axis, single arch), NOT a parallel lane within a case: # -# encode/ <name>.s + <name>.expected.hex golden hex bytes -# decode/ <name>.hex + <name>.expected.txt golden decoded text -# listing/ <name>.in.bin + <name>.expected.lst golden objdump-style +# encode/ <name>.s + <name>.expected.hex golden hex bytes +# (+ optional <name>.expected integer exit code) +# decode/ <name>.hex + <name>.expected.txt golden decoded text +# listing/ <name>.in.bin + <name>.expected.lst golden objdump-style # # Path matrix (6 letters, default HTLDJE): # # H Hex encode — encode/ only. asm-runner --encode IN.s → hex; diff -# vs <name>.expected.hex. +# vs <name>.expected.hex. Missing golden => SKIP. # T Text decode — decode/ only. asm-runner --decode IN.hex → text; diff -# vs <name>.expected.txt. +# vs <name>.expected.txt. Missing golden => FAIL. # L Listing — listing/ only. asm-runner --listing IN.in.bin → text; -# diff vs <name>.expected.lst. +# diff vs <name>.expected.lst. Missing golden => FAIL. # D Direct JIT — encode/ only, when <name>.expected (integer exit) is # present. asm-runner --jit IN.s → exit code matches. -# Host arch must match the cross-target. +# Host arch must match the cross-target. Missing +# .expected exit => SKIP. # J JIT via file — encode/ only, when <name>.expected is present. # asm-runner --emit + jit-runner. Host arch must match. # E ELF exec — encode/ only, when <name>.expected is present. # asm-runner --emit + start.o → link-exe-runner → -# qemu/podman → exit code. Cross-host friendly. +# qemu/podman → exit code. Cross-host friendly (deferred +# batched exec via cf_queue_e). # # Reuses the test/link harness binaries (link-exe-runner, jit-runner) plus # test/link/harness/start.c verbatim — same convention as test/parse/run.sh. # -# Phase 1 (doc/ASM.md §5): asm_parse and the disasm iterator are still -# stubs in src/api/stubs.c. Every smoke case carries a .skip sidecar so -# the harness reports SKIP cleanly; the wiring still runs on every CI -# pass. CFREE_TEST_ALLOW_SKIP defaults to 1 here for the duration of -# phase 1 — flip to 0 (matching the rest of the suite) once the +# Whole-case <name>.targets carries bare arch tokens (aa64/x64/rv64 + synonyms); +# a case applies only when one of them matches CFREE_TEST_ARCH (else SKIP-NA). +# Per-case <name>.skip (+ .<arch>.skip / .<lane>.skip) sidecars skip cleanly. +# +# Phase 1 (doc/ASM.md §5): asm_parse and the disasm iterator are still stubs +# in src/api/stubs.c. CFREE_TEST_ALLOW_SKIP defaults to 1 here for the +# duration of phase 1 — flip to 0 (matching the rest of the suite) once the # assembler / disasm iterator are real. # # Filtering: @@ -39,6 +47,10 @@ # name_filter substring match against case basename # paths subset of "HTLDJE" (default "HTLDJE") # Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS. +# +# Parallelism: every lane hook writes only under $CF_WORK and records only via +# cf_*, so the console summary is identical serial or parallel. CFREE_ASM_PARALLEL +# flips dispatch (default on). set -u @@ -48,10 +60,20 @@ LINK_TEST_DIR="$ROOT/test/link" BUILD_DIR="$ROOT/build/test" LIB_AR="$ROOT/build/libcfree.a" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + ASM_RUNNER="$BUILD_DIR/asm-runner" LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" JIT_RUNNER="$BUILD_DIR/jit-runner" +# Phase 1: ALLOW_SKIP defaults to 1 (smoke cases skip cleanly because +# asm_parse / cfree_disasm_iter_* are still stubs). The engine's cf_exit reads +# CFREE_TEST_ALLOW_SKIP (default 0), so export the phase-1 default of 1 unless +# the caller overrides it. +export CFREE_TEST_ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-1}" + # CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the # pre-multiarch behavior. The asm-runner reads the same env via # test/lib/cfree_test_target.h. @@ -63,14 +85,17 @@ case "$CFREE_TEST_ARCH" in *) printf 'unknown CFREE_TEST_ARCH=%s\n' "$CFREE_TEST_ARCH" >&2; exit 2 ;; esac export CFREE_TEST_ARCH - CLANG_TARGET="--target=$CLANG_TRIPLE" -# Phase 1: ALLOW_SKIP defaults to 1 (smoke cases skip cleanly because -# asm_parse / cfree_disasm_iter_* are still stubs). Flip to 0 once the -# assembler / disassembler land. -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-1}" +# Synthetic single tuple — there is no opt axis and this harness is single-arch. +# Whole-case .targets applicability is computed by cf_read_case (bare arch +# tokens), so the engine's tuple matcher is disabled (CF_TARGETS_EXT=""). +CUR_TUPLE="$TEST_ARCH-asm" + +# Filtering: positional [name_filter] [paths] mirror CFREE_TEST_FILTER / PATHS. +# The engine's discovery honors CFREE_TEST_FILTER, so export the positional one. FILTER="${1:-${CFREE_TEST_FILTER:-}}" +[ -n "$FILTER" ] && export CFREE_TEST_FILTER="$FILTER" PATHS="${2:-${CFREE_TEST_PATHS:-HTLDJE}}" case "$PATHS" in *H*) RUN_H=1;; *) RUN_H=0;; esac case "$PATHS" in *T*) RUN_T=1;; *) RUN_T=0;; esac @@ -78,22 +103,8 @@ case "$PATHS" in *L*) RUN_L=1;; *) RUN_L=0;; esac case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac -T_H=0; T_T=0; T_L=0; T_D=0; T_J=0; T_E=0 -now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } - -mkdir -p "$BUILD_DIR" "$BUILD_DIR/asm" -PASS=0; FAIL=0; SKIP=0 -FAIL_NAMES=(); SKIP_NAMES=() - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } -note_na() { printf ' %s %s — not applicable to %s\n' "$(color_yel SKIP-NA)" "$1" "$TEST_ARCH"; } +PAR="${CFREE_ASM_PARALLEL:-1}" # ---- tool detection (mirrors test/parse/run.sh) ---------------------------- @@ -124,37 +135,39 @@ case "$TEST_ARCH" in rv64) [ "$arch_raw" = "riscv64" ] && is_native_target=1 ;; esac -# Shared per-arch exec helper — see test/lib/exec_target.sh. +# Shared per-arch exec helper — see test/lib/exec_target.sh. Path E queues bare +# EXEC_ARCH tags (== <arch>-linux), so set the caller-contract knobs it reads. EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +. "$ROOT/test/lib/exec_target.sh" # ---- harness binaries ------------------------------------------------------ printf 'Checking harness...\n' if [ ! -x "$ASM_RUNNER" ]; then - printf ' %s asm-runner missing — run "make test-asm"\n' \ - "$(color_red FATAL)" >&2 + printf ' %sFATAL%s asm-runner missing — run "make test-asm"\n' \ + "$_CF_RED" "$_CF_RST" >&2 exit 1 fi -printf ' %s asm-runner\n' "$(color_grn found)" +printf ' %sfound%s asm-runner\n' "$_CF_GRN" "$_CF_RST" # link-exe-runner — for path E. if [ -x "$LINK_EXE_RUNNER" ]; then have_exe_runner=1 - printf ' %s link-exe-runner\n' "$(color_grn found)" + printf ' %sfound%s link-exe-runner\n' "$_CF_GRN" "$_CF_RST" else - printf ' %s link-exe-runner missing; E path will skip\n' "$(color_yel warn)" + printf ' %swarn%s link-exe-runner missing; E path will skip\n' "$_CF_YEL" "$_CF_RST" fi # jit-runner — for path J. Only meaningful when host arch matches the cross-target. if [ $is_native_target -eq 1 ]; then if [ -x "$JIT_RUNNER" ]; then have_jit_runner=1 - printf ' %s jit-runner\n' "$(color_grn found)" + printf ' %sfound%s jit-runner\n' "$_CF_GRN" "$_CF_RST" else - printf ' %s jit-runner missing; J path will skip\n' "$(color_yel warn)" + printf ' %swarn%s jit-runner missing; J path will skip\n' "$_CF_YEL" "$_CF_RST" fi fi @@ -172,23 +185,17 @@ fi printf 'Running cases...\n' -# ---- helpers -------------------------------------------------------------- - -# diff_case <name>/<P> <work> <expected> <actual> <link_dt_or_0> -# Emits PASS or FAIL based on byte-exact match. -diff_case() { - local label="$1" work="$2" expected="$3" actual="$4" - if diff -u "$expected" "$actual" >"$work/diff" 2>&1; then - note_pass "$label" - else - note_fail "$label (golden mismatch; see $work/diff)" - fi -} - -case_applies() { - local dir="$1" name="$2" targets tuple - targets="$dir/$name.targets" +# ---- whole-case .targets applicability (bare arch tokens) ------------------ +# The original .targets sidecars list bare arch names (aa64/x64/rv64 and +# synonyms), NOT <arch>-<obj> tuples, so the engine's cf_tuple_applicable does +# not apply. Read them here and emit SKIP-NA via CF_SKIP_NA_CASE. The engine's +# CF_TARGETS_EXT matcher stays disabled (CF_TARGETS_EXT=""). +cf_read_case() { + # Reset per-case emit state (serial mode reuses this shell across cases). + _asm_emit_done=0 + local targets="$CF_SIDECAR_DIR/$CF_BASE.targets" [ -f "$targets" ] || return 0 + local tuple for tuple in $(cat "$targets"); do case "$tuple:$TEST_ARCH" in aa64:aa64|aarch64:aa64|arm64:aa64) return 0 ;; @@ -196,272 +203,184 @@ case_applies() { rv64:rv64|riscv64:rv64) return 0 ;; esac done - return 1 + CF_SKIP_NA_CASE=1 +} +CF_READ_CASE=cf_read_case + +# ---- golden-diff helper (CF_WORK-confined) --------------------------------- +# cf_golden_diff LABEL EXPECTED ACTUAL : PASS on byte-exact match, else FAIL. +cf_golden_diff() { + local label="$1" expected="$2" actual="$3" + if diff -u "$expected" "$actual" >"$CF_WORK/diff" 2>&1; then + cf_pass "$label" + else + cf_fail "$label" "golden mismatch; see $CF_WORK/diff" + fi } -# ---- decode and listing loops — single-path, golden-driven only ----------- - -if [ $RUN_T -eq 1 ] && [ -d "$TEST_DIR/decode" ]; then - for in_path in "$TEST_DIR"/decode/*.hex; do - [ -e "$in_path" ] || continue - name="$(basename "$in_path" .hex)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if ! case_applies "$TEST_DIR/decode" "$name"; then - note_na "$name/T" - continue - fi - work="$BUILD_DIR/asm/decode/$name" - mkdir -p "$work" - if [ -e "$TEST_DIR/decode/$name.skip" ]; then - reason=$(head -n1 "$TEST_DIR/decode/$name.skip") - note_skip "$name/T" "$reason" - continue - fi - expected="$TEST_DIR/decode/$name.expected.txt" - if [ ! -e "$expected" ]; then - note_fail "$name/T (missing golden $(basename "$expected"))" - continue - fi - t0=$(now_ms) - out="$work/out.txt" - if ! "$ASM_RUNNER" --decode "$in_path" "$out" >"$work/stdout" 2>"$work/stderr"; then - dt=$(( $(now_ms) - t0 )); T_T=$(( T_T + dt )) - note_fail "$name/T (asm-runner --decode failed; see $work/stderr, ${dt}ms)" - continue - fi - dt=$(( $(now_ms) - t0 )); T_T=$(( T_T + dt )) - diff_case "$name/T (${dt}ms)" "$work" "$expected" "$out" - done -fi - -if [ $RUN_L -eq 1 ] && [ -d "$TEST_DIR/listing" ]; then - for in_path in "$TEST_DIR"/listing/*.in.bin; do - [ -e "$in_path" ] || continue - name="$(basename "$in_path" .in.bin)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if ! case_applies "$TEST_DIR/listing" "$name"; then - note_na "$name/L" - continue - fi - work="$BUILD_DIR/asm/listing/$name" - mkdir -p "$work" - if [ -e "$TEST_DIR/listing/$name.skip" ]; then - reason=$(head -n1 "$TEST_DIR/listing/$name.skip") - note_skip "$name/L" "$reason" - continue - fi - expected="$TEST_DIR/listing/$name.expected.lst" - if [ ! -e "$expected" ]; then - note_fail "$name/L (missing golden $(basename "$expected"))" - continue - fi - t0=$(now_ms) - out="$work/out.lst" - if ! "$ASM_RUNNER" --listing "$in_path" "$out" >"$work/stdout" 2>"$work/stderr"; then - dt=$(( $(now_ms) - t0 )); T_L=$(( T_L + dt )) - note_fail "$name/L (asm-runner --listing failed; see $work/stderr, ${dt}ms)" - continue - fi - dt=$(( $(now_ms) - t0 )); T_L=$(( T_L + dt )) - diff_case "$name/L (${dt}ms)" "$work" "$expected" "$out" - done -fi +# ---- decode corpus: lane T (text golden-diff) ------------------------------ +cf_lane_T() { + local expected="$CF_SIDECAR_DIR/$CF_BASE.expected.txt" + if [ ! -e "$expected" ]; then + cf_fail "$CF_NAME/T" "missing golden $(basename "$expected")"; return + fi + local out="$CF_WORK/out.txt" + if ! "$ASM_RUNNER" --decode "$CF_SRC" "$out" >"$CF_WORK/stdout" 2>"$CF_WORK/stderr"; then + cf_fail "$CF_NAME/T" "asm-runner --decode failed; see $CF_WORK/stderr"; return + fi + cf_golden_diff "$CF_NAME/T" "$expected" "$out" +} -# ---- encode loop — drives H + D + J + E per .s case ---------------------- - -# Path E result bookkeeping — same shape as test/parse. -E_NAMES=() -E_WORK=() -E_LINK_MS=() -E_EXPECTED=() - -if [ -d "$TEST_DIR/encode" ]; then - for src in "$TEST_DIR"/encode/*.s; do - [ -e "$src" ] || continue - name="$(basename "$src" .s)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - if ! case_applies "$TEST_DIR/encode" "$name"; then - [ $RUN_H -eq 1 ] && note_na "$name/H" - [ $RUN_D -eq 1 ] && note_na "$name/D" - [ $RUN_J -eq 1 ] && note_na "$name/J" - [ $RUN_E -eq 1 ] && note_na "$name/E" - continue - fi - work="$BUILD_DIR/asm/encode/$name" - mkdir -p "$work" - - # Per-case skip sidecar — applies to every path for this case. - if [ -e "$TEST_DIR/encode/$name.skip" ]; then - reason=$(head -n1 "$TEST_DIR/encode/$name.skip") - [ $RUN_H -eq 1 ] && note_skip "$name/H" "$reason" - [ $RUN_D -eq 1 ] && note_skip "$name/D" "$reason" - [ $RUN_J -eq 1 ] && note_skip "$name/J" "$reason" - [ $RUN_E -eq 1 ] && note_skip "$name/E" "$reason" - continue - fi +# ---- listing corpus: lane L (listing golden-diff) -------------------------- +cf_lane_L() { + local expected="$CF_SIDECAR_DIR/$CF_BASE.expected.lst" + if [ ! -e "$expected" ]; then + cf_fail "$CF_NAME/L" "missing golden $(basename "$expected")"; return + fi + local out="$CF_WORK/out.lst" + if ! "$ASM_RUNNER" --listing "$CF_SRC" "$out" >"$CF_WORK/stdout" 2>"$CF_WORK/stderr"; then + cf_fail "$CF_NAME/L" "asm-runner --listing failed; see $CF_WORK/stderr"; return + fi + cf_golden_diff "$CF_NAME/L" "$expected" "$out" +} - # Expected exit code (for D/J/E). Absent → those paths skip. - expected_exit_file="$TEST_DIR/encode/$name.expected" - has_exit=0 - if [ -e "$expected_exit_file" ]; then - expected=$(head -n1 "$expected_exit_file") - expected_byte=$(( expected & 0xff )) - has_exit=1 - fi +# ---- encode corpus lanes: H / D / J / E ------------------------------------ +# CF_EXPECTED carries the .expected integer exit code (0 when absent); a missing +# .expected file means the exec lanes (D/J/E) SKIP. Detect "has exit" via the +# sidecar's presence directly so a legitimate expected exit of 0 still runs. +_asm_has_exit() { [ -f "$CF_SIDECAR_DIR/$CF_BASE.expected" ]; } +_asm_exit_byte() { echo $(( CF_EXPECTED & 0xff )); } + +# Path H: hex encode roundtrip. Missing .expected.hex golden => SKIP. +cf_lane_H() { + local expected_hex="$CF_SIDECAR_DIR/$CF_BASE.expected.hex" + if [ ! -e "$expected_hex" ]; then + cf_skip "$CF_NAME/H" "no .expected.hex golden"; return + fi + local out="$CF_WORK/out.hex" + if ! "$ASM_RUNNER" --encode "$CF_SRC" "$out" >"$CF_WORK/h.out" 2>"$CF_WORK/h.err"; then + cf_fail "$CF_NAME/H" "asm-runner --encode failed; see $CF_WORK/h.err"; return + fi + cf_golden_diff "$CF_NAME/H" "$expected_hex" "$out" +} - # ---- Path H: hex encode roundtrip ---------------------------------- - if [ $RUN_H -eq 1 ]; then - expected_hex="$TEST_DIR/encode/$name.expected.hex" - if [ ! -e "$expected_hex" ]; then - note_skip "$name/H" "no .expected.hex golden" - else - t0=$(now_ms) - out="$work/out.hex" - if ! "$ASM_RUNNER" --encode "$src" "$out" >"$work/h.out" 2>"$work/h.err"; then - dt=$(( $(now_ms) - t0 )); T_H=$(( T_H + dt )) - note_fail "$name/H (asm-runner --encode failed; see $work/h.err, ${dt}ms)" - else - dt=$(( $(now_ms) - t0 )); T_H=$(( T_H + dt )) - diff_case "$name/H (${dt}ms)" "$work" "$expected_hex" "$out" - fi - fi - fi +# Ensure the .o needed by J/E exists. Sets ASM_OBJ to its path and returns 0 on +# success; on the first failure emits FAIL "$CF_NAME/emit" (matching the +# original standalone emit failure record) and returns 1. Must NOT be called in +# a $(...) subshell — cf_fail mutates counters that a subshell would discard. +# D does not need a .o — asm-runner --jit does the full parse+jit in process. +_asm_emit_done=0 +_asm_emit() { + ASM_OBJ="$CF_WORK/$CF_BASE.o" + [ -f "$ASM_OBJ" ] && return 0 + if [ "$_asm_emit_done" -eq 1 ]; then return 1; fi + _asm_emit_done=1 + if "$ASM_RUNNER" --emit "$CF_SRC" "$ASM_OBJ" 2>"$CF_WORK/emit.err"; then + return 0 + fi + cf_fail "$CF_NAME/emit" "asm-runner --emit failed; see $CF_WORK/emit.err" + return 1 +} - # ---- emit (needed by D/J/E exec paths) ----------------------------- - # D doesn't strictly need a .o on disk — asm-runner --jit does the - # full parse+jit in process. But J and E need the file emit. - obj="$work/$name.o" - need_emit=0 - if [ $has_exit -eq 1 ]; then - if [ $RUN_J -eq 1 ] || [ $RUN_E -eq 1 ]; then need_emit=1; fi - fi - if [ $need_emit -eq 1 ]; then - if ! "$ASM_RUNNER" --emit "$src" "$obj" 2>"$work/emit.err"; then - # D may still run independently; the J/E paths below detect - # the missing .o and skip themselves. - printf ' %s %s/emit (asm-runner --emit failed; see %s)\n' \ - "$(color_red FAIL)" "$name" "$work/emit.err" - FAIL=$((FAIL+1)); FAIL_NAMES+=("$name/emit") - fi - fi +# Path D: in-process JIT. Missing .expected exit => SKIP. +cf_lane_D() { + if ! _asm_has_exit; then cf_skip "$CF_NAME/D" "no .expected exit code"; return; fi + if [ $is_native_target -eq 0 ]; then + cf_skip "$CF_NAME/D" "host arch != $TEST_ARCH (no native JIT)"; return + fi + local want; want=$(_asm_exit_byte) + "$ASM_RUNNER" --jit "$CF_SRC" >"$CF_WORK/d.out" 2>"$CF_WORK/d.err" + local rc=$? + if [ "$rc" -eq "$want" ]; then cf_pass "$CF_NAME/D" + else cf_fail "$CF_NAME/D" "expected $want got $rc"; fi +} - # ---- Path D: in-process JIT ---------------------------------------- - if [ $RUN_D -eq 1 ]; then - if [ $has_exit -eq 0 ]; then - note_skip "$name/D" "no .expected exit code" - elif [ $is_native_target -eq 0 ]; then - note_skip "$name/D" "host arch != $TEST_ARCH (no native JIT)" - else - t0=$(now_ms) - "$ASM_RUNNER" --jit "$src" >"$work/d.out" 2>"$work/d.err" - d_rc=$? - dt=$(( $(now_ms) - t0 )); T_D=$(( T_D + dt )) - if [ "$d_rc" -eq "$expected_byte" ]; then - note_pass "$name/D (${dt}ms)" - else - note_fail "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" - fi - fi - fi +# Path J: jit-via-file. Missing .expected exit => SKIP. +cf_lane_J() { + if ! _asm_has_exit; then cf_skip "$CF_NAME/J" "no .expected exit code"; return; fi + if [ $have_jit_runner -eq 0 ]; then + cf_skip "$CF_NAME/J" "no jit-runner (host arch != $TEST_ARCH)"; return + fi + _asm_emit || { cf_skip "$CF_NAME/J" "no .o (--emit failed)"; return; } + local want; want=$(_asm_exit_byte) + "$JIT_RUNNER" "$ASM_OBJ" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err" + local rc=$? + if [ "$rc" -eq "$want" ]; then cf_pass "$CF_NAME/J" + else cf_fail "$CF_NAME/J" "expected $want got $rc"; fi +} - # ---- Path J: jit-via-file ------------------------------------------ - if [ $RUN_J -eq 1 ]; then - if [ $has_exit -eq 0 ]; then - note_skip "$name/J" "no .expected exit code" - elif [ $have_jit_runner -eq 0 ]; then - note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH)" - elif [ ! -e "$obj" ]; then - note_skip "$name/J" "no .o (--emit failed)" - else - t0=$(now_ms) - "$JIT_RUNNER" "$obj" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) - if [ "$j_rc" -eq "$expected_byte" ]; then - note_pass "$name/J (${dt}ms)" - else - note_fail "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" - fi - fi - fi +# Path E: link + deferred (batched) qemu/podman exec. Missing .expected => SKIP. +cf_lane_E() { + if ! _asm_has_exit; then cf_skip "$CF_NAME/E" "no .expected exit code"; return; fi + if [ $have_exe_runner -eq 0 ] || [ $have_clang_cross -eq 0 ] || [ $have_start_obj -eq 0 ]; then + cf_skip "$CF_NAME/E" "no link-exe-runner, $TEST_ARCH clang, or start.o"; return + fi + _asm_emit || { cf_skip "$CF_NAME/E" "no .o (--emit failed)"; return; } + local exe="$CF_WORK/linked.exe" + if ! "$LINK_EXE_RUNNER" -o "$exe" "$ASM_OBJ" "$START_OBJ" \ + >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then + cf_fail "$CF_NAME/E" "link failed"; return + fi + if exec_target_supported "$EXEC_ARCH"; then + cf_queue_e "$CF_NAME/E" "$exe" \ + "$CF_WORK/exec.out" "$CF_WORK/exec.err" "$CF_WORK/exec.rc" \ + "$(_asm_exit_byte)" "$EXEC_ARCH" + else + cf_skip "$CF_NAME/E" "no runner for $EXEC_ARCH" + fi +} - # ---- Path E: link + (batched) qemu/podman -------------------------- - if [ $RUN_E -eq 1 ]; then - if [ $has_exit -eq 0 ]; then - note_skip "$name/E" "no .expected exit code" - elif [ $have_exe_runner -eq 0 ] || [ $have_clang_cross -eq 0 ] \ - || [ $have_start_obj -eq 0 ]; then - note_skip "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" - elif [ ! -e "$obj" ]; then - note_skip "$name/E" "no .o (--emit failed)" - else - t0=$(now_ms) - exe="$work/linked.exe" - if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" \ - >"$work/exec_link.out" 2>"$work/exec_link.err"; then - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - note_fail "$name/E (link failed, ${dt}ms)" - elif exec_target_supported "$EXEC_ARCH"; then - link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) - E_NAMES+=("$name") - E_WORK+=("$work") - E_LINK_MS+=("$link_dt") - E_EXPECTED+=("$expected_byte") - exec_target_queue "$EXEC_ARCH" "$name" "$exe" \ - "$work/exec.out" "$work/exec.err" "$work/exec.rc" - else - note_skip "$name/E" "no runner for $EXEC_ARCH" - fi - fi +# ---- has-cases guard ------------------------------------------------------- +# cf_corpus_run exits 2 if a sub-corpus has zero matching cases (after the +# CFREE_TEST_FILTER), so only invoke it when at least one case is present. +# _asm_have_cases GLOB EXT : mirrors the engine's discovery (basename minus +# .EXT, CFREE_TEST_FILTER substring) and returns 0 iff a case survives. +_asm_have_cases() { + local glob="$1" ext="$2" f base + shopt -s nullglob + for f in $glob; do + base="$(basename "$f")"; base="${base%.$ext}" + if [ -n "${CFREE_TEST_FILTER:-}" ]; then + case "$base" in *"$CFREE_TEST_FILTER"*) ;; *) continue ;; esac fi + return 0 done -fi + return 1 +} -# ---- batched path-E flush + verification ----------------------------------- - -T_E_BATCH=0 -if [ "$(exec_target_queue_size)" -gt 0 ]; then - printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)" - t0=$(now_ms) - exec_target_flush - T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) - - i=0 - while [ $i -lt ${#E_NAMES[@]} ]; do - name="${E_NAMES[$i]}" - work="${E_WORK[$i]}" - link_dt="${E_LINK_MS[$i]}" - expected_byte="${E_EXPECTED[$i]}" - if [ ! -f "$work/exec.rc" ]; then - note_fail "$name/E (no rc; podman batch did not produce results)" - else - RUN_RC="$(cat "$work/exec.rc")" - if [ "$RUN_RC" -eq "$expected_byte" ]; then - note_pass "$name/E (link ${link_dt}ms)" - else - note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)" - fi - fi - i=$((i+1)) - done +# ---- drive the three sub-corpora ------------------------------------------- + +# (1) encode/*.s — lanes H (golden hex), D (in-proc JIT), J (jit-runner), +# E (link + deferred exec). Active lanes in PATHS order. +ENCODE_LANES= +[ "$RUN_H" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES H" +[ "$RUN_D" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES D" +[ "$RUN_J" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES J" +[ "$RUN_E" -eq 1 ] && ENCODE_LANES="$ENCODE_LANES E" +if [ -n "$ENCODE_LANES" ] && _asm_have_cases "$TEST_DIR/encode/*.s" s; then + CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/encode" \ + CF_CORPUS_GLOBS="$TEST_DIR/encode/*.s" CF_CORPUS_EXT=s CF_SIDECAR_DIR="$TEST_DIR/encode" \ + CF_LANES="$ENCODE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run fi -# ---- summary --------------------------------------------------------------- - -if [ ${#FAIL_NAMES[@]} -gt 0 ]; then - printf '\nFailed:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done +# (2) decode/*.hex — lane T (golden decoded text). +if [ "$RUN_T" -eq 1 ] && _asm_have_cases "$TEST_DIR/decode/*.hex" hex; then + CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/decode" \ + CF_CORPUS_GLOBS="$TEST_DIR/decode/*.hex" CF_CORPUS_EXT=hex CF_SIDECAR_DIR="$TEST_DIR/decode" \ + CF_LANES="T" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run fi -if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then - printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' - for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done +# (3) listing/*.in.bin — lane L (golden listing). +if [ "$RUN_L" -eq 1 ] && _asm_have_cases "$TEST_DIR/listing/*.in.bin" in.bin; then + CF_LABEL=test-asm CF_BUILD_DIR="$BUILD_DIR/asm/listing" \ + CF_CORPUS_GLOBS="$TEST_DIR/listing/*.in.bin" CF_CORPUS_EXT=in.bin CF_SIDECAR_DIR="$TEST_DIR/listing" \ + CF_LANES="L" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" cf_corpus_run fi -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: H=%dms T=%dms L=%dms D=%dms J=%dms E=%dms (batch %dms)\n' \ - "$T_H" "$T_T" "$T_L" "$T_D" "$T_J" "$T_E" "$T_E_BATCH" +# ---- summary --------------------------------------------------------------- -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-asm +cf_exit diff --git a/test/asm/symmetry.sh b/test/asm/symmetry.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# test/asm/symmetry.sh — asm<->disasm self-symmetry sweep (aa64). +# test/asm/symmetry.sh — Type D (differential, baseline mode): +# asm<->disasm self-symmetry sweep (aa64). # # Systematically checks that the assembler and disassembler agree on the # instruction set, in both directions: @@ -22,26 +23,35 @@ # the baseline documents the disasm-completeness backlog. Closing a gap shrinks # the baseline (regenerate with --update). See doc/ASM_ROUNDTRIP_TESTING.md. # +# Type D oracle: AGREEMENT against a checked-in baseline. The producer here is +# the freshly-generated, sorted asymmetry report; cf_diff_baseline compares it +# to test/asm/symmetry.baseline (or regenerates it under --update / +# CF_DIFF_UPDATE=1). +# # Opt-in; host-independent (no execution). Decode-side assembles line-by-line, # so it is a few seconds. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +. "$ROOT/test/lib/cfree_sh_report.sh" +. "$ROOT/test/lib/cf_differential.sh" + AR="$ROOT/build/test/asm-runner" GEN="$ROOT/build/test/aa64_sweep_gen" ENCODE_DIR="$ROOT/test/asm/encode" WORK="$ROOT/build/test/asm/symmetry" BASELINE="$ROOT/test/asm/symmetry.baseline" -UPDATE=0 -[ "${1:-}" = "--update" ] && UPDATE=1 -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } +# --update is the long-standing regen flag; CF_DIFF_UPDATE=1 is the canonical +# Type D knob. Either one regenerates the baseline (cf_diff_baseline honors +# CF_DIFF_UPDATE). +[ "${1:-}" = "--update" ] && CF_DIFF_UPDATE=1 +: "${CF_DIFF_UPDATE:=0}" +export CF_DIFF_UPDATE if [ ! -x "$AR" ] || [ ! -x "$GEN" ]; then - printf '%s asm-runner / aa64_sweep_gen missing — run the test target\n' \ - "$(color_red FATAL)" >&2 + echo "symmetry: asm-runner / aa64_sweep_gen missing — run the test target" >&2 exit 2 fi @@ -50,6 +60,8 @@ mkdir -p "$WORK" report="$WORK/report" : > "$report" +cf_report_init + strip_off() { sed -E 's/^[0-9a-f]+:\t//'; } # ---- decode-side: table sweep --------------------------------------------- @@ -102,19 +114,10 @@ shopt -u nullglob sort -u "$report" -o "$report" # ---- compare against the baseline snapshot -------------------------------- -if [ $UPDATE -eq 1 ]; then - cp "$report" "$BASELINE" - printf 'symmetry: baseline updated (%d known asymmetries)\n' "$(wc -l < "$report" | tr -d ' ')" - exit 0 -fi -[ -f "$BASELINE" ] || : > "$BASELINE" -if diff -u "$BASELINE" "$report" > "$WORK/delta.diff" 2>&1; then - printf 'symmetry: %s (%d known asymmetries in baseline)\n' \ - "$(color_grn 'no new asm<->disasm asymmetry')" \ - "$(wc -l < "$BASELINE" | tr -d ' ')" - exit 0 -fi -printf 'symmetry: %s\n' "$(color_red 'asymmetry set changed vs baseline')" -printf ' (+ = new asymmetry / regression, - = fixed; regenerate with --update)\n' -grep -E '^[-+][^-+]' "$WORK/delta.diff" | head -30 | sed 's/^/ /' -exit 1 +# Single Type D verdict: the freshly produced, normalized report must equal the +# checked-in baseline (cf_diff_baseline regenerates it under CF_DIFF_UPDATE=1 +# and shows the unified delta on drift). +cf_diff_baseline symmetry "$BASELINE" "$report" + +cf_summary symmetry +cf_exit diff --git a/test/bounce/bounce.sh b/test/bounce/bounce.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# test/bounce/bounce.sh — format-bounce stress test. +# test/bounce/bounce.sh — format-bounce stress test, on the shared corpus +# harness (test/lib/cf_corpus.sh). # # Goal: stress cfree's object read/write/reloc, archive, and partial-link # paths by taking one compiled program and *bouncing* its object through @@ -8,8 +9,11 @@ # cross-arch run coverage elsewhere; this test exists to exercise the # format machinery, not the arches. # -# For each (arch, case, -O level) the program is compiled to a relocatable -# object once, then each bounce chain rebuilds an executable from it: +# Matrix = case x opt x tuple (one <arch>-linux tuple per requested arch). +# For each (case, opt, arch) item the program is compiled to a relocatable +# object once, then each applicable bounce CHAIN rebuilds an executable from +# it (the chain axis is iterated inside the lane, like wasm's multi-result +# lanes): # # direct baseline: link prog.o + crt.o straight through # macho_rt ELF -> Mach-O -> ELF (objcopy) then link @@ -19,13 +23,15 @@ # strip_dbg objcopy --strip-debug on prog.o, then link # ar `ar rcs` prog.o into an archive, on-demand link with crt.o # -# Every chain's executable is run (batched through one podman exec) and its -# exit code compared to a host-cc reference for that case. Skip-vs-fail -# follows the smoke harness: a skip is a failure unless CFREE_TEST_ALLOW_SKIP=1. +# Each chain's executable is queued through cf_queue_e and run in ONE batched +# container exec at flush; its exit code is compared to a host-cc reference +# for that case. The chain transforms + compute_ref host-reference stay +# LANE-LOCAL. Every lane hook writes only under $CF_WORK and records via cf_*, +# so the runner is parallel-safe; CFREE_BOUNCE_PARALLEL flips dispatch. # # Arch selection: defaults to the host-native Linux arch (fast, no # emulation). Override with CFREE_BOUNCE_ARCHES="aarch64 x64 rv64" to sweep -# others through podman/qemu. +# others through podman/qemu. Skip is a failure unless CFREE_TEST_ALLOW_SKIP=1. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" @@ -33,21 +39,15 @@ BIN="${CFREE_BIN:-$ROOT/build/cfree}" HOSTCC="${CC:-cc}" BUILD="$ROOT/build/test/bounce" CRT="$ROOT/test/bounce/crt/crt.c" -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" OPT_LEVELS="${CFREE_BOUNCE_OPTS:-0 1}" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + rm -rf "$BUILD" mkdir -p "$BUILD" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -PASS=0; FAIL=0; SKIP=0 -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } - # ---- arch selection -------------------------------------------------------- case "$(uname -m 2>/dev/null)" in @@ -83,7 +83,7 @@ export have_podman have_qemu QEMU_BIN is_aarch64 EXEC_TARGET_MOUNT_ROOT="$BUILD" export EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +. "$ROOT/test/lib/exec_target.sh" # ---- prerequisites --------------------------------------------------------- @@ -92,36 +92,35 @@ if [ ! -x "$BIN" ]; then exit 2 fi if [ -z "$ARCHES" ]; then - note_skip "arch" "unsupported host arch $(uname -m) — set CFREE_BOUNCE_ARCHES" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 + cf_skip "arch" "unsupported host arch $(uname -m) — set CFREE_BOUNCE_ARCHES" + cf_summary test-bounce; cf_exit fi if ! "$HOSTCC" -x c -c /dev/null -o /dev/null 2>/dev/null; then - note_skip "hostcc" "host '$HOSTCC' cannot compile — set CC" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 + cf_skip "hostcc" "host '$HOSTCC' cannot compile — set CC" + cf_summary test-bounce; cf_exit fi -CASES="$(ls "$ROOT"/test/bounce/cases/*.c 2>/dev/null | sort)" -if [ -z "$CASES" ]; then +CASES_GLOB="$ROOT/test/bounce/cases/*.c" +# shellcheck disable=SC2086 +if ! ls $CASES_GLOB >/dev/null 2>&1; then echo "no cases under test/bounce/cases" >&2 exit 2 fi -# ---- per-case host reference ---------------------------------------------- +# ---- per-case host reference (LANE-LOCAL) ---------------------------------- # Reference exit code for a case = running it on the host. The cases are # portable LP64 integer C, so the host result is the oracle every target -# and every bounce chain must reproduce. +# and every bounce chain must reproduce. Writes only under $CF_WORK. compute_ref() { - local case="$1" name="$2" ref_exe="$BUILD/$name.ref" drv="$BUILD/$name.refmain.c" + local case="$1" ref_exe="$CF_WORK/ref" drv="$CF_WORK/refmain.c" printf 'int bounce_main(void);\nint main(void){return bounce_main();}\n' >"$drv" - if ! "$HOSTCC" -O1 "$case" "$drv" -o "$ref_exe" 2>"$BUILD/$name.ref.err"; then + if ! "$HOSTCC" -O1 "$case" "$drv" -o "$ref_exe" 2>"$CF_WORK/ref.err"; then return 1 fi "$ref_exe"; echo $? } -# ---- bounce chains --------------------------------------------------------- +# ---- bounce chains (LANE-LOCAL) -------------------------------------------- # Each chain takes prog.o + crt.o and produces an executable at $out, or # returns nonzero (with a diagnostic on stdout) if any transform/link fails. # $1=prog.o $2=crt.o $3=out $4=workdir @@ -174,90 +173,72 @@ chain_applies() { esac } -# ---- queue bookkeeping (parallel indexed arrays; bash 3.2 safe) ----------- -Q_LABEL=(); Q_RCFILE=(); Q_EXPECT=() - -# ---- main sweep ------------------------------------------------------------ - -for arch in $ARCHES; do +# ---- lane B: compile prog.o, then run every applicable chain --------------- +# CF_ARCH is the corpus tuple's arch (aarch64/x64/rv64); CF_OBJ is "linux". +# crt + host reference + prog.o are built here (CF_WORK-confined). Each chain's +# executable is deferred via cf_queue_e and scored at flush (rc == host ref). +cf_lane_B() { + local arch="$CF_ARCH" exec_tag="$CF_ARCH-linux" + # Disambiguate result names by arch+opt (the engine's CF_NAME does not carry + # the tuple), matching the original "<name>.<arch>.O<opt>.<chain>" labels. + local stem="$CF_BASE.$arch.O$CF_OPT" + local triple define triple="$(arch_triple "$arch")" define="$(arch_define "$arch")" - if [ -z "$triple" ]; then - note_skip "$arch" "unknown arch tag" - continue - fi - if ! exec_target_supported "$arch"; then - note_skip "$arch" "no runner (need podman or qemu for $arch-linux)" - continue + if [ -z "$triple" ]; then cf_skip "$stem" "unknown arch tag"; return; fi + if ! exec_target_supported "$exec_tag"; then + cf_skip "$stem" "no runner (need podman or qemu for $arch-linux)"; return fi - for opt in $OPT_LEVELS; do - # crt depends only on (arch, opt); build it once. - crt_o="$BUILD/crt.$arch.O$opt.o" - if ! "$BIN" cc -target "$triple" -D"$define" -O"$opt" -c "$CRT" \ - -o "$crt_o" 2>"$BUILD/crt.$arch.O$opt.err"; then - note_fail "$arch/O$opt crt.c (see $BUILD/crt.$arch.O$opt.err)" - continue - fi + # crt depends only on (arch, opt); built per-item under $CF_WORK. + local crt_o="$CF_WORK/crt.o" + if ! "$BIN" cc -target "$triple" -D"$define" -O"$CF_OPT" -c "$CRT" \ + -o "$crt_o" 2>"$CF_WORK/crt.err"; then + cf_fail "$stem" "crt.c (see $CF_WORK/crt.err)"; return + fi - for case in $CASES; do - name="$(basename "$case" .c)" - ref="$(compute_ref "$case" "$name")" - if [ -z "$ref" ]; then - note_fail "$name reference build (host cc; see $BUILD/$name.ref.err)" - continue - fi + local ref + ref="$(compute_ref "$CF_SRC")" + if [ -z "$ref" ]; then + cf_fail "$stem" "reference build (host cc; see $CF_WORK/ref.err)"; return + fi - prog_o="$BUILD/$name.$arch.O$opt.o" - if ! "$BIN" cc -target "$triple" -O"$opt" -c "$case" -o "$prog_o" \ - 2>"$BUILD/$name.$arch.O$opt.err"; then - note_fail "$name $arch/O$opt compile (see $BUILD/$name.$arch.O$opt.err)" - continue - fi + local prog_o="$CF_WORK/prog.o" + if ! "$BIN" cc -target "$triple" -O"$CF_OPT" -c "$CF_SRC" -o "$prog_o" \ + 2>"$CF_WORK/prog.err"; then + cf_fail "$stem" "compile (see $CF_WORK/prog.err)"; return + fi - for chain in $CHAINS; do - chain_applies "$arch" "$chain" || continue - tag="$name.$arch.O$opt.$chain" - wd="$BUILD/$tag.d"; mkdir -p "$wd" - exe="$BUILD/$tag.exe" - if ! "chain_$chain" "$prog_o" "$crt_o" "$exe" "$wd" \ - >"$BUILD/$tag.link.log" 2>&1; then - note_fail "$tag (transform/link; see $BUILD/$tag.link.log)" - continue - fi - # Re-read the produced executable's headers to catch malformed - # output the linker accepted but a reader would choke on. - rcfile="$BUILD/$tag.rc" - Q_LABEL+=("$tag"); Q_RCFILE+=("$rcfile"); Q_EXPECT+=("$ref") - exec_target_queue "$arch" "$tag" "$exe" \ - "$BUILD/$tag.out" "$BUILD/$tag.err" "$rcfile" - done - done + local chain tag wd exe + for chain in $CHAINS; do + chain_applies "$arch" "$chain" || continue + tag="$stem.$chain" + wd="$CF_WORK/$chain.d"; mkdir -p "$wd" + exe="$CF_WORK/$chain.exe" + if ! "chain_$chain" "$prog_o" "$crt_o" "$exe" "$wd" \ + >"$CF_WORK/$chain.link.log" 2>&1; then + cf_fail "$tag" "transform/link; see $CF_WORK/$chain.link.log" + continue + fi + # Defer exec: the engine batches all queued cases into one container run + # at flush, then scores rc against the host reference (EXPECTED). + cf_queue_e "$tag" "$exe" \ + "$CF_WORK/$chain.out" "$CF_WORK/$chain.err" "$CF_WORK/$chain.rc" \ + "$ref" "$exec_tag" done -done - -# ---- run everything in batched container execs, then score ----------------- -exec_target_flush +} -i=0; n="${#Q_LABEL[@]}" -while [ "$i" -lt "$n" ]; do - label="${Q_LABEL[$i]}"; rcfile="${Q_RCFILE[$i]}"; expect="${Q_EXPECT[$i]}" - i=$((i+1)) - if [ ! -f "$rcfile" ]; then - note_fail "$label (no rc recorded)" - continue - fi - got="$(cat "$rcfile")" - if [ "$got" = "$expect" ]; then - note_pass "$label (rc=$got)" - else - note_fail "$label (expected $expect got $got)" - fi +# ---- drive the corpus ------------------------------------------------------ +# One tuple per requested arch; opt axis from CFREE_BOUNCE_OPTS; single lane B. +TUPLES= +for arch in $ARCHES; do + TUPLES="$TUPLES $arch-linux" done -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -[ "$FAIL" -eq 0 ] || exit 1 -if [ "$PASS" -eq 0 ]; then - [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 -fi -exit 0 +CF_LABEL=test-bounce CF_BUILD_DIR="$BUILD/work" \ + CF_CORPUS_GLOBS="$CASES_GLOB" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$ROOT/test/bounce/cases" \ + CF_LANES="B" CF_OPT_LEVELS="$OPT_LEVELS" CF_TUPLES="$TUPLES" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="${CFREE_BOUNCE_PARALLEL:-1}" cf_corpus_run + +cf_summary test-bounce +cf_exit diff --git a/test/cas/run.sh b/test/cas/run.sh @@ -18,138 +18,12 @@ trap 'rm -rf "$work"' EXIT mkdir -p "$work/src/bin" "$work/src/share" "$work/cas" "$work/out" -pass=0 -fail=0 -skip=0 -failures= - -ok() { - printf 'PASS %s\n' "$1" - pass=$((pass + 1)) -} - -not_ok() { - printf 'FAIL %s\n' "$1" - if [ "$#" -gt 1 ] && [ -s "$2" ]; then - sed 's/^/ | /' "$2" - fi - fail=$((fail + 1)) - failures="$failures $1" -} - -skip_test() { - printf 'SKIP %s\n' "$1" - skip=$((skip + 1)) -} - -run_ok() { - name=$1 - shift - if "$@" > "$work/$name.out" 2> "$work/$name.err"; then - ok "$name" - else - not_ok "$name" "$work/$name.err" - fi -} - -run_fail() { - name=$1 - shift - if "$@" > "$work/$name.out" 2> "$work/$name.err"; then - { - echo "command unexpectedly succeeded" - sed 's/^/stdout: /' "$work/$name.out" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - else - ok "$name" - fi -} - -contains() { - name=$1 - file=$2 - needle=$3 - if grep -F "$needle" "$file" >/dev/null 2>&1; then - ok "$name" - else - { - printf 'missing text: %s\n' "$needle" - sed 's/^/file: /' "$file" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - -same_file() { - name=$1 - want=$2 - got=$3 - if cmp -s "$want" "$got"; then - ok "$name" - else - { - printf 'files differ:\n' - printf ' want: %s\n' "$want" - printf ' got: %s\n' "$got" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - -is_executable() { - name=$1 - file=$2 - if [ -x "$file" ]; then - ok "$name" - else - echo "not executable: $file" > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - -first_hex_id() { - sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p' -} - -id_prefix() { - printf '%.2s' "$1" -} - -cas_object_path() { - root=$1 - kind=$2 - id=$3 - prefix=$(id_prefix "$id") - printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id" -} - -tree_blob_for_path() { - tree_file=$1 - want=$2 - awk -v want="$want" ' - $0 == "[file]" { in_file = 1; path = ""; blob = ""; next } - in_file && /^path = / { path = substr($0, 8); next } - in_file && /^blob = / { - blob = substr($0, 8); - if (path == want) { - print blob; - exit; - } - } - ' "$tree_file" -} - -assert_file_exists() { - name=$1 - file=$2 - if [ -f "$file" ]; then - ok "$name" - else - echo "missing file: $file" > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} +# Type-K mode-P kit: ok/run_ok/run_fail/contains/same_file/... + the CAS +# helpers (first_hex_id/cas_object_path/tree_blob_for_path), all recording +# through the unified cf_* counters over $work. +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" +cf_report_init make_fixtures() { printf alpha > "$work/src/share/a.txt" @@ -273,10 +147,5 @@ else skip_test "cas-corruption-tests" fi -if [ "$fail" -ne 0 ]; then - printf 'cas: failures:%s\n' "$failures" - printf 'cas: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip" - exit 1 -fi - -printf 'cas: %d passed, %d skipped\n' "$pass" "$skip" +cf_summary cas +cf_exit diff --git a/test/cg/ir_recorder_test.c b/test/cg/ir_recorder_test.c @@ -7,55 +7,16 @@ #include <string.h> #include "core/pool.h" +#include "lib/cfree_unit.h" -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} - -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} - -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static int g_fails; -static int g_checks; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -#define EXPECT(cond, ...) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* One shared test context replaces the per-file heap/diag/counter globals. + * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged. The + * table-of-tests harness re-creates a compiler per test via tc_init, but the + * single g_u (with ctx.now == -1, set once in main) backs them all. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) typedef struct TestCtx { - CfreeContext ctx; Compiler* c; CfreeCgTypeId i32; CfreeCgTypeId ptr; @@ -65,16 +26,8 @@ static void tc_init(TestCtx* tc) { CfreeTarget target; CfreeCgBuiltinTypes b; memset(tc, 0, sizeof *tc); - tc->ctx.heap = &g_heap; - tc->ctx.diag = &g_diag; - tc->ctx.now = -1; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_X86_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != + target = cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); + if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) != CFREE_OK || !tc->c) { fprintf(stderr, "fatal: compiler allocation failed\n"); @@ -388,7 +341,7 @@ static void test_func_dump_renders_text(void) { t->func_end(t); f = cg_ir_recorder_module(t)->funcs[0]; - cfree_writer_mem(&g_heap, &w); + cfree_writer_mem(&g_u.heap, &w); cg_ir_func_dump(f, w); bytes = cfree_writer_mem_bytes(w, &len); EXPECT(len > 0 && bytes && len < sizeof s, "dump should produce output"); @@ -408,11 +361,14 @@ static void test_func_dump_renders_text(void) { } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; test_records_basic_function_shape(); test_deep_copies_call_switch_and_const_payloads(); test_labels_scopes_and_address_taken_locals(); test_aliases_and_data_label_diagnostic_hook(); test_func_dump_renders_text(); - fprintf(stderr, "ir-recorder: %d checks, %d failures\n", g_checks, g_fails); - return g_fails ? 1 : 0; + fprintf(stderr, "ir-recorder: %d checks, %d failures\n", g_u.checks, + g_u.fails); + return g_u.fails ? 1 : 0; } diff --git a/test/cg/native_direct_target_test.c b/test/cg/native_direct_target_test.c @@ -7,55 +7,15 @@ #include <string.h> #include "core/arena.h" +#include "lib/cfree_unit.h" -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} - -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} - -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static int g_fails; -static int g_checks; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -#define EXPECT(cond, ...) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original + * ctx.now of -1 is preserved (set once in main after cfree_unit_init). */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) typedef struct TestCtx { - CfreeContext ctx; Compiler* c; CfreeCgTypeId i32; CfreeCgTypeId ptr; @@ -65,16 +25,9 @@ static void tc_init(TestCtx* tc) { CfreeTarget target; CfreeCgBuiltinTypes b; memset(tc, 0, sizeof *tc); - tc->ctx.heap = &g_heap; - tc->ctx.diag = &g_diag; - tc->ctx.now = -1; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_X86_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != + target = + cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); + if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) != CFREE_OK || !tc->c) { fprintf(stderr, "fatal: compiler allocation failed\n"); @@ -653,16 +606,18 @@ static void test_b_call_still_flushes(void) { } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; test_frame_locals_scratch_storeback_and_branches(); test_call_barrier_storeback_and_max_outgoing(); test_b_cached_pointer_base_not_reloaded(); test_b_cache_survives_store(); test_b_volatile_load_flushes(); test_b_call_still_flushes(); - if (g_fails) { - fprintf(stderr, "%d/%d checks failed\n", g_fails, g_checks); + if (g_u.fails) { + fprintf(stderr, "%d/%d checks failed\n", g_u.fails, g_u.checks); return 1; } - printf("native_direct_target_test: %d checks passed\n", g_checks); + printf("native_direct_target_test: %d checks passed\n", g_u.checks); return 0; } diff --git a/test/coff/windows-system-dlls-smoke.sh b/test/coff/windows-system-dlls-smoke.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Windows system-DLL coverage smoke. +# test/coff/windows-system-dlls-smoke.sh — Type-K (mode P) Windows system-DLL +# coverage smoke, on the shared scripted-shell kit (test/lib/cfree_sh_kit.sh). # -# Companion to windows-ucrt-hosted-smoke.sh: that script proves the UCRT -# console + GUI link round-trip for one program per surface (Sleep, -# runtime, stdio, TLS, GUI WinMain). This script broadens the surface -# across the typical large system DLLs an application links against: +# Companion to windows-ucrt-hosted-smoke.sh: that proves the UCRT console + GUI +# round-trip for one program per surface; this broadens coverage across the +# large system DLLs an application links against: # # user32 + gdi32 (GUI window + drawing) # advapi32 (registry) @@ -15,20 +15,26 @@ # libucrt.a (mixed short-import + long-form members) # # Each program is built with `cfree cc` for both x86_64-windows and -# aarch64-windows; the link-level check inspects the PE import -# directory via `cfree objdump -p`. The Wine runtime check is run -# conditionally — same pattern as windows-ucrt-hosted-smoke.sh, and -# silently skipped when the matching podman/Wine container is absent. +# aarch64-windows; the link-level check inspects the PE import directory via +# `cfree objdump -p` (run_ok + contains). The Wine runtime check is run +# conditionally and self-skips when the matching podman/Wine container is +# absent. # -# Skip semantics: prints `SKIP: ...` and exits 0 when the llvm-mingw -# UCRT sysroot is not discoverable. - -set -euo pipefail +# Serial by design (mode P): all programs share one $work sandbox. +# +# Self-skip: prints SKIP and exits 0 when no llvm-mingw UCRT sysroot is found. +set -u ROOT=${CFREE_TEST_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)} CFREE=${CFREE:-"$ROOT/build/cfree"} SDK=${CFREE_MINGW_SYSROOT:-} +CF_KIT_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cfree_sh_kit.sh" +cf_report_init + +LABEL_SUITE=windows-system-dlls-smoke + find_sdk() { local arch=$1 local d @@ -63,24 +69,26 @@ sdk_for_arch() { find_sdk "$arch" } +# cfree presence: was a hard FAIL+exit 1 in the original. Preserve that verdict. if [ ! -x "$CFREE" ]; then - echo "FAIL windows-system-dlls-smoke: cfree binary not found: $CFREE" >&2 - exit 1 + cf_fail "$LABEL_SUITE/cfree-present" "cfree binary not found: $CFREE" + cf_summary "$LABEL_SUITE" + cf_exit fi TMP=${TMPDIR:-/tmp} -WORK=$(mktemp -d "$TMP/cfree-windows-system-dlls-smoke.XXXXXX") -WORK_REAL=$(cd "$WORK" && pwd -P) -trap 'rm -rf "$WORK"' EXIT - -GUI_C=$WORK/gui_hello_window.c -GDI_C=$WORK/gdi_drawing.c -REG_C=$WORK/advapi32_registry.c -WS_C=$WORK/ws2_32_socket.c -OLE_C=$WORK/ole32_coinit.c -SHELL_C=$WORK/shell32_argv.c -COMCTL_C=$WORK/comctl32_init.c -MIXED_C=$WORK/mixed_ucrt.c +work=$(mktemp -d "$TMP/cfree-windows-system-dlls-smoke.XXXXXX") +WORK_REAL=$(cd "$work" && pwd -P) +trap 'rm -rf "$work"' EXIT + +GUI_C=$work/gui_hello_window.c +GDI_C=$work/gdi_drawing.c +REG_C=$work/advapi32_registry.c +WS_C=$work/ws2_32_socket.c +OLE_C=$work/ole32_coinit.c +SHELL_C=$work/shell32_argv.c +COMCTL_C=$work/comctl32_init.c +MIXED_C=$work/mixed_ucrt.c # A hidden window + minimal message-pump program. Wine in a headless # container may legitimately refuse to create a real window; the @@ -244,61 +252,55 @@ int main(void) { } SRC -check_no_legacy_crt_imports() { - local dump=$1 - local what=$2 +# ---- assert helpers (mode-P verbs, $work-confined) ------------------------- + +no_legacy_crt_imports() { + local name=$1 dump=$2 if grep -Eiq 'DLL Name: (msvcrt|ucrt)\.dll' "$dump"; then - echo "FAIL windows-system-dlls-smoke: $what imports legacy CRT DLL" >&2 - grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" >&2 - exit 1 + grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + else + ok "$name" fi } run_wine_if_available() { - local label=$1 - local image=$2 - local pod_arch=$3 - local exe=$4 + local label=$1 image=$2 pod_arch=$3 exe=$4 shift 4 - if ! command -v podman >/dev/null 2>&1; then - echo "SKIP windows-system-dlls-smoke: podman unavailable for $label Wine run" + skip_test "$label-wine" "podman unavailable" return 0 fi if ! podman image exists "$image" >/dev/null 2>&1; then - echo "SKIP windows-system-dlls-smoke: $image unavailable for $label Wine run" + skip_test "$label-wine" "$image unavailable" return 0 fi - - podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \ - bash -lc " - export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix - timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $* - rc=\$? - echo \"$label exit=\$rc\" - test \"\$rc\" -eq 0 - " + if podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \ + bash -lc " + export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix + timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $* + rc=\$? + echo \"$label exit=\$rc\" + test \"\$rc\" -eq 0 + " > "$work/$label-wine.out" 2> "$work/$label-wine.err"; then + ok "$label-wine" + else + not_ok "$label-wine" "$work/$label-wine.err" + fi } # build_and_check <label> <c-source> <exe> <dump> <link-mode> <libs> # <expected-dll-1> [<expected-dll-2> ...] -- [<expected-sym-1> ...] # -# link-mode is "console" or "windows" (drives -mconsole vs -mwindows). -# libs is a space-separated list of `-l<name>` archives to add (e.g. -# "gdi32 ws2_32") beyond the driver-auto-linked set -# (kernel32/user32/advapi32/shell32/msvcrt/mingwex/mingw32/moldname). +# link-mode is "console" or "windows" (drives -mconsole vs -mwindows). libs is +# a space-separated list of `-l<name>` archives to add (e.g. "gdi32 ws2_32") +# beyond the driver-auto-linked set (kernel32/user32/advapi32/shell32/msvcrt/ +# mingwex/mingw32/moldname). Each expected DLL/symbol is one mode-P assert. build_and_check() { - local label=$1 - local csrc=$2 - local exe=$3 - local dump=$4 - local mode=$5 - local libs=$6 + local label=$1 csrc=$2 exe=$3 dump=$4 mode=$5 libs=$6 shift 6 - local dlls=() - local syms=() - local in_syms=0 + local dlls=() syms=() in_syms=0 while [ $# -gt 0 ]; do if [ "$1" = "--" ]; then in_syms=1; shift; continue; fi if [ "$in_syms" -eq 0 ]; then dlls+=("$1"); else syms+=("$1"); fi @@ -308,36 +310,32 @@ build_and_check() { local mode_flag=-mconsole if [ "$mode" = "windows" ]; then mode_flag=-mwindows; fi - local extra_lflags=() - local lib + local extra_lflags=() lib for lib in $libs; do extra_lflags+=("-l$lib"); done - "$CFREE" cc -target "$TARGET" --sysroot "$ARCH_SDK" "$mode_flag" \ - "$csrc" "${extra_lflags[@]}" -o "$exe" - "$CFREE" objdump -p "$exe" >"$dump" - check_no_legacy_crt_imports "$dump" "$label PE" + local bn; bn=$(printf '%s' "$label" | tr ' ' '-') + + run_ok "$bn-build" "$CFREE" cc -target "$TARGET" --sysroot "$ARCH_SDK" \ + "$mode_flag" "$csrc" "${extra_lflags[@]}" -o "$exe" + if [ ! -f "$exe" ]; then return 1; fi + run_ok "$bn-objdump" "$CFREE" objdump -p "$exe" + if [ ! -s "$work/$bn-objdump.out" ]; then return 1; fi + cp "$work/$bn-objdump.out" "$dump" + + no_legacy_crt_imports "$bn-no-legacy-crt" "$dump" local d for d in "${dlls[@]}"; do - if ! grep -Fq "DLL Name: $d" "$dump"; then - echo "FAIL windows-system-dlls-smoke: $label: expected import of $d" >&2 - grep -F 'DLL Name:' "$dump" >&2 || true - exit 1 - fi + contains "$bn-dll-$d" "$dump" "DLL Name: $d" done local s for s in "${syms[@]}"; do - if ! grep -Fq "Name: $s" "$dump"; then - echo "FAIL windows-system-dlls-smoke: $label: expected import symbol $s" >&2 - exit 1 - fi + contains "$bn-sym-$s" "$dump" "Name: $s" done - if [ "$mode" = "windows" ] && - ! grep -Fq "Subsystem: 2 (WINDOWS_GUI)" "$dump"; then - echo "FAIL windows-system-dlls-smoke: $label: subsystem != WINDOWS_GUI" >&2 - exit 1 + if [ "$mode" = "windows" ]; then + contains "$bn-subsystem-gui" "$dump" "Subsystem: 2 (WINDOWS_GUI)" fi } @@ -359,86 +357,83 @@ for arch in x86_64 aarch64; do esac if ! ARCH_SDK=$(sdk_for_arch "$arch"); then - echo "SKIP windows-system-dlls-smoke: no $arch llvm-mingw UCRT sysroot" + skip_test "$LABEL_SUITE/$LABEL-sysroot" "no $arch llvm-mingw UCRT sysroot" continue fi + # Discovered-but-invalid sysroot was a SKIP (exit 0 via continue) originally. if [ ! -r "$ARCH_SDK/include/windows.h" ] || [ ! -r "$ARCH_SDK/lib/libucrt.a" ]; then - echo "SKIP windows-system-dlls-smoke: invalid UCRT llvm-mingw sysroot: $ARCH_SDK" + skip_test "$LABEL_SUITE/$LABEL-sysroot" "invalid UCRT llvm-mingw sysroot: $ARCH_SDK" continue fi ran=1 # ---- GUI hello window (user32 + gdi32 + kernel32) ---- - GUI_EXE=$WORK/gui_hello_window-$LABEL.exe - GUI_DUMP=$WORK/gui_hello_window-$LABEL.dump + GUI_EXE=$work/gui_hello_window-$LABEL.exe + GUI_DUMP=$work/gui_hello_window-$LABEL.dump build_and_check "$LABEL gui_hello_window" "$GUI_C" "$GUI_EXE" "$GUI_DUMP" \ windows "" USER32.dll KERNEL32.dll -- RegisterClassExW CreateWindowExW \ DefWindowProcW PeekMessageW DispatchMessageW - run_wine_if_available "$LABEL gui_hello_window" "$IMAGE" "$POD_ARCH" \ - "$GUI_EXE" + run_wine_if_available "$LABEL gui_hello_window" "$IMAGE" "$POD_ARCH" "$GUI_EXE" # ---- gdi32 surface ---- - GDI_EXE=$WORK/gdi_drawing-$LABEL.exe - GDI_DUMP=$WORK/gdi_drawing-$LABEL.dump + GDI_EXE=$work/gdi_drawing-$LABEL.exe + GDI_DUMP=$work/gdi_drawing-$LABEL.dump build_and_check "$LABEL gdi_drawing" "$GDI_C" "$GDI_EXE" "$GDI_DUMP" \ console gdi32 GDI32.dll USER32.dll -- CreateCompatibleDC GetStockObject \ SelectObject DeleteDC run_wine_if_available "$LABEL gdi_drawing" "$IMAGE" "$POD_ARCH" "$GDI_EXE" # ---- advapi32 surface ---- - REG_EXE=$WORK/advapi32_registry-$LABEL.exe - REG_DUMP=$WORK/advapi32_registry-$LABEL.dump + REG_EXE=$work/advapi32_registry-$LABEL.exe + REG_DUMP=$work/advapi32_registry-$LABEL.dump build_and_check "$LABEL advapi32_registry" "$REG_C" "$REG_EXE" "$REG_DUMP" \ console "" ADVAPI32.dll KERNEL32.dll -- RegOpenKeyExW RegCloseKey \ RegQueryInfoKeyW - run_wine_if_available "$LABEL advapi32_registry" "$IMAGE" "$POD_ARCH" \ - "$REG_EXE" + run_wine_if_available "$LABEL advapi32_registry" "$IMAGE" "$POD_ARCH" "$REG_EXE" # ---- ws2_32 surface ---- - WS_EXE=$WORK/ws2_32_socket-$LABEL.exe - WS_DUMP=$WORK/ws2_32_socket-$LABEL.dump + WS_EXE=$work/ws2_32_socket-$LABEL.exe + WS_DUMP=$work/ws2_32_socket-$LABEL.dump build_and_check "$LABEL ws2_32_socket" "$WS_C" "$WS_EXE" "$WS_DUMP" \ console ws2_32 WS2_32.dll KERNEL32.dll -- WSAStartup WSACleanup socket \ closesocket run_wine_if_available "$LABEL ws2_32_socket" "$IMAGE" "$POD_ARCH" "$WS_EXE" # ---- ole32 surface ---- - OLE_EXE=$WORK/ole32_coinit-$LABEL.exe - OLE_DUMP=$WORK/ole32_coinit-$LABEL.dump + OLE_EXE=$work/ole32_coinit-$LABEL.exe + OLE_DUMP=$work/ole32_coinit-$LABEL.dump build_and_check "$LABEL ole32_coinit" "$OLE_C" "$OLE_EXE" "$OLE_DUMP" \ console ole32 ole32.dll KERNEL32.dll -- CoInitializeEx CoUninitialize run_wine_if_available "$LABEL ole32_coinit" "$IMAGE" "$POD_ARCH" "$OLE_EXE" # ---- shell32 surface ---- - SHELL_EXE=$WORK/shell32_argv-$LABEL.exe - SHELL_DUMP=$WORK/shell32_argv-$LABEL.dump + SHELL_EXE=$work/shell32_argv-$LABEL.exe + SHELL_DUMP=$work/shell32_argv-$LABEL.dump build_and_check "$LABEL shell32_argv" "$SHELL_C" "$SHELL_EXE" "$SHELL_DUMP" \ console "" SHELL32.dll KERNEL32.dll -- CommandLineToArgvW - run_wine_if_available "$LABEL shell32_argv" "$IMAGE" "$POD_ARCH" \ - "$SHELL_EXE" + run_wine_if_available "$LABEL shell32_argv" "$IMAGE" "$POD_ARCH" "$SHELL_EXE" # ---- comctl32 surface ---- - COMCTL_EXE=$WORK/comctl32_init-$LABEL.exe - COMCTL_DUMP=$WORK/comctl32_init-$LABEL.dump + COMCTL_EXE=$work/comctl32_init-$LABEL.exe + COMCTL_DUMP=$work/comctl32_init-$LABEL.dump build_and_check "$LABEL comctl32_init" "$COMCTL_C" "$COMCTL_EXE" \ "$COMCTL_DUMP" console comctl32 COMCTL32.dll KERNEL32.dll -- \ InitCommonControls InitCommonControlsEx - run_wine_if_available "$LABEL comctl32_init" "$IMAGE" "$POD_ARCH" \ - "$COMCTL_EXE" + run_wine_if_available "$LABEL comctl32_init" "$IMAGE" "$POD_ARCH" "$COMCTL_EXE" # ---- mixed libucrt.a (short-import + long-form helper) ---- - MIXED_EXE=$WORK/mixed_ucrt-$LABEL.exe - MIXED_DUMP=$WORK/mixed_ucrt-$LABEL.dump + MIXED_EXE=$work/mixed_ucrt-$LABEL.exe + MIXED_DUMP=$work/mixed_ucrt-$LABEL.dump build_and_check "$LABEL mixed_ucrt" "$MIXED_C" "$MIXED_EXE" "$MIXED_DUMP" \ console "" KERNEL32.dll api-ms-win-crt-stdio-l1-1-0.dll -- fflush run_wine_if_available "$LABEL mixed_ucrt" "$IMAGE" "$POD_ARCH" "$MIXED_EXE" done if [ "$ran" -eq 0 ]; then - echo "SKIP windows-system-dlls-smoke: set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*" - exit 0 + skip_test "$LABEL_SUITE" "set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*" fi -echo "PASS windows-system-dlls-smoke: user32/gdi32, advapi32, ws2_32, ole32, shell32, comctl32, mixed UCRT for x64/aarch64" +cf_summary "$LABEL_SUITE" +cf_exit diff --git a/test/coff/windows-ucrt-hosted-smoke.sh b/test/coff/windows-ucrt-hosted-smoke.sh @@ -1,10 +1,29 @@ #!/usr/bin/env bash -set -euo pipefail +# test/coff/windows-ucrt-hosted-smoke.sh — Type-K (mode P) hosted Windows UCRT +# smoke, on the shared scripted-shell kit (test/lib/cfree_sh_kit.sh). +# +# One program per UCRT surface (Sleep, windows.h coverage, runtime, UCRT stdio, +# imported-data, TLS, GUI WinMain) is built with `cfree cc` for both +# x86_64-windows and aarch64-windows; the link-level checks inspect the PE +# import directory via `cfree objdump -p` (run_ok + contains), and a negative +# check asserts no legacy CRT DLL is imported directly. The Wine runtime check +# is run conditionally and self-skips when podman/Wine are absent. +# +# Serial by design (mode P): all programs share one $work sandbox. +# +# Self-skip: prints SKIP and exits 0 when no llvm-mingw UCRT sysroot is found. +set -u ROOT=${CFREE_TEST_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)} CFREE=${CFREE:-"$ROOT/build/cfree"} SDK=${CFREE_MINGW_SYSROOT:-} +CF_KIT_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cfree_sh_kit.sh" +cf_report_init + +LABEL_SUITE=windows-ucrt-hosted-smoke + find_sdk() { local arch=$1 local d @@ -39,23 +58,25 @@ sdk_for_arch() { find_sdk "$arch" } +# cfree presence: was a hard FAIL+exit 1 in the original. Preserve that verdict. if [ ! -x "$CFREE" ]; then - echo "FAIL windows-ucrt-hosted-smoke: cfree binary not found: $CFREE" >&2 - exit 1 + cf_fail "$LABEL_SUITE/cfree-present" "cfree binary not found: $CFREE" + cf_summary "$LABEL_SUITE" + cf_exit fi TMP=${TMPDIR:-/tmp} -WORK=$(mktemp -d "$TMP/cfree-windows-ucrt-smoke.XXXXXX") -WORK_REAL=$(cd "$WORK" && pwd -P) -trap 'rm -rf "$WORK"' EXIT - -CONSOLE_C=$WORK/windows-h.c -HEADER_C=$WORK/windows-h-coverage.c -RUNTIME_C=$WORK/runtime.c -STDIO_C=$WORK/stdio.c -IMPORTDATA_C=$WORK/import-data.c -GUI_C=$WORK/gui.c -TLS_C=$WORK/tls.c +work=$(mktemp -d "$TMP/cfree-windows-ucrt-smoke.XXXXXX") +WORK_REAL=$(cd "$work" && pwd -P) +trap 'rm -rf "$work"' EXIT + +CONSOLE_C=$work/windows-h.c +HEADER_C=$work/windows-h-coverage.c +RUNTIME_C=$work/runtime.c +STDIO_C=$work/stdio.c +IMPORTDATA_C=$work/import-data.c +GUI_C=$work/gui.c +TLS_C=$work/tls.c cat >"$CONSOLE_C" <<'SRC' #include <windows.h> @@ -297,40 +318,63 @@ int main(void) { } SRC -check_no_legacy_crt_imports() { - local dump=$1 - local what=$2 +# ---- assert helpers (mode-P verbs, $work-confined) ------------------------- + +# no_legacy_crt_imports NAME DUMP : pass iff DUMP imports no msvcrt/ucrt DLL. +no_legacy_crt_imports() { + local name=$1 dump=$2 if grep -Eiq 'DLL Name: (msvcrt|ucrt)\.dll' "$dump"; then - echo "FAIL windows-ucrt-hosted-smoke: $what imports legacy CRT DLL directly" >&2 - grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" >&2 - exit 1 + grep -Ei 'DLL Name: (msvcrt|ucrt)\.dll' "$dump" > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + else + ok "$name" fi } +# not_contains NAME FILE NEEDLE : pass iff NEEDLE is absent from FILE. +not_contains() { + local name=$1 file=$2 needle=$3 + if grep -Fq "$needle" "$file"; then + { printf 'unexpected text: %s\n' "$needle"; } > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + else + ok "$name" + fi +} + +# matches NAME FILE EREGEX : pass iff FILE has a line matching EREGEX. +matches() { + local name=$1 file=$2 re=$3 + if grep -Eq "$re" "$file"; then ok "$name" + else { printf 'no line matched: %s\n' "$re"; } > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi +} + +# run_wine_if_available LABEL IMAGE POD_ARCH EXE [ARGS...] : conditional Wine +# run. Self-skips (cf_skip) when podman or the container image is absent; +# otherwise pass iff the program exits 0 under Wine. run_wine_if_available() { - local label=$1 - local image=$2 - local pod_arch=$3 - local exe=$4 + local label=$1 image=$2 pod_arch=$3 exe=$4 shift 4 - if ! command -v podman >/dev/null 2>&1; then - echo "SKIP windows-ucrt-hosted-smoke: podman unavailable for $label Wine run" + skip_test "$label-wine" "podman unavailable" return 0 fi if ! podman image exists "$image" >/dev/null 2>&1; then - echo "SKIP windows-ucrt-hosted-smoke: $image unavailable for $label Wine run" + skip_test "$label-wine" "$image unavailable" return 0 fi - - podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \ - bash -lc " - export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix CFREE_WIN_PROBE=present - timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $* - rc=\$? - echo \"$label exit=\$rc\" - test \"\$rc\" -eq 0 - " + if podman run --rm --arch "$pod_arch" -v "$WORK_REAL:/probe:ro" "$image" \ + bash -lc " + export WINEDEBUG=-all WINEPREFIX=/tmp/wineprefix CFREE_WIN_PROBE=present + timeout 120s /usr/lib/wine/wine64 /probe/$(basename "$exe") $* + rc=\$? + echo \"$label exit=\$rc\" + test \"\$rc\" -eq 0 + " > "$work/$label-wine.out" 2> "$work/$label-wine.err"; then + ok "$label-wine" + else + not_ok "$label-wine" "$work/$label-wine.err" + fi } ran=0 @@ -351,100 +395,111 @@ for arch in x86_64 aarch64; do esac if ! ARCH_SDK=$(sdk_for_arch "$arch"); then - echo "SKIP windows-ucrt-hosted-smoke: no $arch llvm-mingw UCRT sysroot" + skip_test "$LABEL_SUITE/$label-sysroot" "no $arch llvm-mingw UCRT sysroot" continue fi + # Discovered-but-invalid sysroot was a hard FAIL+exit 1 originally. if [ ! -r "$ARCH_SDK/include/windows.h" ] || [ ! -r "$ARCH_SDK/lib/libmsvcrt.a" ]; then - echo "FAIL windows-ucrt-hosted-smoke: invalid UCRT llvm-mingw sysroot: $ARCH_SDK" >&2 - exit 1 + echo "invalid UCRT llvm-mingw sysroot: $ARCH_SDK" > "$work/$label-sysroot.diag" + not_ok "$LABEL_SUITE/$label-sysroot" "$work/$label-sysroot.diag" + cf_summary "$LABEL_SUITE" + cf_exit fi ran=1 - CONSOLE_EXE=$WORK/windows-h-$arch.exe - CONSOLE_DUMP=$WORK/windows-h-$arch.dump - HEADER_EXE=$WORK/windows-h-coverage-$arch.exe - HEADER_DUMP=$WORK/windows-h-coverage-$arch.dump - RUNTIME_EXE=$WORK/runtime-$arch.exe - RUNTIME_DUMP=$WORK/runtime-$arch.dump - STDIO_EXE=$WORK/stdio-$arch.exe - STDIO_DUMP=$WORK/stdio-$arch.dump - IMPORTDATA_EXE=$WORK/import-data-$arch.exe - IMPORTDATA_DUMP=$WORK/import-data-$arch.dump - TLS_EXE=$WORK/tls-$arch.exe - TLS_DUMP=$WORK/tls-$arch.dump - GUI_EXE=$WORK/gui-$arch.exe - GUI_DUMP=$WORK/gui-$arch.dump - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mconsole \ - "$CONSOLE_C" -o "$CONSOLE_EXE" - "$CFREE" objdump -p "$CONSOLE_EXE" >"$CONSOLE_DUMP" - check_no_legacy_crt_imports "$CONSOLE_DUMP" "$label console PE" - if grep -Fq "Name: __set_app_type" "$CONSOLE_DUMP"; then - echo "FAIL windows-ucrt-hosted-smoke: weak alias leaked as __set_app_type import" >&2 - exit 1 + CONSOLE_EXE=$work/windows-h-$arch.exe + CONSOLE_DUMP=$work/windows-h-$arch.dump + HEADER_EXE=$work/windows-h-coverage-$arch.exe + HEADER_DUMP=$work/windows-h-coverage-$arch.dump + RUNTIME_EXE=$work/runtime-$arch.exe + RUNTIME_DUMP=$work/runtime-$arch.dump + STDIO_EXE=$work/stdio-$arch.exe + STDIO_DUMP=$work/stdio-$arch.dump + IMPORTDATA_EXE=$work/import-data-$arch.exe + IMPORTDATA_DUMP=$work/import-data-$arch.dump + TLS_EXE=$work/tls-$arch.exe + TLS_DUMP=$work/tls-$arch.dump + GUI_EXE=$work/gui-$arch.exe + GUI_DUMP=$work/gui-$arch.dump + + # build_dump NAME EXE DUMP -- CC_ARGS... : build EXE, then `objdump -p` it + # into DUMP. Returns 0 only if both the EXE and the DUMP were produced, so + # the per-program import asserts below run iff the link round-tripped. + build_dump() { + local bn=$1 exe=$2 dump=$3; shift 3 + [ "$1" = "--" ] && shift + run_ok "$bn-build" "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" "$@" -o "$exe" + [ -f "$exe" ] || return 1 + run_ok "$bn-objdump" "$CFREE" objdump -p "$exe" + [ -s "$work/$bn-objdump.out" ] || return 1 + cp "$work/$bn-objdump.out" "$dump" + } + + # ---- console Sleep ---- + if build_dump "$label-console" "$CONSOLE_EXE" "$CONSOLE_DUMP" -- -mconsole "$CONSOLE_C"; then + no_legacy_crt_imports "$label-console-no-legacy-crt" "$CONSOLE_DUMP" + not_contains "$label-console-no-weak-set-app-type" "$CONSOLE_DUMP" "Name: __set_app_type" + contains "$label-console-kernel32" "$CONSOLE_DUMP" "DLL Name: KERNEL32.dll" + contains "$label-console-sleep" "$CONSOLE_DUMP" "Name: Sleep" + contains "$label-console-crt-runtime-dll" "$CONSOLE_DUMP" "DLL Name: api-ms-win-crt-runtime-l1-1-0.dll" + contains "$label-console-set-app-type" "$CONSOLE_DUMP" "Name: _set_app_type" + run_wine_if_available "$label-console-Sleep" "$image" "$pod_arch" "$CONSOLE_EXE" fi - grep -Fq "DLL Name: KERNEL32.dll" "$CONSOLE_DUMP" - grep -Fq "Name: Sleep" "$CONSOLE_DUMP" - grep -Fq "DLL Name: api-ms-win-crt-runtime-l1-1-0.dll" "$CONSOLE_DUMP" - grep -Fq "Name: _set_app_type" "$CONSOLE_DUMP" - run_wine_if_available "$label Sleep" "$image" "$pod_arch" "$CONSOLE_EXE" - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mconsole \ - "$HEADER_C" -o "$HEADER_EXE" - "$CFREE" objdump -p "$HEADER_EXE" >"$HEADER_DUMP" - check_no_legacy_crt_imports "$HEADER_DUMP" "$label windows.h coverage PE" - grep -Fq "Name: CreateFileW" "$HEADER_DUMP" - grep -Fq "Name: CreateThread" "$HEADER_DUMP" - grep -Fq "Name: WaitForSingleObject" "$HEADER_DUMP" - grep -Fq "Name: MessageBoxW" "$HEADER_DUMP" - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \ - "$RUNTIME_C" -o "$RUNTIME_EXE" - "$CFREE" objdump -p "$RUNTIME_EXE" >"$RUNTIME_DUMP" - check_no_legacy_crt_imports "$RUNTIME_DUMP" "$label runtime PE" - grep -Fq "Name: HeapAlloc" "$RUNTIME_DUMP" - grep -Fq "Name: CreateFileA" "$RUNTIME_DUMP" - grep -Fq "Name: qsort" "$RUNTIME_DUMP" - run_wine_if_available "$label runtime" "$image" "$pod_arch" "$RUNTIME_EXE" \ - alpha beta - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \ - "$STDIO_C" -o "$STDIO_EXE" - "$CFREE" objdump -p "$STDIO_EXE" >"$STDIO_DUMP" - check_no_legacy_crt_imports "$STDIO_DUMP" "$label UCRT stdio PE" - grep -Fq "DLL Name: api-ms-win-crt-stdio-l1-1-0.dll" "$STDIO_DUMP" - grep -Fq "Name: fflush" "$STDIO_DUMP" - run_wine_if_available "$label UCRT stdio" "$image" "$pod_arch" "$STDIO_EXE" - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \ - "$IMPORTDATA_C" -o "$IMPORTDATA_EXE" - "$CFREE" objdump -p "$IMPORTDATA_EXE" >"$IMPORTDATA_DUMP" - check_no_legacy_crt_imports "$IMPORTDATA_DUMP" "$label imported-data PE" - grep -Fq "DLL Name: api-ms-win-crt-private-l1-1-0.dll" "$IMPORTDATA_DUMP" - grep -Fq "Name: __dcrt_initial_narrow_environment" "$IMPORTDATA_DUMP" - run_wine_if_available "$label imported-data" "$image" "$pod_arch" \ - "$IMPORTDATA_EXE" - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" \ - "$TLS_C" -o "$TLS_EXE" - "$CFREE" objdump -p "$TLS_EXE" >"$TLS_DUMP" - check_no_legacy_crt_imports "$TLS_DUMP" "$label TLS PE" - grep -Eq '^[[:space:]]*9[[:space:]]+TLS[[:space:]]+0x[0-9a-fA-F]+[[:space:]]+0x00000028' \ - "$TLS_DUMP" - - "$CFREE" cc -target "$target" --sysroot "$ARCH_SDK" -mwindows \ - "$GUI_C" -o "$GUI_EXE" - "$CFREE" objdump -p "$GUI_EXE" >"$GUI_DUMP" - grep -Fq "Subsystem: 2 (WINDOWS_GUI)" "$GUI_DUMP" - check_no_legacy_crt_imports "$GUI_DUMP" "$label GUI PE" - - run_wine_if_available "$label TLS" "$image" "$pod_arch" "$TLS_EXE" + + # ---- windows.h coverage ---- + if build_dump "$label-header" "$HEADER_EXE" "$HEADER_DUMP" -- -mconsole "$HEADER_C"; then + no_legacy_crt_imports "$label-header-no-legacy-crt" "$HEADER_DUMP" + contains "$label-header-CreateFileW" "$HEADER_DUMP" "Name: CreateFileW" + contains "$label-header-CreateThread" "$HEADER_DUMP" "Name: CreateThread" + contains "$label-header-WaitForSingleObject" "$HEADER_DUMP" "Name: WaitForSingleObject" + contains "$label-header-MessageBoxW" "$HEADER_DUMP" "Name: MessageBoxW" + fi + + # ---- runtime ---- + if build_dump "$label-runtime" "$RUNTIME_EXE" "$RUNTIME_DUMP" -- "$RUNTIME_C"; then + no_legacy_crt_imports "$label-runtime-no-legacy-crt" "$RUNTIME_DUMP" + contains "$label-runtime-HeapAlloc" "$RUNTIME_DUMP" "Name: HeapAlloc" + contains "$label-runtime-CreateFileA" "$RUNTIME_DUMP" "Name: CreateFileA" + contains "$label-runtime-qsort" "$RUNTIME_DUMP" "Name: qsort" + run_wine_if_available "$label-runtime" "$image" "$pod_arch" "$RUNTIME_EXE" alpha beta + fi + + # ---- UCRT stdio ---- + if build_dump "$label-stdio" "$STDIO_EXE" "$STDIO_DUMP" -- "$STDIO_C"; then + no_legacy_crt_imports "$label-stdio-no-legacy-crt" "$STDIO_DUMP" + contains "$label-stdio-crt-stdio-dll" "$STDIO_DUMP" "DLL Name: api-ms-win-crt-stdio-l1-1-0.dll" + contains "$label-stdio-fflush" "$STDIO_DUMP" "Name: fflush" + run_wine_if_available "$label-UCRT-stdio" "$image" "$pod_arch" "$STDIO_EXE" + fi + + # ---- imported-data ---- + if build_dump "$label-importdata" "$IMPORTDATA_EXE" "$IMPORTDATA_DUMP" -- "$IMPORTDATA_C"; then + no_legacy_crt_imports "$label-importdata-no-legacy-crt" "$IMPORTDATA_DUMP" + contains "$label-importdata-crt-private-dll" "$IMPORTDATA_DUMP" "DLL Name: api-ms-win-crt-private-l1-1-0.dll" + contains "$label-importdata-dcrt-env" "$IMPORTDATA_DUMP" "Name: __dcrt_initial_narrow_environment" + run_wine_if_available "$label-imported-data" "$image" "$pod_arch" "$IMPORTDATA_EXE" + fi + + # ---- TLS ---- + if build_dump "$label-tls" "$TLS_EXE" "$TLS_DUMP" -- "$TLS_C"; then + no_legacy_crt_imports "$label-tls-no-legacy-crt" "$TLS_DUMP" + matches "$label-tls-directory" "$TLS_DUMP" \ + '^[[:space:]]*9[[:space:]]+TLS[[:space:]]+0x[0-9a-fA-F]+[[:space:]]+0x00000028' + fi + + # ---- GUI WinMain ---- + if build_dump "$label-gui" "$GUI_EXE" "$GUI_DUMP" -- -mwindows "$GUI_C"; then + contains "$label-gui-subsystem" "$GUI_DUMP" "Subsystem: 2 (WINDOWS_GUI)" + no_legacy_crt_imports "$label-gui-no-legacy-crt" "$GUI_DUMP" + fi + + run_wine_if_available "$label-TLS" "$image" "$pod_arch" "$TLS_EXE" done if [ "$ran" -eq 0 ]; then - echo "SKIP windows-ucrt-hosted-smoke: set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*" - exit 0 + skip_test "$LABEL_SUITE" "set CFREE_MINGW_SYSROOT or install llvm-mingw UCRT under /tmp/llvm-mingw*" fi -echo "PASS windows-ucrt-hosted-smoke: Sleep, windows.h, runtime, UCRT stdio/imports/imported-data, GUI, and TLS for x64/aarch64" +cf_summary "$LABEL_SUITE" +cf_exit diff --git a/test/dbg/run.sh b/test/dbg/run.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Scripted `cfree dbg` transcript tests. +# Scripted `cfree dbg` transcript tests — Type K, mode G (golden transcript). # # Each case lives in test/dbg/cases/<name>/ and may contain: # args one cfree-dbg argument per line; @CASE@ expands to the case dir @@ -8,8 +8,15 @@ # stderr optional exact stderr golden; absent means stderr must be empty # xfail optional reason; expected failure until the feature is implemented # +# dbg is mode G, but cf_scenario_case (the shared mode-G oracle) does not cover +# dbg's needs: per-case args/stdin templating, an stdin script piped to the +# REPL, a multi-stage stdout normalizer, separate stderr handling, and xfail/ +# xpass. So the per-case oracle (dbg_case) and the normalizer live lane-local +# here, while results, summary, and exit go through the shared Type-K kit. +# # The stdout normalizer removes interactive prompts so goldens focus on -# debugger-visible events. It intentionally leaves generation numbers and +# debugger-visible events, masks run-to-run addresses, and re-tokenizes the +# case dir back to @CASE@. It intentionally leaves generation numbers and # command output intact. set -u @@ -18,14 +25,15 @@ script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" + CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree dbg -if [ ! -x "$CFREE" ]; then - echo "dbg: cfree binary not found at $CFREE" >&2 - exit 2 -fi - +# dbg drives a JIT debugger that single-steps native code; only hosts whose +# native arch matches the JIT target can run it. (Preserved host gate.) host_arch=$(uname -m 2>/dev/null || true) host_os=$(uname -s 2>/dev/null || true) case "$host_os:$host_arch" in @@ -36,16 +44,18 @@ case "$host_os:$host_arch" in ;; esac -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-dbg-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT +CF_WORK=$(cf_workdir dbg) -pass=0 -fail=0 -xfail=0 -xpass=0 -skip=0 -failures= +# DBG_STRICT_XFAIL drives the report layer's strict-xfail gate: an xfail case +# that unexpectedly passes becomes a real failure (stale marker) under strict. +CF_STRICT_XFAIL=${DBG_STRICT_XFAIL:-0} +cf_report_init + +# normalize_stdout RAW CASE_DIR : multi-stage stdout normalizer (lane-local). +# Strips the "(cfree) " prompt and continuation markers, re-tokenizes CASE_DIR +# to @CASE@, masks run-to-run hex addresses to 0xADDR, trims trailing +# whitespace, and elides blank lines. Idempotent on already-normalized goldens. normalize_stdout() { sed -e "s|$2|@CASE@|g" \ -e 's/(cfree) //g' -e 's/^ > //' -e 's/^expr > //' "$1" | @@ -54,6 +64,8 @@ normalize_stdout() { sed '/^$/d' } +# case_selected NAME : honor the DBG_CASE comma-separated glob filter. With no +# filter, every case runs. (Preserved selection knob.) case_selected() { [ -n "${DBG_CASE:-}" ] || return 0 for pat in $(printf '%s' "$DBG_CASE" | tr ',' ' '); do @@ -64,141 +76,138 @@ case_selected() { return 1 } -for case_dir in "$cases_dir"/*; do - [ -d "$case_dir" ] || continue - name=$(basename "$case_dir") - if ! case_selected "$name"; then - skip=$((skip + 1)) - continue - fi - stdin_file="$case_dir/stdin" - expected="$case_dir/expected" - expected_stderr="$case_dir/stderr" - xfail_file="$case_dir/xfail" - raw_stdout="$work_root/$name.stdout.raw" - actual_stdout="$work_root/$name.stdout" - raw_stderr="$work_root/$name.stderr" - actual_stderr="$work_root/$name.stderr.actual" - case_stdin="$work_root/$name.stdin" - is_xfail=0 - reason= - ok=1 - why= - - if [ -e "$xfail_file" ]; then - is_xfail=1 - reason=$(sed -n '1p' "$xfail_file") +# dbg_case NAME CASE_DIR : run one transcript case and record exactly one +# verdict via the shared cf_* verbs (cf_pass/cf_fail/cf_xfail/cf_xpass). +dbg_case() { + dc_name=$1 + dc_dir=$2 + dc_stdin_file="$dc_dir/stdin" + dc_expected="$dc_dir/expected" + dc_expected_stderr="$dc_dir/stderr" + dc_xfail_file="$dc_dir/xfail" + dc_raw_stdout="$CF_WORK/$dc_name.stdout.raw" + dc_actual_stdout="$CF_WORK/$dc_name.stdout" + dc_expected_norm="$CF_WORK/$dc_name.expected" + dc_raw_stderr="$CF_WORK/$dc_name.stderr" + dc_actual_stderr="$CF_WORK/$dc_name.stderr.actual" + dc_case_stdin="$CF_WORK/$dc_name.stdin" + dc_is_xfail=0 + dc_reason= + dc_ok=1 + dc_why= + + if [ -e "$dc_xfail_file" ]; then + dc_is_xfail=1 + dc_reason=$(sed -n '1p' "$dc_xfail_file") fi - if [ ! -e "$stdin_file" ]; then - ok=0 - why="missing stdin" - elif [ ! -e "$expected" ]; then - ok=0 - why="missing expected" + # Missing required inputs: a setup failure, routed through xfail when the + # case is marked (and not strict), else a plain failure. + if [ ! -e "$dc_stdin_file" ]; then + dc_ok=0 + dc_why="missing stdin" + elif [ ! -e "$dc_expected" ]; then + dc_ok=0 + dc_why="missing expected" fi - - if [ "$ok" -eq 0 ]; then - if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then - printf 'XFAIL %s (%s)\n' "$name" "$why" - xfail=$((xfail + 1)) + if [ "$dc_ok" -eq 0 ]; then + if [ "$dc_is_xfail" -eq 1 ]; then + cf_xfail "$dc_name" "$dc_why" else - printf 'FAIL %s (%s)\n' "$name" "$why" - fail=$((fail + 1)) - failures="$failures $name" + cf_fail "$dc_name" "$dc_why" fi - continue + return fi + # Build the dbg argv, templating @CASE@ -> case dir; skip blanks/#comments. set -- - args_file="$case_dir/args" - if [ -e "$args_file" ]; then - while IFS= read -r arg || [ -n "$arg" ]; do - [ -n "$arg" ] || continue - case "$arg" in + dc_args_file="$dc_dir/args" + if [ -e "$dc_args_file" ]; then + while IFS= read -r dc_arg || [ -n "$dc_arg" ]; do + [ -n "$dc_arg" ] || continue + case "$dc_arg" in \#*) continue ;; esac - arg=$(printf '%s' "$arg" | sed "s|@CASE@|$case_dir|g") - set -- "$@" "$arg" - done < "$args_file" + dc_arg=$(printf '%s' "$dc_arg" | sed "s|@CASE@|$dc_dir|g") + set -- "$@" "$dc_arg" + done < "$dc_args_file" fi - sed "s|@CASE@|$case_dir|g" "$stdin_file" > "$case_stdin" - "$CFREE" dbg "$@" < "$case_stdin" > "$raw_stdout" 2> "$raw_stderr" - rc=$? - normalize_stdout "$raw_stdout" "$case_dir" > "$actual_stdout" - if [ -e "$expected_stderr" ]; then - cp "$raw_stderr" "$actual_stderr" + sed "s|@CASE@|$dc_dir|g" "$dc_stdin_file" > "$dc_case_stdin" + "$CFREE" dbg "$@" < "$dc_case_stdin" > "$dc_raw_stdout" 2> "$dc_raw_stderr" + dc_rc=$? + + # Normalize BOTH actual and the (already-normalized, so idempotent) golden. + normalize_stdout "$dc_raw_stdout" "$dc_dir" > "$dc_actual_stdout" + normalize_stdout "$dc_expected" "$dc_dir" > "$dc_expected_norm" + if [ -e "$dc_expected_stderr" ]; then + cp "$dc_raw_stderr" "$dc_actual_stderr" else - : > "$actual_stderr" + : > "$dc_actual_stderr" fi - if [ "$rc" -ne 0 ]; then - ok=0 - why="cfree dbg exit=$rc" - elif ! diff -u "$expected" "$actual_stdout" >/dev/null 2>&1; then - ok=0 - why="stdout" - elif [ -e "$expected_stderr" ]; then - if ! diff -u "$expected_stderr" "$actual_stderr" >/dev/null 2>&1; then - ok=0 - why="stderr" + if [ "$dc_rc" -ne 0 ]; then + dc_ok=0 + dc_why="cfree dbg exit=$dc_rc" + elif ! diff -u "$dc_expected_norm" "$dc_actual_stdout" >/dev/null 2>&1; then + dc_ok=0 + dc_why="stdout" + elif [ -e "$dc_expected_stderr" ]; then + if ! diff -u "$dc_expected_stderr" "$dc_actual_stderr" >/dev/null 2>&1; then + dc_ok=0 + dc_why="stderr" fi - elif [ -s "$raw_stderr" ]; then - ok=0 - why="unexpected stderr" + elif [ -s "$dc_raw_stderr" ]; then + dc_ok=0 + dc_why="unexpected stderr" fi - if [ "$ok" -eq 1 ]; then - if [ "$is_xfail" -eq 1 ]; then - printf 'XPASS %s' "$name" - [ -n "$reason" ] && printf ' (%s)' "$reason" - printf '\n' - xpass=$((xpass + 1)) - fail=$((fail + 1)) - failures="$failures $name" + if [ "$dc_ok" -eq 1 ]; then + if [ "$dc_is_xfail" -eq 1 ]; then + # Marked xfail but passed (a stale xfail marker): cf_xpass is ALWAYS + # a failure in the report layer — matching the original's + # unconditional xpass-counts-as-fail behavior. + cf_xpass "$dc_name" else - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) + cf_pass "$dc_name" fi - continue + return fi - if [ "$is_xfail" -eq 1 ] && [ "${DBG_STRICT_XFAIL:-0}" != 1 ]; then - printf 'XFAIL %s (%s' "$name" "$why" - [ -n "$reason" ] && printf ': %s' "$reason" - printf ')\n' - xfail=$((xfail + 1)) - continue + if [ "$dc_is_xfail" -eq 1 ]; then + if [ -n "$dc_reason" ]; then + cf_xfail "$dc_name" "$dc_why: $dc_reason" + else + cf_xfail "$dc_name" "$dc_why" + fi + return fi - printf 'FAIL %s (%s)\n' "$name" "$why" - case "$why" in + cf_fail "$dc_name" "$dc_why" + case "$dc_why" in stdout) - diff -u "$expected" "$actual_stdout" || true - cp "$actual_stdout" "$case_dir/actual" 2>/dev/null || true + diff -u "$dc_expected_norm" "$dc_actual_stdout" || true + cp "$dc_actual_stdout" "$dc_dir/actual" 2>/dev/null || true ;; stderr) - diff -u "$expected_stderr" "$actual_stderr" || true + diff -u "$dc_expected_stderr" "$dc_actual_stderr" || true ;; "unexpected stderr") - sed 's/^/ | /' "$raw_stderr" + sed 's/^/ | /' "$dc_raw_stderr" ;; cfree\ dbg\ exit=*) - sed 's/^/ stdout| /' "$raw_stdout" - sed 's/^/ stderr| /' "$raw_stderr" + sed 's/^/ stdout| /' "$dc_raw_stdout" + sed 's/^/ stderr| /' "$dc_raw_stderr" ;; esac - fail=$((fail + 1)) - failures="$failures $name" +} + +for case_dir in "$cases_dir"/*; do + [ -d "$case_dir" ] || continue + name=$(basename "$case_dir") + case_selected "$name" || continue + dbg_case "$name" "$case_dir" done -total=$((pass + fail + xfail + xpass)) -if [ "$fail" -gt 0 ]; then - printf '\ndbg: failures:%s\n' "$failures" - printf 'dbg: %d/%d passed, %d xfailed, %d xpassed, %d skipped\n' \ - "$pass" "$total" "$xfail" "$xpass" "$skip" - exit 1 -fi -printf '\ndbg: %d/%d passed, %d xfailed, %d skipped\n' \ - "$pass" "$total" "$xfail" "$skip" +cf_summary dbg +cf_exit diff --git a/test/debug/cfi_unit.c b/test/debug/cfi_unit.c @@ -20,52 +20,14 @@ #include "core/core.h" #include "core/pool.h" #include "debug/dwarf_defs.h" +#include "lib/cfree_unit.h" #include "obj/obj.h" -/* ---- env ---- */ - -static void* heap_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* heap_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void heap_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "[%s] ", - k == CFREE_DIAG_ERROR ? "error" - : k == CFREE_DIAG_WARN ? "warning" - : "note"); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0}; -static CfreeContext g_ctx = {.heap = &g_heap, .diag = &g_sink, .now = -1}; - -static int g_fail = 0; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - g_fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } \ - } while (0) +/* One shared test context replaces the per-file heap/diag/counter globals. + * EXPECT is aliased to CU_EXPECT so the call sites below are unchanged. The + * original ctx used now = -1, replicated in main() after cfree_unit_init. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static const Section* sec_by_name(const ObjBuilder* ob, Pool* pool, const char* name) { @@ -140,16 +102,11 @@ static void check_arch(const CfiExpect* ex) { u32 size; u32 off; - memset(&t, 0, sizeof(t)); - t.arch = ex->arch; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; + t = cfree_unit_target(ex->arch, CFREE_OS_LINUX, CFREE_OBJ_ELF); - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "[%s] compiler_new failed\n", ex->tag); - g_fail++; + g_u.fails++; return; } ob = obj_new(c); @@ -326,6 +283,9 @@ cleanup: } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; /* preserve the original ctx.now */ + /* aa64: RA=x30 (DWARF 30), code_align=4, data_align=-8, CFA init = sp. */ { CfiExpect ex = { @@ -377,8 +337,8 @@ int main(void) { check_arch(&ex); } - if (g_fail) { - fprintf(stderr, "%d FAILED\n", g_fail); + if (g_u.fails) { + fprintf(stderr, "%d FAILED\n", g_u.fails); return 1; } printf("debug cfi_unit: OK\n"); diff --git a/test/debug/roundtrip_unit.c b/test/debug/roundtrip_unit.c @@ -26,54 +26,14 @@ #include "core/core.h" #include "core/pool.h" #include "debug/debug.h" +#include "lib/cfree_unit.h" #include "obj/obj.h" -/* ---- env ---- */ - -static void* heap_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* heap_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void heap_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "[%s] ", - k == CFREE_DIAG_ERROR ? "error" - : k == CFREE_DIAG_WARN ? "warning" - : "note"); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_sink = {diag_emit, 0, 0, 0}; -static CfreeContext g_ctx = {.heap = &g_heap, .diag = &g_sink, .now = -1}; - -/* ---- fail counters ---- */ - -static int g_fail = 0; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - g_fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. The original + * context used now=-1, set in main after cfree_unit_init. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static const Section* sec_by_name(const ObjBuilder* ob, Pool* pool, const char* name) { @@ -126,14 +86,9 @@ static int run_one(CfreeArchKind arch, uint32_t nop_word, const char* tag) { Pool* pool; int local_fail = 0; - memset(&t, 0, sizeof(t)); - t.arch = arch; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; + t = cfree_unit_target(arch, CFREE_OS_LINUX, CFREE_OBJ_ELF); - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "[%s] compiler_new failed\n", tag); return 2; } @@ -356,14 +311,9 @@ static int run_x64_debug_line_check(void) { ObjSymId xfsym; Pool* xpool; - memset(&xt, 0, sizeof(xt)); - xt.arch = CFREE_ARCH_X86_64; - xt.os = CFREE_OS_LINUX; - xt.obj = CFREE_OBJ_ELF; - xt.ptr_size = 8; - xt.ptr_align = 8; + xt = cfree_unit_target(CFREE_ARCH_X86_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); - if (cfree_compiler_new(xt, &g_ctx, &xc) != CFREE_OK || !xc) { + if (cfree_unit_compiler_new(&g_u, xt, &xc) != CFREE_OK || !xc) { fprintf(stderr, "x64 compiler_new failed\n"); return 2; } @@ -416,13 +366,17 @@ static int run_x64_debug_line_check(void) { int main(void) { int rc = 0; + + cfree_unit_init(&g_u); + g_u.ctx.now = -1; + rc |= run_one(CFREE_ARCH_ARM_64, ARCH_NOP_AA64, "aa64"); rc |= run_one(CFREE_ARCH_RV64, ARCH_NOP_RV64, "rv64"); rc |= run_x64_debug_line_check(); run_arch_register_checks(); - if (g_fail || rc) { - fprintf(stderr, "%d FAILED\n", g_fail); + if (g_u.fails || rc) { + fprintf(stderr, "%d FAILED\n", g_u.fails); return 1; } printf("debug roundtrip_unit: OK\n"); diff --git a/test/driver/run.sh b/test/driver/run.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Driver-level permission checks for executable outputs. +# Driver-level behavior checks for the cfree multitool CLI. set -u @@ -13,17 +13,19 @@ if [ ! -x "$CFREE" ]; then exit 2 fi -stat_mode() { - if mode=$(stat -f '%Lp' "$1" 2>/dev/null); then - printf '%s' "$mode" - else - stat -c '%a' "$1" - fi -} - work=$(mktemp -d "${TMPDIR:-/tmp}/cfree-driver-test.XXXXXX") trap 'rm -rf "$work"' EXIT +# Type-K mode-P kit: ok/run_ok/run_fail/contains/same_file/is_executable/ +# assert_file_exists/check_mode, all recording through the unified cf_* counters +# over $work. check_mode replaces the inline stat -f/-c permission probe; the +# driver-specific scenarios that need shell control flow (umask, cd, stdin +# piping, runtime auto-build via nm) stay inline but route verdicts through +# ok/not_ok. Mode-P suites are SERIAL — fixtures under $work are shared/mutated. +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" +cf_report_init + cat > "$work/main.c" <<'SRC' int main(void) { return 0; } int _start(void) { return 0; } @@ -33,30 +35,12 @@ cat > "$work/other.c" <<'SRC' int other(void) { return 0; } SRC -pass=0 -fail=0 - -check_mode() { - name=$1 - path=$2 - want=$3 - got=$(stat_mode "$path") - if [ "$got" = "$want" ]; then - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s (mode %s, want %s)\n' "$name" "$got" "$want" - fail=$((fail + 1)) - fi -} - +# ---- executable permission bits (cc/ld) ---- if (umask 077; "$CFREE" cc "$work/main.c" -o "$work/cc-exe") \ > "$work/cc.out" 2> "$work/cc.err"; then check_mode "cc-executable-mode" "$work/cc-exe" 700 else - printf 'FAIL cc-executable-mode (cfree cc failed)\n' - sed 's/^/ | /' "$work/cc.err" - fail=$((fail + 1)) + not_ok "cc-executable-mode" "$work/cc.err" fi rm -f "$work/a.out" @@ -65,13 +49,11 @@ if (cd "$work" && umask 077 && "$CFREE" cc main.c) \ if [ -f "$work/a.out" ]; then check_mode "cc-link-default-output" "$work/a.out" 700 else - printf 'FAIL %s (a.out not created)\n' "cc-link-default-output" - fail=$((fail + 1)) + echo "a.out not created" > "$work/cc-link-default.diag" + not_ok "cc-link-default-output" "$work/cc-link-default.diag" fi else - printf 'FAIL cc-link-default-output (cfree cc failed)\n' - sed 's/^/ | /' "$work/cc-link-default.err" - fail=$((fail + 1)) + not_ok "cc-link-default-output" "$work/cc-link-default.err" fi if (umask 077; "$CFREE" cc -c "$work/main.c" -o "$work/main.o") \ @@ -82,46 +64,39 @@ if (umask 077; "$CFREE" cc -c "$work/main.c" -o "$work/main.o") \ > "$work/ld.out" 2> "$work/ld.err"; then check_mode "ld-executable-mode" "$work/ld-exe" 700 else - printf 'FAIL ld-executable-mode (cfree ld failed)\n' - sed 's/^/ | /' "$work/ld.err" - fail=$((fail + 1)) + not_ok "ld-executable-mode" "$work/ld.err" fi else - printf 'FAIL ld-executable-mode (cfree cc -c failed)\n' - sed 's/^/ | /' "$work/cc-c.err" - fail=$((fail + 1)) + not_ok "ld-executable-mode" "$work/cc-c.err" fi +# ---- stdin piping: -x asm/s/asm-cpp/S → object ---- for xlang in asm s asm-cpp S; do if printf '.globl asm_stdin\nasm_stdin:\n ret\n' | "$CFREE" cc -target x86_64-linux -x "$xlang" -c - \ -o "$work/stdin-$xlang.o" \ > "$work/stdin-$xlang.out" 2> "$work/stdin-$xlang.err" && [ -f "$work/stdin-$xlang.o" ]; then - printf 'PASS %s\n' "cc-stdin-x-$xlang" - pass=$((pass + 1)) + ok "cc-stdin-x-$xlang" else - printf 'FAIL %s\n' "cc-stdin-x-$xlang" - sed 's/^/ | /' "$work/stdin-$xlang.err" - fail=$((fail + 1)) + not_ok "cc-stdin-x-$xlang" "$work/stdin-$xlang.err" fi done +# ---- stdin piping: -x wasm/wat → emitted C ---- for xlang in wasm wat; do if printf '(module (func (export "test_main") (result i32) i32.const 0))\n' | "$CFREE" cc --emit=c -x "$xlang" - \ -o "$work/stdin-$xlang.c" \ > "$work/stdin-$xlang.out" 2> "$work/stdin-$xlang.err" && [ -s "$work/stdin-$xlang.c" ]; then - printf 'PASS %s\n' "cc-stdin-x-$xlang" - pass=$((pass + 1)) + ok "cc-stdin-x-$xlang" else - printf 'FAIL %s\n' "cc-stdin-x-$xlang" - sed 's/^/ | /' "$work/stdin-$xlang.err" - fail=$((fail + 1)) + not_ok "cc-stdin-x-$xlang" "$work/stdin-$xlang.err" fi done +# ---- ld -r partial link: output mode + relinkability ---- cat > "$work/partial-main.c" <<'SRC' int foo(void); int _start(void) { return foo(); } @@ -144,121 +119,100 @@ if "$CFREE" cc -target x86_64-linux -c "$work/partial-main.c" \ check_mode "ld-r-output-mode" "$work/partial.o" 644 if "$CFREE" ld "$work/partial.o" -o "$work/partial-exe" \ > "$work/ld-r-final.out" 2> "$work/ld-r-final.err"; then - printf 'PASS %s\n' "ld-r-final-link" - pass=$((pass + 1)) + ok "ld-r-final-link" else - printf 'FAIL %s (final link of partial object failed)\n' \ - "ld-r-final-link" - sed 's/^/ | /' "$work/ld-r-final.err" - fail=$((fail + 1)) + not_ok "ld-r-final-link" "$work/ld-r-final.err" fi else - printf 'FAIL ld-r-output-mode (cfree ld -r failed)\n' - sed 's/^/ | /' "$work/ld-r.err" - fail=$((fail + 1)) + not_ok "ld-r-output-mode" "$work/ld-r.err" fi else - printf 'FAIL ld-r-output-mode (setup compile failed)\n' - sed 's/^/ | /' "$work/partial-main.err" "$work/partial-foo.err" - fail=$((fail + 1)) + { sed 's/^/main: /' "$work/partial-main.err" + sed 's/^/foo: /' "$work/partial-foo.err"; } > "$work/ld-r-setup.diag" + not_ok "ld-r-output-mode" "$work/ld-r-setup.diag" fi +# ---- default output names (cc -c) ---- rm -f "$work/main.o" if (cd "$work" && "$CFREE" cc -c main.c) \ > "$work/cc-c-default.out" 2> "$work/cc-c-default.err"; then if [ -f "$work/main.o" ]; then - printf 'PASS %s\n' "cc-c-default-output" - pass=$((pass + 1)) + ok "cc-c-default-output" else - printf 'FAIL %s (main.o not created)\n' "cc-c-default-output" - fail=$((fail + 1)) + echo "main.o not created" > "$work/cc-c-default.diag" + not_ok "cc-c-default-output" "$work/cc-c-default.diag" fi else - printf 'FAIL cc-c-default-output (cfree cc -c failed)\n' - sed 's/^/ | /' "$work/cc-c-default.err" - fail=$((fail + 1)) + not_ok "cc-c-default-output" "$work/cc-c-default.err" fi rm -f "$work/main.o" "$work/other.o" if (cd "$work" && "$CFREE" cc -c main.c other.c) \ > "$work/cc-c-multi.out" 2> "$work/cc-c-multi.err"; then if [ -f "$work/main.o" ] && [ -f "$work/other.o" ]; then - printf 'PASS %s\n' "cc-c-multi-default-output" - pass=$((pass + 1)) + ok "cc-c-multi-default-output" else - printf 'FAIL %s (expected main.o and other.o)\n' "cc-c-multi-default-output" - fail=$((fail + 1)) + echo "expected main.o and other.o" > "$work/cc-c-multi.diag" + not_ok "cc-c-multi-default-output" "$work/cc-c-multi.diag" fi else - printf 'FAIL cc-c-multi-default-output (cfree cc -c failed)\n' - sed 's/^/ | /' "$work/cc-c-multi.err" - fail=$((fail + 1)) + not_ok "cc-c-multi-default-output" "$work/cc-c-multi.err" fi +# ---- cc -E → stdout ---- if "$CFREE" cc -E "$work/main.c" > "$work/cc-E-default.out" 2> "$work/cc-E-default.err"; then if [ -s "$work/cc-E-default.out" ]; then - printf 'PASS %s\n' "cc-E-default-stdout" - pass=$((pass + 1)) + ok "cc-E-default-stdout" else - printf 'FAIL %s (stdout was empty)\n' "cc-E-default-stdout" - fail=$((fail + 1)) + echo "stdout was empty" > "$work/cc-E-default.diag" + not_ok "cc-E-default-stdout" "$work/cc-E-default.diag" fi else - printf 'FAIL cc-E-default-stdout (cfree cc -E failed)\n' - sed 's/^/ | /' "$work/cc-E-default.err" - fail=$((fail + 1)) + not_ok "cc-E-default-stdout" "$work/cc-E-default.err" fi +# ---- cc -dumpmachine probe ---- if "$CFREE" cc -target riscv64-linux -dumpmachine \ > "$work/cc-dumpmachine.out" 2> "$work/cc-dumpmachine.err" && [ "$(cat "$work/cc-dumpmachine.out")" = "riscv64-linux" ]; then - printf 'PASS %s\n' "cc-dumpmachine-probe" - pass=$((pass + 1)) + ok "cc-dumpmachine-probe" else - printf 'FAIL %s\n' "cc-dumpmachine-probe" - sed 's/^/ | /' "$work/cc-dumpmachine.err" - fail=$((fail + 1)) + not_ok "cc-dumpmachine-probe" "$work/cc-dumpmachine.err" fi +# ---- cc -print-file-name probe ---- if "$CFREE" cc -print-file-name=crt1.o \ > "$work/cc-print-file-name.out" 2> "$work/cc-print-file-name.err" && [ "$(cat "$work/cc-print-file-name.out")" = "crt1.o" ]; then - printf 'PASS %s\n' "cc-print-file-name-probe" - pass=$((pass + 1)) + ok "cc-print-file-name-probe" else - printf 'FAIL %s\n' "cc-print-file-name-probe" - sed 's/^/ | /' "$work/cc-print-file-name.err" - fail=$((fail + 1)) + not_ok "cc-print-file-name-probe" "$work/cc-print-file-name.err" fi +# ---- accepted-but-ignored compatibility flags log to stderr ---- if "$CFREE" cc -Wall -Wextra -std=c11 -ffreestanding -c "$work/main.c" \ -o "$work/ignored.o" > "$work/cc-ignored.out" 2> "$work/cc-ignored.err"; then if grep -q "ignoring accepted compatibility flag: -Wall" "$work/cc-ignored.err" && grep -q "ignoring accepted compatibility flag: -std=c11" "$work/cc-ignored.err"; then - printf 'PASS %s\n' "cc-ignored-compat-flags" - pass=$((pass + 1)) + ok "cc-ignored-compat-flags" else - printf 'FAIL %s (missing ignore log)\n' "cc-ignored-compat-flags" - sed 's/^/ | /' "$work/cc-ignored.err" - fail=$((fail + 1)) + cp "$work/cc-ignored.err" "$work/cc-ignored.diag" + not_ok "cc-ignored-compat-flags" "$work/cc-ignored.diag" fi else - printf 'FAIL cc-ignored-compat-flags (cfree cc failed)\n' - sed 's/^/ | /' "$work/cc-ignored.err" - fail=$((fail + 1)) + not_ok "cc-ignored-compat-flags" "$work/cc-ignored.err" fi +# ---- --output= long form (cc) ---- if "$CFREE" cc -c "$work/main.c" --output="$work/cc-long-output.o" \ > "$work/cc-long-output.out" 2> "$work/cc-long-output.err" && [ -f "$work/cc-long-output.o" ]; then - printf 'PASS %s\n' "cc-long-output" - pass=$((pass + 1)) + ok "cc-long-output" else - printf 'FAIL %s\n' "cc-long-output" - sed 's/^/ | /' "$work/cc-long-output.err" - fail=$((fail + 1)) + not_ok "cc-long-output" "$work/cc-long-output.err" fi +# ---- -iquote include path ---- mkdir -p "$work/quote-inc" cat > "$work/quote-inc/q.h" <<'SRC' #define Q_VALUE 7 @@ -267,33 +221,19 @@ cat > "$work/quote-include.c" <<'SRC' #include "q.h" int q(void) { return Q_VALUE; } SRC -if "$CFREE" cc -c "$work/quote-include.c" -iquote "$work/quote-inc" \ - -o "$work/quote-include.o" \ - > "$work/quote-include.out" 2> "$work/quote-include.err"; then - printf 'PASS %s\n' "cc-iquote-include" - pass=$((pass + 1)) -else - printf 'FAIL %s\n' "cc-iquote-include" - sed 's/^/ | /' "$work/quote-include.err" - fail=$((fail + 1)) -fi +run_ok "cc-iquote-include" "$CFREE" cc -c "$work/quote-include.c" \ + -iquote "$work/quote-inc" -o "$work/quote-include.o" +# ---- implicit freestanding headers ---- cat > "$work/implicit-header.c" <<'SRC' #include <stddef.h> #include <stdint.h> int f(void) { return (int)sizeof(size_t) + (int)UINT8_MAX; } SRC -if "$CFREE" cc -target aarch64-linux -c "$work/implicit-header.c" \ - -o "$work/implicit-header.o" \ - > "$work/implicit-header.out" 2> "$work/implicit-header.err"; then - printf 'PASS %s\n' "cc-implicit-freestanding-headers" - pass=$((pass + 1)) -else - printf 'FAIL %s (cfree cc failed)\n' "cc-implicit-freestanding-headers" - sed 's/^/ | /' "$work/implicit-header.err" - fail=$((fail + 1)) -fi +run_ok "cc-implicit-freestanding-headers" "$CFREE" cc -target aarch64-linux \ + -c "$work/implicit-header.c" -o "$work/implicit-header.o" +# ---- runtime auto-build + link via nm (aarch64) ---- mkdir -p "$work/rt-support/rt" cp -R "$repo_root/rt/include" "$work/rt-support/rt/include" cp -R "$repo_root/rt/lib" "$work/rt-support/rt/lib" @@ -316,12 +256,9 @@ if "$CFREE" cc --support-dir "$work/rt-support" -target aarch64-linux \ > "$work/rt-div.out" 2> "$work/rt-div.err" && "$CFREE" nm "$work/rt-div" 2> "$work/rt-div-nm.err" \ | grep -qE '[Tt] __udivti3'; then - printf 'PASS %s\n' "cc-auto-builds-and-links-libcfree-rt" - pass=$((pass + 1)) + ok "cc-auto-builds-and-links-libcfree-rt" else - printf 'FAIL %s (cfree cc failed)\n' "cc-auto-builds-and-links-libcfree-rt" - sed 's/^/ | /' "$work/rt-div.err" - fail=$((fail + 1)) + not_ok "cc-auto-builds-and-links-libcfree-rt" "$work/rt-div.err" fi cat > "$work/rt-x64-start.c" <<'SRC' @@ -342,16 +279,14 @@ if "$CFREE" cc --support-dir "$work/rt-support" -target x86_64-linux \ -o "$work/rt-x64" > "$work/rt-x64.out" 2> "$work/rt-x64.err" && "$CFREE" nm "$work/rt-x64" 2> "$work/rt-x64-nm.err" \ | grep -qE '[Tt] vsnprintf'; then - printf 'PASS %s\n' "cc-auto-builds-and-links-libcfree-rt-x64" - pass=$((pass + 1)) + ok "cc-auto-builds-and-links-libcfree-rt-x64" else - printf 'FAIL %s (cfree cc failed)\n' \ - "cc-auto-builds-and-links-libcfree-rt-x64" - sed 's/^/ | /' "$work/rt-x64.err" - sed 's/^/ | /' "$work/rt-x64-nm.err" 2>/dev/null || true - fail=$((fail + 1)) + { sed 's/^/cc: /' "$work/rt-x64.err" + sed 's/^/nm: /' "$work/rt-x64-nm.err" 2>/dev/null; } > "$work/rt-x64.diag" + not_ok "cc-auto-builds-and-links-libcfree-rt-x64" "$work/rt-x64.diag" fi +# ---- ld auto-builds + links the runtime (rt archive cached under support-dir) ---- mkdir -p "$work/ld-rt-support/rt" cp -R "$repo_root/rt/include" "$work/ld-rt-support/rt/include" cp -R "$repo_root/rt/lib" "$work/ld-rt-support/rt/lib" @@ -362,27 +297,23 @@ if "$CFREE" cc --support-dir "$work/ld-rt-support" -target aarch64-linux \ -e _start "$work/ld-rt-div.o" -o "$work/ld-rt-div" \ > "$work/ld-rt-div.out" 2> "$work/ld-rt-div.err" && [ -f "$work/ld-rt-support/build/rt/aarch64-linux/libcfree_rt.a" ]; then - printf 'PASS %s\n' "ld-auto-builds-and-links-libcfree-rt" - pass=$((pass + 1)) + ok "ld-auto-builds-and-links-libcfree-rt" else - printf 'FAIL %s (cfree ld failed)\n' \ - "ld-auto-builds-and-links-libcfree-rt" - sed 's/^/ | /' "$work/ld-rt-div-cc.err" - sed 's/^/ | /' "$work/ld-rt-div.err" 2>/dev/null || true - fail=$((fail + 1)) + { sed 's/^/cc: /' "$work/ld-rt-div-cc.err" + sed 's/^/ld: /' "$work/ld-rt-div.err" 2>/dev/null; } > "$work/ld-rt.diag" + not_ok "ld-auto-builds-and-links-libcfree-rt" "$work/ld-rt.diag" fi +# ---- --output= long form (ld) ---- if "$CFREE" ld "$work/main.o" --output="$work/ld-long-output" \ > "$work/ld-long-output.out" 2> "$work/ld-long-output.err" && [ -f "$work/ld-long-output" ]; then - printf 'PASS %s\n' "ld-long-output" - pass=$((pass + 1)) + ok "ld-long-output" else - printf 'FAIL %s\n' "ld-long-output" - sed 's/^/ | /' "$work/ld-long-output.err" - fail=$((fail + 1)) + not_ok "ld-long-output" "$work/ld-long-output.err" fi +# ---- ld --no-undefined rejects unresolved refs in a shared object ---- cat > "$work/shared-undef.c" <<'SRC' int missing(void); int f(void) { return missing(); } @@ -390,33 +321,23 @@ SRC if "$CFREE" cc -target x86_64-linux -fPIC -c "$work/shared-undef.c" \ -o "$work/shared-undef.o" > "$work/shared-undef-cc.out" \ 2> "$work/shared-undef-cc.err"; then - if ! "$CFREE" ld -shared --no-undefined "$work/shared-undef.o" \ - -o "$work/shared-undef.so" > "$work/shared-undef-ld.out" \ - 2> "$work/shared-undef-ld.err"; then - printf 'PASS %s\n' "ld-no-undefined" - pass=$((pass + 1)) - else - printf 'FAIL %s (link unexpectedly succeeded)\n' "ld-no-undefined" - fail=$((fail + 1)) - fi + run_fail "ld-no-undefined" "$CFREE" ld -shared --no-undefined \ + "$work/shared-undef.o" -o "$work/shared-undef.so" else - printf 'FAIL %s (setup compile failed)\n' "ld-no-undefined" - sed 's/^/ | /' "$work/shared-undef-cc.err" - fail=$((fail + 1)) + not_ok "ld-no-undefined" "$work/shared-undef-cc.err" fi +# ---- objdump -x aggregate (sections + symbol table) ---- if "$CFREE" objdump -x "$work/main.o" \ > "$work/objdump-x.out" 2> "$work/objdump-x.err" && grep -q "Sections:" "$work/objdump-x.out" && grep -q "SYMBOL TABLE:" "$work/objdump-x.out"; then - printf 'PASS %s\n' "objdump-x-aggregate" - pass=$((pass + 1)) + ok "objdump-x-aggregate" else - printf 'FAIL %s\n' "objdump-x-aggregate" - sed 's/^/ | /' "$work/objdump-x.err" - fail=$((fail + 1)) + not_ok "objdump-x-aggregate" "$work/objdump-x.err" fi +# ---- Mach-O -ffunction-sections atom coalescing ---- { printf 'int live(void) { return 0; }\n' i=0 @@ -441,21 +362,21 @@ if "$CFREE" cc -target arm64-apple-macos -O1 -ffunction-sections \ if [ "$macho_sec_count" -le 8 ] && ! grep -q '\*UND\*' "$work/macho-many-dump.out" && [ "$macho_flags" = "00002000" ]; then - printf 'PASS %s\n' "macho-function-sections-atoms" - pass=$((pass + 1)) + ok "macho-function-sections-atoms" else - printf 'FAIL %s (sections=%s flags=%s; unexpected UND/flags/fanout)\n' \ - "macho-function-sections-atoms" "$macho_sec_count" "$macho_flags" - sed 's/^/ | /' "$work/macho-many-dump.out" - fail=$((fail + 1)) + { printf 'sections=%s flags=%s; unexpected UND/flags/fanout\n' \ + "$macho_sec_count" "$macho_flags" + sed 's/^/ | /' "$work/macho-many-dump.out"; } \ + > "$work/macho-many.diag" + not_ok "macho-function-sections-atoms" "$work/macho-many.diag" fi else - printf 'FAIL %s (compile or objdump failed)\n' \ - "macho-function-sections-atoms" - sed 's/^/ | /' "$work/macho-many-cc.err" "$work/macho-many-dump.err" - fail=$((fail + 1)) + { sed 's/^/cc: /' "$work/macho-many-cc.err" + sed 's/^/dump: /' "$work/macho-many-dump.err"; } > "$work/macho-many-setup.diag" + not_ok "macho-function-sections-atoms" "$work/macho-many-setup.diag" fi +# ---- run: JIT compile a source + archive on demand, exit status is the result ---- cat > "$work/run-main.c" <<'SRC' int add42(int); int main(void) { return add42(0); } @@ -474,20 +395,19 @@ if "$CFREE" cc -I"$repo_root/rt/include" \ > "$work/run-archive.out" 2> "$work/run-archive.err" run_status=$? if [ "$run_status" -eq 42 ]; then - printf 'PASS %s\n' "run-source-archive-demand" - pass=$((pass + 1)) + ok "run-source-archive-demand" else - printf 'FAIL %s (status %s, want 42)\n' \ - "run-source-archive-demand" "$run_status" - sed 's/^/ | /' "$work/run-archive.err" - fail=$((fail + 1)) + { printf 'status %s, want 42\n' "$run_status" + sed 's/^/ | /' "$work/run-archive.err"; } > "$work/run-archive.diag" + not_ok "run-source-archive-demand" "$work/run-archive.diag" fi else - printf 'FAIL run-source-archive-demand (setup failed)\n' - sed 's/^/ | /' "$work/run-lib.err" "$work/run-ar.err" - fail=$((fail + 1)) + { sed 's/^/lib: /' "$work/run-lib.err" + sed 's/^/ar: /' "$work/run-ar.err"; } > "$work/run-setup.diag" + not_ok "run-source-archive-demand" "$work/run-setup.diag" fi +# ---- archive link order is enforced (def after ref vs ref after def) ---- cat > "$work/order-main.c" <<'SRC' int foo(void); int _start(void) { return foo(); } @@ -506,18 +426,17 @@ if "$CFREE" cc -target x86_64-linux -c "$work/order-main.c" -o "$work/order-main -o "$work/order-right" > "$work/order-right.out" 2> "$work/order-right.err" && ! "$CFREE" cc -target x86_64-linux -L"$work" -lfoo "$work/order-main.o" \ -o "$work/order-wrong" > "$work/order-wrong.out" 2> "$work/order-wrong.err"; then - printf 'PASS %s\n' "cc-link-archive-order" - pass=$((pass + 1)) + ok "cc-link-archive-order" else - printf 'FAIL %s (archive order was not enforced)\n' "cc-link-archive-order" - sed 's/^/ right| /' "$work/order-right.err" - sed 's/^/ wrong| /' "$work/order-wrong.err" - fail=$((fail + 1)) + { sed 's/^/right| /' "$work/order-right.err" + sed 's/^/wrong| /' "$work/order-wrong.err"; } > "$work/order.diag" + not_ok "cc-link-archive-order" "$work/order.diag" fi else - printf 'FAIL cc-link-archive-order (setup failed)\n' - sed 's/^/ | /' "$work/order-main.err" "$work/order-foo.err" "$work/order-ar.err" - fail=$((fail + 1)) + { sed 's/^/main: /' "$work/order-main.err" + sed 's/^/foo: /' "$work/order-foo.err" + sed 's/^/ar: /' "$work/order-ar.err"; } > "$work/order-setup.diag" + not_ok "cc-link-archive-order" "$work/order-setup.diag" fi # ---- rv64 cross-target end-to-end (as, cc, ld, objdump) ---- @@ -535,17 +454,13 @@ if "$CFREE" as -target riscv64-linux "$work/rv64-asm.S" -o "$work/rv64-asm.o" \ if "$CFREE" objdump -h "$work/rv64-asm.o" \ > "$work/rv64-as-h.out" 2> "$work/rv64-as-h.err" && grep -q "elf64-riscv64" "$work/rv64-as-h.out"; then - printf 'PASS %s\n' "rv64-as-cc-objdump-elf" - pass=$((pass + 1)) + ok "rv64-as-cc-objdump-elf" else - printf 'FAIL %s (objdump did not report elf64-riscv64)\n' "rv64-as-cc-objdump-elf" - sed 's/^/ | /' "$work/rv64-as-h.out" - fail=$((fail + 1)) + cp "$work/rv64-as-h.out" "$work/rv64-as-h.diag" + not_ok "rv64-as-cc-objdump-elf" "$work/rv64-as-h.diag" fi else - printf 'FAIL %s (cfree as failed)\n' "rv64-as-cc-objdump-elf" - sed 's/^/ | /' "$work/rv64-as.err" - fail=$((fail + 1)) + not_ok "rv64-as-cc-objdump-elf" "$work/rv64-as.err" fi cat > "$work/rv64-cc.c" <<'SRC' @@ -556,17 +471,13 @@ if "$CFREE" cc -target riscv64-linux -c "$work/rv64-cc.c" -o "$work/rv64-cc.o" \ if "$CFREE" objdump -d "$work/rv64-cc.o" \ > "$work/rv64-cc-d.out" 2> "$work/rv64-cc-d.err" && grep -q "ret" "$work/rv64-cc-d.out"; then - printf 'PASS %s\n' "rv64-cc-emits-ret" - pass=$((pass + 1)) + ok "rv64-cc-emits-ret" else - printf 'FAIL %s (objdump -d missing ret)\n' "rv64-cc-emits-ret" - sed 's/^/ | /' "$work/rv64-cc-d.out" - fail=$((fail + 1)) + cp "$work/rv64-cc-d.out" "$work/rv64-cc-d.diag" + not_ok "rv64-cc-emits-ret" "$work/rv64-cc-d.diag" fi else - printf 'FAIL %s (cfree cc failed)\n' "rv64-cc-emits-ret" - sed 's/^/ | /' "$work/rv64-cc.err" - fail=$((fail + 1)) + not_ok "rv64-cc-emits-ret" "$work/rv64-cc.err" fi cat > "$work/rv64-ld-start.c" <<'SRC' @@ -583,37 +494,30 @@ if "$CFREE" cc -target riscv64-linux -ffreestanding -fno-PIC \ # rv64 ELF executable without needing objdump to parse ET_EXEC. em_byte=$(od -An -tx1 -j 18 -N 1 "$work/rv64-ld.exe" | tr -d ' \n') if [ "$em_byte" = "f3" ]; then - printf 'PASS %s\n' "rv64-ld-static-exe" - pass=$((pass + 1)) + ok "rv64-ld-static-exe" else - printf 'FAIL %s (e_machine byte=%s want=f3)\n' \ - "rv64-ld-static-exe" "$em_byte" - fail=$((fail + 1)) + printf 'e_machine byte=%s want=f3\n' "$em_byte" \ + > "$work/rv64-ld.diag" + not_ok "rv64-ld-static-exe" "$work/rv64-ld.diag" fi else - printf 'FAIL %s (cfree ld failed)\n' "rv64-ld-static-exe" - sed 's/^/ | /' "$work/rv64-ld.err" - fail=$((fail + 1)) + not_ok "rv64-ld-static-exe" "$work/rv64-ld.err" fi else - printf 'FAIL %s (cfree cc -c failed)\n' "rv64-ld-static-exe" - sed 's/^/ | /' "$work/rv64-ld-cc.err" - fail=$((fail + 1)) + not_ok "rv64-ld-static-exe" "$work/rv64-ld-cc.err" fi +# ---- check: frontend-only, emits no output files ---- rm -f "$work/check.o" "$work/a.out" if "$CFREE" check "$work/main.c" > "$work/check.out" 2> "$work/check.err"; then if [ ! -e "$work/check.o" ] && [ ! -e "$work/a.out" ]; then - printf 'PASS %s\n' "check-no-output" - pass=$((pass + 1)) + ok "check-no-output" else - printf 'FAIL %s (unexpected output file)\n' "check-no-output" - fail=$((fail + 1)) + echo "unexpected output file" > "$work/check.diag" + not_ok "check-no-output" "$work/check.diag" fi else - printf 'FAIL %s (cfree check failed)\n' "check-no-output" - sed 's/^/ | /' "$work/check.err" - fail=$((fail + 1)) + not_ok "check-no-output" "$work/check.err" fi cat > "$work/check-bad.c" <<'SRC' @@ -621,16 +525,14 @@ int broken( { return 0; } SRC if "$CFREE" check "$work/check-bad.c" \ > "$work/check-bad.out" 2> "$work/check-bad.err"; then - printf 'FAIL %s (bad source passed)\n' "check-reports-errors" - fail=$((fail + 1)) + echo "bad source passed" > "$work/check-bad.diag" + not_ok "check-reports-errors" "$work/check-bad.diag" else if grep -q "fatal:" "$work/check-bad.err"; then - printf 'PASS %s\n' "check-reports-errors" - pass=$((pass + 1)) + ok "check-reports-errors" else - printf 'FAIL %s (missing diagnostic)\n' "check-reports-errors" - sed 's/^/ | /' "$work/check-bad.err" - fail=$((fail + 1)) + cp "$work/check-bad.err" "$work/check-bad-missing.diag" + not_ok "check-reports-errors" "$work/check-bad-missing.diag" fi fi @@ -642,9 +544,7 @@ rm -f "$work/nm-main.o" nm_obj_ok=1 if ! "$CFREE" cc -target aarch64-linux -c "$work/main.c" -o "$work/nm-main.o" \ > "$work/nm-cc.out" 2> "$work/nm-cc.err"; then - printf 'FAIL nm-setup (cfree cc -c failed)\n' - sed 's/^/ | /' "$work/nm-cc.err" - fail=$((fail + 1)) + not_ok "nm-setup" "$work/nm-cc.err" nm_obj_ok=0 fi @@ -653,12 +553,11 @@ if [ "$nm_obj_ok" = 1 ] && "$CFREE" nm "$work/nm-main.o" > "$work/nm-basic.out" 2> "$work/nm-basic.err" && grep -q "main" "$work/nm-basic.out" && grep -q "_start" "$work/nm-basic.out"; then - printf 'PASS %s\n' "nm-basic" - pass=$((pass + 1)) + ok "nm-basic" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "nm-basic" "$work/nm-basic.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-basic"; sed 's/^/ | /' "$work/nm-basic.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-basic" - fail=$((fail + 1)) + not_ok "nm-basic" fi # nm -g: only global symbols. @@ -676,23 +575,19 @@ if [ "$nm_obj_ok" = 1 ] && "$CFREE" nm -g "$work/nm-global.o" > "$work/nm-global-g.out" 2>/dev/null && grep -q "visible" "$work/nm-global-g.out" && ! grep -q "hidden" "$work/nm-global-g.out"; then - printf 'PASS %s\n' "nm-global-only" - pass=$((pass + 1)) + ok "nm-global-only" else - [ "$nm_obj_ok" = 1 ] && printf 'FAIL %s\n' "nm-global-only" - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-global-only" - fail=$((fail + 1)) + not_ok "nm-global-only" fi # nm -u: undefined only (ELF objects may have no undefined) if [ "$nm_obj_ok" = 1 ] && "$CFREE" nm -u "$work/nm-main.o" > "$work/nm-undef.out" 2>"$work/nm-undef.err"; then - printf 'PASS %s\n' "nm-undefined-only" - pass=$((pass + 1)) + ok "nm-undefined-only" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "nm-undefined-only" "$work/nm-undef.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-undefined-only"; sed 's/^/ | /' "$work/nm-undef.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-undefined-only" - fail=$((fail + 1)) + not_ok "nm-undefined-only" fi # nm on an archive @@ -702,22 +597,19 @@ if [ "$nm_obj_ok" = 1 ] && "$CFREE" nm "$work/libnmtest.a" > "$work/nm-archive.out" \ 2> "$work/nm-archive.err" && grep -q "main" "$work/nm-archive.out"; then - printf 'PASS %s\n' "nm-archive" - pass=$((pass + 1)) + ok "nm-archive" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "nm-archive" "$work/nm-archive.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "nm-archive"; sed 's/^/ | /' "$work/nm-archive.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "nm-archive" - fail=$((fail + 1)) + not_ok "nm-archive" fi # nm -h (help) if "$CFREE" nm --help > "$work/nm-help.out" 2> "$work/nm-help.err" && grep -q "USAGE" "$work/nm-help.out"; then - printf 'PASS %s\n' "nm-help" - pass=$((pass + 1)) + ok "nm-help" else - printf 'FAIL %s\n' "nm-help" - fail=$((fail + 1)) + not_ok "nm-help" "$work/nm-help.err" fi # ---- size ---- @@ -725,24 +617,22 @@ fi if [ "$nm_obj_ok" = 1 ] && "$CFREE" size "$work/nm-main.o" > "$work/size-basic.out" 2> "$work/size-basic.err" && grep -q "text" "$work/size-basic.out"; then - printf 'PASS %s\n' "size-basic" - pass=$((pass + 1)) + ok "size-basic" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "size-basic" "$work/size-basic.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-basic"; sed 's/^/ | /' "$work/size-basic.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-basic" - fail=$((fail + 1)) + not_ok "size-basic" fi # size -A: SysV format if [ "$nm_obj_ok" = 1 ] && "$CFREE" size -A "$work/nm-main.o" > "$work/size-sysv.out" 2> "$work/size-sysv.err" && grep -q "Total" "$work/size-sysv.out"; then - printf 'PASS %s\n' "size-sysv" - pass=$((pass + 1)) + ok "size-sysv" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "size-sysv" "$work/size-sysv.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-sysv"; sed 's/^/ | /' "$work/size-sysv.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-sysv" - fail=$((fail + 1)) + not_ok "size-sysv" fi # size on an archive (uses libnmtest.a from nm test) @@ -750,22 +640,19 @@ if [ "$nm_obj_ok" = 1 ] && [ -f "$work/libnmtest.a" ] && "$CFREE" size "$work/libnmtest.a" > "$work/size-archive.out" \ 2> "$work/size-archive.err" && grep -q "text" "$work/size-archive.out"; then - printf 'PASS %s\n' "size-archive" - pass=$((pass + 1)) + ok "size-archive" +elif [ "$nm_obj_ok" = 1 ]; then + not_ok "size-archive" "$work/size-archive.err" else - [ "$nm_obj_ok" = 1 ] && { printf 'FAIL %s\n' "size-archive"; sed 's/^/ | /' "$work/size-archive.err"; } - [ "$nm_obj_ok" != 1 ] && printf 'FAIL %s (setup)\n' "size-archive" - fail=$((fail + 1)) + not_ok "size-archive" fi # size -h (help) if "$CFREE" size --help > "$work/size-help.out" 2> "$work/size-help.err" && grep -q "USAGE" "$work/size-help.out"; then - printf 'PASS %s\n' "size-help" - pass=$((pass + 1)) + ok "size-help" else - printf 'FAIL %s\n' "size-help" - fail=$((fail + 1)) + not_ok "size-help" "$work/size-help.err" fi # ---- addr2line ---- @@ -775,8 +662,8 @@ int main(void) { return calc(42); } SRC if [ "$nm_obj_ok" != 1 ]; then - printf 'FAIL addr2line-basic (setup)\n'; fail=$((fail + 1)) - printf 'FAIL addr2line-nonzero (setup)\n'; fail=$((fail + 1)) + not_ok "addr2line-basic" + not_ok "addr2line-nonzero" elif "$CFREE" cc -g -target aarch64-linux -c "$work/a2l.c" -o "$work/a2l.o" \ > "$work/a2l-cc.out" 2> "$work/a2l-cc.err"; then calc_addr=$("$CFREE" nm "$work/a2l.o" 2>/dev/null | \ @@ -785,26 +672,23 @@ elif "$CFREE" cc -g -target aarch64-linux -c "$work/a2l.c" -o "$work/a2l.o" \ "$CFREE" addr2line -e "$work/a2l.o" "$calc_addr" \ > "$work/a2l-hit.out" 2> "$work/a2l-hit.err" && grep -q "a2l.c" "$work/a2l-hit.out"; then - printf 'PASS %s\n' "addr2line-basic" - pass=$((pass + 1)) + ok "addr2line-basic" else - printf 'FAIL %s (calc_addr=%s)\n' "addr2line-basic" "$calc_addr" - sed 's/^/ | /' "$work/a2l-hit.err" - fail=$((fail + 1)) + { printf 'calc_addr=%s\n' "$calc_addr" + sed 's/^/ | /' "$work/a2l-hit.err"; } > "$work/a2l-hit.diag" + not_ok "addr2line-basic" "$work/a2l-hit.diag" fi # addr2line should produce output (not crash) on any address if "$CFREE" addr2line -e "$work/a2l.o" 0x0 \ > "$work/a2l-miss.out" 2> "$work/a2l-miss.err" && [ -s "$work/a2l-miss.out" ]; then - printf 'PASS %s\n' "addr2line-nonzero" - pass=$((pass + 1)) + ok "addr2line-nonzero" else - printf 'FAIL %s\n' "addr2line-nonzero" - fail=$((fail + 1)) + not_ok "addr2line-nonzero" "$work/a2l-miss.err" fi else - printf 'FAIL addr2line-basic (setup compile failed)\n'; fail=$((fail + 1)) - printf 'FAIL addr2line-nonzero (setup)\n'; fail=$((fail + 1)) + not_ok "addr2line-basic" "$work/a2l-cc.err" + not_ok "addr2line-nonzero" fi # addr2line over Mach-O DWARF: the producer emits .debug_* into the @@ -819,15 +703,15 @@ if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2l.c" \ "$CFREE" addr2line -e "$work/a2l.macho.o" "$m_addr" \ > "$work/a2l-m.out" 2> "$work/a2l-m.err" && grep -q "a2l.c" "$work/a2l-m.out"; then - printf 'PASS %s\n' "addr2line-macho" - pass=$((pass + 1)) + ok "addr2line-macho" else - printf 'FAIL %s (m_addr=%s)\n' "addr2line-macho" "$m_addr" - sed 's/^/ | /' "$work/a2l-m.out" "$work/a2l-m.err" - fail=$((fail + 1)) + { printf 'm_addr=%s\n' "$m_addr" + sed 's/^/out| /' "$work/a2l-m.out" + sed 's/^/err| /' "$work/a2l-m.err"; } > "$work/a2l-m.diag" + not_ok "addr2line-macho" "$work/a2l-m.diag" fi else - printf 'FAIL addr2line-macho (setup compile failed)\n'; fail=$((fail + 1)) + not_ok "addr2line-macho" "$work/a2l-m-cc.err" fi # Debug-info retention through the Mach-O linker: link a self-contained @@ -853,26 +737,22 @@ if "$CFREE" cc -g -target arm64-apple-macos -c "$work/a2lm.c" \ "$CFREE" addr2line -e "$work/a2lm.exe" "0x$cm_addr" \ > "$work/a2lm-a2l.out" 2> "$work/a2lm-a2l.err" && grep -q "a2lm.c" "$work/a2lm-a2l.out"; then - printf 'PASS %s\n' "addr2line-macho-linked" - pass=$((pass + 1)) + ok "addr2line-macho-linked" else - printf 'FAIL %s (compute=%s: %s)\n' "addr2line-macho-linked" \ - "$cm_addr" "$(cat "$work/a2lm-a2l.out" 2>/dev/null)" - fail=$((fail + 1)) + printf 'compute=%s: %s\n' "$cm_addr" \ + "$(cat "$work/a2lm-a2l.out" 2>/dev/null)" > "$work/a2lm.diag" + not_ok "addr2line-macho-linked" "$work/a2lm.diag" fi else - printf 'FAIL addr2line-macho-linked (link failed)\n'; fail=$((fail + 1)) - sed 's/^/ | /' "$work/a2lm-ld.err" + not_ok "addr2line-macho-linked" "$work/a2lm-ld.err" fi # addr2line -h (help) if "$CFREE" addr2line --help > "$work/a2l-help.out" 2> "$work/a2l-help.err" && grep -q "USAGE" "$work/a2l-help.out"; then - printf 'PASS %s\n' "addr2line-help" - pass=$((pass + 1)) + ok "addr2line-help" else - printf 'FAIL %s\n' "addr2line-help" - fail=$((fail + 1)) + not_ok "addr2line-help" "$work/a2l-help.err" fi # --emit=ir: dump the semantic CG IR tape (requires -O1+). @@ -889,31 +769,22 @@ if "$CFREE" cc -O1 --emit=ir -c "$work/ir.c" -o "$work/ir.out" \ grep -q "binop" "$work/ir.out" && grep -q "iadd" "$work/ir.out" && grep -q "ret values=\[" "$work/ir.out"; then - printf 'PASS %s\n' "cc-emit-ir" - pass=$((pass + 1)) + ok "cc-emit-ir" else - printf 'FAIL %s\n' "cc-emit-ir" - sed 's/^/ | /' "$work/ir-emit.err" - fail=$((fail + 1)) + not_ok "cc-emit-ir" "$work/ir-emit.err" fi # --emit=ir without -O1 must be rejected (no IR tape is recorded at -O0). if "$CFREE" cc --emit=ir -c "$work/ir.c" -o "$work/ir-o0.out" \ > "$work/ir-o0.out.log" 2> "$work/ir-o0.err"; then - printf 'FAIL %s (expected failure at -O0)\n' "cc-emit-ir-requires-opt" - fail=$((fail + 1)) + echo "expected failure at -O0" > "$work/ir-o0.diag" + not_ok "cc-emit-ir-requires-opt" "$work/ir-o0.diag" elif grep -q "requires -O1" "$work/ir-o0.err"; then - printf 'PASS %s\n' "cc-emit-ir-requires-opt" - pass=$((pass + 1)) + ok "cc-emit-ir-requires-opt" else - printf 'FAIL %s (wrong diagnostic)\n' "cc-emit-ir-requires-opt" - sed 's/^/ | /' "$work/ir-o0.err" - fail=$((fail + 1)) + cp "$work/ir-o0.err" "$work/ir-o0-wrong.diag" + not_ok "cc-emit-ir-requires-opt" "$work/ir-o0-wrong.diag" fi -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\ndriver: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\ndriver: %d/%d passed\n' "$pass" "$total" +cf_summary driver-cc +cf_exit diff --git a/test/dwarf/dwarf_test.c b/test/dwarf/dwarf_test.c @@ -22,6 +22,8 @@ #include <stdlib.h> #include <string.h> +#include "lib/cfree_unit.h" + /* This test reaches into the internal obj/ surface to construct a * DWARF-bearing ELF without going through the parser/codegen path. * That's deliberate: we want to test the *consumer* in isolation against @@ -30,46 +32,10 @@ #include "core/pool.h" #include "obj/obj.h" -/* ---- env ---- */ -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -static int g_fail; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - g_fail++; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) /* ---- byte builders -------------------------------------------------- */ @@ -877,7 +843,7 @@ static void run_tests(CfreeDebugInfo* di) { EXPECT(file.s && strstr(file.s, "test.c") != NULL, "file should contain test.c, got %.*s", CFREE_SLICE_ARG(file)); } else { - g_fail++; + g_u.fails++; fprintf(stderr, "FAIL: addr_to_line(0x1000) returned no entry\n"); } /* 2. line_to_addr round trip. */ @@ -894,7 +860,7 @@ static void run_tests(CfreeDebugInfo* di) { EXPECT(pc == 0x1000, "expected pc 0x1000 for test.c:10, got 0x%llx", (unsigned long long)pc); } else { - g_fail++; + g_u.fails++; fprintf(stderr, "FAIL: line_to_addr could not find any test.c:10\n"); } } @@ -912,7 +878,7 @@ static void run_tests(CfreeDebugInfo* di) { CfreeDwarfVarLoc loc; EXPECT(cfree_dwarf_var_at(di, 0x1000, CFREE_SLICE_LIT("x"), &loc) == 0, "var_at(0x1000, x) failed"); - if (g_fail == 0) { + if (g_u.fails == 0) { EXPECT(loc.kind == CFREE_DLOC_FRAME_OFS, "expected x.kind=FRAME_OFS, got %d", (int)loc.kind); if (loc.kind == CFREE_DLOC_FRAME_OFS) { @@ -966,7 +932,7 @@ static void run_tests(CfreeDebugInfo* di) { if (cfree_dwarf_addr_to_line(di, 0x1004, &f2, &l2, &c2) == 0) { EXPECT(l2 == 11, "expected line 11 at 0x1004, got %u", l2); } else { - g_fail++; + g_u.fails++; fprintf(stderr, "FAIL: addr_to_line(0x1004) failed\n"); } } @@ -1037,7 +1003,7 @@ static void run_tests(CfreeDebugInfo* di) { (int)it.kind); } } else { - g_fail++; + g_u.fails++; fprintf(stderr, "FAIL: var_at(my_int) returned nothing\n"); } } @@ -1103,20 +1069,13 @@ static void run_tests(CfreeDebugInfo* di) { int main(void) { CfreeTarget target; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; - CfreeContext ctx; - memset(&ctx, 0, sizeof ctx); - ctx.heap = &g_heap; - ctx.diag = &g_diag; - ctx.now = -1; + cfree_unit_init(&g_u); + g_u.ctx.now = -1; + target = + cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_LINUX, CFREE_OBJ_ELF); CfreeCompiler* cc = NULL; - if (cfree_compiler_new(target, &ctx, &cc) != CFREE_OK || !cc) { + if (cfree_unit_compiler_new(&g_u, target, &cc) != CFREE_OK || !cc) { fprintf(stderr, "compiler_new failed\n"); return 1; } @@ -1167,7 +1126,7 @@ int main(void) { /* Emit ELF to memory. */ CfreeWriter* w = NULL; - (void)cfree_writer_mem(&g_heap, &w); + (void)cfree_writer_mem(&g_u.heap, &w); emit_elf(cc, ob, w); size_t obj_len = 0; const uint8_t* obj_bytes = cfree_writer_mem_bytes(w, &obj_len); @@ -1194,13 +1153,13 @@ int main(void) { in.data = obj_bytes; in.len = obj_len; CfreeObjFile* obj = NULL; - EXPECT( - cfree_obj_open(&ctx, CFREE_SLICE_LIT("test.o"), &in, &obj) == CFREE_OK && - obj != NULL, - "cfree_obj_open failed"); + EXPECT(cfree_obj_open(&g_u.ctx, CFREE_SLICE_LIT("test.o"), &in, &obj) == + CFREE_OK && + obj != NULL, + "cfree_obj_open failed"); if (obj) { CfreeDebugInfo* di = NULL; - EXPECT(cfree_dwarf_open(&ctx, obj, &di) == CFREE_OK && di != NULL, + EXPECT(cfree_dwarf_open(&g_u.ctx, obj, &di) == CFREE_OK && di != NULL, "cfree_dwarf_open failed"); if (di) { run_tests(di); @@ -1221,8 +1180,8 @@ int main(void) { cfree_compiler_free(cc); - if (g_fail) { - fprintf(stderr, "%d failure(s)\n", g_fail); + if (g_u.fails) { + fprintf(stderr, "%d failure(s)\n", g_u.fails); return 1; } printf("OK\n"); diff --git a/test/elf/run.sh b/test/elf/run.sh @@ -1,32 +1,40 @@ #!/usr/bin/env bash -# test/elf/run.sh — ELF object-file fidelity tests. +# test/elf/run.sh — ELF object-file fidelity tests, on the shared corpus +# harness (test/lib/cf_corpus.sh). # # Scope: read/write/roundtrip of relocatable ELF .o files only. Linker # and exe behavior live in test/link/ — do not duplicate them here. # -# Layers (each runs only when its prerequisites are present): +# Layers — each is a SEQUENTIAL corpus (its own cf_corpus_run, single lane, +# no opt axis), NOT a parallel lane within a case: # -# A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests. -# Each .c links against build/libcfree.a, runs, -# and exits 0 on success. +# A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests. Each .c +# links against build/libcfree.a, runs, exits 0 +# on success. (lane A) # B. test/elf/cases/*.c clang-oracle structural roundtrip tests: # clang -c case.c -> golden.o # cfree-roundtrip golden.o -> rt.o # normalized readelf diff golden.o vs rt.o. -# C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip -# to fail with a substring from <name>.expect. +# Honors per-case NN_name.targets tuple +# applicability (engine SKIP-NA) and per-case +# NN_name.cflags extra compiler flags. (lane B) +# C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip to +# fail (nonzero, non-signal) with a substring from +# <name>.expect on stderr. (lane C) # -# Tools detected: clang, llvm-readelf, python3. Missing tools cause the -# dependent layer to be skipped, not failed. Set CFREE_TEST_ALLOW_SKIP=1 -# to allow the harness to exit 0 with skips; otherwise any skip makes -# the run fail (so CI catches a silently degraded run). +# Tools detected: clang, llvm-readelf/readelf, python3. Missing tools cause the +# dependent layer's cases to be SKIPped, not failed. Set CFREE_TEST_ALLOW_SKIP=1 +# to allow the harness to exit 0 with skips; otherwise any skip makes the run +# fail (so CI catches a silently degraded run). # # Parallelism: # default run in parallel with a capped CPU-count default. # CFREE_TEST_JOBS=N run up to N cases per layer concurrently. # CFREE_TEST_JOBS=auto same as the default. -# The console summary is replayed by the parent in deterministic order; -# per-case command output remains in build/test/ for failure inspection. +# CFREE_ELF_PARALLEL=0 force serial dispatch. +# Every lane hook writes only under $CF_WORK and records only via cf_*, so the +# console summary is identical serial or parallel; per-case command output for +# a failure is surfaced by the engine in deterministic index order. set -u @@ -36,122 +44,14 @@ BUILD_DIR="$ROOT/build/test" LIB_AR="$ROOT/build/libcfree.a" NORMALIZE="$TEST_DIR/normalize.py" -# shellcheck source=../lib/parallel.sh -source "$ROOT/test/lib/parallel.sh" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" mkdir -p "$BUILD_DIR" -TEST_JOBS="$(cfree_parallel_jobs)" || exit 2 -PARALLEL_DIR="$BUILD_DIR/elf.parallel/$$" -mkdir -p "$PARALLEL_DIR" - -PASS=0 -FAIL=0 -SKIP=0 -FAIL_NAMES=() -SKIP_NAMES=() - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } - -event_path() { - printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2" -} - -worker_stdout_path() { - printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2" -} - -worker_stderr_path() { - printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2" -} - -emit_event() { - local file="$1" kind="$2" arg1="${3:-}" arg2="${4:-}" - printf '%s\t%s\t%s\n' "$kind" "$arg1" "$arg2" >> "$file" -} - -replay_events() { - local event="$1" stdout_log="$2" stderr_log="$3" - local kind arg1 arg2 - - if [ ! -s "$event" ]; then - note_fail "internal: missing worker result $event" - if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi - if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi - return - fi - - while IFS=$'\t' read -r kind arg1 arg2; do - case "$kind" in - PASS) - note_pass "$arg1" - ;; - FAIL) - note_fail "$arg1" - ;; - SKIP) - note_skip "$arg1" "$arg2" - ;; - SKIP_NA) - printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$arg1" "$arg2" - ;; - DUMP) - if [ -f "$arg1" ]; then - if [ -n "$arg2" ] && [ "$arg2" -gt 0 ] 2>/dev/null; then - head -n "$arg2" "$arg1" | sed 's/^/ | /' - else - sed 's/^/ | /' "$arg1" - fi - else - printf ' | (missing %s)\n' "$arg1" - fi - ;; - *) - note_fail "internal: malformed worker event in $event" - ;; - esac - done < "$event" -} - -run_parallel_items() { - local layer="$1" worker="$2" - shift 2 - - local events=() - local stdout_logs=() - local stderr_logs=() - local idx=0 - local item event stdout_log stderr_log - - for item in "$@"; do - event="$(event_path "$layer" "$idx")" - stdout_log="$(worker_stdout_path "$layer" "$idx")" - stderr_log="$(worker_stderr_path "$layer" "$idx")" - : > "$event" - : > "$stdout_log" - : > "$stderr_log" - events+=("$event") - stdout_logs+=("$stdout_log") - stderr_logs+=("$stderr_log") - cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \ - > "$stdout_log" 2> "$stderr_log" - idx=$((idx+1)) - done - - cfree_parallel_wait_all || true - - idx=0 - while [ $idx -lt ${#events[@]} ]; do - replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}" - idx=$((idx+1)) - done -} +CFREE_BIN="${CFREE:-$ROOT/build/cfree}" +PAR="${CFREE_ELF_PARALLEL:-1}" # ----- tool detection ---------------------------------------------------- @@ -164,8 +64,6 @@ command -v llvm-readelf >/dev/null 2>&1 && have_llvm_readelf=1 command -v readelf >/dev/null 2>&1 && have_llvm_readelf=1 command -v python3 >/dev/null 2>&1 && have_python3=1 -READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || echo)" - # ----- locate cfree-roundtrip ------------------------------------------- # Built as a Make target (test/test.mk) so it picks up libcfree.a changes. @@ -173,17 +71,7 @@ ROUNDTRIP_BIN="$BUILD_DIR/cfree-roundtrip" roundtrip_ok=0 [ -x "$ROUNDTRIP_BIN" ] && roundtrip_ok=1 -# ----- header summary ---------------------------------------------------- - -printf 'cfree ELF test harness\n' -printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)" -printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)" -printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)" -printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")" -printf ' parallel jobs: %s\n' "$TEST_JOBS" -printf '\n' - -# ----- Layer A: unit/*.c ------------------------------------------------- +# ----- Layer A toolchain wiring ------------------------------------------ UNIT_CC="${CC:-clang}" UNIT_SYSROOT_FLAGS=() @@ -200,53 +88,7 @@ if [ -n "${CFREE_ELF_UNIT_LDFLAGS:-}" ]; then UNIT_EXTRA_LDFLAGS=(${CFREE_ELF_UNIT_LDFLAGS}) fi -run_unit_case() { - local _idx="$1" src="$2" event="$3" - local name stem bin build_log out_log - : "$_idx" - - name="unit/$(basename "$src" .c)" - stem="$(basename "$src" .c)" - bin="$BUILD_DIR/$stem" - build_log="$BUILD_DIR/$stem.build.log" - out_log="$BUILD_DIR/$stem.out" - - if [ ! -f "$LIB_AR" ]; then - emit_event "$event" SKIP "$name" "build/libcfree.a missing" - return 0 - fi - - if ! "$UNIT_CC" -std=c11 -Wall -Wextra -Werror \ - "${UNIT_EXTRA_CFLAGS[@]}" \ - "${UNIT_SYSROOT_FLAGS[@]}" \ - -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \ - "$src" "$LIB_AR" "${UNIT_EXTRA_LDFLAGS[@]}" -o "$bin" \ - 2> "$build_log"; then - emit_event "$event" FAIL "$name (build failed; see $build_log)" - return 0 - fi - - if "$bin" > "$out_log" 2>&1; then - emit_event "$event" PASS "$name" - else - emit_event "$event" FAIL "$name" - emit_event "$event" DUMP "$out_log" - fi - return 0 -} - -printf 'Layer A — unit tests\n' -shopt -s nullglob -unit_srcs=( "$TEST_DIR"/unit/*.c ) -if [ ${#unit_srcs[@]} -eq 0 ]; then - printf ' (no unit tests yet)\n' -else - run_parallel_items "unit" run_unit_case "${unit_srcs[@]}" -fi -printf '\n' - -# ----- Layer B: cases/*.c ------------------------------------------------ - +# ----- Layer B target wiring --------------------------------------------- # Map (CFREE_TEST_ARCH, CFREE_TEST_OBJ) (defaults aa64+elf) to the clang # `--target=` triple the Layer B golden objects are compiled against. # cfree-roundtrip then detects the input's e_machine / Mach-O cputype and @@ -273,158 +115,153 @@ esac CUR_TUPLE="${CFREE_TEST_ARCH:-aa64}-${CFREE_TEST_OBJ:-elf}" -run_oracle_case() { - local _idx="$1" src="$2" event="$3" - local name targets_file applicable tuple stem wd extra_cflags tok - : "$_idx" - - name="cases/$(basename "$src" .c)" - - # Per-case applicability: a NN_name.targets file lists the - # <arch>-<obj> tuples the case applies to (one per line, or - # whitespace-separated). When the current tuple isn't listed - # the case is silently filtered out — not a skip — because it - # exercises target-specific features with no equivalent - # elsewhere (e.g. AArch64 BTI/PAC notes have no RISC-V analogue; - # ELF features have no Mach-O peer). - targets_file="${src%.c}.targets" - if [ -f "$targets_file" ]; then - applicable=0 - for tuple in $(cat "$targets_file"); do - [ "$tuple" = "$CUR_TUPLE" ] && applicable=1 - done - if [ $applicable -eq 0 ]; then - emit_event "$event" SKIP_NA "$name" "$CUR_TUPLE" - return 0 - fi +# ----- header summary ---------------------------------------------------- + +printf 'cfree ELF test harness\n' +printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)" +printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)" +printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)" +printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")" +printf ' target: %s (tuple %s)\n' "$CLANG_TARGET" "$CUR_TUPLE" +printf '\n' + +# ----- Layer A lane: unit/*.c -------------------------------------------- +# Build a native binary against libcfree.a, run it, expect exit 0. All +# artifacts (binary + logs) live under $CF_WORK for parallel safety. +cf_lane_A() { + local name="unit/$CF_NAME" + if [ ! -f "$LIB_AR" ]; then + cf_skip "$name" "build/libcfree.a missing"; return fi - # Per-case skip reasons: - if [ $roundtrip_ok -ne 1 ]; then emit_event "$event" SKIP "$name" "cfree-roundtrip not built"; return 0; fi - if [ $have_clang -ne 1 ]; then emit_event "$event" SKIP "$name" "clang missing"; return 0; fi - if [ $have_llvm_readelf -ne 1 ]; then emit_event "$event" SKIP "$name" "llvm-readelf missing"; return 0; fi - if [ $have_python3 -ne 1 ]; then emit_event "$event" SKIP "$name" "python3 missing"; return 0; fi - - stem="$(basename "$src" .c)" - wd="$BUILD_DIR/$stem" - mkdir -p "$wd" - - # Per-case extra compiler flags: drop a NN_name.cflags file alongside - # the .c to pass additional flags (e.g. -ffunction-sections, -x c++). - extra_cflags=() - if [ -f "${src%.c}.cflags" ]; then - # Existing harness behavior tokenized this file on shell - # whitespace; keep that convention. - for tok in $(cat "${src%.c}.cflags"); do + local bin="$CF_WORK/$CF_BASE" + local build_log="$CF_WORK/build.log" + local out_log="$CF_WORK/out.log" + + if ! "$UNIT_CC" -std=c11 -Wall -Wextra -Werror \ + "${UNIT_EXTRA_CFLAGS[@]}" \ + "${UNIT_SYSROOT_FLAGS[@]}" \ + -I"$ROOT/include" -I"$ROOT/src" -I"$ROOT/test" \ + "$CF_SRC" "$LIB_AR" "${UNIT_EXTRA_LDFLAGS[@]}" -o "$bin" \ + 2> "$build_log"; then + cf_fail "$name" "build failed; see $build_log" + return + fi + + if "$bin" > "$out_log" 2>&1; then + cf_pass "$name" + else + cf_fail "$name" + sed 's/^/ | /' "$out_log" + fi +} + +# ----- Layer B lane: cases/*.c ------------------------------------------- +# clang -c -> golden.o ; cfree-roundtrip -> rt.o ; normalized readelf diff. +# Whole-case .targets applicability is handled by the engine (SKIP-NA) via +# CF_TARGETS_EXT. Per-case .cflags are read here, CF_WORK-confined. +cf_lane_B() { + local name="cases/$CF_NAME" + + # Per-case skip reasons (missing tooling). + if [ $roundtrip_ok -ne 1 ]; then cf_skip "$name" "cfree-roundtrip not built"; return; fi + if [ $have_clang -ne 1 ]; then cf_skip "$name" "clang missing"; return; fi + if [ $have_llvm_readelf -ne 1 ]; then cf_skip "$name" "llvm-readelf missing"; return; fi + if [ $have_python3 -ne 1 ]; then cf_skip "$name" "python3 missing"; return; fi + + # Per-case extra compiler flags: NN_name.cflags alongside the .c passes + # additional flags (e.g. -ffunction-sections, -x c++). Tokenized on shell + # whitespace, matching the original harness convention. + local extra_cflags=() cflags_file tok + cflags_file="${CF_SRC%.c}.cflags" + if [ -f "$cflags_file" ]; then + for tok in $(cat "$cflags_file"); do [ -n "$tok" ] && extra_cflags+=("$tok") done fi if ! clang --target="$CLANG_TARGET" -c -O0 "${extra_cflags[@]}" \ - "$src" -o "$wd/golden.o" 2> "$wd/clang.log"; then - emit_event "$event" SKIP "$name" "clang -c failed (cross-compile not configured?)" - return 0 + "$CF_SRC" -o "$CF_WORK/golden.o" 2> "$CF_WORK/clang.log"; then + cf_skip "$name" "clang -c failed (cross-compile not configured?)" + return fi - if ! "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log"; then - emit_event "$event" FAIL "$name (roundtrip failed)" - emit_event "$event" DUMP "$wd/roundtrip.log" - return 0 + if ! "$ROUNDTRIP_BIN" "$CF_WORK/golden.o" "$CF_WORK/rt.o" 2> "$CF_WORK/roundtrip.log"; then + cf_fail "$name" "roundtrip failed" + sed 's/^/ | /' "$CF_WORK/roundtrip.log" + return fi # Structural diff: readelf normalized. - python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true - python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true - if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then - emit_event "$event" FAIL "$name (readelf diff)" - emit_event "$event" DUMP "$wd/readelf.diff" 40 - return 0 + python3 "$NORMALIZE" readelf "$CF_WORK/golden.o" > "$CF_WORK/golden.readelf" 2> /dev/null || true + python3 "$NORMALIZE" readelf "$CF_WORK/rt.o" > "$CF_WORK/rt.readelf" 2> /dev/null || true + if ! diff -u "$CF_WORK/golden.readelf" "$CF_WORK/rt.readelf" > "$CF_WORK/readelf.diff"; then + cf_fail "$name" "readelf diff" + head -n 40 "$CF_WORK/readelf.diff" | sed 's/^/ | /' + return fi - emit_event "$event" PASS "$name" - return 0 + cf_pass "$name" } -printf 'Layer B — clang-oracle cases\n' -case_srcs=( "$TEST_DIR"/cases/*.c ) -if [ ${#case_srcs[@]} -eq 0 ]; then - printf ' (no cases yet)\n' -else - run_parallel_items "cases" run_oracle_case "${case_srcs[@]}" -fi -printf '\n' - -# ----- Layer C: bad/*.elf ------------------------------------------------ - -run_bad_case() { - local _idx="$1" blob="$2" event="$3" - local name stem wd rc expect_file expect - : "$_idx" - - name="bad/$(basename "$blob" .elf)" +# ----- Layer C lane: bad/*.elf ------------------------------------------- +# Expect cfree-roundtrip to fail (nonzero, non-signal) and emit a substring +# from <name>.expect on stderr. +cf_lane_C() { + local name="bad/$CF_NAME" if [ $roundtrip_ok -ne 1 ]; then - emit_event "$event" SKIP "$name" "cfree-roundtrip not built" - return 0 + cf_skip "$name" "cfree-roundtrip not built"; return fi - stem="$(basename "$blob" .elf)" - wd="$BUILD_DIR/bad_$stem" - mkdir -p "$wd" - - rc=0 - "$ROUNDTRIP_BIN" "$blob" "$wd/out.o" > "$wd/stdout.log" 2> "$wd/stderr.log" || rc=$? + local rc=0 + "$ROUNDTRIP_BIN" "$CF_SRC" "$CF_WORK/out.o" \ + > "$CF_WORK/stdout.log" 2> "$CF_WORK/stderr.log" || rc=$? if [ $rc -eq 0 ]; then - emit_event "$event" FAIL "$name (expected nonzero exit, got 0)" - return 0 + cf_fail "$name" "expected nonzero exit, got 0"; return fi if [ $rc -ge 128 ]; then - emit_event "$event" FAIL "$name (terminated by signal $((rc - 128)) — segfault?)" - return 0 + cf_fail "$name" "terminated by signal $((rc - 128)) — segfault?"; return fi - expect_file="${blob%.elf}.expect" + local expect_file expect + expect_file="${CF_SRC%.elf}.expect" if [ ! -f "$expect_file" ]; then - emit_event "$event" FAIL "$name (missing $expect_file)" - return 0 + cf_fail "$name" "missing $expect_file"; return fi expect="$(cat "$expect_file")" - if grep -qF -- "$expect" "$wd/stderr.log"; then - emit_event "$event" PASS "$name" + if grep -qF -- "$expect" "$CF_WORK/stderr.log"; then + cf_pass "$name" else - emit_event "$event" FAIL "$name (stderr did not contain: $expect)" - emit_event "$event" DUMP "$wd/stderr.log" + cf_fail "$name" "stderr did not contain: $expect" + sed 's/^/ | /' "$CF_WORK/stderr.log" fi - return 0 } -printf 'Layer C — negative read_elf inputs\n' -bad_files=( "$TEST_DIR"/bad/*.elf ) -if [ ${#bad_files[@]} -eq 0 ]; then - printf ' (no bad inputs yet)\n' -else - run_parallel_items "bad" run_bad_case "${bad_files[@]}" -fi +# ----- drive the three layers -------------------------------------------- + +printf 'Layer A — unit tests\n' +CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/unit" \ + CF_CORPUS_GLOBS="$TEST_DIR/unit/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/unit" \ + CF_LANES="A" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run printf '\n' -# ----- summary ----------------------------------------------------------- +printf 'Layer B — clang-oracle cases\n' +CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/cases" \ + CF_CORPUS_GLOBS="$TEST_DIR/cases/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/cases" \ + CF_LANES="B" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT=".targets" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run +printf '\n' -if [ $FAIL -gt 0 ]; then - printf 'Failures:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' - %s\n' "$n"; done - printf '\n' -fi -if [ $SKIP -gt 0 ] && [ "${CFREE_TEST_ALLOW_SKIP:-}" != "1" ]; then - printf 'Skips treated as failures (set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' - for n in "${SKIP_NAMES[@]}"; do printf ' - %s\n' "$n"; done - printf '\n' -fi +printf 'Layer C — negative read_elf inputs\n' +CF_LABEL=test-elf CF_BUILD_DIR="$BUILD_DIR/elf/bad" \ + CF_CORPUS_GLOBS="$TEST_DIR/bad/*.elf" CF_CORPUS_EXT=elf CF_SIDECAR_DIR="$TEST_DIR/bad" \ + CF_LANES="C" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run +printf '\n' -printf 'Summary: %s passed, %s failed, %s skipped\n' \ - "$(color_grn $PASS)" \ - "$([ $FAIL -eq 0 ] && echo $FAIL || color_red $FAIL)" \ - "$([ $SKIP -eq 0 ] && echo $SKIP || color_yel $SKIP)" +# ----- summary ----------------------------------------------------------- -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "${CFREE_TEST_ALLOW_SKIP:-}" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-elf +cf_exit diff --git a/test/emu/rv64_interp_smoke_test.c b/test/emu/rv64_interp_smoke_test.c @@ -25,6 +25,9 @@ #include <string.h> #include <sys/mman.h> #include <unistd.h> + +#include "lib/cfree_unit.h" + #if defined(__APPLE__) #include <mach/mach.h> #include <mach/mach_vm.h> @@ -42,35 +45,10 @@ /* Not in the public header; provided by the linked library objects. */ EmuCPUState* emu_internal_cpu(CfreeEmu*); -/* ---- Host heap / diag glue ---- */ -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; -static CfreeContext g_ctx; +/* One shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites below are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) /* ---- Dual-mapped (W^X) exec memory for the per-block JIT image (still built * in INTERP mode to resolve helper externs). Same shape as rv64_smoke_test. ---- */ @@ -213,45 +191,32 @@ static CfreeExecMem g_execmem = { 16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL, }; -static int g_fail; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) - static CfreeCompiler* new_host_compiler(void) { + CfreeArchKind arch; + CfreeOSKind os; + CfreeObjFmt obj; CfreeTarget t; CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); #if defined(__x86_64__) || defined(_M_X64) - t.arch = CFREE_ARCH_X86_64; + arch = CFREE_ARCH_X86_64; #elif defined(__aarch64__) || defined(_M_ARM64) - t.arch = CFREE_ARCH_ARM_64; + arch = CFREE_ARCH_ARM_64; #elif defined(__riscv) && __riscv_xlen == 64 - t.arch = CFREE_ARCH_RV64; + arch = CFREE_ARCH_RV64; #else return NULL; #endif #if defined(__APPLE__) - t.os = CFREE_OS_MACOS; - t.obj = CFREE_OBJ_MACHO; + os = CFREE_OS_MACOS; + obj = CFREE_OBJ_MACHO; #elif defined(__linux__) - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; + os = CFREE_OS_LINUX; + obj = CFREE_OBJ_ELF; #else return NULL; #endif - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + t = cfree_unit_target(arch, os, obj); + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "host compiler_new failed\n"); exit(2); } @@ -591,6 +556,8 @@ static void check_differential(const char* name, unsigned char* elf, int main(void) { size_t len = 0; + cfree_unit_init(&g_u); + /* (1) SD/LD/ecall: exercises the guest-memory helper path. */ check_differential("sd-ld-ecall", build_sd_ld_elf(&len, 99u), len, 99); @@ -598,7 +565,7 @@ int main(void) { * invalidation (a stale interp lookup would make INTERP diverge from JIT). */ check_differential("self-modifying-code", build_smc_elf(&len), len, 2); - fprintf(stderr, "interp-emu-smoke: %d failure%s\n", g_fail, - g_fail == 1 ? "" : "s"); - return g_fail ? 1 : 0; + fprintf(stderr, "interp-emu-smoke: %d failure%s\n", g_u.fails, + g_u.fails == 1 ? "" : "s"); + return g_u.fails ? 1 : 0; } diff --git a/test/emu/rv64_smoke_test.c b/test/emu/rv64_smoke_test.c @@ -48,37 +48,13 @@ * rv64_vm_unit_test.c, which links the library objects directly. */ #include "arch/rv64/isa.h" #include "core/core.h" +#include "lib/cfree_unit.h" #include "obj/elf/elf.h" -/* Host heap glue (same shape as test/api). */ -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; -static CfreeContext g_ctx; +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites below are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static int xm_to_posix(int p) { int q = 0; @@ -219,45 +195,32 @@ static CfreeExecMem g_execmem = { 16 * 1024, xm_reserve, xm_protect, xm_release, xm_flush, NULL, }; -static int g_fail; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) - static CfreeCompiler* new_host_compiler(void) { + CfreeArchKind arch; + CfreeOSKind os; + CfreeObjFmt obj; CfreeTarget t; CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); #if defined(__x86_64__) || defined(_M_X64) - t.arch = CFREE_ARCH_X86_64; + arch = CFREE_ARCH_X86_64; #elif defined(__aarch64__) || defined(_M_ARM64) - t.arch = CFREE_ARCH_ARM_64; + arch = CFREE_ARCH_ARM_64; #elif defined(__riscv) && __riscv_xlen == 64 - t.arch = CFREE_ARCH_RV64; + arch = CFREE_ARCH_RV64; #else return NULL; #endif #if defined(__APPLE__) - t.os = CFREE_OS_MACOS; - t.obj = CFREE_OBJ_MACHO; + os = CFREE_OS_MACOS; + obj = CFREE_OBJ_MACHO; #elif defined(__linux__) - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; + os = CFREE_OS_LINUX; + obj = CFREE_OBJ_ELF; #else return NULL; #endif - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + t = cfree_unit_target(arch, os, obj); + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "host compiler_new failed\n"); exit(2); } @@ -1234,6 +1197,7 @@ static void signal_sigreturn_smoke(void) { } int main(void) { + cfree_unit_init(&g_u); jit_vertical_smoke(); dynamic_import_tls_red(); host_import_bridge_smoke(); @@ -1242,8 +1206,8 @@ int main(void) { signal_perms_red(); signal_load_fault_smoke(); signal_sigreturn_smoke(); - if (g_fail) { - fprintf(stderr, "FAILED %d check(s)\n", g_fail); + if (g_u.fails) { + fprintf(stderr, "FAILED %d check(s)\n", g_u.fails); return 1; } fprintf(stderr, "OK\n"); diff --git a/test/emu/rv64_vm_unit_test.c b/test/emu/rv64_vm_unit_test.c @@ -19,61 +19,18 @@ #include "arch/rv64/isa.h" #include "core/core.h" #include "emu/emu.h" +#include "lib/cfree_unit.h" -/* ---- Host heap / diag glue ---- */ -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; -static CfreeContext g_ctx; - -static int g_fail; -#define EXPECT(cond, ...) \ - do { \ - if (!(cond)) { \ - ++g_fail; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) static CfreeCompiler* new_compiler(void) { - CfreeTarget t; + CfreeTarget t = cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX, + CFREE_OBJ_ELF); CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); - t.arch = CFREE_ARCH_RV64; - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "compiler_new failed\n"); exit(2); } @@ -81,33 +38,31 @@ static CfreeCompiler* new_compiler(void) { } static CfreeCompiler* new_host_compiler(void) { + CfreeArchKind arch; + CfreeOSKind os; + CfreeObjFmt obj; CfreeTarget t; CfreeCompiler* c = NULL; - memset(&t, 0, sizeof t); #if defined(__x86_64__) || defined(_M_X64) - t.arch = CFREE_ARCH_X86_64; + arch = CFREE_ARCH_X86_64; #elif defined(__aarch64__) || defined(_M_ARM64) - t.arch = CFREE_ARCH_ARM_64; + arch = CFREE_ARCH_ARM_64; #elif defined(__riscv) && __riscv_xlen == 64 - t.arch = CFREE_ARCH_RV64; + arch = CFREE_ARCH_RV64; #else return NULL; #endif #if defined(__APPLE__) - t.os = CFREE_OS_MACOS; - t.obj = CFREE_OBJ_MACHO; + os = CFREE_OS_MACOS; + obj = CFREE_OBJ_MACHO; #elif defined(__linux__) - t.os = CFREE_OS_LINUX; - t.obj = CFREE_OBJ_ELF; + os = CFREE_OS_LINUX; + obj = CFREE_OBJ_ELF; #else return NULL; #endif - t.ptr_size = 8; - t.ptr_align = 8; - memset(&g_ctx, 0, sizeof g_ctx); - g_ctx.heap = &g_heap; - g_ctx.diag = &g_diag; - if (cfree_compiler_new(t, &g_ctx, &c) != CFREE_OK || !c) { + t = cfree_unit_target(arch, os, obj); + if (cfree_unit_compiler_new(&g_u, t, &c) != CFREE_OK || !c) { fprintf(stderr, "host compiler_new failed\n"); exit(2); } @@ -328,13 +283,10 @@ static void linux_vm_syscall_smoke(void) { } int main(void) { + cfree_unit_init(&g_u); decoder_smoke(); vm_unit_smoke(); linux_vm_syscall_smoke(); - if (g_fail) { - fprintf(stderr, "FAILED %d check(s)\n", g_fail); - return 1; - } - fprintf(stderr, "OK\n"); - return 0; + cfree_unit_summary(&g_u, "emu_rv64_unit_test"); + return cfree_unit_status(&g_u); } diff --git a/test/interp/interp_smoke_test.c b/test/interp/interp_smoke_test.c @@ -15,6 +15,7 @@ #include "cg/ir.h" #include "interp/interp.h" +#include "lib/cfree_unit.h" #include "opt/opt.h" #undef Operand @@ -23,51 +24,13 @@ #undef CGCallDesc #undef CGLocalStorage -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static int g_fails; -static int g_checks; - -static void diag_sink(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_sink, NULL, 0, 0}; - -#define EXPECT(cond, ...) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. cfree_unit_init + * runs once in main (ctx.now is then set to -1 to match the original). */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) typedef struct TestCtx { - CfreeContext ctx; Compiler* c; CfreeCgTypeId i32; CfreeCgTypeId i64; @@ -77,16 +40,9 @@ static void tc_init(TestCtx* tc) { CfreeTarget target; CfreeCgBuiltinTypes b; memset(tc, 0, sizeof *tc); - tc->ctx.heap = &g_heap; - tc->ctx.diag = &g_diag; - tc->ctx.now = -1; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_MACOS; - target.obj = CFREE_OBJ_MACHO; - target.ptr_size = 8; - target.ptr_align = 8; - if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != CFREE_OK || + target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO); + if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) != + CFREE_OK || !tc->c) { fprintf(stderr, "fatal: compiler allocation failed\n"); abort(); @@ -239,12 +195,14 @@ static void interp_runs_branch(void) { } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; interp_runs_arithmetic(); interp_runs_branch(); - if (g_fails) { - fprintf(stderr, "interp-smoke: %d/%d failed\n", g_fails, g_checks); + if (g_u.fails) { + fprintf(stderr, "interp-smoke: %d/%d failed\n", g_u.fails, g_u.checks); return 1; } - printf("interp-smoke: %d checks, 0 failures\n", g_checks); + printf("interp-smoke: %d checks, 0 failures\n", g_u.checks); return 0; } diff --git a/test/lib/cf_corpus.sh b/test/lib/cf_corpus.sh @@ -0,0 +1,282 @@ +# test/lib/cf_corpus.sh — the unified corpus test harness engine. +# +# ONE harness for the lane-matrix corpus runners (parse/toy/asm/link/elf/wasm). +# A runner declares its corpus + lanes and calls cf_corpus_run; the engine owns +# discovery, the {case}x{opt}x{tuple} matrix, per-case workdirs, serial-or- +# parallel dispatch, event replay, deferred cross-arch exec, and per-lane +# timing. Reporting goes through cfree_sh_report.sh's mode-transparent verbs. +# +# Requires bash (indexed arrays). Sources, relative to $CF_LIB_DIR: +# cfree_sh_report.sh (cf_pass/cf_fail/cf_skip/cf_skip_na/cf_time/summary) +# cf_skip.sh (cf_skip_sidecar/cf_skip_diag/cf_tuple_applicable) +# parallel.sh (cfree_parallel_jobs/run/wait_all) +# exec_target.sh (exec_target_queue/flush/supported) [sourced by the runner if it uses path E] +# +# ============================================================================ +# RUNNER CONTRACT — set these before calling cf_corpus_run: +# CFREE binary under test +# CF_LABEL summary label (e.g. "toy") +# CF_BUILD_DIR scratch root; per-item workdirs live under it +# CF_CORPUS_GLOBS space-separated globs of case source files +# CF_CORPUS_EXT extension stripped for the basename (e.g. "toy","c","s") +# CF_SIDECAR_DIR dir holding sidecars (default: dir of the first glob) +# CF_LANES active lane ids, space-separated (runner derives from CFREE_TEST_PATHS) +# CF_OPT_LEVELS opt levels to expand (default "$CFREE_OPT_LEVELS" or "0 1"); +# set to "" for corpora with no opt axis (e.g. elf layers) +# CF_TUPLES target tuples "<arch>-<obj>" (default "$CF_DEFAULT_TUPLE") +# CF_OPT0ONLY lanes that run only at opt 0 (space list; e.g. "C W") +# CF_PARALLELIZABLE 1 to allow parallel dispatch (default 1); 0 forces serial +# CF_EXPECTED_EXT sidecar suffix for the expected exit code (default ".expected") +# CF_TARGETS_EXT sidecar suffix for whole-case tuple applicability (default ".targets"; empty disables) +# cf_lane_<ID>() per-lane hook (see below) +# CF_READ_CASE optional fn, called per item after basics are set, to read +# bespoke markers (may override CF_EXPECTED, set CF_SKIP_CASE/CF_SKIP_NA_CASE) +# CF_FLUSH_VERIFY optional fn, called per queued-E item after flush: args +# (label, payload, rc); return 0 to keep pass, 1 to fail +# +# Per-item vars the engine sets before each cf_lane_<ID> call: +# CF_BASE CF_SRC CF_WORK CF_OPT CF_LANE CF_ARCH CF_OBJ CF_TUPLE CF_EXPECTED +# CF_NAME (display label "base" or "base/Oopt") CF_SIDECAR_DIR +# A hook ends by calling exactly one of: cf_pass/cf_fail/cf_skip/cf_skip_na, OR +# cf_queue_e (deferred exec, resolved at flush). Hooks must write only under +# CF_WORK and record only via these verbs — that is what makes a runner +# parallel-safe by construction (flip CF_PARALLELIZABLE with no other change). +# ============================================================================ + +CF_LIB_DIR="${CF_LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +. "$CF_LIB_DIR/cfree_sh_report.sh" +. "$CF_LIB_DIR/cf_skip.sh" +. "$CF_LIB_DIR/parallel.sh" + +# Corpus runners treat an unexplained SKIP as a soft failure (gated by +# CFREE_TEST_ALLOW_SKIP); the driver-scenario harnesses leave this 0. +CF_SKIP_IS_FAILURE=1 + +# Millisecond clock for per-lane timing (python3 if present; else second res). +cf_now_ms() { + if command -v python3 >/dev/null 2>&1; then + python3 -c 'import time;print(int(time.time()*1000))' + else + echo $(( $(date +%s) * 1000 )) + fi +} + +# Deferred-exec bookkeeping (populated ONLY during serial execution / replay, +# never inside a worker — workers merely emit QUEUE_E events). +CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=() + +# cf_queue_e LABEL EXE OUT ERR RC_FILE EXPECTED EXEC_TAG [PAYLOAD] +# Record a case whose execution is deferred to a batched exec_target flush. +# In a worker: emit a QUEUE_E event (the parent does the actual queueing). +# In serial/replay: enqueue with exec_target and remember it for the flush. +cf_queue_e() { + if [ -n "${CF_EV:-}" ]; then + printf 'QUEUE_E\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$1" "$2" "$3" "$4" "$5" "$6" "$7" "${8:-}" >> "$CF_EV" + return + fi + exec_target_queue "$7" "$1" "$2" "$3" "$4" "$5" + CFQ_LABELS+=("$1"); CFQ_RCS+=("$5"); CFQ_EXPS+=("$6"); CFQ_PAYLOADS+=("${8:-}") +} + +# ---- discovery ------------------------------------------------------------- +cf_corpus_discover() { + CF_CASES=() + local g f base + shopt -s nullglob + for g in $CF_CORPUS_GLOBS; do + for f in $g; do + base=$(basename "$f") + [ -n "$CF_CORPUS_EXT" ] && base="${base%.$CF_CORPUS_EXT}" + if [ -n "${CFREE_TEST_FILTER:-}" ]; then + case "$base" in *"$CFREE_TEST_FILTER"*) ;; *) continue ;; esac + fi + CF_CASES+=("$f") + done + done +} + +# ---- per-item driver (runs every active lane for one case,opt,tuple) ------- +# Item encoding: "src|base|opt|tuple" (src/base never contain '|'). +cf_corpus_item() { + local item="$1" + CF_SRC="${item%%|*}"; item="${item#*|}" + CF_BASE="${item%%|*}"; item="${item#*|}" + CF_OPT="${item%%|*}"; CF_TUPLE="${item#*|}" + CF_ARCH="${CF_TUPLE%-*}"; CF_OBJ="${CF_TUPLE#*-}" + + if [ "$CF_OPT" = "-" ]; then + CF_NAME="$CF_BASE"; CF_WORK="$CF_BUILD_DIR/$CF_BASE" + else + CF_NAME="$CF_BASE/O$CF_OPT"; CF_WORK="$CF_BUILD_DIR/$CF_BASE/O$CF_OPT" + fi + rm -rf "$CF_WORK"; mkdir -p "$CF_WORK" + + CF_EXPECTED=0 + if [ -f "$CF_SIDECAR_DIR/$CF_BASE$CF_EXPECTED_EXT" ]; then + CF_EXPECTED=$(tr -d '[:space:]' < "$CF_SIDECAR_DIR/$CF_BASE$CF_EXPECTED_EXT") + fi + + # Hook for bespoke per-case marker reading (link's expected/jit_only/targets, + # etc.). May set CF_EXPECTED, CF_SKIP_CASE (reason), CF_SKIP_NA_CASE (1). + CF_SKIP_CASE=; CF_SKIP_NA_CASE= + if [ -n "${CF_READ_CASE:-}" ] && command -v "$CF_READ_CASE" >/dev/null 2>&1; then + "$CF_READ_CASE" + fi + if [ -n "$CF_SKIP_CASE" ]; then cf_skip "$CF_NAME" "$CF_SKIP_CASE"; return; fi + if [ -n "$CF_SKIP_NA_CASE" ]; then cf_skip_na "$CF_NAME"; return; fi + + # Whole-case sidecar skip (one event for the item, not per lane). + local reason + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "$CF_ARCH"); then + cf_skip "$CF_NAME" "$reason"; return + fi + # Whole-case tuple applicability (SKIP-NA, uncounted). + if [ -n "$CF_TARGETS_EXT" ] && + ! cf_tuple_applicable "$CF_TUPLE" "$CF_SIDECAR_DIR/$CF_BASE$CF_TARGETS_EXT"; then + cf_skip_na "$CF_NAME"; return + fi + + local lane + for lane in $CF_LANES; do + CF_LANE="$lane" + # opt-0-only lanes (C/W: backend ignores -O) run once, at opt 0. + case " $CF_OPT0ONLY " in + *" $lane "*) [ "$CF_OPT" = "0" ] || [ "$CF_OPT" = "-" ] || continue ;; + esac + # per-lane sidecar skip (.<lane>.skip) + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" "$lane"); then + cf_skip "$CF_NAME/$lane" "$reason"; continue + fi + "cf_lane_$lane" + done +} + +# ---- worker (parallel mode): isolate output to the event file -------------- +# Runs in a background subshell; the ONLY parent-visible channel is the event +# file (+ captured stdout/stderr logs). Ends with a DONE sentinel so the parent +# can tell "produced no records" (fine) from "crashed mid-case" (fail). +cf_corpus_worker() { + CF_EV="$3" + cf_corpus_item "$2" > "$4" 2> "$5" + printf 'DONE\n' >> "$CF_EV" +} + +# ---- replay one worker's event file through the counting verbs ------------- +cf_corpus_replay() { + local ev="$1" out="$2" err="$3" idx="$4" + if [ ! -s "$ev" ] || [ "$(tail -n1 "$ev" 2>/dev/null)" != "DONE" ]; then + cf_fail "internal/worker-$idx" "no DONE sentinel (worker crashed?)" + else + local kind a b c d e f g h + while IFS="$(printf '\t')" read -r kind a b c d e f g h; do + case "$kind" in + PASS) cf_pass "$a" ;; + FAIL) cf_fail "$a" "$b" ;; + SKIP) cf_skip "$a" "$b" ;; + SKIP_NA) cf_skip_na "$a" "$b" ;; + XFAIL) cf_xfail "$a" "$b" ;; + XPASS) cf_xpass "$a" ;; + TIME) cf_time "$a" "$b" ;; + QUEUE_E) cf_queue_e "$a" "$b" "$c" "$d" "$e" "$f" "$g" "$h" ;; + DONE) ;; + esac + done < "$ev" + fi + # Surface hook diagnostics (diffs/stderr) deterministically, in index order. + [ -s "$out" ] && cat "$out" + [ -s "$err" ] && cat "$err" >&2 + return 0 +} + +# ---- deferred-exec flush + per-case verification --------------------------- +cf_corpus_flush_e() { + [ "${#CFQ_LABELS[@]}" -eq 0 ] && return 0 + exec_target_flush + local i rc ok + for i in "${!CFQ_LABELS[@]}"; do + rc=127; [ -f "${CFQ_RCS[$i]}" ] && rc=$(cat "${CFQ_RCS[$i]}") + rc=$((rc & 255)) + ok=0; [ "$rc" -eq "$(( ${CFQ_EXPS[$i]} & 255 ))" ] && ok=1 + if [ "$ok" -eq 1 ] && [ -n "${CF_FLUSH_VERIFY:-}" ] && + command -v "$CF_FLUSH_VERIFY" >/dev/null 2>&1; then + "$CF_FLUSH_VERIFY" "${CFQ_LABELS[$i]}" "${CFQ_PAYLOADS[$i]}" "$rc" || ok=0 + fi + if [ "$ok" -eq 1 ]; then cf_pass "${CFQ_LABELS[$i]}" + else cf_fail "${CFQ_LABELS[$i]}" "expected ${CFQ_EXPS[$i]} got $rc"; fi + done + CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=() +} + +# ---- the entrypoint -------------------------------------------------------- +# Discovers + expands the matrix + dispatches (serial or parallel) + flushes +# deferred exec. Accumulates into the shared counters (so a runner may call it +# multiple times — e.g. elf's A/B/C layers — then summarize once). Does NOT +# print the summary or exit; the runner calls cf_summary "$CF_LABEL"; cf_exit. +cf_corpus_run() { + : "${CF_OPT_LEVELS=${CFREE_OPT_LEVELS:-0 1}}" + : "${CF_TUPLES:=${CF_DEFAULT_TUPLE:-aarch64-elf}}" + : "${CF_OPT0ONLY:=}" + : "${CF_PARALLELIZABLE:=1}" + : "${CF_EXPECTED_EXT:=.expected}" + : "${CF_TARGETS_EXT:=.targets}" + if [ -z "${CF_SIDECAR_DIR:-}" ]; then + set -- $CF_CORPUS_GLOBS; CF_SIDECAR_DIR=$(dirname "$1") + fi + + cf_corpus_discover + if [ "${#CF_CASES[@]}" -eq 0 ]; then + echo "$CF_LABEL: no cases under $CF_CORPUS_GLOBS" >&2 + exit 2 + fi + + # Build the flat work-item list = case x opt x tuple. + local opts="$CF_OPT_LEVELS"; [ -z "$opts" ] && opts="-" + local f base o t + CF_ITEMS=() + for f in "${CF_CASES[@]}"; do + base=$(basename "$f"); [ -n "$CF_CORPUS_EXT" ] && base="${base%.$CF_CORPUS_EXT}" + for o in $opts; do + for t in $CF_TUPLES; do + CF_ITEMS+=("$f|$base|$o|$t") + done + done + done + + local jobs; jobs="$(cfree_parallel_jobs)" || jobs=1 + if [ "$CF_PARALLELIZABLE" = "1" ] && [ "$jobs" -gt 1 ] && [ "${#CF_ITEMS[@]}" -gt 4 ]; then + cf_corpus_dispatch_parallel "$jobs" + else + local idx=0 + for item in "${CF_ITEMS[@]}"; do + CF_EV=; cf_corpus_item "$item" + idx=$((idx + 1)) + done + fi + + cf_corpus_flush_e +} + +cf_corpus_dispatch_parallel() { + local jobs="$1" + local pdir="$CF_BUILD_DIR/.parallel.$$" + rm -rf "$pdir"; mkdir -p "$pdir" + local evs=() outs=() errs=() idx=0 ev out err + for item in "${CF_ITEMS[@]}"; do + ev="$pdir/$idx.events"; out="$pdir/$idx.out"; err="$pdir/$idx.err" + : > "$ev" + evs+=("$ev"); outs+=("$out"); errs+=("$err") + cfree_parallel_run "$jobs" cf_corpus_worker "$idx" "$item" "$ev" "$out" "$err" + idx=$((idx + 1)) + done + cfree_parallel_wait_all || true + # Serial replay in strict index order -> deterministic counts + output + + # exec_target queueing (which happens here, never in a worker). + CF_EV= + local i=0 + while [ "$i" -lt "${#evs[@]}" ]; do + cf_corpus_replay "${evs[$i]}" "${outs[$i]}" "${errs[$i]}" "$i" + i=$((i + 1)) + done + rm -rf "$pdir" +} diff --git a/test/lib/cf_corpus_selftest.sh b/test/lib/cf_corpus_selftest.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# test/lib/cf_corpus_selftest.sh — hermetic proof of the cf_corpus.sh engine. +# +# No cfree binary, no podman/qemu: stubs exec_target_queue/flush and uses fake +# lanes over a throwaway corpus. Asserts the properties that make the engine +# safe to build runners on: +# 1. serial and parallel runs produce IDENTICAL counts + failing-name order +# + identical output (modulo timing) — determinism; +# 2. SKIP-NA is uncounted and never gates; +# 3. deferred-exec (cf_queue_e) flushes correctly and detects a wrong rc; +# 4. exec_target_queue is called exactly once per E item DURING THE PARENT +# (serial replay), never in a worker — if a worker queued, the parent +# count would be 0 (background-subshell mutations are lost). This is the +# invariant the whole parallel model rests on. +# +# Run: bash test/lib/cf_corpus_selftest.sh (exit 0 = ok) + +set -u +SELF_DIR=$(cd "$(dirname "$0")" && pwd) +export CF_LIB_DIR="$SELF_DIR" +. "$SELF_DIR/cf_corpus.sh" + +WORK=$(mktemp -d "${TMPDIR:-/tmp}/cf-corpus-selftest.XXXXXX") +trap 'rm -rf "$WORK"' EXIT +CORPUS="$WORK/corpus"; mkdir -p "$CORPUS" +i=1; while [ "$i" -le 12 ]; do printf 'x' > "$CORPUS/$(printf 'case%02d' "$i").t"; i=$((i+1)); done +# case05 is not-applicable to the test tuple. +printf 'other-z\n' > "$CORPUS/case05.targets" + +# ---- exec_target stubs (count parent-side queue calls) --------------------- +ET_QUEUE_CALLS=0 +_et_pairs= +exec_target_queue() { # TAG NAME EXE OUT ERR RC + ET_QUEUE_CALLS=$((ET_QUEUE_CALLS + 1)) + _et_pairs="$_et_pairs $3:$6" # exe:rcfile (exe file content = rc to emit) +} +exec_target_flush() { + local pair exe rcf + for pair in $_et_pairs; do exe="${pair%%:*}"; rcf="${pair#*:}"; cat "$exe" > "$rcf"; done + _et_pairs= +} + +# ---- fake lanes ------------------------------------------------------------ +cf_lane_P() { cf_pass "$CF_NAME/P"; cf_time P 1; } +cf_lane_S() { + if [ "$CF_BASE" = "case03" ]; then cf_skip "$CF_NAME/S" "deliberate"; else cf_pass "$CF_NAME/S"; fi +} +cf_lane_E() { + local exe="$CF_WORK/run" rcf="$CF_WORK/rc" + if [ "$CF_BASE" = "case07" ]; then echo 9 > "$exe"; else echo 0 > "$exe"; fi + cf_queue_e "$CF_NAME/E" "$exe" "$CF_WORK/o" "$CF_WORK/e" "$rcf" "$CF_EXPECTED" "faketag" +} + +# Run in the CURRENT shell (NOT $(...) — command substitution is a subshell +# that would discard the counter globals); redirect output to a file instead. +export CFREE_TEST_FILTER= +run_corpus() { # $1 = CF_PARALLELIZABLE $2 = jobs $3 = outfile + cf_report_init + CFQ_LABELS=(); CFQ_RCS=(); CFQ_EXPS=(); CFQ_PAYLOADS=() + ET_QUEUE_CALLS=0; _et_pairs= + CF_LABEL=selftest CF_BUILD_DIR="$WORK/b$1" CF_CORPUS_GLOBS="$CORPUS/*.t" \ + CF_CORPUS_EXT=t CF_SIDECAR_DIR="$CORPUS" CF_LANES="P S E" CF_OPT_LEVELS="0" \ + CF_TUPLES="aa64-elf" CF_TARGETS_EXT=.targets CF_PARALLELIZABLE="$1" \ + CFREE_TEST_JOBS="$2" cf_corpus_run > "$3" 2>&1 + cf_summary selftest >> "$3" 2>&1 +} + +NORM='s/[0-9]\{1,\}ms/Nms/g' # erase timing for output comparison + +run_corpus 0 1 "$WORK/ser.out" +ser_out=$(cat "$WORK/ser.out") +ser_counts="$CF_PASS/$CF_FAIL/$CF_SKIP/$CF_SKIP_NA"; ser_fails="$CF_FAILURES"; ser_q=$ET_QUEUE_CALLS + +run_corpus 1 4 "$WORK/par.out" +par_out=$(cat "$WORK/par.out") +par_counts="$CF_PASS/$CF_FAIL/$CF_SKIP/$CF_SKIP_NA"; par_fails="$CF_FAILURES"; par_q=$ET_QUEUE_CALLS + +fail=0 +check() { if [ "$2" = "$3" ]; then echo " ok $1 ($2)"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } + +echo "cf_corpus selftest:" +check "serial counts pass/fail/skip/na" "$ser_counts" "31/1/1/1" +check "parallel counts pass/fail/skip/na" "$par_counts" "31/1/1/1" +check "serial == parallel counts" "$ser_counts" "$par_counts" +check "serial == parallel failing names" "$ser_fails" "$par_fails" +check "failing name is case07/O0/E" "$(echo "$ser_fails" | tr -s ' ')" " case07/O0/E" +check "serial queue calls (parent)" "$ser_q" "11" +check "parallel queue calls (parent, not worker)" "$par_q" "11" +# Determinism of full output (counts/order/summary), timing erased. +if [ "$(printf '%s' "$ser_out" | sed "$NORM")" = "$(printf '%s' "$par_out" | sed "$NORM")" ]; then + echo " ok serial output == parallel output (timing-normalized)" +else + echo " FAIL serial/parallel output differ:"; diff <(printf '%s' "$ser_out" | sed "$NORM") <(printf '%s' "$par_out" | sed "$NORM") | head -30; fail=1 +fi + +[ "$fail" -eq 0 ] && echo "cf_corpus selftest: OK" || echo "cf_corpus selftest: FAILED" +exit $fail diff --git a/test/lib/cf_differential.sh b/test/lib/cf_differential.sh @@ -0,0 +1,49 @@ +# test/lib/cf_differential.sh — Type D (differential / agreement) helpers. +# +# Sourced AFTER cfree_sh_report.sh (a D harness sources both). Type D's oracle +# is AGREEMENT, not a per-case fixed expectation: +# - baseline mode: a freshly-generated, normalized report must equal a +# checked-in baseline snapshot (which documents a known backlog). +# CF_DIFF_UPDATE=1 regenerates the baseline instead of comparing. +# - reference mode: two independent producers (e.g. cfree vs a third-party +# tool) must agree byte-for-byte, with explicit equivalence-skips for +# known-benign divergences (the caller cf_skip's those before comparing). +# Both record exactly one verdict through cf_pass/cf_fail/cf_skip, so summary, +# exit, color, and skip-gating match every other harness. A D harness still +# calls cf_report_init at the top and cf_summary LABEL + cf_exit at the end. + +# cf_diff_baseline LABEL BASELINE ACTUAL +# ACTUAL is a freshly produced + already-normalized report file; it must +# equal the checked-in BASELINE. CF_DIFF_UPDATE=1 rewrites BASELINE from +# ACTUAL and passes (the regen path). On mismatch, fail and show the unified +# delta so the new asymmetries are visible. +cf_diff_baseline() { + if [ "${CF_DIFF_UPDATE:-0}" = "1" ]; then + cp "$3" "$2" + cf_pass "$1 (baseline updated)" + return + fi + if [ ! -f "$2" ]; then + cf_fail "$1" "missing baseline $(basename "$2")" + return + fi + if diff -u "$2" "$3" >/dev/null 2>&1; then + cf_pass "$1" + else + cf_fail "$1" "report drifted from baseline (run with CF_DIFF_UPDATE=1 to refresh)" + diff -u "$2" "$3" || true + fi +} + +# cf_diff_agree LABEL A B +# A and B (two producers' outputs over the same input) must be byte-equal. +# The caller is responsible for equivalence-skips: cf_skip the case BEFORE +# calling this when a known-benign divergence applies, and skip the call. +cf_diff_agree() { + if cmp -s "$2" "$3"; then + cf_pass "$1" + else + cf_fail "$1" "producers disagree" + cmp "$2" "$3" 2>&1 | head -3 || true + fi +} diff --git a/test/lib/cf_skip.sh b/test/lib/cf_skip.sh @@ -0,0 +1,52 @@ +# test/lib/cf_skip.sh — unified skip detection for the corpus harness. +# +# Sourced (POSIX sh). Three independent skip sources the corpus runners all +# reimplemented; each returns 0 and echoes a reason when it fires, else 1. + +# cf_skip_sidecar DIR BASE [ARCH] [LANE] +# Looks, in priority order, for sidecar files next to a case: +# DIR/BASE.skip — skip the whole case on every arch/lane +# DIR/BASE.<ARCH>.skip — skip on this arch (when ARCH given) +# DIR/BASE.<LANE>.skip — skip just this lane (when LANE given; covers +# .cbackend.skip via LANE=cbackend, .wasm.skip +# via LANE=wasm, toy's .link.skip via LANE=link) +# Echoes the first line of the matching sidecar as the reason. +cf_skip_sidecar() { + cf_sk_dir=$1; cf_sk_base=$2; cf_sk_arch=${3:-}; cf_sk_lane=${4:-} + for cf_sk_f in \ + "$cf_sk_dir/$cf_sk_base.skip" \ + ${cf_sk_arch:+"$cf_sk_dir/$cf_sk_base.$cf_sk_arch.skip"} \ + ${cf_sk_lane:+"$cf_sk_dir/$cf_sk_base.$cf_sk_lane.skip"}; do + if [ -e "$cf_sk_f" ]; then + head -n1 "$cf_sk_f" 2>/dev/null + return 0 + fi + done + return 1 +} + +# cf_skip_diag ERRFILE REGEX +# Phased-rollout detector: if ERRFILE contains a line matching the extended +# regex REGEX (a backend's "not yet implemented" diagnostic), echo the +# matched text as the reason and return 0. Each backend passes its own regex +# (e.g. 'interp: .*not supported', 'C target: .*not (implemented|yet supported)', +# 'wasm.*not yet implemented'), keeping the panic format local to its lane. +cf_skip_diag() { + [ -f "$1" ] || return 1 + cf_sd_hit=$(grep -oE "$2" "$1" 2>/dev/null | head -n1 || true) + [ -n "$cf_sd_hit" ] || return 1 + printf '%s' "$cf_sd_hit" + return 0 +} + +# cf_tuple_applicable TUPLE TARGETS_FILE +# Returns 0 (applicable) if TARGETS_FILE is absent/empty, or lists TUPLE +# (whitespace-separated <arch>-<obj> tuples). Returns 1 (not applicable -> +# caller emits SKIP_NA) when the file exists and does NOT list TUPLE. +cf_tuple_applicable() { + [ -s "$2" ] || return 0 + for cf_ta_t in $(cat "$2"); do + [ "$cf_ta_t" = "$1" ] && return 0 + done + return 1 +} diff --git a/test/lib/cfree_sh_assert.sh b/test/lib/cfree_sh_assert.sh @@ -0,0 +1,101 @@ +# test/lib/cfree_sh_assert.sh — Type-K mode-P procedural assert verbs + the +# CAS object/tree helpers shared by cas/ and pkg/. +# +# Sourced AFTER cfree_sh_report.sh (cfree_sh_kit.sh does this). The verbs route +# through cf_pass/cf_fail/cf_skip so counting, summary, and exit are unified — +# a harness calls cf_report_init at the top and cf_summary/cf_exit at the end, +# never its own counters. +# +# Contract: the caller provides a writable scratch dir $work; per-check +# .out/.err/.diag files are written under it. Mode-P suites are SERIAL (they +# share one $work and mutate fixtures), so these verbs do not emit CF_EV +# events / participate in the corpus engine's parallel replay. + +# ---- result verbs (thin wrappers over the report layer) -------------------- + +ok() { cf_pass "$1"; } + +# not_ok NAME [DIAGFILE] : record a failure; if DIAGFILE is given and non-empty, +# show it indented after the FAIL line. +not_ok() { + cf_fail "$1" + if [ "$#" -gt 1 ] && [ -s "$2" ]; then sed 's/^/ | /' "$2"; fi +} + +skip_test() { cf_skip "$1" "${2:-}"; } + +# ---- command-result asserts ------------------------------------------------ + +run_ok() { + name=$1; shift + if "$@" > "$work/$name.out" 2> "$work/$name.err"; then ok "$name" + else not_ok "$name" "$work/$name.err"; fi +} + +run_fail() { + name=$1; shift + if "$@" > "$work/$name.out" 2> "$work/$name.err"; then + { echo "command unexpectedly succeeded"; sed 's/^/stdout: /' "$work/$name.out"; } > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + else ok "$name"; fi +} + +# ---- content asserts ------------------------------------------------------- + +contains() { + name=$1; file=$2; needle=$3 + if grep -F "$needle" "$file" >/dev/null 2>&1; then ok "$name" + else + { printf 'missing text: %s\n' "$needle"; sed 's/^/file: /' "$file"; } > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + fi +} + +same_file() { + name=$1; want=$2; got=$3 + if cmp -s "$want" "$got"; then ok "$name" + else + { printf 'files differ:\n'; printf ' want: %s\n' "$want"; printf ' got: %s\n' "$got"; } > "$work/$name.diag" + not_ok "$name" "$work/$name.diag" + fi +} + +is_executable() { + name=$1; file=$2 + if [ -x "$file" ]; then ok "$name" + else echo "not executable: $file" > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi +} + +assert_file_exists() { + name=$1; file=$2 + if [ -f "$file" ]; then ok "$name" + else echo "missing file: $file" > "$work/$name.diag"; not_ok "$name" "$work/$name.diag"; fi +} + +# ---- CAS object/tree helpers (shared by cas + pkg) ------------------------- + +first_hex_id() { + sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p' +} + +id_prefix() { + printf '%.2s' "$1" +} + +cas_object_path() { + root=$1; kind=$2; id=$3 + prefix=$(id_prefix "$id") + printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id" +} + +tree_blob_for_path() { + tree_file=$1; want=$2 + awk -v want="$want" ' + $0 == "[file]" { in_file = 1; path = ""; blob = ""; next } + in_file && /^path = / { path = substr($0, 8); next } + in_file && /^blob = / { + blob = substr($0, 8); + if (path == want) { print blob; exit; } + } + ' "$tree_file" +} diff --git a/test/lib/cfree_sh_kit.sh b/test/lib/cfree_sh_kit.sh @@ -0,0 +1,86 @@ +# test/lib/cfree_sh_kit.sh — the Type-K (scripted-shell) test kit. +# +# ONE source point for hand-written tool/driver tests. A K harness sources this +# file and renders verdicts via ONE OF TWO oracle modes, both recording through +# the single report layer (cfree_sh_report.sh): +# +# mode G (golden transcript): loop cases/<name>.sh and call +# cf_scenario_case NAME SH EXPECTED — runs the case in a sandbox and +# diffs stdout+stderr vs <name>.expected. (ar/strip/objcopy/strings/ +# objdump/dbg.) cf_scenario_case lives in cfree_sh_report.sh. +# mode P (procedural asserts): set up $work and call ok / run_ok / run_fail / +# contains / same_file / is_executable / assert_file_exists / +# check_mode. (cas/pkg/driver/windows-smokes.) These come from +# cfree_sh_assert.sh and route through cf_pass/cf_fail/cf_skip. +# +# Either way: cf_report_init at the top, cf_summary LABEL + cf_exit at the end. +# No harness reimplements counters/summary. Mode-P suites are SERIAL by design +# (cas/pkg/driver share one $work and mutate fixtures), so the assert verbs do +# NOT participate in the corpus engine's parallel event-replay. + +# The caller sets CF_KIT_DIR to the test/lib directory before sourcing. (This +# kit is sourced by /bin/sh harnesses, where BASH_SOURCE is unavailable, so we +# do not self-resolve.) +: "${CF_KIT_DIR:?cfree_sh_kit.sh: caller must set CF_KIT_DIR=<repo>/test/lib}" +. "$CF_KIT_DIR/cfree_sh_report.sh" # counters + verbs + cf_summary/cf_exit +. "$CF_KIT_DIR/cfree_sh_assert.sh" # mode-P procedural verbs (route through cf_*) + +# check_mode NAME FILE EXPECTED_OCTAL : assert FILE's permission bits, portably +# (Darwin stat -f vs GNU stat -c). The stat invocation is the one host-specific +# bit; the verdict goes through cf_pass/cf_fail. +check_mode() { + cf_km_got= + if [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then + cf_km_got=$(stat -f '%Lp' "$2" 2>/dev/null) + else + cf_km_got=$(stat -c '%a' "$2" 2>/dev/null) + fi + if [ "$cf_km_got" = "$3" ]; then cf_pass "$1" + else cf_fail "$1" "mode $cf_km_got want $3"; fi +} + +# cf_scenario_case NAME SH EXPECTED [KEEP_DIR] : mode-G oracle. Run scenario +# script SH in a sandbox under $CF_WORK and diff stdout+stderr against the +# golden EXPECTED. Records exactly one pass/fail/skip; honors a leading "SKIP" +# line when CF_SCENARIO_SKIP=1. (ar/strip/objcopy/strings/objdump; dbg extends +# it with stdin/normalizer/xfail in its own migration.) +cf_scenario_case() { + cf_sc_name=$1 + cf_sc_sh=$2 + cf_sc_exp=$3 + cf_sc_keep=${4:-} + cf_sc_tag=$(printf '%s' "$cf_sc_name" | tr / -) + cf_sc_actual="$CF_WORK/$cf_sc_tag.actual" + + if [ ! -e "$cf_sc_exp" ]; then + cf_fail "$cf_sc_name" "missing $(basename "$cf_sc_exp")" + return + fi + + cf_sc_box="$CF_WORK/$cf_sc_tag" + mkdir -p "$cf_sc_box" + ( cd "$cf_sc_box" && sh "$cf_sc_sh" ) > "$cf_sc_actual" 2>&1 + cf_sc_rc=$? + + if [ "${CF_SCENARIO_SKIP:-0}" = "1" ] && [ "$cf_sc_rc" -eq 0 ] && + head -n1 "$cf_sc_actual" 2>/dev/null | grep -q '^SKIP'; then + cf_skip "$cf_sc_name" "$(head -n1 "$cf_sc_actual" | sed 's/^SKIP[: ]*//')" + return + fi + + if [ "$cf_sc_rc" -ne 0 ]; then + cf_fail "$cf_sc_name" "script exit=$cf_sc_rc" + diff -u "$cf_sc_exp" "$cf_sc_actual" || true + return + fi + + if diff -u "$cf_sc_exp" "$cf_sc_actual" >/dev/null 2>&1; then + cf_pass "$cf_sc_name" + else + cf_fail "$cf_sc_name" + diff -u "$cf_sc_exp" "$cf_sc_actual" || true + if [ -n "$cf_sc_keep" ]; then + cp "$cf_sc_actual" "$cf_sc_keep/$cf_sc_name.actual" 2>/dev/null || true + fi + fi +} diff --git a/test/lib/cfree_sh_report.sh b/test/lib/cfree_sh_report.sh @@ -0,0 +1,175 @@ +# test/lib/cfree_sh_report.sh — the single shared reporting layer for every +# cfree shell test harness (driver-scenario AND corpus runners). +# +# Sourced. POSIX sh (works under /bin/sh and bash 3.2): counters are scalars, +# name lists are space-joined strings, per-lane timing uses eval'd dynamic +# vars — no bash arrays here, so the /bin/sh driver harnesses can source it. +# (The corpus engine cf_corpus.sh adds the bash-only matrix/dispatch on top.) +# +# THE KEY SEAM — mode-transparent result verbs. cf_pass/cf_fail/cf_skip/ +# cf_skip_na/cf_time normally bump counters and print. But when CF_EV is set +# (the harness ran the case in a background worker), they instead append a +# tab-separated event to "$CF_EV"; the parent later replays those events in +# index order through the SAME verbs (CF_EV unset during replay), so counts, +# failing-name order, and printed output are deterministic regardless of +# worker completion order. A lane hook calls cf_pass/... and never knows or +# cares which mode it is in — that is what makes parallelism a pure flag flip. +# +# Color is emitted only to a TTY and honors NO_COLOR, so piped/CI output is +# plain text. + +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + _CF_RED=$(printf '\033[31m'); _CF_GRN=$(printf '\033[32m') + _CF_YEL=$(printf '\033[33m'); _CF_RST=$(printf '\033[0m') +else + _CF_RED=; _CF_GRN=; _CF_YEL=; _CF_RST= +fi + +# ---- state ----------------------------------------------------------------- +CF_PASS=0 +CF_FAIL=0 +CF_SKIP=0 +CF_SKIP_NA=0 # "not applicable" (e.g. arch tuple filtered) — tracked, never a failure +CF_XFAIL=0 # expected failures (known-red cases that failed as expected) +CF_XPASS=0 # xfail cases that unexpectedly passed (a real failure under CF_STRICT_XFAIL) +CF_FAILURES= +CF_SKIPS= +CF_TIME_LANES= # space-joined lane ids that have accrued time +# CF_EV — when set, verbs emit events here instead of counting +# CF_SKIP_IS_FAILURE — corpus runners set =1 so SKIP gates the exit (unless CFREE_TEST_ALLOW_SKIP=1) + +cf_report_init() { + CF_PASS=0; CF_FAIL=0; CF_SKIP=0; CF_SKIP_NA=0; CF_XFAIL=0; CF_XPASS=0 + CF_FAILURES=; CF_SKIPS=; CF_TIME_LANES= +} + +# ---- mode-transparent result verbs ---------------------------------------- + +cf_pass() { + if [ -n "${CF_EV:-}" ]; then printf 'PASS\t%s\n' "$1" >> "$CF_EV"; return; fi + CF_PASS=$((CF_PASS + 1)) + printf ' %sPASS%s %s\n' "$_CF_GRN" "$_CF_RST" "$1" +} + +# cf_fail NAME [DETAIL] : NAME is recorded in the failures list; DETAIL (single +# line) is shown only on the printed line. +cf_fail() { + if [ -n "${CF_EV:-}" ]; then printf 'FAIL\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi + CF_FAIL=$((CF_FAIL + 1)) + CF_FAILURES="$CF_FAILURES $1" + if [ -n "${2:-}" ]; then + printf ' %sFAIL%s %s (%s)\n' "$_CF_RED" "$_CF_RST" "$1" "$2" + else + printf ' %sFAIL%s %s\n' "$_CF_RED" "$_CF_RST" "$1" + fi +} + +cf_skip() { + if [ -n "${CF_EV:-}" ]; then printf 'SKIP\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi + CF_SKIP=$((CF_SKIP + 1)) + CF_SKIPS="$CF_SKIPS $1" + if [ -n "${2:-}" ]; then + printf ' %sSKIP%s %s — %s\n' "$_CF_YEL" "$_CF_RST" "$1" "$2" + else + printf ' %sSKIP%s %s\n' "$_CF_YEL" "$_CF_RST" "$1" + fi +} + +# cf_skip_na : "not applicable" — a case/lane that structurally does not apply +# to the current target tuple. Counted separately and NEVER gates the exit. +cf_skip_na() { + if [ -n "${CF_EV:-}" ]; then printf 'SKIP_NA\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi + CF_SKIP_NA=$((CF_SKIP_NA + 1)) + # quiet by default; uncomment for verbose n/a tracing + [ -n "${CF_VERBOSE_NA:-}" ] && printf ' %sn/a%s %s — %s\n' "$_CF_YEL" "$_CF_RST" "$1" "${2:-}" + return 0 +} + +# cf_xfail NAME [REASON] : an expected failure (a known-red case that failed as +# expected). Counted as xfail and does NOT gate the exit — UNLESS +# CF_STRICT_XFAIL=1 (e.g. dbg's DBG_STRICT_XFAIL), under which even expected +# failures are promoted to a hard failure (no known-red allowed in strict mode). +cf_xfail() { + if [ -n "${CF_EV:-}" ]; then printf 'XFAIL\t%s\t%s\n' "$1" "${2:-}" >> "$CF_EV"; return; fi + if [ "${CF_STRICT_XFAIL:-0}" = "1" ]; then + cf_fail "$1" "expected failure not allowed under strict xfail${2:+ ($2)}"; return + fi + CF_XFAIL=$((CF_XFAIL + 1)) + printf ' %sXFAIL%s %s%s\n' "$_CF_YEL" "$_CF_RST" "$1" "${2:+ — $2}" +} + +# cf_xpass NAME : a case marked xfail that UNEXPECTEDLY passed — the xfail +# marker is stale and should be removed. This is ALWAYS a failure (cf_exit +# gates on CF_XPASS), matching the lit/DejaGnu convention and dbg's original +# always-count-xpass-as-failure behavior. Tracked in its own bucket so the +# summary distinguishes it from a plain FAIL. +cf_xpass() { + if [ -n "${CF_EV:-}" ]; then printf 'XPASS\t%s\n' "$1" >> "$CF_EV"; return; fi + CF_XPASS=$((CF_XPASS + 1)) + printf ' %sXPASS%s %s (unexpected pass — xfail marker is stale)\n' "$_CF_RED" "$_CF_RST" "$1" +} + +# cf_time LANE MS : accumulate per-lane wall time for the summary's Time line. +cf_time() { + if [ -n "${CF_EV:-}" ]; then printf 'TIME\t%s\t%s\n' "$1" "$2" >> "$CF_EV"; return; fi + case " $CF_TIME_LANES " in + *" $1 "*) ;; + *) CF_TIME_LANES="$CF_TIME_LANES $1" ;; + esac + eval "cf_t_$1=\$(( \${cf_t_$1:-0} + $2 ))" +} + +# ---- summary + exit -------------------------------------------------------- + +# cf_summary LABEL : unified summary — failing/skipped name lists, the +# "P pass, F fail, S skip" line (+ " N n/a" when any), and a per-lane Time +# line when timing was recorded. +cf_summary() { + if [ -n "$CF_FAILURES" ]; then printf '\n%s: failures:%s\n' "$1" "$CF_FAILURES"; fi + if [ -n "$CF_SKIPS" ]; then printf '%s: skipped:%s\n' "$1" "$CF_SKIPS"; fi + cf_sm_extra= + [ "$CF_SKIP_NA" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_SKIP_NA n/a" + [ "$CF_XFAIL" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_XFAIL xfail" + [ "$CF_XPASS" -gt 0 ] && cf_sm_extra="$cf_sm_extra, $CF_XPASS xpass" + printf '\n%s: %d pass, %d fail, %d skip%s\n' "$1" "$CF_PASS" "$CF_FAIL" "$CF_SKIP" "$cf_sm_extra" + if [ -n "$CF_TIME_LANES" ]; then + cf_sm_t="$1: time" + for cf_sm_l in $CF_TIME_LANES; do + eval "cf_sm_v=\${cf_t_$cf_sm_l:-0}" + cf_sm_t="$cf_sm_t $cf_sm_l=${cf_sm_v}ms" + done + printf '%s\n' "$cf_sm_t" + fi +} + +# cf_exit : exit 1 on any failure or any XPASS (stale xfail); also exit 1 on +# skips when CF_SKIP_IS_FAILURE=1 unless CFREE_TEST_ALLOW_SKIP=1. SKIP_NA and +# XFAIL never gate (XFAIL only via cf_xfail's strict promotion to FAIL). +cf_exit() { + [ "$CF_FAIL" -gt 0 ] && exit 1 + [ "${CF_XPASS:-0}" -gt 0 ] && exit 1 + if [ "${CF_SKIP_IS_FAILURE:-0}" = "1" ] && [ "$CF_SKIP" -gt 0 ] && + [ "${CFREE_TEST_ALLOW_SKIP:-0}" != "1" ]; then + exit 1 + fi + exit 0 +} + +# ---- harness setup helpers ------------------------------------------------- + +# cf_require_cfree LABEL : ensure $CFREE points at an executable, else exit 2. +cf_require_cfree() { + if [ ! -x "${CFREE:-}" ]; then + echo "$1: cfree binary not found at ${CFREE:-<unset>}" >&2 + exit 2 + fi +} + +# cf_workdir TOOL : mktemp -d under TMPDIR with the canonical name and install +# an EXIT trap removing it. Echoes the path. NOTE: the EXIT trap is a singleton; +# call once per harness. +cf_workdir() { + cf_wd_dir=$(mktemp -d "${TMPDIR:-/tmp}/cfree-$1-test.XXXXXX") + trap 'rm -rf "$cf_wd_dir"' EXIT + printf '%s' "$cf_wd_dir" +} diff --git a/test/lib/cfree_unit.h b/test/lib/cfree_unit.h @@ -0,0 +1,180 @@ +/* test/lib/cfree_unit.h — shared scaffolding for cfree C unit tests. + * + * The unit suite copy-pasted the same prologue into ~18 files: a + * malloc/realloc/free shim wrapping the host allocator into a CfreeHeap, a + * diagnostic sink that prints to stderr, an EXPECT/CHECK macro, a + * CfreeTarget + CfreeContext + cfree_compiler_new dance, and a pass/fail + * counter checked in main(). This header folds all of that into one + * stack-resident CfreeUnit context. + * + * Design constraints honored: + * - No global mutable state: every test threads a CfreeUnit it owns + * (matches the project's "everything hangs off a context struct" rule). + * - Public headers only (<cfree/core.h>), so the header compiles + * unchanged in BOTH linkage regimes: linking the public archive + * (LIB_AR, where ld -r has hidden internal symbols) and linking the raw + * objects (LIB_OBJS + -Isrc, internal symbols visible). It never names + * an internal symbol, so a test that additionally pulls internal + * headers (its own #include "abi/abi.h" etc.) is unaffected. + * - Every helper is `static inline` so a TU that uses only part of the + * API does not trip -Werror,-Wunused-function (verified: unused + * `static inline` is warning-clean; unused bare `static` is not). + * + * The build rule adds -Itest, so include as: + * #include "lib/cfree_unit.h" + */ + +#ifndef CFREE_TEST_LIB_CFREE_UNIT_H +#define CFREE_TEST_LIB_CFREE_UNIT_H + +#include <cfree/core.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* One context per test. Zero-initialize with cfree_unit_init. The diag sink + * captures the most recent message body into last_diag (before any + * suppression), which expected-panic tests strstr() against. */ +typedef struct CfreeUnit { + CfreeHeap heap; + CfreeDiagSink diag; + CfreeContext ctx; + char last_diag[256]; + int suppress_fatal; /* when set, FATAL diagnostics are captured but not + * printed — used by tests that drive a deliberate + * panic and assert on last_diag. */ + int checks; /* total CU_CHECK/CU_EXPECT evaluations */ + int fails; /* subset that failed */ +} CfreeUnit; + +static inline void* cfree_unit_alloc(CfreeHeap* h, size_t n, size_t a) { + (void)h; + (void)a; + return n ? malloc(n) : NULL; +} + +static inline void* cfree_unit_realloc(CfreeHeap* h, void* p, size_t old_n, + size_t n, size_t a) { + (void)h; + (void)old_n; + (void)a; + return realloc(p, n); +} + +static inline void cfree_unit_free(CfreeHeap* h, void* p, size_t n) { + (void)h; + (void)n; + free(p); +} + +static inline void cfree_unit_diag_emit(CfreeDiagSink* s, CfreeDiagKind k, + CfreeSrcLoc loc, const char* fmt, + va_list ap) { + static const char* const names[] = {"note", "warning", "error", "fatal"}; + CfreeUnit* u = (CfreeUnit*)s->user; + va_list copy; + (void)loc; + /* Capture the message body first so it is available even when suppressed. */ + if (u) { + va_copy(copy, ap); + vsnprintf(u->last_diag, sizeof u->last_diag, fmt, copy); + va_end(copy); + if (u->suppress_fatal && k == CFREE_DIAG_FATAL) return; + } + fprintf(stderr, "%s: ", (unsigned)k < 4u ? names[k] : "diag"); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); +} + +/* Wire heap + diag + ctx onto u and zero the counters. ctx.now is set to 0 + * (the value the memset-to-zero majority of the suite already used); a test + * that needs a specific clock may overwrite u->ctx.now afterward. */ +static inline void cfree_unit_init(CfreeUnit* u) { + memset(u, 0, sizeof *u); + u->heap.alloc = cfree_unit_alloc; + u->heap.realloc = cfree_unit_realloc; + u->heap.free = cfree_unit_free; + u->diag.emit = cfree_unit_diag_emit; + u->diag.user = u; + u->ctx.heap = &u->heap; + u->ctx.diag = &u->diag; + u->ctx.now = 0; +} + +static inline CfreeTarget cfree_unit_target(CfreeArchKind arch, CfreeOSKind os, + CfreeObjFmt obj) { + CfreeTarget t; + memset(&t, 0, sizeof t); + t.arch = arch; + t.os = os; + t.obj = obj; + t.ptr_size = 8; + t.ptr_align = 8; + return t; +} + +static inline CfreeStatus cfree_unit_compiler_new(CfreeUnit* u, CfreeTarget t, + CfreeCompiler** out) { + return cfree_compiler_new(t, &u->ctx, out); +} + +/* Byte-substring search, for tests asserting emitted machine code contains a + * given encoding. Returns 1 if pat occurs in data (empty pat matches). */ +static inline int cfree_unit_contains(const uint8_t* data, size_t len, + const uint8_t* pat, size_t pat_len) { + size_t i; + if (pat_len == 0) return 1; + if (len < pat_len) return 0; + for (i = 0; i <= len - pat_len; ++i) { + if (memcmp(data + i, pat, pat_len) == 0) return 1; + } + return 0; +} + +static inline void cfree_unit_summary(CfreeUnit* u, const char* name) { + if (u->fails) { + fprintf(stderr, "%s: %d/%d checks failed\n", name, u->fails, u->checks); + } else { + printf("%s: %d checks, 0 failures\n", name, u->checks); + } +} + +static inline int cfree_unit_status(CfreeUnit* u) { return u->fails ? 1 : 0; } + +/* Record one check. `cond` is evaluated EXACTLY ONCE — it may have side + * effects (e.g. EXPECT(cfree_ar_iter_next(it, &m), ...) advances an + * iterator). Always bumps u->checks; on a miss bumps u->fails and prints a + * located FAIL line. Threads the context explicitly (no hidden global). */ +#define CU_CHECK(u, cond, ...) \ + do { \ + int cu_ok_ = (cond) ? 1 : 0; \ + ++(u)->checks; \ + if (!cu_ok_) { \ + ++(u)->fails; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + } \ + } while (0) + +/* Spelling preferred by most existing call sites. */ +#define CU_EXPECT(u, cond, ...) CU_CHECK(u, cond, __VA_ARGS__) + +/* CU_CHECK then `return 0;` on a miss — also evaluates `cond` exactly once. + * Preserves the per-test-function early-return shape used by table-of-tests + * harnesses (e.g. ar_test). */ +#define CU_CHECK_RET(u, cond, ...) \ + do { \ + int cu_ok_ = (cond) ? 1 : 0; \ + ++(u)->checks; \ + if (!cu_ok_) { \ + ++(u)->fails; \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fputc('\n', stderr); \ + return 0; \ + } \ + } while (0) + +#endif /* CFREE_TEST_LIB_CFREE_UNIT_H */ diff --git a/test/lib/unit.mk b/test/lib/unit.mk @@ -0,0 +1,77 @@ +# test/lib/unit.mk — build rules for the C unit-test binaries. +# +# Included by test/test.mk. Replaces the hand-written per-binary rules (each a +# $(CC) ... SRC $(LIB_AR|LIB_OBJS) -o $@ triple) with two static-pattern rules, +# one per regime. Registering a unit test is two lines: add its stem to a list +# and set <stem>_SRC. Run-targets (test-isa, test-cg-api, ...) stay in test.mk +# and invoke build/test/<stem> directly, so per-binary sequencing and skip +# semantics (e.g. rv64-jit's exit-77 wrapper) are unchanged. +# +# Two regimes, by the interface under test. We prefer tests that exercise the +# PUBLIC interface — keep a test public unless it genuinely needs internals. +# +# PUBLIC public headers only (-Iinclude), linked against the public +# archive libcfree.a. Internal (visibility-hidden) symbols are +# unreachable, so these prove the public surface is self-sufficient. +# INTERNAL additionally sees src/ internal headers (-Isrc) and links the raw +# objects (LIB_OBJS), exposing internal symbols. For units that have +# no public API (ISA tables, ABI classifier, IR/opt internals, ...). +# +# Both add -Itest so `#include "lib/cfree_unit.h"` resolves; neither adds -Ilang +# (no unit test needs the frontend-private headers). emu_rv64_test is a +# deliberate exception kept in test.mk — see the note there. + +UNIT_HDR_DEPS := test/lib/cfree_unit.h test/arch/inline_public_test.h + +# Deferred (=) so they pick up HOST_CFLAGS regardless of include order. +UNIT_CFLAGS_PUBLIC = $(HOST_CFLAGS) -Iinclude -Itest +UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest + +# ---- registrations: stem lists + per-stem source --------------------------- + +UNIT_TESTS_PUBLIC := \ + ar_test cg_api_test cg_switch_test rv64_jit_test \ + aa64_inline_test rv64_inline_test x64_inline_test +ar_test_SRC := test/ar/ar_test.c +cg_api_test_SRC := test/api/cg_type_test.c +cg_switch_test_SRC := test/api/cg_switch_test.c +rv64_jit_test_SRC := test/link/rv64_jit_test.c +aa64_inline_test_SRC := test/arch/aa64_inline_test.c +rv64_inline_test_SRC := test/arch/rv64_inline_test.c +x64_inline_test_SRC := test/arch/x64_inline_test.c + +UNIT_TESTS_INTERNAL := \ + dwarf_test debug_roundtrip_unit debug_cfi_unit \ + aa64_isa_test rv64_decode_test aa64_sweep_gen \ + reloc_uleb128_unit emu_rv64_unit_test interp_smoke_test \ + rv64_interp_smoke_test abi_classify_test ir_recorder_test \ + native_direct_target_test x64_dbg_test cg_ir_lower_test tiny_inline_test +dwarf_test_SRC := test/dwarf/dwarf_test.c +debug_roundtrip_unit_SRC := test/debug/roundtrip_unit.c +debug_cfi_unit_SRC := test/debug/cfi_unit.c +aa64_isa_test_SRC := test/arch/aa64_isa_test.c +rv64_decode_test_SRC := test/arch/rv64_decode_test.c +aa64_sweep_gen_SRC := test/arch/aa64_sweep_gen.c +reloc_uleb128_unit_SRC := test/link/reloc_uleb128_unit.c +emu_rv64_unit_test_SRC := test/emu/rv64_vm_unit_test.c +interp_smoke_test_SRC := test/interp/interp_smoke_test.c +rv64_interp_smoke_test_SRC := test/emu/rv64_interp_smoke_test.c +abi_classify_test_SRC := test/api/abi_classify_test.c +ir_recorder_test_SRC := test/cg/ir_recorder_test.c +native_direct_target_test_SRC := test/cg/native_direct_target_test.c +x64_dbg_test_SRC := test/arch/x64_dbg_test.c +cg_ir_lower_test_SRC := test/opt/cg_ir_lower_test.c +tiny_inline_test_SRC := test/opt/tiny_inline_test.c + +# ---- build rules ------------------------------------------------------------ +# Secondary expansion lets a static-pattern prerequisite reference the +# per-stem source via $$($$*_SRC). +.SECONDEXPANSION: + +$(UNIT_TESTS_PUBLIC:%=build/test/%): build/test/%: $$($$*_SRC) $(UNIT_HDR_DEPS) $(LIB_AR) + @mkdir -p $(dir $@) + $(CC) $(UNIT_CFLAGS_PUBLIC) $($*_SRC) $(LIB_AR) -o $@ + +$(UNIT_TESTS_INTERNAL:%=build/test/%): build/test/%: $$($$*_SRC) $(UNIT_HDR_DEPS) $(LIB_OBJS) + @mkdir -p $(dir $@) + $(CC) $(UNIT_CFLAGS_INTERNAL) $($*_SRC) $(LIB_OBJS) -o $@ diff --git a/test/libc/glibc/run.sh b/test/libc/glibc/run.sh @@ -1,11 +1,13 @@ #!/usr/bin/env bash -# test/libc/glibc/run.sh — drive cfree ld against a real glibc sysroot. -# Dynamic-link only — static-linked glibc is officially discouraged -# (libc.a relies on dlopen-loaded NSS modules, has its own entire reloc -# surface area, and isn't a real-world deployment shape), so we don't -# carry the variant. Each case in test/libc/cases/*.c is exercised once: +# test/libc/glibc/run.sh — drive cfree ld against a real glibc sysroot, on the +# shared corpus harness (test/lib/cf_corpus.sh). # -# dynamic — PIE object + libc.so.6, with explicit dynamic linker +# Dynamic-link only — static-linked glibc is officially discouraged (libc.a +# relies on dlopen-loaded NSS modules, has its own entire reloc surface area, +# and isn't a real-world deployment shape), so we don't carry the variant. +# Each case in test/libc/cases/*.c is exercised once, in a single lane: +# +# D dynamic — PIE object + libc.so.6, with explicit dynamic linker # cfree ld -pie \ # -dynamic-linker <loader> \ # -o case.exe \ @@ -14,43 +16,48 @@ # $SYSROOT/lib/libc.so.6 $SYSROOT/lib/libc_nonshared.a $CFREE_RT \ # $SYSROOT/lib/crtn.o # -# Unlike musl, where ld-musl-<arch>.so.1 is the same file as libc, -# glibc's loader is a separate ELF — cfree ld's default interp is musl, -# so we override via -dynamic-linker. The per-arch loader path is: +# Unlike musl, where ld-musl-<arch>.so.1 is the same file as libc, glibc's +# loader is a separate ELF — cfree ld's default interp is musl, so we override +# via -dynamic-linker. The per-arch loader path is: # aa64 -> /lib/ld-linux-aarch64.so.1 # x64 -> /lib64/ld-linux-x86-64.so.2 +# rv64 -> /lib/ld-linux-riscv64-lp64d.so.1 # libc.so.6 carries SONAME=libc.so.6 so DT_NEEDED is correct without a -# linker-script intermediary (the on-disk libc.so is a GROUP script -# that cfree ld doesn't parse — we hand the SO directly). -# libc_nonshared.a contributes the handful of non-shared callbacks -# every glibc dyn-exe pulls in — atexit, __stack_chk_fail_local, -# __libc_csu_init/fini on older glibc, etc. — and must follow -# libc.so.6 in the demand chain. +# linker-script intermediary (the on-disk libc.so is a GROUP script that cfree +# ld doesn't parse — we hand the SO directly). libc_nonshared.a contributes the +# handful of non-shared callbacks every glibc dyn-exe pulls in — atexit, +# __stack_chk_fail_local, __libc_csu_init/fini on older glibc, etc. — and must +# follow libc.so.6 in the demand chain. # -# Each case file may carry an `expected` companion (default 0) and an -# optional `expected_stdout` file checked with substring match. +# Each case file may carry an `expected` companion (default 0) and an optional +# `expected_stdout` (.stdout) file checked with substring match. # -# Designed to fail fast and clearly: the *first* failure surface (compile -# / link / run / output) is the gap to fix next. Run with -# CFREE_GLIBC_KEEP=1 to leave intermediates in build/glibc/<arch>/<case>/. +# The cross-exec is LANE-LOCAL: the lane runs the case either under qemu-user +# with QEMU_LD_PREFIX=$sysroot (so the loader search resolves to the SYSROOT +# copy), or under podman with an arch-pinned debian/glibc image (which ships the +# loader + libc at the expected paths). This is deliberately NOT routed through +# exec_target's default-image queue; the image + loader provenance + the +# QEMU_LD_PREFIX requirement are glibc-specific and differ from musl. # -# Arch selection: -# CFREE_LIBC_ARCHES (default "aa64") — space-separated list. Valid -# values: aa64, x64, rv64. Each arch maps to: -# aa64 -> build/glibc-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a -# + --target=aarch64-linux-gnu -# x64 -> build/glibc-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a -# + --target=x86_64-linux-gnu -# rv64 -> build/glibc-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a -# + --target=riscv64-linux-gnu -# Missing sysroot / rt for an enabled arch is reported as SKIP -# (non-fatal); only test failures cause a nonzero exit. +# Arch selection — CFREE_LIBC_ARCHES (default "aa64"), space-separated; each +# token becomes an "<arch>-elf" corpus tuple: +# aa64 -> build/glibc-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a +# + --target=aarch64-linux-gnu +# x64 -> build/glibc-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a +# + --target=x86_64-linux-gnu +# rv64 -> build/glibc-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a +# + --target=riscv64-linux-gnu +# A missing sysroot / rt / clang-target for an enabled arch is reported as a +# non-gating SKIP-NA (never a failure); only test failures cause a nonzero exit. set -u ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + CASES_DIR="$ROOT/test/libc/cases" BUILD_DIR="$ROOT/build/glibc" -CFREE="$ROOT/build/cfree" +CFREE="${CFREE:-$ROOT/build/cfree}" if [ ! -x "$CFREE" ]; then echo "cfree driver missing at $CFREE — run 'make' first" >&2 @@ -59,14 +66,8 @@ fi mkdir -p "$BUILD_DIR" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -PASS=0; FAIL=0; FAIL_NAMES=() -SKIP_ARCHES=() - -# ---- arch lookup tables --------------------------------------------------- +# ---- arch lookup tables (LANE-LOCAL config) ------------------------------- +# Keyed on the arch token (aa64/x64/rv64), which is the tuple's arch half. arch_sysroot() { case "$1" in @@ -114,9 +115,9 @@ arch_extract_name() { } arch_loader() { - # Dynamic-linker path baked into PT_INTERP. Both paths are the - # canonical Linux glibc loader locations and match the layout the - # extracted sysroots ship. + # Dynamic-linker path baked into PT_INTERP. All paths are the canonical + # Linux glibc loader locations and match the layout the extracted sysroots + # ship. case "$1" in aa64) echo "/lib/ld-linux-aarch64.so.1" ;; x64) echo "/lib64/ld-linux-x86-64.so.2" ;; @@ -125,7 +126,34 @@ arch_loader() { esac } -# ---- per-arch runners ------------------------------------------------------ +# Container image carrying the glibc loader + libc at the expected paths, used +# by the lane-local runner. Pinned to arch-specific repos (not multi-arch tags) +# to dodge the cached-wrong-arch-manifest trap and avoid --platform (which would +# force a registry manifest lookup on every run). +arch_image() { + case "$1" in + # arm64v8/debian:bookworm-slim ships the matching glibc loader. + aa64) echo "docker.io/arm64v8/debian:bookworm-slim" ;; + # amd64-pinned debian peer; ships /lib64/ld-linux-x86-64.so.2 + libc. + x64) echo "docker.io/amd64/debian:bookworm-slim" ;; + # trixie ships the riscv64 glibc loader at + # /lib/ld-linux-riscv64-lp64d.so.1. + rv64) echo "docker.io/riscv64/debian:trixie-slim" ;; + *) echo "" ;; + esac +} + +# qemu-user fallback binary per arch (used when no native exec is possible). +arch_qemu() { + case "$1" in + aa64) echo "$QEMU_AA64" ;; + x64) echo "$QEMU_X64" ;; + rv64) echo "$QEMU_RV64" ;; + *) echo "" ;; + esac +} + +# ---- host capability detection -------------------------------------------- # # Native linux/<arch> hosts can exec ELFs directly under podman without # binfmt; otherwise we fall back to qemu-<arch>-static. @@ -141,52 +169,22 @@ QEMU_X64="$(command -v qemu-x86_64-static 2>/dev/null || command -v qemu-x86_64 QEMU_RV64="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)" have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 +# ---- lane-local target runner --------------------------------------------- # run_target <arch> <sysroot> <exe> <out> <err> -> sets RUN_RC # -# Dynamic-variant exes need /lib/ld-linux-<arch>.so.{1,2} + libc.so.6 -# to load. qemu-user resolves them relative to QEMU_LD_PREFIX; the -# podman fallback uses a debian:bookworm image which ships them at the -# expected paths. +# Dynamic exes need /lib/ld-linux-<arch>.so.{1,2} + libc.so.6 to load. +# qemu-user resolves them relative to QEMU_LD_PREFIX=$sysroot; the podman +# fallback uses an arch-pinned debian image which ships them at the expected +# paths. run_target() { local arch="$1" sysroot="$2" exe="$3" out="$4" err="$5" - local qemu="" image="" - case "$arch" in - aa64) - qemu="$QEMU_AA64" - # Pin the image name to the arm64-specific repo - # (docker.io/arm64v8/...) instead of the multi-arch - # debian:bookworm-slim. Two reasons: - # 1. Avoids the cached-amd64-manifest trap that - # debian:bookworm-slim hits on arm64 hosts where an - # amd64 pull happened earlier — podman silently uses - # the wrong arch and the dyn-exe fails to load. - # 2. Avoids passing --platform, which forces podman to - # hit the registry on every run to verify the - # manifest matches. Pinning the repo + relying on the - # local cache keeps subsequent runs offline + fast. - # arm64v8/debian:bookworm-slim ships the matching glibc - # loader, so the dynamic variant resolves PT_INTERP - # without extra mounts. - image="docker.io/arm64v8/debian:bookworm-slim" - ;; - x64) - qemu="$QEMU_X64" - # amd64-pinned debian peer of the arm64v8 image above. - # Same rationale: avoid --platform + multi-arch tag - # cache traps. Ships /lib64/ld-linux-x86-64.so.2 + libc. - image="docker.io/amd64/debian:bookworm-slim" - ;; - rv64) - qemu="$QEMU_RV64" - # riscv64-pinned Debian image. The trixie image ships the - # riscv64 glibc loader at /lib/ld-linux-riscv64-lp64d.so.1. - image="docker.io/riscv64/debian:trixie-slim" - ;; - esac + local qemu image + qemu="$(arch_qemu "$arch")" + image="$(arch_image "$arch")" if [ -n "$qemu" ]; then - # Point qemu-user at our extracted sysroot so the loader - # search resolves to the SYSROOT copy rather than the - # (possibly-absent) host one. + # Point qemu-user at our extracted sysroot so the loader search + # resolves to the SYSROOT copy rather than the (possibly-absent) host + # one. QEMU_LD_PREFIX="$sysroot" \ "$qemu" "$exe" >"$out" 2>"$err" RUN_RC=$?; return @@ -203,175 +201,142 @@ run_target() { RUN_RC=127 } -# ---- case driver ----------------------------------------------------------- - -# run_case <arch> <sysroot> <rt> <target> <loader> <triple_inc> <src> -run_case() { - local arch="$1" sysroot="$2" rt="$3" target="$4" loader="$5" - local triple_inc="$6" src="$7" - local name="$(basename "$src" .c)" - local work="$BUILD_DIR/$arch/$name" - local label="$arch/$name" - mkdir -p "$work" - - local expected=0 - [ -f "$CASES_DIR/${name}.expected" ] && \ - expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')" +# ---- per-case marker reading (CF_READ_CASE) -------------------------------- +# Reads the optional .stdout substring oracle and resolves this tuple's arch +# config. The .expected sidecar (default 0) is read by the engine already +# (CF_EXPECTED_EXT=.expected). A tuple whose sysroot/rt/clang-target is +# unavailable is marked SKIP-NA (non-gating), mirroring the original's +# SKIP_ARCHES informational handling. +libc_read_case() { + LC_ARCH="$CF_ARCH" + LC_SYSROOT="$(arch_sysroot "$LC_ARCH")" + LC_RT="$(arch_rt "$LC_ARCH")" + LC_TARGET="$(arch_target "$LC_ARCH")" + LC_LOADER="$(arch_loader "$LC_ARCH")" + LC_TRIPLE_INC="$(arch_triple_include "$LC_ARCH")" + + if [ -z "$LC_SYSROOT" ] || [ -z "$LC_RT" ] || [ -z "$LC_TARGET" ]; then + CF_SKIP_NA_CASE=1; return + fi + if [ ! -d "$LC_SYSROOT" ]; then + CF_SKIP_NA_CASE=1; return + fi + if [ ! -f "$LC_RT" ]; then + CF_SKIP_NA_CASE=1; return + fi + # clang must understand --target=<target>. Every system path is overridden + # via --sysroot / -isystem so the host's headers / libraries are not + # consulted. + if ! clang --target="$LC_TARGET" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + CF_SKIP_NA_CASE=1; return + fi - local expect_stdout="" - if [ -f "$CASES_DIR/${name}.stdout" ]; then - expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" + LC_EXPECT_STDOUT="" + if [ -f "$CASES_DIR/${CF_BASE}.stdout" ]; then + LC_EXPECT_STDOUT="$(cat "$CASES_DIR/${CF_BASE}.stdout")" fi +} + +# ---- dynamic-link lane (compile -> link -> run -> stdout) ------------------ +# Emits exactly one cf_pass/cf_fail. All artifacts live under $CF_WORK +# (parallel-safe). +cf_lane_D() { + local label="$CF_ARCH/$CF_BASE" + local work="$CF_WORK" # ---- compile ---- # Three -isystem layers, in order of precedence: - # sysroot/include/ — glibc + linux-libc-dev - # headers (top-level uapi). + # sysroot/include/ — glibc + linux-libc-dev headers + # (top-level uapi). # sysroot/include/<triple> — glibc multi-arch (bits/*, # gnu/stubs-lp64.h, ...); # <features.h> reaches in. # rt/include/ — cfree's freestanding overlay # (stddef.h, stdarg.h, stdint.h). - # glibc's stdio.h #include - # <stddef.h> for size_t; glibc - # doesn't ship compiler headers - # so rt/include must be reachable. - # -nostdinc strips clang's default include path so cross targets - # don't accidentally pick up the host's compiler headers. - local cc_flags=(--target="$target" --sysroot="$sysroot" + # glibc's stdio.h #include <stddef.h> + # for size_t; glibc doesn't ship + # compiler headers so rt/include must + # be reachable. + # -nostdinc strips clang's default include path so cross targets don't + # accidentally pick up the host's compiler headers. + local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT" -nostdinc - -isystem "$sysroot/include" - -isystem "$sysroot/include/$triple_inc" + -isystem "$LC_SYSROOT/include" + -isystem "$LC_SYSROOT/include/$LC_TRIPLE_INC" -isystem "$ROOT/rt/include" -fPIE -fpic -O0) - local obj="$work/${name}.o" - if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then - FAIL=$((FAIL+1)) - FAIL_NAMES+=("$label (compile)") - printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)" + local obj="$work/${CF_BASE}.o" + if ! clang "${cc_flags[@]}" -c "$CF_SRC" -o "$obj" 2>"$work/cc.err"; then + cf_fail "$label (compile)" sed 's/^/ cc| /' "$work/cc.err" return fi # ---- link ---- - # PIE start file, libc.so.6 as the *shared* input (cfree ld - # doesn't read the libc.so linker script, so we hand the actual - # SO directly), with -dynamic-linker overriding the musl default. - # Expects cfree ld to: + # PIE start file, libc.so.6 as the *shared* input (cfree ld doesn't read + # the libc.so linker script, so we hand the actual SO directly), with + # -dynamic-linker overriding the musl default. Expects cfree ld to: # - accept ET_DYN ELF objects as input, # - emit PT_INTERP "$loader", # - emit PT_DYNAMIC with DT_NEEDED libc.so.6, - # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt - # so the loader can bind imported symbols at runtime. - # libc_nonshared.a still links statically; libcfree_rt.a stays — - # soft-float TF helpers are static-bound from our side. - # crti/crtn are unchanged. - local exe="$work/${name}.exe" + # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt so the loader + # can bind imported symbols at runtime. + # libc_nonshared.a still links statically; libcfree_rt.a stays — soft-float + # TF helpers are static-bound from our side. crti/crtn are unchanged. + local exe="$work/${CF_BASE}.exe" local link_cmd=("$CFREE" "ld" -pie - -dynamic-linker "$loader" + -dynamic-linker "$LC_LOADER" -o "$exe" - "$sysroot/lib/Scrt1.o" "$sysroot/lib/crti.o" + "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o" "$obj" - "$sysroot/lib/libc.so.6" "$sysroot/lib/libc_nonshared.a" - "$rt" - "$sysroot/lib/crtn.o") + "$LC_SYSROOT/lib/libc.so.6" "$LC_SYSROOT/lib/libc_nonshared.a" + "$LC_RT" + "$LC_SYSROOT/lib/crtn.o") if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then - FAIL=$((FAIL+1)) - FAIL_NAMES+=("$label (link)") - printf ' %s %s\n' "$(color_red FAIL)" "$label (link)" + cf_fail "$label (link)" sed 's/^/ ld| /' "$work/link.err" | head -10 return fi - # ---- run ---- - run_target "$arch" "$sysroot" "$exe" "$work/run.out" "$work/run.err" - if [ "$RUN_RC" -ne "$expected" ]; then - FAIL=$((FAIL+1)) - FAIL_NAMES+=("$label (run rc=$RUN_RC, want $expected)") - printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \ - "$RUN_RC" "$expected" + # ---- run (lane-local cross-exec) ---- + run_target "$LC_ARCH" "$LC_SYSROOT" "$exe" "$work/run.out" "$work/run.err" + if [ "$RUN_RC" -ne "$CF_EXPECTED" ]; then + cf_fail "$label (run rc=$RUN_RC, want $CF_EXPECTED)" [ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5 [ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5 return fi - if [ -n "$expect_stdout" ]; then - if ! grep -qF -- "$expect_stdout" "$work/run.out"; then - FAIL=$((FAIL+1)) - FAIL_NAMES+=("$label (stdout)") - printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label" - printf ' expected substring: %s\n' "$expect_stdout" + if [ -n "$LC_EXPECT_STDOUT" ]; then + if ! grep -qF -- "$LC_EXPECT_STDOUT" "$work/run.out"; then + cf_fail "$label (stdout mismatch)" + printf ' expected substring: %s\n' "$LC_EXPECT_STDOUT" sed 's/^/ got| /' "$work/run.out" | head -5 return fi fi - PASS=$((PASS+1)) - printf ' %s %s\n' "$(color_grn PASS)" "$label" -} - -# run_arch_cases <arch> <sysroot> <rt> <target> -run_arch_cases() { - local arch="$1" sysroot="$2" rt="$3" target="$4" - local loader triple_inc - loader="$(arch_loader "$arch")" - triple_inc="$(arch_triple_include "$arch")" - - # clang must understand --target=<target>. Every system path is - # overridden via --sysroot / -isystem so the host's headers / - # libraries are not consulted. - if ! clang --target="$target" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then - printf ' %s %s (clang does not accept --target=%s)\n' \ - "$(color_yel SKIP)" "$arch" "$target" - SKIP_ARCHES+=("$arch (no clang --target=$target)") - return - fi - - printf 'Running glibc dynamic-link cases [%s]...\n' "$arch" - for src in "$CASES_DIR"/*.c; do - run_case "$arch" "$sysroot" "$rt" "$target" "$loader" "$triple_inc" "$src" - done - printf '\n' + cf_pass "$label" } -shopt -s nullglob - -ARCHES="${CFREE_LIBC_ARCHES:-aa64}" -for arch in $ARCHES; do - sysroot="$(arch_sysroot "$arch")" - rt="$(arch_rt "$arch")" - target="$(arch_target "$arch")" - if [ -z "$sysroot" ] || [ -z "$rt" ] || [ -z "$target" ]; then - printf ' %s %s (unknown arch)\n' "$(color_yel SKIP)" "$arch" - SKIP_ARCHES+=("$arch (unknown)") - continue - fi - if [ ! -d "$sysroot" ]; then - printf ' %s %s (glibc sysroot missing at %s — run test/libc/glibc/extract.sh -a %s)\n' \ - "$(color_yel SKIP)" "$arch" "$sysroot" "$(arch_extract_name "$arch")" - SKIP_ARCHES+=("$arch (sysroot)") - continue - fi - if [ ! -f "$rt" ]; then - printf ' %s %s (cfree rt missing at %s)\n' \ - "$(color_yel SKIP)" "$arch" "$rt" - SKIP_ARCHES+=("$arch (rt)") - continue - fi - run_arch_cases "$arch" "$sysroot" "$rt" "$target" +# ---- drive the corpus ------------------------------------------------------ +# One cf_corpus_run per arch (single "<arch>-elf" tuple), so each arch gets a +# distinct per-case $CF_WORK ($CF_BUILD_DIR/<arch>/<base>) and never collides +# with another arch's under parallel dispatch. Results accumulate into the +# shared counters; one summary at the end. +printf 'test-libc-glibc arches=%s\n' "${CFREE_LIBC_ARCHES:-aa64}" + +PAR="${CFREE_GLIBC_PARALLEL:-1}" +for arch in ${CFREE_LIBC_ARCHES:-aa64}; do + CF_LABEL=test-libc-glibc CF_BUILD_DIR="$BUILD_DIR/$arch" \ + CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \ + CF_LANES="D" CF_OPT_LEVELS="" CF_TUPLES="$arch-elf" \ + CF_EXPECTED_EXT=.expected CF_TARGETS_EXT="" \ + CF_READ_CASE=libc_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run done -if [ ${#FAIL_NAMES[@]} -gt 0 ]; then - printf '\nFailed:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done -fi - -printf '\nResults: %s pass, %s fail\n' "$PASS" "$FAIL" -if [ ${#SKIP_ARCHES[@]} -gt 0 ]; then - printf ' skipped: %s\n' "${SKIP_ARCHES[*]}" -fi - -if [ ${#FAIL_NAMES[@]} -gt 0 ]; then exit 1; fi -exit 0 +cf_summary test-libc-glibc +cf_exit diff --git a/test/libc/musl/run.sh b/test/libc/musl/run.sh @@ -1,49 +1,56 @@ #!/usr/bin/env bash -# test/libc/musl/run.sh — drive cfree ld against a real musl sysroot. -# Each case in test/libc/cases/*.c is exercised in two variants: +# test/libc/musl/run.sh — drive cfree ld against a real musl sysroot, on the +# shared corpus harness (test/lib/cf_corpus.sh). # -# static — non-PIC object + libc.a, classic static-exe link +# Cases under test/libc/cases/*.c are compiled, linked, and run against a +# podman-pinned Alpine/musl sysroot, in two link variants (two lanes): +# +# S static — non-PIC object + libc.a, classic static-exe link # cfree ld -static -o case.exe \ # $SYSROOT/lib/crt1.o $SYSROOT/lib/crti.o \ # case.o \ # $SYSROOT/lib/libc.a $CFREE_RT \ # $SYSROOT/lib/crtn.o # -# dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-<arch>.so.1 +# D dynamic — PIE object + libc.so, expects PT_INTERP /lib/ld-musl-<arch>.so.1 # cfree ld -pie -o case.exe \ # $SYSROOT/lib/Scrt1.o $SYSROOT/lib/crti.o \ # case.o \ # $SYSROOT/lib/libc.so $CFREE_RT \ # $SYSROOT/lib/crtn.o -# (musl ships ld-musl-<arch>.so.1 *as* libc — same file. The -# harness intentionally has no -dynamic-linker flag yet because -# cfree ld currently doesn't accept one; this is one of the gaps -# we expect the dynamic variant to surface.) +# (musl ships ld-musl-<arch>.so.1 *as* libc — same file. The harness +# intentionally has no -dynamic-linker flag yet because cfree ld +# currently doesn't accept one; this is one of the gaps we expect the +# dynamic variant to surface.) # -# Each case file may carry an `expected` companion (default 0) and an -# optional `expected_stdout` file checked with substring match. +# Each case file may carry an `expected` companion (default 0) and an optional +# `expected_stdout` (.stdout) file checked with substring match. # -# Designed to fail fast and clearly: the *first* failure surface (compile -# / link / run / output) is the gap to fix next. Run with -# CFREE_MUSL_KEEP=1 to leave intermediates in build/musl/<arch>/<case>/. +# The cross-exec is LANE-LOCAL: the lane runs the case under podman with musl's +# own arch-pinned Alpine image, whose rootfs already carries the musl loader at +# /lib/ld-musl-<arch>.so.1 — so NO QEMU_LD_PREFIX and no -dynamic-linker are +# needed (unlike glibc). This is deliberately NOT routed through exec_target's +# default-image queue; the image + loader provenance are musl-specific. # -# Arch selection: -# CFREE_LIBC_ARCHES (default "aa64") — space-separated list. Valid -# values: aa64, x64, rv64. Each arch maps to: -# aa64 -> build/musl-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a -# + --target=aarch64-linux-musl -# x64 -> build/musl-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a -# + --target=x86_64-linux-musl -# rv64 -> build/musl-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a -# + --target=riscv64-linux-musl -# Missing sysroot / rt for an enabled arch is reported as SKIP -# (non-fatal); only test failures cause a nonzero exit. +# Arch selection — CFREE_LIBC_ARCHES (default "aa64"), space-separated; each +# token becomes an "<arch>-elf" corpus tuple: +# aa64 -> build/musl-sysroot/ + build/rt/aarch64-linux/libcfree_rt.a +# + --target=aarch64-linux-musl +# x64 -> build/musl-sysroot-x64/ + build/rt/x86_64-linux/libcfree_rt.a +# + --target=x86_64-linux-musl +# rv64 -> build/musl-sysroot-rv64/ + build/rt/riscv64-linux/libcfree_rt.a +# + --target=riscv64-linux-musl +# A missing sysroot / rt / clang-target for an enabled arch is reported as a +# non-gating SKIP-NA (never a failure); only test failures cause a nonzero exit. set -u ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + CASES_DIR="$ROOT/test/libc/cases" BUILD_DIR="$ROOT/build/musl" -CFREE="$ROOT/build/cfree" +CFREE="${CFREE:-$ROOT/build/cfree}" if [ ! -x "$CFREE" ]; then echo "cfree driver missing at $CFREE — run 'make' first" >&2 @@ -52,19 +59,8 @@ fi mkdir -p "$BUILD_DIR" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -# Per-variant counters so the dynamic-link surface is visible in its own -# right rather than being averaged into one total. Counters are global -# across arches; the arch tag is baked into each case label so failures -# remain disambiguated in the summary. -PASS_static=0; FAIL_static=0; FAIL_NAMES_static=() -PASS_dynamic=0; FAIL_dynamic=0; FAIL_NAMES_dynamic=() -SKIP_ARCHES=() - -# ---- arch lookup tables --------------------------------------------------- +# ---- arch lookup tables (LANE-LOCAL config) ------------------------------- +# Keyed on the arch token (aa64/x64/rv64), which is the tuple's arch half. arch_sysroot() { case "$1" in @@ -93,6 +89,23 @@ arch_target() { esac } +# Container image carrying the musl loader at /lib/ld-musl-<arch>.so.1, used by +# the lane-local runner. Pinned to arch-specific repos (not multi-arch tags) to +# dodge the cached-wrong-arch-manifest trap and to avoid --platform (which would +# force a registry manifest lookup on every run). +arch_image() { + case "$1" in + # arm64v8/alpine ships the musl loader at /lib/ld-musl-aarch64.so.1. + aa64) echo "docker.io/arm64v8/alpine:latest" ;; + # amd64v2/alpine isn't a thing; amd64/alpine is the canonical pin. + # Ships /lib/ld-musl-x86_64.so.1. + x64) echo "docker.io/amd64/alpine:latest" ;; + # alpine:edge currently carries the riscv64 musl loader. + rv64) echo "docker.io/riscv64/alpine:edge" ;; + *) echo "" ;; + esac +} + # Spelling extract.sh accepts for `-a`: aa64 -> aarch64; x64 -> x64. arch_extract_name() { case "$1" in @@ -101,7 +114,17 @@ arch_extract_name() { esac } -# ---- per-arch runners ------------------------------------------------------ +# qemu-user fallback binary per arch (used when no native exec is possible). +arch_qemu() { + case "$1" in + aa64) echo "$QEMU_AA64" ;; + x64) echo "$QEMU_X64" ;; + rv64) echo "$QEMU_RV64" ;; + *) echo "" ;; + esac +} + +# ---- host capability detection -------------------------------------------- # # Native linux/<arch> hosts can exec ELFs directly under podman without # binfmt; otherwise we fall back to qemu-<arch>-static. @@ -117,40 +140,14 @@ QEMU_X64="$(command -v qemu-x86_64-static 2>/dev/null || command -v qemu-x86_64 QEMU_RV64="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)" have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 +# ---- lane-local target runner --------------------------------------------- # run_target <arch> <exe> <out> <err> -> sets RUN_RC +# Musl: rootfs-baked loader, NO QEMU_LD_PREFIX. run_target() { local arch="$1" exe="$2" out="$3" err="$4" - local qemu="" image="" platform="" - case "$arch" in - aa64) - qemu="$QEMU_AA64" - # Pin the image name to the arm64-specific repo - # (docker.io/arm64v8/...) instead of the multi-arch - # alpine:latest. Avoids the cached-wrong-arch-manifest - # trap that bare alpine:latest hits when an unrelated - # pull cached a different arch; also avoids --platform, - # which would force a registry manifest lookup on every - # run. arm64v8/alpine ships the musl loader at - # /lib/ld-musl-aarch64.so.1 so the dynamic variant - # resolves PT_INTERP without extra mounts. - image="docker.io/arm64v8/alpine:latest" - ;; - x64) - qemu="$QEMU_X64" - # amd64v2/alpine isn't a thing on Docker Hub; the - # canonical arch-pinned alpine for amd64 is amd64/alpine. - # Same rationale as arm64v8/alpine above: pin the repo, - # skip --platform, rely on the local cache. Ships the - # musl loader at /lib/ld-musl-x86_64.so.1. - image="docker.io/amd64/alpine:latest" - ;; - rv64) - qemu="$QEMU_RV64" - # riscv64-pinned Alpine image. alpine:edge currently carries - # the riscv64 musl loader used by this sysroot. - image="docker.io/riscv64/alpine:edge" - ;; - esac + local qemu image + qemu="$(arch_qemu "$arch")" + image="$(arch_image "$arch")" if [ -n "$qemu" ]; then "$qemu" "$exe" >"$out" 2>"$err"; RUN_RC=$?; return fi @@ -166,188 +163,151 @@ run_target() { RUN_RC=127 } -# ---- case driver ----------------------------------------------------------- -# -# run_case <arch> <sysroot> <rt> <target> <variant> <src> -# variant ∈ {static, dynamic} -run_case() { - local arch="$1" sysroot="$2" rt="$3" target="$4" variant="$5" src="$6" - local name="$(basename "$src" .c)" - local work="$BUILD_DIR/$arch/$name/$variant" - local label="$arch/$name [$variant]" - mkdir -p "$work" - - local expected=0 - [ -f "$CASES_DIR/${name}.expected" ] && \ - expected="$(cat "$CASES_DIR/${name}.expected" | tr -d '[:space:]')" +# ---- per-case marker reading (CF_READ_CASE) -------------------------------- +# Reads the optional .stdout substring oracle and resolves this tuple's arch +# config. The .expected sidecar (default 0) is read by the engine already +# (CF_EXPECTED_EXT=.expected). A tuple whose sysroot/rt/clang-target is +# unavailable is marked SKIP-NA (non-gating), mirroring the original's +# SKIP_ARCHES informational handling. +libc_read_case() { + LC_ARCH="$CF_ARCH" + LC_SYSROOT="$(arch_sysroot "$LC_ARCH")" + LC_RT="$(arch_rt "$LC_ARCH")" + LC_TARGET="$(arch_target "$LC_ARCH")" + + if [ -z "$LC_SYSROOT" ] || [ -z "$LC_RT" ] || [ -z "$LC_TARGET" ]; then + CF_SKIP_NA_CASE=1; return + fi + if [ ! -d "$LC_SYSROOT" ]; then + CF_SKIP_NA_CASE=1; return + fi + if [ ! -f "$LC_RT" ]; then + CF_SKIP_NA_CASE=1; return + fi + # clang must understand --target=<target>. Recent clang ships linux-musl as + # a target alias of linux-gnu for our purposes (we override every system + # path via --sysroot). + if ! clang --target="$LC_TARGET" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then + CF_SKIP_NA_CASE=1; return + fi - local expect_stdout="" - if [ -f "$CASES_DIR/${name}.stdout" ]; then - expect_stdout="$(cat "$CASES_DIR/${name}.stdout")" + LC_EXPECT_STDOUT="" + if [ -f "$CASES_DIR/${CF_BASE}.stdout" ]; then + LC_EXPECT_STDOUT="$(cat "$CASES_DIR/${CF_BASE}.stdout")" fi +} + +# ---- shared per-variant pipeline (compile -> link -> run -> stdout) -------- +# libc_run_variant <variant> <label> +# variant ∈ {static, dynamic}; emits exactly one cf_pass/cf_fail. +# All artifacts live under $CF_WORK (parallel-safe). +libc_run_variant() { + local variant="$1" label="$2" + local work="$CF_WORK/$variant" + mkdir -p "$work" # ---- compile ---- # -nostdinc strips clang's default include path (resource dir + - # /usr/include) so the sysroot's musl + linux-headers tree is the - # sole source. -isystem $sysroot/include picks it up. - local cc_flags=(--target="$target" --sysroot="$sysroot" + # /usr/include) so the sysroot's musl + linux-headers tree is the sole + # source. -isystem $sysroot/include picks it up. + local cc_flags=(--target="$LC_TARGET" --sysroot="$LC_SYSROOT" -nostdinc - -isystem "$sysroot/include" + -isystem "$LC_SYSROOT/include" -O0) case "$variant" in static) cc_flags+=(-fno-PIC -fno-pie) ;; dynamic) cc_flags+=(-fPIE -fpic) ;; esac - local obj="$work/${name}.o" - if ! clang "${cc_flags[@]}" -c "$src" -o "$obj" 2>"$work/cc.err"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (compile)\")" - printf ' %s %s\n' "$(color_red FAIL)" "$label (compile)" + local obj="$work/${CF_BASE}.o" + if ! clang "${cc_flags[@]}" -c "$CF_SRC" -o "$obj" 2>"$work/cc.err"; then + cf_fail "$label (compile)" sed 's/^/ cc| /' "$work/cc.err" return fi # ---- link ---- - local exe="$work/${name}.exe" + local exe="$work/${CF_BASE}.exe" local link_cmd case "$variant" in static) # Link order mirrors a typical static-musl invocation: # crt1.o crti.o obj libc.a libcfree_rt.a crtn.o - # libcfree_rt provides the TF / soft-float builtins - # (__addtf3, __extenddftf2 etc.) that musl's libc.a calls - # from printf's long-double formatting. Archive ingestion - # iterates demand-load to a fixed point so one trailing - # libcfree_rt.a is enough. + # libcfree_rt provides the TF / soft-float builtins (__addtf3, + # __extenddftf2 etc.) that musl's libc.a calls from printf's + # long-double formatting. Archive ingestion iterates demand-load + # to a fixed point so one trailing libcfree_rt.a is enough. link_cmd=("$CFREE" "ld" -static -o "$exe" - "$sysroot/lib/crt1.o" "$sysroot/lib/crti.o" + "$LC_SYSROOT/lib/crt1.o" "$LC_SYSROOT/lib/crti.o" "$obj" - "$sysroot/lib/libc.a" "$rt" - "$sysroot/lib/crtn.o") + "$LC_SYSROOT/lib/libc.a" "$LC_RT" + "$LC_SYSROOT/lib/crtn.o") ;; dynamic) - # Dynamic-exe link: PIE start file, libc.so as a *shared* - # input (not an archive), expects cfree ld to: + # Dynamic-exe link: PIE start file, libc.so as a *shared* input + # (not an archive), expects cfree ld to: # - accept ET_DYN ELF objects as input, # - emit PT_INTERP "/lib/ld-musl-<arch>.so.1", # - emit PT_DYNAMIC with DT_NEEDED libc.so, - # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt - # so the loader can bind imported symbols at runtime. + # - emit a .dynsym/.dynstr/.gnu.hash + .rela.plt/.got.plt so the + # loader can bind imported symbols at runtime. # libcfree_rt.a stays — soft-float TF helpers are still # static-bound from our side. crti/crtn are unchanged. link_cmd=("$CFREE" "ld" -pie -o "$exe" - "$sysroot/lib/Scrt1.o" "$sysroot/lib/crti.o" + "$LC_SYSROOT/lib/Scrt1.o" "$LC_SYSROOT/lib/crti.o" "$obj" - "$sysroot/lib/libc.so" "$rt" - "$sysroot/lib/crtn.o") + "$LC_SYSROOT/lib/libc.so" "$LC_RT" + "$LC_SYSROOT/lib/crtn.o") ;; esac if ! "${link_cmd[@]}" >"$work/link.out" 2>"$work/link.err"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (link)\")" - printf ' %s %s\n' "$(color_red FAIL)" "$label (link)" + cf_fail "$label (link)" sed 's/^/ ld| /' "$work/link.err" | head -10 return fi - # ---- run ---- - run_target "$arch" "$exe" "$work/run.out" "$work/run.err" - if [ "$RUN_RC" -ne "$expected" ]; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (run rc=\$RUN_RC, want \$expected)\")" - printf ' %s %s (rc=%s, want %s)\n' "$(color_red FAIL)" "$label" \ - "$RUN_RC" "$expected" + # ---- run (lane-local cross-exec) ---- + run_target "$LC_ARCH" "$exe" "$work/run.out" "$work/run.err" + if [ "$RUN_RC" -ne "$CF_EXPECTED" ]; then + cf_fail "$label (run rc=$RUN_RC, want $CF_EXPECTED)" [ -s "$work/run.err" ] && sed 's/^/ err| /' "$work/run.err" | head -5 [ -s "$work/run.out" ] && sed 's/^/ out| /' "$work/run.out" | head -5 return fi - if [ -n "$expect_stdout" ]; then - if ! grep -qF -- "$expect_stdout" "$work/run.out"; then - eval "FAIL_${variant}=\$((FAIL_${variant}+1))" - eval "FAIL_NAMES_${variant}+=(\"\$label (stdout)\")" - printf ' %s %s (stdout mismatch)\n' "$(color_red FAIL)" "$label" - printf ' expected substring: %s\n' "$expect_stdout" + if [ -n "$LC_EXPECT_STDOUT" ]; then + if ! grep -qF -- "$LC_EXPECT_STDOUT" "$work/run.out"; then + cf_fail "$label (stdout mismatch)" + printf ' expected substring: %s\n' "$LC_EXPECT_STDOUT" sed 's/^/ got| /' "$work/run.out" | head -5 return fi fi - eval "PASS_${variant}=\$((PASS_${variant}+1))" - printf ' %s %s\n' "$(color_grn PASS)" "$label" -} - -# run_arch_cases <arch> <sysroot> <rt> <target> -run_arch_cases() { - local arch="$1" sysroot="$2" rt="$3" target="$4" - - # clang must understand --target=<target>. Recent clang ships - # linux-musl as a target alias of linux-gnu for our purposes (we - # override every system path via --sysroot). - if ! clang --target="$target" -c -x c - -o /dev/null < /dev/null 2>/dev/null; then - printf ' %s %s (clang does not accept --target=%s)\n' \ - "$(color_yel SKIP)" "$arch" "$target" - SKIP_ARCHES+=("$arch (no clang --target=$target)") - return - fi - - printf 'Running musl static-link cases [%s]...\n' "$arch" - for src in "$CASES_DIR"/*.c; do - run_case "$arch" "$sysroot" "$rt" "$target" static "$src" - done - - printf '\nRunning musl dynamic-link cases [%s]...\n' "$arch" - for src in "$CASES_DIR"/*.c; do - run_case "$arch" "$sysroot" "$rt" "$target" dynamic "$src" - done - printf '\n' + cf_pass "$label" } -shopt -s nullglob - -ARCHES="${CFREE_LIBC_ARCHES:-aa64}" -for arch in $ARCHES; do - sysroot="$(arch_sysroot "$arch")" - rt="$(arch_rt "$arch")" - target="$(arch_target "$arch")" - if [ -z "$sysroot" ] || [ -z "$rt" ] || [ -z "$target" ]; then - printf ' %s %s (unknown arch)\n' "$(color_yel SKIP)" "$arch" - SKIP_ARCHES+=("$arch (unknown)") - continue - fi - if [ ! -d "$sysroot" ]; then - printf ' %s %s (musl sysroot missing at %s — run test/libc/musl/extract.sh -a %s)\n' \ - "$(color_yel SKIP)" "$arch" "$sysroot" "$(arch_extract_name "$arch")" - SKIP_ARCHES+=("$arch (sysroot)") - continue - fi - if [ ! -f "$rt" ]; then - printf ' %s %s (cfree rt missing at %s)\n' \ - "$(color_yel SKIP)" "$arch" "$rt" - SKIP_ARCHES+=("$arch (rt)") - continue - fi - run_arch_cases "$arch" "$sysroot" "$rt" "$target" +# ---- lanes (static / dynamic variant axis) -------------------------------- +cf_lane_S() { libc_run_variant static "$CF_ARCH/$CF_BASE [static]"; } +cf_lane_D() { libc_run_variant dynamic "$CF_ARCH/$CF_BASE [dynamic]"; } + +# ---- drive the corpus ------------------------------------------------------ +# One cf_corpus_run per arch (single "<arch>-elf" tuple), so each arch gets a +# distinct per-case $CF_WORK ($CF_BUILD_DIR/<arch>/<base>) and the static/ +# dynamic lanes for one arch never collide with another arch's under parallel +# dispatch. Results accumulate into the shared counters; one summary at the end. +printf 'test-libc-musl arches=%s\n' "${CFREE_LIBC_ARCHES:-aa64}" + +PAR="${CFREE_MUSL_PARALLEL:-1}" +for arch in ${CFREE_LIBC_ARCHES:-aa64}; do + CF_LABEL=test-libc-musl CF_BUILD_DIR="$BUILD_DIR/$arch" \ + CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \ + CF_LANES="S D" CF_OPT_LEVELS="" CF_TUPLES="$arch-elf" \ + CF_EXPECTED_EXT=.expected CF_TARGETS_EXT="" \ + CF_READ_CASE=libc_read_case CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run done -if [ ${#FAIL_NAMES_static[@]} -gt 0 ]; then - printf '\nFailed (static):\n' - for n in "${FAIL_NAMES_static[@]}"; do printf ' %s\n' "$n"; done -fi -if [ ${#FAIL_NAMES_dynamic[@]} -gt 0 ]; then - printf '\nFailed (dynamic):\n' - for n in "${FAIL_NAMES_dynamic[@]}"; do printf ' %s\n' "$n"; done -fi - -printf '\nResults:\n' -printf ' static : %s pass, %s fail\n' "$PASS_static" "$FAIL_static" -printf ' dynamic: %s pass, %s fail\n' "$PASS_dynamic" "$FAIL_dynamic" -if [ ${#SKIP_ARCHES[@]} -gt 0 ]; then - printf ' skipped: %s\n' "${SKIP_ARCHES[*]}" -fi - -total_fail=$((FAIL_static + FAIL_dynamic)) -if [ $total_fail -gt 0 ]; then exit 1; fi -exit 0 +cf_summary test-libc-musl +cf_exit diff --git a/test/link/reloc_uleb128_unit.c b/test/link/reloc_uleb128_unit.c @@ -29,19 +29,15 @@ #include <stdio.h> #include <string.h> +#include "lib/cfree_unit.h" #include "obj/obj.h" #include "obj/reloc_apply.h" -static int g_failures; -#define CHECK(cond, ...) \ - do { \ - if (!(cond)) { \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - g_failures++; \ - } \ - } while (0) +/* Shared test context replaces the per-file counter global; CHECK aliases + * CU_CHECK so the call sites are unchanged. This file builds no compiler/ + * heap/diag — it only needs the check counters. */ +static CfreeUnit g_u; +#define CHECK(cond, ...) CU_CHECK(&g_u, cond, __VA_ARGS__) /* Decode a ULEB128 at p, returning value and (via *len_out) byte length. */ static uint64_t decode_uleb128(const uint8_t* p, uint32_t* len_out) { @@ -86,6 +82,8 @@ static void expect_field(const char* label, const uint8_t* site, } int main(void) { + cfree_unit_init(&g_u); + /* ---- Case 1: real-object fixtures (1-byte fields) ---- */ { /* off 0x12 of .debug_rnglists: pre-filled assembler value 0x18, a @@ -156,8 +154,8 @@ int main(void) { expect_field("set-only", buf, 0x2au, 1u, 0x9d, buf[1]); } - if (g_failures) { - fprintf(stderr, "reloc_uleb128_unit: %d failure(s)\n", g_failures); + if (g_u.fails) { + fprintf(stderr, "reloc_uleb128_unit: %d failure(s)\n", g_u.fails); return 1; } fputs("reloc_uleb128_unit: OK\n", stderr); diff --git a/test/link/run.sh b/test/link/run.sh @@ -1,22 +1,25 @@ #!/usr/bin/env bash -# test/link/run.sh — linker and JIT test harness. +# test/link/run.sh — linker and JIT test harness, on the shared corpus +# harness (test/lib/cf_corpus.sh). # -# Three paths per case: +# Cases are DIRECTORIES: test/link/cases/<name>/ and bad/<name>/, each holding +# source(s) + marker files. Three lanes per case (CFREE_TEST_PATHS, default REJ): # # R test/link/cases/*/ clang-c → .o → cfree-roundtrip → structural diff. # Validates obj reader + writer fidelity. -# E clang-c → .o → link-exe-runner (cfree_link_exe) → AArch64 ELF -# → qemu/podman → check exit code. +# E clang-c → .o → link-exe-runner (cfree_link_exe) → exe → qemu/podman +# → check exit code (DEFERRED batched exec, except +# kernel_image cases which run a SYNCHRONOUS +# qemu-system-* via exec_kernel.sh inline). # Validates linker layout and reloc application. # J clang-c → .o → jit-runner (cfree_link_jit) → call test_main() -# → check return value. -# Validates JIT path (runs on aarch64 host only). +# → check return value (host-native arch only). +# Validates JIT path. # # Negative tests live in test/link/bad/<name>/ — sources that compile -# cleanly but should cause the linker (and JIT) to reject. Each bad/ case -# requires an `expect` file containing a substring that must appear in -# stderr. No special markers in test/link/cases/ trigger negative-test -# behavior. +# cleanly but should cause the linker (E) and JIT (J) to reject. Each bad/ +# case requires an `expect` file containing a substring that must appear in +# stderr. The bad/ corpus is a SECOND cf_corpus_run with negative E/J lanes. # # Case markers (files in the case directory): # expected — expected exit/return value (default 0) @@ -25,41 +28,44 @@ # linker_flags — one flag per line; passed to link-exe-runner and jit-runner # cflags — extra clang -c flags appended to every TU compile # gc_absent — one symbol per line; verified absent post-link -# (jit_runner --check-absent for J; readelf -s for E) +# (jit_runner --check-absent for J; llvm-nm scan for E) # gc_present — one symbol per line; verified present post-link -# (jit_runner --check-present for J; readelf -s for E) +# (jit_runner --check-present for J; llvm-nm scan for E) # archive_b — package b.o into b.a; content "demand" or "whole" # linker_script — basename of an .lds file in the case dir; passed via -# --linker-script to both runners. The harness first -# looks for a per-arch variant (foo.<arch>.lds) before -# falling back to the literal name. -# kernel_image — empty marker; case is a freestanding kernel image. -# Skips paths R and J; on E, runs the linked exe via -# a per-arch qemu-system-* invocation (semihosting on -# aa64; SIFIVE_TEST MMIO exit on rv64). -# j_targets — per-path applicability for J only. Listed tuples -# (one per line) run J; others print SKIP-NA for J -# and continue running R/E. For cases whose JIT -# success criterion depends on an ABI feature with -# no Mach-O analogue (ELF .fini_array destructors, -# -ffunction-sections per-fn dead-strip). +# --linker-script to both runners. The harness first looks +# for a per-arch variant (foo.<arch>.lds) before the literal. +# kernel_image — empty marker; case is a freestanding kernel image. Skips +# R and J; on E runs the linked exe via a per-arch +# qemu-system-* invocation (semihosting on aa64; SIFIVE_TEST +# MMIO exit on rv64) SYNCHRONOUSLY. +# targets — whole-case applicability: listed <arch>-<obj> tuples (one +# per line) run; others print SKIP-NA and don't count. +# j_targets — per-lane applicability for J only. Listed tuples run J; +# others SKIP-NA for J and continue running R/E. +# e_targets — peer of j_targets for lane E. # -# Per-arch source variants: -# For each candidate source filename (entry.S, a.S, b.S, a.c, b.c, c.c), -# the harness picks <name>.<TEST_ARCH>.<ext> if present, else falls back -# to the bare <name>.<ext>. Same for any file referenced by linker_script. -# Existing aa64-only cases keep their bare names; per-arch variants are -# purely additive. +# Per-arch source variants: for each candidate (entry.S, a.S, b.S, a.c, b.c, +# c.c), prefer <name>.<TEST_ARCH>.<ext> if present, else the bare <name>.<ext>. +# Same for any file referenced by linker_script. # # Filtering: # ./run.sh [name_filter] [paths] # name_filter substring match against case name (e.g. "02", "rodata") # paths subset of "REJ" (default "REJ") # Equivalent env vars: CFREE_TEST_FILTER, CFREE_TEST_PATHS. +# +# All lane hooks write only under CF_WORK and record via cf_*, so the runner +# is parallel-safe by construction; CFREE_LINK_PARALLEL flips dispatch. The +# default is serial (0) because of the cached start.o + synchronous kernel +# path; flipping to 1 is a one-line change since every hook is CF_WORK-confined. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + TEST_DIR="$ROOT/test/link" BUILD_DIR="$ROOT/build/test" LIB_AR="$ROOT/build/libcfree.a" @@ -109,35 +115,21 @@ EXEC_TAG="${EXEC_ARCH}-${EXEC_OS}" export CFREE_TEST_ARCH CFREE_TEST_OBJ CLANG_TARGET="--target=$CLANG_TRIPLE" -CC="${CC:-cc}" -CFREE_CFLAGS="-I$ROOT/include -I$ROOT/test" -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" # Filters (env vars or positional args; args win): -# $1 / CFREE_TEST_FILTER — substring match against case name (e.g. "02" or "rodata") -# $2 / CFREE_TEST_PATHS — subset of "REJ" (default "REJ"); selects which paths run +# $1 / CFREE_TEST_FILTER — substring match against case name (e.g. "02") +# $2 / CFREE_TEST_PATHS — subset of "REJ" (default "REJ"); selects paths FILTER="${1:-${CFREE_TEST_FILTER:-}}" PATHS="${2:-${CFREE_TEST_PATHS:-REJ}}" +# cf_corpus_discover filters discovery by CFREE_TEST_FILTER (substring vs base). +export CFREE_TEST_FILTER="$FILTER" case "$PATHS" in *R*) RUN_R=1;; *) RUN_R=0;; esac case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac -T_R=0; T_E=0; T_J=0 # accumulated wall-clock seconds per path -now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR/link" -PASS=0; FAIL=0; SKIP=0 -FAIL_NAMES=(); SKIP_NAMES=() - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } - # ---- tool detection -------------------------------------------------------- have_clang_cross=0 @@ -160,17 +152,16 @@ have_readobj=0 command -v llvm-readobj >/dev/null 2>&1 && have_readobj=1 command -v readobj >/dev/null 2>&1 && have_readobj=1 # Path R needs the right dump tool for the obj format. ELF wants -# llvm-readelf; Mach-O wants llvm-readobj. The harness exposes a -# single have_dump flag so the per-case skip logic doesn't have to -# branch on CFREE_TEST_OBJ. +# llvm-readelf; Mach-O wants llvm-readobj. The harness exposes a single +# have_dump flag so per-case skip logic doesn't have to branch on the obj. have_dump=0 case "$CFREE_TEST_OBJ" in elf) [ $have_readelf -eq 1 ] && have_dump=1 ;; macho) [ $have_readobj -eq 1 ] && have_dump=1 ;; esac -# Prefer llvm-ar for archive creation: Apple's /usr/bin/ar requires -# Mach-O members and silently drops ELF objects (leaving only a SYMDEF -# stub), which breaks the cross-target archive cases here. +# Prefer llvm-ar for archive creation: Apple's /usr/bin/ar requires Mach-O +# members and silently drops ELF objects, breaking the cross-target archive +# cases here. AR_BIN="$(command -v llvm-ar 2>/dev/null || command -v ar 2>/dev/null || true)" [ -n "$AR_BIN" ] && have_ar=1 [ -f "$ROUNDTRIP_BIN" ] && have_roundtrip=1 @@ -184,7 +175,7 @@ arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 # is_native_target=1 when the cross-target arch matches the host arch. -# Required for in-process JIT (path D) and the jit-runner (path J). +# Required for the jit-runner (path J). is_native_target=0 case "$TEST_ARCH" in aa64) [ $is_aarch64 -eq 1 ] && is_native_target=1 ;; @@ -193,24 +184,24 @@ case "$TEST_ARCH" in esac READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" -# llvm-nm works on both ELF and Mach-O; it's the format-agnostic tool for -# the gc_present / gc_absent symbol-presence checks. Falls back to plain -# `nm` which is also format-aware on most platforms. +# llvm-nm works on both ELF and Mach-O; format-agnostic tool for the +# gc_present / gc_absent symbol-presence checks. Falls back to plain `nm`. NM_BIN="$(command -v llvm-nm 2>/dev/null || command -v nm 2>/dev/null || true)" have_nm=0 [ -n "$NM_BIN" ] && have_nm=1 -# Shared per-arch exec helper. Path E queues each linked.exe and we -# drain all cases in a single `podman run` per arch after the main -# loop — amortizes the ~150 ms per-launch podman client overhead -# across the whole suite. +# Shared per-arch exec helpers. Path E queues each linked.exe and the engine +# drains all cases in one batched flush per arch (amortizes the ~150 ms +# per-launch podman client overhead). Kernel images run synchronously via +# exec_kernel.sh. EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +. "$ROOT/test/lib/exec_target.sh" # shellcheck source=../lib/exec_kernel.sh -source "$ROOT/test/lib/exec_kernel.sh" +. "$ROOT/test/lib/exec_kernel.sh" -# ---- locate harness binaries ------------------------------------------------ +# ---- locate harness binaries ----------------------------------------------- # The Makefile's `test-link` target builds these as proper Make targets so # they pick up libcfree.a changes. Running this script directly without # `make test-link` is supported but requires the binaries to already exist. @@ -227,24 +218,23 @@ have_jit_runner=0 if [ -x "$LINK_EXE_RUNNER" ]; then have_exe_runner=1 - printf ' %s link-exe-runner\n' "$(color_grn found)" + printf ' found link-exe-runner\n' else - printf ' %s link-exe-runner missing (run "make %s")\n' \ - "$(color_yel warn)" "$LINK_EXE_RUNNER" >&2 + printf ' warn link-exe-runner missing (run "make %s")\n' "$LINK_EXE_RUNNER" >&2 fi if [ $is_native_target -eq 1 ]; then if [ -x "$JIT_RUNNER" ]; then have_jit_runner=1 - printf ' %s jit-runner\n' "$(color_grn found)" + printf ' found jit-runner\n' else - printf ' %s jit-runner missing (run "make %s")\n' \ - "$(color_yel warn)" "$JIT_RUNNER" >&2 + printf ' warn jit-runner missing (run "make %s")\n' "$JIT_RUNNER" >&2 fi fi # Cached start.o — every case used to recompile this from the same source -# (~40 ms × N cases). Build it once for the whole harness run. +# (~40 ms × N cases). Build it once for the whole harness run. Hooks only +# READ it, so this stays parallel-safe. START_OBJ="$BUILD_DIR/link_start.$TEST_ARCH.$CFREE_TEST_OBJ.o" have_start_obj=0 if [ $have_clang_cross -eq 1 ]; then @@ -255,10 +245,10 @@ if [ $have_clang_cross -eq 1 ]; then fi fi -# Mach-O Path E needs libSystem.tbd for the `exit` import in start.c (and -# any libc calls user TUs make). Resolve it via xcrun on Darwin hosts. -# On non-Darwin hosts there is no SDK to point at and Mach-O exec is SKIP -# anyway (see exec_target.sh), so leaving these empty is fine. +# Mach-O Path E needs libSystem.tbd for the `exit` import in start.c (and any +# libc calls user TUs make). Resolve it via xcrun on Darwin hosts. On non-Darwin +# hosts there is no SDK to point at and Mach-O exec is SKIP anyway, so leaving +# these empty is fine. MACHO_LIBSYSTEM="" MACHO_DSO_ARGS=() if [ "$CFREE_TEST_OBJ" = "macho" ]; then @@ -271,575 +261,501 @@ if [ "$CFREE_TEST_OBJ" = "macho" ]; then fi fi -printf 'Running cases...\n' - -# Path E result bookkeeping. We queue each linked.exe during the main loop -# and drain them all in one podman invocation after the loop, then verify -# the recorded exit codes / symbol checks here. -# -# bad/ cases are NOT queued: their linker is expected to fail, so the -# podman runner never gets invoked. -# -# The kernel_image case is also not queued: it uses qemu-system-aarch64 -# rather than the user-mode runner and runs synchronously. -E_NAMES=() -E_WORK=() -E_EXE=() -E_LINK_MS=() -E_EXPECTED=() -E_GC_ABSENT_LIST=() # newline-joined per case (empty if none) -E_GC_PRESENT_LIST=() - -# ---- per-case loop --------------------------------------------------------- - CUR_TUPLE="${TEST_ARCH}-${CFREE_TEST_OBJ}" -for case_dir in "$TEST_DIR/cases"/*/; do - [ -d "$case_dir" ] || continue - name="$(basename "$case_dir")" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - # Per-case applicability: a `targets` file lists the <arch>-<obj> - # tuples the case applies to (one per line, or whitespace-separated). - # Cases with no `targets` file run on every tuple. Filtered cases - # print a SKIP-NA line and don't count against pass/fail/skip — they - # exercise target-specific features with no analogue elsewhere - # (e.g. ELF __start_/__stop_ boundary syms have no Mach-O peer; - # ELF TLS local-exec relocs differ fundamentally from Mach-O TLVP). - if [ -f "$case_dir/targets" ]; then +# ---- per-case marker reading (CF_READ_CASE) -------------------------------- +# Runs once per item, before lanes. Reads markers + per-arch source variants +# into shell vars the lane hooks consume, then compiles the TUs (shared by all +# lanes within the item) into $CF_WORK. On compile failure, records one FAIL +# for the whole case and sets CF_SKIP_NA_CASE to suppress all lanes (mirrors +# the original "note_fail; continue"). +link_read_case() { + CASE_DIR="${CF_SRC%/}" # CF_SRC is the glob match "cases/<name>/" + NAME="$CF_BASE" # case name (== dir basename) + + # Whole-case applicability via `targets` (literal marker name, no base/ext). + if [ -f "$CASE_DIR/targets" ]; then applicable=0 - for tuple in $(cat "$case_dir/targets"); do + for tuple in $(cat "$CASE_DIR/targets"); do [ "$tuple" = "$CUR_TUPLE" ] && applicable=1 done if [ $applicable -eq 0 ]; then - printf ' %s %s — N/A on %s\n' "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE" - continue + # SKIP-NA whole case (uncounted) — target-specific feature with no + # analogue on this tuple. + printf ' SKIP-NA %s — N/A on %s\n' "$NAME" "$CUR_TUPLE" + CF_SKIP_NA_CASE=1 + return fi fi - work="$BUILD_DIR/link/$name" - mkdir -p "$work" - - # Read markers - expected=0; [ -f "$case_dir/expected" ] && expected="$(cat "$case_dir/expected" | tr -d '[:space:]')" - jit_only=0; [ -f "$case_dir/jit_only" ] && jit_only=1 - use_resolver=0; [ -f "$case_dir/use_resolver" ] && use_resolver=1 - # Per-path applicability — `j_targets` lists tuples on which the - # J path can run. Used for cases that exercise an ELF-specific - # ABI feature inapplicable to Mach-O at the test level (e.g. ELF - # `.fini_array` destructors vs Mach-O `__StaticInit` + `atexit`; - # `-ffunction-sections` per-function dead-strip vs Mach-O's single - # `__TEXT,__text`). R and E still run — they don't depend on the - # ABI feature the J path's pass/fail criterion does. - j_applicable=1 - if [ -f "$case_dir/j_targets" ]; then - j_applicable=0 - for tuple in $(cat "$case_dir/j_targets"); do - [ "$tuple" = "$CUR_TUPLE" ] && j_applicable=1 + + # Markers + CF_EXPECTED=0 + [ -f "$CASE_DIR/expected" ] && CF_EXPECTED="$(tr -d '[:space:]' < "$CASE_DIR/expected")" + JIT_ONLY=0; [ -f "$CASE_DIR/jit_only" ] && JIT_ONLY=1 + USE_RESOLVER=0; [ -f "$CASE_DIR/use_resolver" ] && USE_RESOLVER=1 + + # Per-lane applicability — j_targets / e_targets list tuples on which the + # J / E lane can run. R and the other lane still run; only the gated lane + # prints SKIP-NA. + J_APPLICABLE=1 + if [ -f "$CASE_DIR/j_targets" ]; then + J_APPLICABLE=0 + for tuple in $(cat "$CASE_DIR/j_targets"); do + [ "$tuple" = "$CUR_TUPLE" ] && J_APPLICABLE=1 done fi - # `e_targets` — peer of `j_targets` for path E. Same shape, same - # semantics: cases that exercise an ELF-specific runtime feature - # the linked-exe-runner can't validate on Mach-O (ELF .fini_array - # walk in start.c, --gc-sections per-function granularity that - # Apple's clang doesn't emit, etc.) list the tuples on which E - # runs. R and J still run. - e_applicable=1 - if [ -f "$case_dir/e_targets" ]; then - e_applicable=0 - for tuple in $(cat "$case_dir/e_targets"); do - [ "$tuple" = "$CUR_TUPLE" ] && e_applicable=1 + E_APPLICABLE=1 + if [ -f "$CASE_DIR/e_targets" ]; then + E_APPLICABLE=0 + for tuple in $(cat "$CASE_DIR/e_targets"); do + [ "$tuple" = "$CUR_TUPLE" ] && E_APPLICABLE=1 done fi - archive_mode="none" - if [ -f "$case_dir/archive_b" ]; then - archive_mode="$(cat "$case_dir/archive_b" | tr -d '[:space:]')" - fi - # Collect extra linker flags - extra_flags=() - if [ -f "$case_dir/linker_flags" ]; then + ARCHIVE_MODE="none" + [ -f "$CASE_DIR/archive_b" ] && ARCHIVE_MODE="$(tr -d '[:space:]' < "$CASE_DIR/archive_b")" + + # Extra linker flags (one per line). + EXTRA_FLAGS=() + if [ -f "$CASE_DIR/linker_flags" ]; then while IFS= read -r flag; do - [ -n "$flag" ] && extra_flags+=("$flag") - done < "$case_dir/linker_flags" + [ -n "$flag" ] && EXTRA_FLAGS+=("$flag") + done < "$CASE_DIR/linker_flags" fi - # Collect GC-absent / GC-present symbols - gc_absent_syms=() - if [ -f "$case_dir/gc_absent" ]; then + # GC-absent / GC-present symbols. + GC_ABSENT_SYMS=() + if [ -f "$CASE_DIR/gc_absent" ]; then while IFS= read -r sym; do - [ -n "$sym" ] && gc_absent_syms+=("$sym") - done < "$case_dir/gc_absent" + [ -n "$sym" ] && GC_ABSENT_SYMS+=("$sym") + done < "$CASE_DIR/gc_absent" fi - gc_present_syms=() - if [ -f "$case_dir/gc_present" ]; then + GC_PRESENT_SYMS=() + if [ -f "$CASE_DIR/gc_present" ]; then while IFS= read -r sym; do - [ -n "$sym" ] && gc_present_syms+=("$sym") - done < "$case_dir/gc_present" + [ -n "$sym" ] && GC_PRESENT_SYMS+=("$sym") + done < "$CASE_DIR/gc_present" fi - # Per-case extra clang cflags (mirrors test/elf/cases/*.cflags but as a - # file per case dir). One token per whitespace. - case_cflags=() - if [ -f "$case_dir/cflags" ]; then + # Per-case extra clang cflags (one token per whitespace). + CASE_CFLAGS=() + if [ -f "$CASE_DIR/cflags" ]; then while IFS= read -r line; do for tok in $line; do - [ -n "$tok" ] && case_cflags+=("$tok") + [ -n "$tok" ] && CASE_CFLAGS+=("$tok") done - done < "$case_dir/cflags" + done < "$CASE_DIR/cflags" fi - # Collect source files (.c and .S; clang -c accepts both). For each - # candidate, prefer the per-arch variant (entry.aa64.S beats entry.S - # when TEST_ARCH=aa64) so cases can ship arch-specific entry stubs - # alongside arch-agnostic shared sources. The bare name is the - # fallback — existing aa64-only cases keep working. - tu_srcs=() - pick_variant() { - local base="$1" ext="$2" - if [ -f "$case_dir/${base}.${TEST_ARCH}.${ext}" ]; then - echo "$case_dir/${base}.${TEST_ARCH}.${ext}" - elif [ -f "$case_dir/${base}.${ext}" ]; then - echo "$case_dir/${base}.${ext}" - else - echo "" - fi - } + KERNEL_IMAGE=0 + [ -f "$CASE_DIR/kernel_image" ] && KERNEL_IMAGE=1 + + # Source files (.c and .S). For each candidate, prefer the per-arch variant + # (entry.aa64.S beats entry.S when TEST_ARCH=aa64); the bare name is the + # fallback. + TU_SRCS=() for spec in entry:S a:S b:S a:c b:c c:c; do base="${spec%%:*}"; ext="${spec##*:}" - f="$(pick_variant "$base" "$ext")" - [ -n "$f" ] && tu_srcs+=("$f") + if [ -f "$CASE_DIR/${base}.${TEST_ARCH}.${ext}" ]; then + TU_SRCS+=("$CASE_DIR/${base}.${TEST_ARCH}.${ext}") + elif [ -f "$CASE_DIR/${base}.${ext}" ]; then + TU_SRCS+=("$CASE_DIR/${base}.${ext}") + fi done - # Linker script + kernel-image markers. The marker file content is a - # basename (e.g. "kernel.lds"); the harness derives a per-arch - # variant (kernel.<arch>.lds) first, falling back to the literal. - linker_script_file="" - if [ -f "$case_dir/linker_script" ]; then - ls_base="$(cat "$case_dir/linker_script" | tr -d '[:space:]')" + # Linker script (marker content is a basename; derive per-arch variant + # first, fall back to literal). + LINKER_SCRIPT_FILE="" + if [ -f "$CASE_DIR/linker_script" ]; then + ls_base="$(tr -d '[:space:]' < "$CASE_DIR/linker_script")" ls_stem="${ls_base%.*}" ls_ext="${ls_base##*.}" - if [ -f "$case_dir/${ls_stem}.${TEST_ARCH}.${ls_ext}" ]; then - linker_script_file="$case_dir/${ls_stem}.${TEST_ARCH}.${ls_ext}" - elif [ -f "$case_dir/${ls_base}" ]; then - linker_script_file="$case_dir/${ls_base}" + if [ -f "$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}" ]; then + LINKER_SCRIPT_FILE="$CASE_DIR/${ls_stem}.${TEST_ARCH}.${ls_ext}" + elif [ -f "$CASE_DIR/${ls_base}" ]; then + LINKER_SCRIPT_FILE="$CASE_DIR/${ls_base}" fi fi - kernel_image=0 - [ -f "$case_dir/kernel_image" ] && kernel_image=1 - - # kernel_image cases need an arch-specific entry stub. If the case - # ships no entry.<arch>.S (and no bare entry.S that happens to - # build), the case is structurally inapplicable to this arch and - # the harness skips it. - if [ $kernel_image -eq 1 ] && \ - [ ! -f "$case_dir/entry.${TEST_ARCH}.S" ] && \ - [ ! -f "$case_dir/entry.S" ]; then - note_skip "$name" "kernel_image: no entry.${TEST_ARCH}.S in case" - continue + + # kernel_image cases need an arch-specific entry stub. If the case ships no + # entry.<arch>.S (and no bare entry.S), it's structurally inapplicable here. + if [ $KERNEL_IMAGE -eq 1 ] && \ + [ ! -f "$CASE_DIR/entry.${TEST_ARCH}.S" ] && \ + [ ! -f "$CASE_DIR/entry.S" ]; then + CF_SKIP_CASE="kernel_image: no entry.${TEST_ARCH}.S in case" + return fi - # ---- compile with clang cross ------------------------------------------ + # ---- compile with clang cross ----------------------------------------- + COMPILE_OK=1 if [ $have_clang_cross -eq 0 ]; then - note_skip "$name/R" "no $TEST_ARCH clang" - [ $jit_only -eq 0 ] && note_skip "$name/E" "no $TEST_ARCH clang" - note_skip "$name/J" "no $TEST_ARCH clang" - continue + # Original emits per-path SKIP lines (R, optionally E, J) and continues. + cf_skip "$NAME/R" "no $TEST_ARCH clang" + [ $JIT_ONLY -eq 0 ] && cf_skip "$NAME/E" "no $TEST_ARCH clang" + cf_skip "$NAME/J" "no $TEST_ARCH clang" + CF_SKIP_NA_CASE=1 + return fi - obj_files=(); compile_ok=1 - for src in "${tu_srcs[@]}"; do + OBJ_FILES=() + for src in "${TU_SRCS[@]}"; do base="$(basename "$src")"; base="${base%.c}"; base="${base%.S}" - obj="$work/${base}.o" + obj="$CF_WORK/${base}.o" if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \ -fno-PIC -fno-pie -fcommon \ - "${case_cflags[@]}" \ - -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then - compile_ok=0; break + "${CASE_CFLAGS[@]}" \ + -c "$src" -o "$obj" 2>"$CF_WORK/compile_${base}.err"; then + COMPILE_OK=0; break fi - obj_files+=("$obj") + OBJ_FILES+=("$obj") done - if [ $compile_ok -eq 0 ]; then - note_fail "$name (compile failed)" - continue + if [ $COMPILE_OK -eq 0 ]; then + cf_fail "$NAME (compile failed)" + CF_SKIP_NA_CASE=1 + return fi - # ---- build archive from b.o if requested -------------------------------- + # ---- build archive from b.o if requested ------------------------------ # Roundtrip always uses raw .o files; exec/JIT use the archive. - rt_obj_files=("${obj_files[@]}") # for roundtrip: all .o files - link_obj_files=() # for exec/JIT - link_arc_flags=() - - for o in "${obj_files[@]}"; do + RT_OBJ_FILES=("${OBJ_FILES[@]}") # for roundtrip: all .o files + LINK_OBJ_FILES=() # for exec/JIT + LINK_ARC_FLAGS=() + for o in "${OBJ_FILES[@]}"; do base="$(basename "$o" .o)" - if [ "$base" = "b" ] && [ "$archive_mode" != "none" ]; then + if [ "$base" = "b" ] && [ "$ARCHIVE_MODE" != "none" ]; then if [ $have_ar -eq 1 ]; then - arc="$work/b.a" + arc="$CF_WORK/b.a" "$AR_BIN" rcs "$arc" "$o" 2>/dev/null - if [ "$archive_mode" = "whole" ]; then - link_arc_flags+=(--whole-archive --archive "$arc") + if [ "$ARCHIVE_MODE" = "whole" ]; then + LINK_ARC_FLAGS+=(--whole-archive --archive "$arc") else - link_arc_flags+=(--archive "$arc") + LINK_ARC_FLAGS+=(--archive "$arc") fi else - # no ar: just pass .o directly (archive test degrades to obj test) - link_obj_files+=("$o") + LINK_OBJ_FILES+=("$o") fi else - link_obj_files+=("$o") + LINK_OBJ_FILES+=("$o") fi done - - # ---- Path R: roundtrip -------------------------------------------------- - if [ $jit_only -eq 0 ] && [ $RUN_R -eq 1 ] && [ $kernel_image -eq 0 ]; then - if [ $have_roundtrip -eq 1 ] && [ $have_dump -eq 1 ] && [ $have_python3 -eq 1 ]; then - t0=$(now_ms) - r_ok=1 - for obj in "${rt_obj_files[@]}"; do - base="$(basename "$obj" .o)" - rt="$work/${base}_rt.o" - if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt_${base}.err"; then - r_ok=0; break - fi - # ELF: pipe `readelf -aW` through normalize.py filter. - # Mach-O: normalize.py runs llvm-readobj on the file - # itself (it knows the right flag set). - if [ "$CFREE_TEST_OBJ" = "macho" ]; then - python3 "$NORMALIZE" "$obj" \ - >"$work/${base}_golden.norm" 2>/dev/null - python3 "$NORMALIZE" "$rt" \ - >"$work/${base}_rt.norm" 2>/dev/null - else - "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \ - >"$work/${base}_golden.norm" 2>/dev/null - "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \ - >"$work/${base}_rt.norm" 2>/dev/null - fi - if ! diff -u "$work/${base}_golden.norm" \ - "$work/${base}_rt.norm" \ - >"$work/${base}_diff.txt" 2>&1; then - r_ok=0; break - fi - done - dt=$(( $(now_ms) - t0 )); T_R=$(( T_R + dt )) - if [ $r_ok -eq 1 ]; then note_pass "$name/R (${dt}ms)" - else note_fail "$name/R"; fi - else - note_skip "$name/R" "missing roundtrip/dump-tool/python3" - fi +} + +# ---- cases-corpus lanes ---------------------------------------------------- + +# R — roundtrip + golden structural diff. Skipped (no record) for jit_only and +# kernel_image cases, matching the original guards. +cf_lane_R() { + [ $JIT_ONLY -eq 1 ] && return + [ $KERNEL_IMAGE -eq 1 ] && return + if [ $have_roundtrip -eq 1 ] && [ $have_dump -eq 1 ] && [ $have_python3 -eq 1 ]; then + local t0; t0=$(cf_now_ms) + local r_ok=1 obj base rt + for obj in "${RT_OBJ_FILES[@]}"; do + base="$(basename "$obj" .o)" + rt="$CF_WORK/${base}_rt.o" + if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$CF_WORK/rt_${base}.err"; then + r_ok=0; break + fi + if [ "$CFREE_TEST_OBJ" = "macho" ]; then + python3 "$NORMALIZE" "$obj" >"$CF_WORK/${base}_golden.norm" 2>/dev/null + python3 "$NORMALIZE" "$rt" >"$CF_WORK/${base}_rt.norm" 2>/dev/null + else + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \ + >"$CF_WORK/${base}_golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \ + >"$CF_WORK/${base}_rt.norm" 2>/dev/null + fi + if ! diff -u "$CF_WORK/${base}_golden.norm" "$CF_WORK/${base}_rt.norm" \ + >"$CF_WORK/${base}_diff.txt" 2>&1; then + r_ok=0; break + fi + done + cf_time R "$(( $(cf_now_ms) - t0 ))" + if [ $r_ok -eq 1 ]; then cf_pass "$NAME/R" + else cf_fail "$NAME/R"; fi + else + cf_skip "$NAME/R" "missing roundtrip/dump-tool/python3" + fi +} + +# E — link now; defer exec (batched) or run kernel synchronously. Branch +# precedence mirrors the original if/elif/elif: the e_targets SKIP-NA check +# (non-kernel, inapplicable tuple) fires first regardless of jit_only or runner +# availability; then jit_only cases produce no E record; then the link path. +cf_lane_E() { + # e_targets SKIP-NA (uncounted) for non-kernel cases on inapplicable tuples. + if [ $E_APPLICABLE -eq 0 ] && [ $KERNEL_IMAGE -eq 0 ]; then + printf ' SKIP-NA %s/E — N/A on %s\n' "$NAME" "$CUR_TUPLE" + cf_skip_na "$NAME/E" + return + fi + # jit_only cases skip E entirely (no record). + [ $JIT_ONLY -eq 1 ] && return + if [ $have_exe_runner -eq 0 ]; then + cf_skip "$NAME/E" "no link-exe-runner" + return fi - # ---- Path E: exec ------------------------------------------------------- - # Two stages: link now (per case), then run later in a single batched - # podman invocation. Kernel-image cases are an exception — they use - # qemu-system-aarch64 and run inline. - if [ $RUN_E -eq 1 ] && [ $e_applicable -eq 0 ] && [ $kernel_image -eq 0 ]; then - printf ' %s %s/E — N/A on %s\n' \ - "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE" - elif [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ] && \ - [ $e_applicable -eq 1 ]; then - t0=$(now_ms) - script_flags=() - if [ -n "$linker_script_file" ]; then - script_flags=(--linker-script "$linker_script_file") - fi + local t0; t0=$(cf_now_ms) + local script_flags=() + [ -n "$LINKER_SCRIPT_FILE" ] && script_flags=(--linker-script "$LINKER_SCRIPT_FILE") + + local exe="$CF_WORK/linked.exe" + local link_cmd + if [ $KERNEL_IMAGE -eq 1 ]; then + # Freestanding kernel image: no harness start.o; the case's own entry.S + # is the program entry. + link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \ + "${script_flags[@]}" -o "$exe" \ + "${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}") + elif [ $have_start_obj -eq 1 ]; then + link_cmd=("$LINK_EXE_RUNNER" "${EXTRA_FLAGS[@]}" \ + "${script_flags[@]}" -o "$exe") + [ ${#MACHO_DSO_ARGS[@]} -gt 0 ] && link_cmd+=("${MACHO_DSO_ARGS[@]}") + link_cmd+=("${LINK_OBJ_FILES[@]}" "$START_OBJ" "${LINK_ARC_FLAGS[@]}") + else + cf_skip "$NAME/E" "no cached start.o" + return + fi - exe="$work/linked.exe" - if [ $kernel_image -eq 1 ]; then - # Freestanding kernel image: no harness start.o, no startup - # crt; the case's own entry.S is the program entry. - link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \ - "${script_flags[@]}" -o "$exe" \ - "${link_obj_files[@]}" "${link_arc_flags[@]}") - elif [ $have_start_obj -eq 1 ]; then - link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" \ - "${script_flags[@]}" -o "$exe") - [ ${#MACHO_DSO_ARGS[@]} -gt 0 ] && \ - link_cmd+=("${MACHO_DSO_ARGS[@]}") - link_cmd+=("${link_obj_files[@]}" "$START_OBJ" \ - "${link_arc_flags[@]}") + if ! "${link_cmd[@]}" >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then + cf_time E "$(( $(cf_now_ms) - t0 ))" + cf_fail "$NAME/E" "link failed" + elif [ $KERNEL_IMAGE -eq 1 ]; then + if ! exec_kernel_supported "$TEST_ARCH"; then + cf_time E "$(( $(cf_now_ms) - t0 ))" + cf_skip "$NAME/E" "no qemu-system-* for $TEST_ARCH" else - note_skip "$name/E" "no cached start.o" - continue + exec_kernel_run "$TEST_ARCH" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err" + cf_time E "$(( $(cf_now_ms) - t0 ))" + if [ "$RUN_RC" -eq "$CF_EXPECTED" ]; then cf_pass "$NAME/E" + else cf_fail "$NAME/E" "expected $CF_EXPECTED got $RUN_RC"; fi fi + elif [ $have_runner -eq 1 ]; then + cf_time E "$(( $(cf_now_ms) - t0 ))" + # Defer to the batched flush. Payload carries the gc symbol-check sets, + # space-joined per list and '|'-separated (absent|present), verified + # post-flush by link_flush_verify. + local gca="" gcp="" s + for s in "${GC_ABSENT_SYMS[@]:-}"; do [ -n "$s" ] && gca="${gca:+$gca }$s"; done + for s in "${GC_PRESENT_SYMS[@]:-}"; do [ -n "$s" ] && gcp="${gcp:+$gcp }$s"; done + cf_queue_e "$NAME/E" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err" \ + "$CF_WORK/exec.rc" "$CF_EXPECTED" "$EXEC_TAG" "${gca}|${gcp}|$exe" + else + cf_skip "$NAME/E" "no runner (qemu/podman)" + fi +} + +# J — JIT runner + inline gc symbol checks. Skipped (no record) for kernel. +cf_lane_J() { + [ $KERNEL_IMAGE -eq 1 ] && return + # j_targets SKIP-NA (uncounted) on inapplicable tuples. + if [ $J_APPLICABLE -eq 0 ]; then + printf ' SKIP-NA %s/J — N/A on %s\n' "$NAME" "$CUR_TUPLE" + cf_skip_na "$NAME/J" + return + fi + if [ $have_jit_runner -eq 0 ]; then + cf_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" + return + fi - if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - note_fail "$name/E (link failed, ${dt}ms)" - elif [ $kernel_image -eq 1 ]; then - if ! exec_kernel_supported "$TEST_ARCH"; then - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - note_skip "$name/E" "no qemu-system-* for $TEST_ARCH" - else - exec_kernel_run "$TEST_ARCH" "$exe" \ - "$work/exec.out" "$work/exec.err" - dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - if [ "$RUN_RC" -eq "$expected" ]; then - note_pass "$name/E (${dt}ms)" - else - note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)" - fi - fi - elif [ $have_runner -eq 1 ]; then - # Queue for the post-loop batched flush. Per-case wall-clock - # for the run portion is amortized; we record link-time only. - link_dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + link_dt )) - E_NAMES+=("$name") - E_WORK+=("$work") - E_EXE+=("$exe") - E_LINK_MS+=("$link_dt") - E_EXPECTED+=("$expected") - # Newline-join the gc symbol lists so we can stash multi-element - # data inside scalar array slots (bash arrays-of-arrays don't - # exist). Empty string = no checks. - gca="" - for s in "${gc_absent_syms[@]:-}"; do [ -n "$s" ] && gca="${gca}${s}"$'\n'; done - gcp="" - for s in "${gc_present_syms[@]:-}"; do [ -n "$s" ] && gcp="${gcp}${s}"$'\n'; done - E_GC_ABSENT_LIST+=("$gca") - E_GC_PRESENT_LIST+=("$gcp") - exec_target_queue "$EXEC_TAG" "$name" "$exe" \ - "$work/exec.out" "$work/exec.err" "$work/exec.rc" + local t0; t0=$(cf_now_ms) + local jit_cmd=("$JIT_RUNNER" "${EXTRA_FLAGS[@]}") + [ $USE_RESOLVER -eq 1 ] && jit_cmd+=(--use-resolver) + [ -n "$LINKER_SCRIPT_FILE" ] && jit_cmd+=(--linker-script "$LINKER_SCRIPT_FILE") + jit_cmd+=("${LINK_OBJ_FILES[@]}" "${LINK_ARC_FLAGS[@]}") + + "${jit_cmd[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err" + local j_rc=$? sym + # gc_absent / gc_present checks via the jit_runner's --check-* flags. + for sym in "${GC_ABSENT_SYMS[@]:-}"; do + [ -z "$sym" ] && continue + if "${jit_cmd[@]}" --check-absent "$sym" \ + >"$CF_WORK/jit_gc.out" 2>"$CF_WORK/jit_gc.err"; then + : else - note_skip "$name/E" "no runner (qemu/podman)" + j_rc=$? + break fi - elif [ $jit_only -eq 0 ] && [ $RUN_E -eq 1 ] && [ $e_applicable -eq 1 ]; then - note_skip "$name/E" "no link-exe-runner" - fi - - # ---- Path J: JIT -------------------------------------------------------- - if [ $RUN_J -eq 1 ] && [ $j_applicable -eq 0 ] && [ $kernel_image -eq 0 ]; then - printf ' %s %s/J — N/A on %s\n' \ - "$(color_yel SKIP-NA)" "$name" "$CUR_TUPLE" - elif [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ] && [ $kernel_image -eq 0 ]; then - t0=$(now_ms) - jit_cmd=("$JIT_RUNNER" "${extra_flags[@]}") - [ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver) - [ -n "$linker_script_file" ] && jit_cmd+=(--linker-script "$linker_script_file") - jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}") - - "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - - # For gc_sections cases, additionally verify the absent and - # present symbol sets via the jit_runner's --check-* flags. - # Re-run jit_runner once per symbol; first failure wins. - for sym in "${gc_absent_syms[@]}"; do - if "${jit_cmd[@]}" --check-absent "$sym" \ - >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then - : # absent check passed + done + if [ "$j_rc" -eq "$CF_EXPECTED" ]; then + for sym in "${GC_PRESENT_SYMS[@]:-}"; do + [ -z "$sym" ] && continue + if "${jit_cmd[@]}" --check-present "$sym" \ + >"$CF_WORK/jit_gc.out" 2>"$CF_WORK/jit_gc.err"; then + : else j_rc=$? break fi done - if [ "$j_rc" -eq "$expected" ]; then - for sym in "${gc_present_syms[@]}"; do - if "${jit_cmd[@]}" --check-present "$sym" \ - >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then - : # present check passed - else - j_rc=$? - break - fi - done + fi + cf_time J "$(( $(cf_now_ms) - t0 ))" + if [ "$j_rc" -eq "$CF_EXPECTED" ]; then cf_pass "$NAME/J" + else cf_fail "$NAME/J" "expected $CF_EXPECTED got $j_rc"; fi +} + +# ---- deferred-E flush verification (gc symbol presence/absence) ------------ +# Called per queued-E item after the batched exec flush, ONLY when rc==expected +# (the engine's rc check passed). Runs the gc symbol presence/absence checks via +# llvm-nm (format-agnostic). Payload = "<absent syms>|<present syms>|<exe>". +# Returns 0 to keep the engine's PASS, 1 to flip it to FAIL — we do NOT call +# cf_fail ourselves (the engine records the single verdict). +link_flush_verify() { + local payload="$2" + local gca="${payload%%|*}"; payload="${payload#*|}" + local gcp="${payload%%|*}"; local exe="${payload#*|}" + { [ -z "$gca" ] && [ -z "$gcp" ]; } && return 0 + [ $have_nm -eq 1 ] || return 0 + + # Write the symbol dump next to the exe (its dir is the persistent per-item + # CF_WORK; parallel-safe). + local syms="$exe.syms" + "$NM_BIN" "$exe" >"$syms" 2>/dev/null + [ -s "$syms" ] || return 0 + + # llvm-nm format: "<addr> <type> <name>". `U` = undefined. On Mach-O the + # asm form of source `name` is `_name`; accept either. + local sym + for sym in $gca; do + if awk -v s="$sym" -v u="_$sym" \ + '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then + return 1 # gc_absent symbol unexpectedly present fi - - dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) - if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)" - else note_fail "$name/J (expected $expected, got $j_rc, ${dt}ms)"; fi - elif [ $RUN_J -eq 1 ] && [ $kernel_image -eq 0 ]; then - note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" + done + for sym in $gcp; do + if ! awk -v s="$sym" -v u="_$sym" \ + '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' "$syms"; then + return 1 # gc_present symbol missing + fi + done + return 0 +} + +# ---- bad/ negative lanes --------------------------------------------------- +# Sources compile cleanly but the linker (E) and JIT (J) must reject: non-zero +# exit with no signal AND stderr contains the substring from `expect`. + +bad_read_case() { + CASE_DIR="${CF_SRC%/}" + NAME="bad/$CF_BASE" + + EXPECT_FILE="$CASE_DIR/expect" + if [ ! -f "$EXPECT_FILE" ]; then + cf_fail "$NAME (missing $EXPECT_FILE)" + CF_SKIP_NA_CASE=1 + return fi + EXPECT="$(cat "$EXPECT_FILE")" -done - -# ---- bad/ — negative tests ------------------------------------------------- -# Sources that compile cleanly but should cause the linker (E) and JIT (J) -# to reject. Pass = runner exits non-zero with no signal AND stderr -# contains the substring from `expect`. No marker files; the bad/ -# directory itself is the marker. - -for case_dir in "$TEST_DIR/bad"/*/; do - [ -d "$case_dir" ] || continue - name="bad/$(basename "$case_dir")" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - work="$BUILD_DIR/link/$name" - mkdir -p "$work" - - expect_file="$case_dir/expect" - if [ ! -f "$expect_file" ]; then - note_fail "$name (missing $expect_file)"; continue + if [ $have_clang_cross -eq 0 ]; then + cf_skip "$NAME" "no $TEST_ARCH clang" + CF_SKIP_NA_CASE=1 + return fi - expect="$(cat "$expect_file")" - if [ $have_clang_cross -eq 0 ]; then note_skip "$name" "no $TEST_ARCH clang"; continue; fi - - tu_srcs=() - for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do - [ -f "$f" ] && tu_srcs+=("$f") + TU_SRCS=() + for f in "$CASE_DIR/a.c" "$CASE_DIR/b.c" "$CASE_DIR/c.c"; do + [ -f "$f" ] && TU_SRCS+=("$f") done - obj_files=(); compile_ok=1 - for src in "${tu_srcs[@]}"; do + OBJ_FILES=(); COMPILE_OK=1 + for src in "${TU_SRCS[@]}"; do base="$(basename "$src" .c)" - obj="$work/${base}.o" + obj="$CF_WORK/${base}.o" if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \ -fno-PIC -fno-pie -fcommon \ - -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then - compile_ok=0; break + -c "$src" -o "$obj" 2>"$CF_WORK/compile_${base}.err"; then + COMPILE_OK=0; break fi - obj_files+=("$obj") + OBJ_FILES+=("$obj") done - if [ $compile_ok -eq 0 ]; then - note_fail "$name (compile failed; sources should compile, only link is expected to fail)" - continue + if [ $COMPILE_OK -eq 0 ]; then + cf_fail "$NAME (compile failed; sources should compile, only link is expected to fail)" + CF_SKIP_NA_CASE=1 + return fi +} - # Path E (negative). Linker is expected to fail; no exe is run, so - # this case never enters the podman queue. - if [ $RUN_E -eq 1 ] && [ $have_exe_runner -eq 1 ]; then - if [ $have_start_obj -eq 0 ]; then - note_skip "$name/E" "no cached start.o" +cf_lane_BE() { # bad/ Path E (negative) + if [ $have_exe_runner -eq 0 ]; then + cf_skip "$NAME/E" "no link-exe-runner" + return + fi + if [ $have_start_obj -eq 0 ]; then + cf_skip "$NAME/E" "no cached start.o" + return + fi + local exe="$CF_WORK/linked.exe" + if "$LINK_EXE_RUNNER" -o "$exe" "${OBJ_FILES[@]}" "$START_OBJ" \ + >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then + cf_fail "$NAME/E" "linker succeeded; expected non-zero exit" + else + local rc=$? + if [ $rc -ge 128 ]; then + cf_fail "$NAME/E" "linker died via signal $((rc-128))" + elif ! grep -qF -- "$EXPECT" "$CF_WORK/link.err"; then + cf_fail "$NAME/E" "stderr did not contain: $EXPECT" + sed 's/^/ | /' "$CF_WORK/link.err" else - exe="$work/linked.exe" - if "$LINK_EXE_RUNNER" -o "$exe" "${obj_files[@]}" "$START_OBJ" \ - >"$work/link.out" 2>"$work/link.err"; then - note_fail "$name/E (linker succeeded; expected non-zero exit)" - else - rc=$? - if [ $rc -ge 128 ]; then - note_fail "$name/E (linker died via signal $((rc-128)))" - elif ! grep -qF -- "$expect" "$work/link.err"; then - note_fail "$name/E (stderr did not contain: $expect)" - sed 's/^/ | /' "$work/link.err" - else - note_pass "$name/E" - fi - fi + cf_pass "$NAME/E" fi - elif [ $RUN_E -eq 1 ]; then - note_skip "$name/E" "no link-exe-runner" fi +} - # Path J - if [ $RUN_J -eq 1 ] && [ $have_jit_runner -eq 1 ]; then - if "$JIT_RUNNER" "${obj_files[@]}" >"$work/jit.out" 2>"$work/jit.err"; then - note_fail "$name/J (jit-runner succeeded; expected non-zero exit)" +cf_lane_BJ() { # bad/ Path J (negative) + if [ $have_jit_runner -eq 0 ]; then + cf_skip "$NAME/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" + return + fi + if "$JIT_RUNNER" "${OBJ_FILES[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err"; then + cf_fail "$NAME/J" "jit-runner succeeded; expected non-zero exit" + else + local rc=$? + if [ $rc -ge 128 ]; then + cf_fail "$NAME/J" "jit-runner died via signal $((rc-128))" + elif ! grep -qF -- "$EXPECT" "$CF_WORK/jit.err"; then + cf_fail "$NAME/J" "stderr did not contain: $EXPECT" + sed 's/^/ | /' "$CF_WORK/jit.err" else - rc=$? - if [ $rc -ge 128 ]; then - note_fail "$name/J (jit-runner died via signal $((rc-128)))" - elif ! grep -qF -- "$expect" "$work/jit.err"; then - note_fail "$name/J (stderr did not contain: $expect)" - sed 's/^/ | /' "$work/jit.err" - else - note_pass "$name/J" - fi + cf_pass "$NAME/J" fi - elif [ $RUN_J -eq 1 ]; then - note_skip "$name/J" "no jit-runner (host arch != $TEST_ARCH or build failed)" fi -done - -# ---- batched path-E flush + verification ----------------------------------- -# Run every queued user case in a single podman invocation, then iterate -# the queue to read each exit code and emit PASS/FAIL. R and J results -# already printed inline above; E results print together here. - -T_E_BATCH=0 -if [ "$(exec_target_queue_size)" -gt 0 ]; then - printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)" - t0=$(now_ms) - exec_target_flush - T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) - - i=0 - while [ $i -lt ${#E_NAMES[@]} ]; do - name="${E_NAMES[$i]}" - work="${E_WORK[$i]}" - exe="${E_EXE[$i]}" - link_dt="${E_LINK_MS[$i]}" - expected="${E_EXPECTED[$i]}" - - if [ ! -f "$work/exec.rc" ]; then - note_fail "$name/E (no rc; podman batch did not produce results)" - i=$((i+1)); continue - fi - RUN_RC="$(cat "$work/exec.rc")" - - e_ok=1 - e_reported=0 - if [ "$RUN_RC" -ne "$expected" ]; then - e_ok=0 - note_fail "$name/E (expected $expected, got $RUN_RC, link ${link_dt}ms)" - e_reported=1 - fi +} - # Symbol presence/absence checks via llvm-nm — format-agnostic - # (ELF + Mach-O). On Mach-O the on-disk name carries the - # leading `_` mangle byte; the awk filter accepts either the - # bare source-level name (ELF) or the `_`-prefixed form (Mach-O) - # to match the same gc_present / gc_absent entry across formats. - if [ $e_ok -eq 1 ] && [ $have_nm -eq 1 ] && \ - { [ -n "${E_GC_ABSENT_LIST[$i]}" ] || \ - [ -n "${E_GC_PRESENT_LIST[$i]}" ]; }; then - "$NM_BIN" "$exe" >"$work/exec_syms.txt" 2>/dev/null - if [ -s "$work/exec_syms.txt" ]; then - # llvm-nm format: "<addr> <type> <name>". `U` = undefined. - # On Mach-O, `_name` is the asm form of source `name`. - while IFS= read -r sym; do - [ -z "$sym" ] && continue - if awk -v s="$sym" -v u="_$sym" \ - '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' \ - "$work/exec_syms.txt"; then - e_ok=0 - note_fail "$name/E gc_absent: '$sym' present" - e_reported=1 - break - fi - done <<< "${E_GC_ABSENT_LIST[$i]}" - if [ $e_ok -eq 1 ]; then - while IFS= read -r sym; do - [ -z "$sym" ] && continue - if ! awk -v s="$sym" -v u="_$sym" \ - '($NF==s || $NF==u) && $(NF-1)!="U" {found=1} END{exit !found}' \ - "$work/exec_syms.txt"; then - e_ok=0 - note_fail "$name/E gc_present: '$sym' missing" - e_reported=1 - break - fi - done <<< "${E_GC_PRESENT_LIST[$i]}" - fi - fi - fi - - if [ $e_ok -eq 1 ]; then - note_pass "$name/E (link ${link_dt}ms)" - elif [ $e_reported -eq 0 ]; then - note_fail "$name/E (e_ok=0, no specific reason captured)" - fi - i=$((i+1)) - done -fi - -# ---- summary --------------------------------------------------------------- - -if [ ${#FAIL_NAMES[@]} -gt 0 ]; then - printf '\nFailed:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done -fi +# ---- drive the corpora ----------------------------------------------------- +printf 'Running cases...\n' -if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then - printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' - for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done +PAR="${CFREE_LINK_PARALLEL:-0}" + +# Active lanes for the cases corpus, in REJ order. +CASE_LANES= +[ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R" +[ "$RUN_E" -eq 1 ] && CASE_LANES="$CASE_LANES E" +[ "$RUN_J" -eq 1 ] && CASE_LANES="$CASE_LANES J" + +CF_LABEL=test-link CF_BUILD_DIR="$BUILD_DIR/link" \ + CF_CORPUS_GLOBS="$TEST_DIR/cases/*/" CF_CORPUS_EXT="" CF_SIDECAR_DIR="$TEST_DIR/cases" \ + CF_LANES="$CASE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_READ_CASE=link_read_case CF_FLUSH_VERIFY=link_flush_verify \ + CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +# bad/ negative corpus — E and J only (no R). +BAD_LANES= +[ "$RUN_E" -eq 1 ] && BAD_LANES="$BAD_LANES BE" +[ "$RUN_J" -eq 1 ] && BAD_LANES="$BAD_LANES BJ" + +if [ -n "$BAD_LANES" ]; then + CF_LABEL=test-link CF_BUILD_DIR="$BUILD_DIR/link/bad" \ + CF_CORPUS_GLOBS="$TEST_DIR/bad/*/" CF_CORPUS_EXT="" CF_SIDECAR_DIR="$TEST_DIR/bad" \ + CF_LANES="$BAD_LANES" CF_OPT_LEVELS="" CF_TUPLES="$CUR_TUPLE" \ + CF_TARGETS_EXT="" CF_READ_CASE=bad_read_case \ + CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run fi -printf '\n' -printf 'Results: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: R=%dms E=%dms (batch %dms) J=%dms\n' \ - "$T_R" "$T_E" "$T_E_BATCH" "$T_J" - -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-link +cf_exit diff --git a/test/link/rv64_jit_test.c b/test/link/rv64_jit_test.c @@ -41,6 +41,8 @@ #include <sys/mman.h> #include <unistd.h> +#include "lib/cfree_unit.h" + /* Native execution requires the host CPU to be rv64 (any OS that gives * us POSIX mmap + mprotect, which on rv64 means Linux today). Anywhere * else we still build the JIT image but skip the call. */ @@ -50,34 +52,8 @@ #define RV64_HOST_NATIVE 0 #endif -/* ---- host glue (heap + diag, copied from other test runners) ---- */ -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - (void)s; - (void)loc; - fprintf(stderr, "diag %d: ", (int)k); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; +/* ---- host glue: heap + diag come from the shared CfreeUnit ---- */ +static CfreeUnit g_u; /* ---- execmem with W^X dual-mapping (mirrors test/link/harness) ---- */ static int xm_to_posix(int p) { @@ -215,22 +191,14 @@ int main(void) { if (ps > 0) g_execmem.page_size = (size_t)ps; } - CfreeTarget target; - memset(&target, 0, sizeof(target)); - target.arch = CFREE_ARCH_RV64; - target.os = CFREE_OS_LINUX; - target.obj = CFREE_OBJ_ELF; - target.ptr_size = 8; - target.ptr_align = 8; + CfreeTarget target = + cfree_unit_target(CFREE_ARCH_RV64, CFREE_OS_LINUX, CFREE_OBJ_ELF); - CfreeContext ctx; - memset(&ctx, 0, sizeof(ctx)); - ctx.heap = &g_heap; - ctx.diag = &g_diag; - ctx.now = -1; + cfree_unit_init(&g_u); + g_u.ctx.now = -1; CfreeCompiler* c = NULL; - if (cfree_compiler_new(target, &ctx, &c) != CFREE_OK || !c) { + if (cfree_unit_compiler_new(&g_u, target, &c) != CFREE_OK || !c) { fprintf(stderr, "rv64_jit_test: compiler_new failed\n"); return 2; } diff --git a/test/objcopy/run.sh b/test/objcopy/run.sh @@ -1,69 +1,26 @@ #!/bin/sh -# Driver-level `cfree objcopy` test harness. Same shape as test/ar/run.sh. +# Driver-level `cfree objcopy` test harness. Shared loop/reporting in +# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and +# diff stdout+stderr against cases/<name>.expected. set -u script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree objcopy-driver -if [ ! -x "$CFREE" ]; then - echo "objcopy-driver: cfree binary not found at $CFREE" >&2 - exit 2 -fi - -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-objcopy-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT - -pass=0 -fail=0 -failures= - +CF_WORK=$(cf_workdir objcopy) +cf_report_init for sh in "$cases_dir"/*.sh; do [ -e "$sh" ] || continue name=$(basename "${sh%.sh}") - expected="${sh%.sh}.expected" - actual="$work_root/$name.actual" - - if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")" - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - sandbox="$work_root/$name" - mkdir -p "$sandbox" - ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1 - case_rc=$? - - if [ "$case_rc" -ne 0 ]; then - printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - if diff -u "$expected" "$actual" >/dev/null 2>&1; then - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "$name" - diff -u "$expected" "$actual" || true - cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true - fail=$((fail + 1)) - failures="$failures $name" - fi + cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir" done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nobjcopy-driver: failures:%s\n' "$failures" - printf 'objcopy-driver: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\nobjcopy-driver: %d/%d passed\n' "$pass" "$total" +cf_summary objcopy-driver +cf_exit diff --git a/test/objdump/run.sh b/test/objdump/run.sh @@ -1,93 +1,35 @@ #!/bin/sh # Driver-level golden tests for linked-image inspection. Mostly `cfree -# objdump`, plus the inherited tools (e.g. `size`) that read the same -# image view; each case script picks its own subcommand. +# objdump`, plus inherited tools (e.g. `size`) that read the same image view; +# each case script picks its own subcommand. # -# Per-arch subdirectories (test/objdump/<arch>/cases/) hold: -# <name>.sh — script invoked with CFREE and a per-case sandbox -# <name>.expected — expected stdout -# -# Each script is run in its own work directory; stdout is diffed against -# the .expected file. Mirrors the test/strip/, test/objcopy/, test/ar/ -# harness structure so failures are localized and goldens are diffable. +# Per-arch subdirectories test/objdump/<arch>/cases/ hold <name>.sh + +# <name>.expected. A case whose first output line begins with "SKIP" opts out +# (e.g. missing llvm-mingw to build a PE fixture). Shared loop/reporting lives +# in test/lib/cfree_sh_report.sh. set -u script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree objdump-driver -if [ ! -x "$CFREE" ]; then - echo "objdump-driver: cfree binary not found at $CFREE" >&2 - exit 2 -fi - -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-objdump-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT - -pass=0 -fail=0 -skip=0 -failures= - +CF_WORK=$(cf_workdir objdump) +CF_SCENARIO_SKIP=1 # honor leading "SKIP" lines from cases +cf_report_init for arch_dir in "$script_dir"/*/; do [ -d "$arch_dir/cases" ] || continue arch=$(basename "$arch_dir") for sh in "$arch_dir/cases"/*.sh; do [ -e "$sh" ] || continue name=$(basename "${sh%.sh}") - expected="${sh%.sh}.expected" - actual="$work_root/$arch-$name.actual" - - if [ ! -e "$expected" ]; then - printf 'FAIL %s/%s (missing %s)\n' "$arch" "$name" "$(basename "$expected")" - fail=$((fail + 1)) - failures="$failures $arch/$name" - continue - fi - - sandbox="$work_root/$arch-$name" - mkdir -p "$sandbox" - ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1 - case_rc=$? - - # A case that prints a leading "SKIP" line is opting out because a - # prerequisite is missing (e.g. no llvm-mingw UCRT sysroot to build a - # PE fixture). Honor it instead of diffing the SKIP text against the - # golden — same skip-vs-fail convention as the other driver harnesses. - if [ "$case_rc" -eq 0 ] && head -n1 "$actual" 2>/dev/null | grep -q '^SKIP'; then - printf 'SKIP %s/%s (%s)\n' "$arch" "$name" \ - "$(head -n1 "$actual" | sed 's/^SKIP[: ]*//')" - skip=$((skip + 1)) - continue - fi - - if [ "$case_rc" -ne 0 ]; then - printf 'FAIL %s/%s (script exit=%d)\n' "$arch" "$name" "$case_rc" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $arch/$name" - continue - fi - - if diff -u "$expected" "$actual" >/dev/null 2>&1; then - printf 'PASS %s/%s\n' "$arch" "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s/%s\n' "$arch" "$name" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $arch/$name" - fi + cf_scenario_case "$arch/$name" "$sh" "${sh%.sh}.expected" done done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nobjdump-driver: failures:%s\n' "$failures" - printf 'objdump-driver: %d/%d passed (%d skipped)\n' "$pass" "$total" "$skip" - exit 1 -fi -printf '\nobjdump-driver: %d/%d passed (%d skipped)\n' "$pass" "$total" "$skip" +cf_summary objdump-driver +cf_exit diff --git a/test/opt/cg_ir_lower_test.c b/test/opt/cg_ir_lower_test.c @@ -5,6 +5,7 @@ #include <string.h> #include "cg/ir.h" +#include "lib/cfree_unit.h" #include "opt/opt.h" #undef Operand @@ -13,54 +14,13 @@ #undef CGCallDesc #undef CGLocalStorage -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} - -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} - -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static int g_fails; -static int g_checks; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -#define EXPECT(cond, ...) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. ctx.now = -1 is + * preserved by setting it once after cfree_unit_init in main(). */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) typedef struct TestCtx { - CfreeContext ctx; Compiler* c; CfreeCgTypeId i32; } TestCtx; @@ -69,16 +29,9 @@ static void tc_init(TestCtx* tc) { CfreeTarget target; CfreeCgBuiltinTypes b; memset(tc, 0, sizeof *tc); - tc->ctx.heap = &g_heap; - tc->ctx.diag = &g_diag; - tc->ctx.now = -1; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_MACOS; - target.obj = CFREE_OBJ_MACHO; - target.ptr_size = 8; - target.ptr_align = 8; - if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != + target = + cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO); + if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) != CFREE_OK || !tc->c) { fprintf(stderr, "fatal: compiler allocation failed\n"); @@ -251,12 +204,14 @@ static void jump_cleanup_threads_empty_fallthrough_target(void) { } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; converter_builds_cfg_and_pregs(); jump_cleanup_threads_empty_fallthrough_target(); - if (g_fails) { - fprintf(stderr, "cg-ir-lower: %d/%d failed\n", g_fails, g_checks); + if (g_u.fails) { + fprintf(stderr, "cg-ir-lower: %d/%d failed\n", g_u.fails, g_u.checks); return 1; } - printf("cg-ir-lower: %d checks, 0 failures\n", g_checks); + printf("cg-ir-lower: %d checks, 0 failures\n", g_u.checks); return 0; } diff --git a/test/opt/tiny_inline_test.c b/test/opt/tiny_inline_test.c @@ -12,6 +12,8 @@ #include <stdlib.h> #include <string.h> +#include "lib/cfree_unit.h" + #include "cg/ir.h" #include "cg/ir_recorder.h" #include "opt/opt.h" @@ -23,54 +25,13 @@ #undef CGCallDesc #undef CGLocalStorage -static void* h_alloc(CfreeHeap* h, size_t n, size_t a) { - (void)h; - (void)a; - return n ? malloc(n) : NULL; -} - -static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) { - (void)h; - (void)o; - (void)a; - return realloc(p, n); -} - -static void h_free(CfreeHeap* h, void* p, size_t n) { - (void)h; - (void)n; - free(p); -} - -static CfreeHeap g_heap = {h_alloc, h_realloc, h_free, NULL}; -static int g_fails; -static int g_checks; - -static void diag_emit(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, - const char* fmt, va_list ap) { - static const char* names[] = {"note", "warning", "error", "fatal"}; - (void)s; - (void)loc; - fprintf(stderr, "%s: ", names[k]); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); -} - -static CfreeDiagSink g_diag = {diag_emit, NULL, 0, 0}; - -#define EXPECT(cond, ...) \ - do { \ - ++g_checks; \ - if (!(cond)) { \ - ++g_fails; \ - fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - } \ - } while (0) +/* Shared test context replaces the per-file heap/diag/counter globals; + * EXPECT aliases CU_EXPECT so the call sites are unchanged. cfree_unit_init + * runs once in main(); tc_init reuses g_u's heap/diag/ctx per compiler. */ +static CfreeUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) typedef struct TestCtx { - CfreeContext ctx; Compiler* c; CfreeCgTypeId i32; CfreeCgTypeId ptr; @@ -83,16 +44,8 @@ static void tc_init(TestCtx* tc) { CfreeCgFuncSig sig; CfreeCgFuncParam params[1]; memset(tc, 0, sizeof *tc); - tc->ctx.heap = &g_heap; - tc->ctx.diag = &g_diag; - tc->ctx.now = -1; - memset(&target, 0, sizeof target); - target.arch = CFREE_ARCH_ARM_64; - target.os = CFREE_OS_MACOS; - target.obj = CFREE_OBJ_MACHO; - target.ptr_size = 8; - target.ptr_align = 8; - if (cfree_compiler_new(target, &tc->ctx, (CfreeCompiler**)&tc->c) != + target = cfree_unit_target(CFREE_ARCH_ARM_64, CFREE_OS_MACOS, CFREE_OBJ_MACHO); + if (cfree_unit_compiler_new(&g_u, target, (CfreeCompiler**)&tc->c) != CFREE_OK || !tc->c) { fprintf(stderr, "fatal: compiler allocation failed\n"); @@ -324,10 +277,13 @@ static void unknown_callee_is_skipped(void) { } int main(void) { + cfree_unit_init(&g_u); + g_u.ctx.now = -1; tiny_callee_is_inlined(); over_budget_respects_policy(); never_policy_is_refused(); unknown_callee_is_skipped(); - fprintf(stderr, "tiny-inline: %d checks, %d failures\n", g_checks, g_fails); - return g_fails ? 1 : 0; + fprintf(stderr, "tiny-inline: %d checks, %d failures\n", g_u.checks, + g_u.fails); + return cfree_unit_status(&g_u); } diff --git a/test/parse/run.sh b/test/parse/run.sh @@ -1,21 +1,26 @@ #!/usr/bin/env bash -# test/parse/run.sh — file-driven C-parser test harness. +# test/parse/run.sh — file-driven C-parser test harness, on the shared corpus +# harness (test/lib/cf_corpus.sh). # -# For each test/parse/cases/*.c, runs up to six paths (DWARF directives may -# be added later via .dwarf sidecars): +# For each test/parse/cases/*.c, runs up to six lanes (CFREE_TEST_PATHS, +# default DREJ — C and W are opt-in): # # D in-process JIT — parse-runner --jit FILE.c → exit code matches -# expected. No file I/O. aarch64 host only. +# expected. No file I/O. Host arch must match cross +# target. # R ELF roundtrip — parse-runner --emit + cfree-roundtrip + readelf # normalize diff. Validates emitter+reader fidelity. # E exec via qemu — parse-runner --emit + start.o → link-exe-runner → -# qemu/podman → exit code. Cross-host friendly. -# J jit-via-file — parse-runner --emit + jit-runner. aarch64 host. +# qemu/podman → exit code. Cross-host friendly. Deferred +# to a batched exec_target flush (cf_queue_e). +# J jit-via-file — parse-runner --emit + jit-runner. Host arch must match +# cross target. # C emit-c host — parse-runner --emit-c + host cc + test_main wrapper, # run native. Validates the --emit=c C-source backend. -# Host arch must match cross target. Cases that hit an -# unimplemented C-target method are reported as SKIP -# (not FAIL) so phased backend rollout is tolerated. +# Host arch must match cross target. opt=0 only. Cases +# that hit an unimplemented C-target method are reported +# as SKIP (not FAIL) so phased backend rollout is +# tolerated. # W wasm roundtrip — cfree cc -target wasm32-none -c case.c -> .wasm, then # cfree run -e test_main on it (the lang/wasm frontend # re-lowers to native CG, JITs, calls test_main). @@ -29,10 +34,13 @@ # # Sidecar conventions (each missing file uses the documented default): # <name>.expected — integer; default 0. Compared mod 256 to test_main. -# <name>.skip — single-line reason. Treated as failure unless -# CFREE_TEST_ALLOW_SKIP=1 (matching the rest of -# the test suite). -# <name>.wasm.skip — single-line reason; opts the case out of path W only. +# <name>.skip — single-line reason. Whole-case skip on every arch. +# <name>.<arch>.skip— single-line reason; whole-case skip on that arch only +# (e.g. asm_01_grammar.rv64.skip). +# <name>.cbackend.skip — single-line reason; opts the case out of lane C only. +# <name>.wasm.skip — single-line reason; opts the case out of lane W only. +# Skips are treated as failure unless CFREE_TEST_ALLOW_SKIP=1 (matching the +# rest of the test suite). # # Filtering: # ./run.sh [name_filter] [paths] @@ -48,11 +56,16 @@ # Parallelism: # default run in parallel with a capped CPU-count default. # CFREE_TEST_JOBS=N run up to N cases concurrently. -# CFREE_TEST_JOBS=auto same as the default. +# CFREE_PARSE_PARALLEL=0 force serial dispatch. +# All lane hooks write only under CF_WORK and record via cf_*, so the runner is +# parallel-safe by construction. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + TEST_DIR="$ROOT/test/parse" LINK_TEST_DIR="$ROOT/test/link" BUILD_DIR="$ROOT/build/test" @@ -65,9 +78,6 @@ LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" JIT_RUNNER="$BUILD_DIR/jit-runner" NORMALIZE="$ROOT/test/elf/normalize.py" -# shellcheck source=../lib/parallel.sh -source "$ROOT/test/lib/parallel.sh" - # CFREE_TEST_ARCH selects the cross-target. Default aa64 preserves the # pre-multiarch behavior. The C runners read the same env via # test/lib/cfree_test_target.h. @@ -92,10 +102,10 @@ fi CLANG_TARGET="--target=$CLANG_TRIPLE" CC="${CC:-cc}" -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" FILTER="${1:-${CFREE_TEST_FILTER:-}}" PATHS="${2:-${CFREE_TEST_PATHS:-DREJ}}" +export CFREE_TEST_FILTER="$FILTER" if [ -n "${CFREE_OPT_LEVELS:-}" ]; then OPT_LEVELS="$CFREE_OPT_LEVELS" elif [ -n "${CFREE_OPT_LEVEL:-}" ]; then @@ -115,137 +125,9 @@ case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac -T_D=0; T_R=0; T_E=0; T_J=0; T_C=0; T_W=0 -now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; } mkdir -p "$BUILD_DIR" "$BUILD_DIR/parse" -TEST_JOBS="$(cfree_parallel_jobs)" || exit 2 -PARALLEL_DIR="$BUILD_DIR/parse.parallel/$$" -mkdir -p "$PARALLEL_DIR" - -PASS=0; FAIL=0; SKIP=0 -FAIL_NAMES=(); SKIP_NAMES=() - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } - -event_path() { printf '%s/%s.%04d.events' "$PARALLEL_DIR" "$1" "$2"; } -worker_stdout_path() { printf '%s/%s.%04d.stdout' "$PARALLEL_DIR" "$1" "$2"; } -worker_stderr_path() { printf '%s/%s.%04d.stderr' "$PARALLEL_DIR" "$1" "$2"; } - -emit_event() { - local file="$1" kind="$2" - shift 2 - printf '%s' "$kind" >> "$file" - while [ $# -gt 0 ]; do - printf '\t%s' "$1" >> "$file" - shift - done - printf '\n' >> "$file" -} - -replay_events() { - local event="$1" stdout_log="$2" stderr_log="$3" - local kind a b c d e f - - if [ ! -s "$event" ]; then - note_fail "internal: missing worker result $event" - if [ -s "$stdout_log" ]; then sed 's/^/ | /' "$stdout_log"; fi - if [ -s "$stderr_log" ]; then sed 's/^/ | /' "$stderr_log"; fi - return - fi - - while IFS=$'\t' read -r kind a b c d e f; do - case "$kind" in - PASS) note_pass "$a" ;; - FAIL) note_fail "$a" ;; - SKIP) note_skip "$a" "$b" ;; - TIME) - case "$a" in - D) T_D=$(( T_D + b )) ;; - R) T_R=$(( T_R + b )) ;; - E) T_E=$(( T_E + b )) ;; - J) T_J=$(( T_J + b )) ;; - C) T_C=$(( T_C + b )) ;; - W) T_W=$(( T_W + b )) ;; - esac - ;; - QUEUE_E) - E_NAMES+=("$a") - E_WORK+=("$b") - E_LINK_MS+=("$c") - E_EXPECTED+=("$d") - T_E=$(( T_E + c )) - exec_target_queue "$f" "$e" "$b/linked.exe" \ - "$b/exec.out" "$b/exec.err" "$b/exec.rc" - ;; - *) - note_fail "internal: malformed worker event in $event" - ;; - esac - done < "$event" -} - -run_serial_items() { - local layer="$1" worker="$2" - shift 2 - - local idx=0 - local item event stdout_log stderr_log - - for item in "$@"; do - event="$(event_path "$layer" "$idx")" - stdout_log="$(worker_stdout_path "$layer" "$idx")" - stderr_log="$(worker_stderr_path "$layer" "$idx")" - : > "$event" - : > "$stdout_log" - : > "$stderr_log" - "$worker" "$idx" "$item" "$event" > "$stdout_log" 2> "$stderr_log" - replay_events "$event" "$stdout_log" "$stderr_log" - idx=$((idx+1)) - done -} - -run_parallel_items() { - local layer="$1" worker="$2" - shift 2 - - local events=() - local stdout_logs=() - local stderr_logs=() - local idx=0 - local item event stdout_log stderr_log - - for item in "$@"; do - event="$(event_path "$layer" "$idx")" - stdout_log="$(worker_stdout_path "$layer" "$idx")" - stderr_log="$(worker_stderr_path "$layer" "$idx")" - : > "$event" - : > "$stdout_log" - : > "$stderr_log" - events+=("$event") - stdout_logs+=("$stdout_log") - stderr_logs+=("$stderr_log") - cfree_parallel_run "$TEST_JOBS" "$worker" "$idx" "$item" "$event" \ - > "$stdout_log" 2> "$stderr_log" - idx=$((idx+1)) - done - - cfree_parallel_wait_all || true - - idx=0 - while [ $idx -lt ${#events[@]} ]; do - replay_events "${events[$idx]}" "${stdout_logs[$idx]}" "${stderr_logs[$idx]}" - idx=$((idx+1)) - done -} - # ---- tool detection (mirrors test/cg/run.sh) ------------------------------- have_clang_cross=0 @@ -253,7 +135,6 @@ have_readelf=0 have_python3=0 have_qemu=0 have_podman=0 -have_runner=0 have_roundtrip=0 have_exe_runner=0 have_jit_runner=0 @@ -269,7 +150,6 @@ command -v python3 >/dev/null 2>&1 && have_python3=1 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" [ -n "$QEMU_BIN" ] && have_qemu=1 command -v podman >/dev/null 2>&1 && have_podman=1 -{ [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; } && have_runner=1 arch_raw="$(uname -m 2>/dev/null || true)" { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 @@ -295,6 +175,7 @@ READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/ # Shared per-arch exec helper — see test/lib/exec_target.sh. EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh source "$ROOT/test/lib/exec_target.sh" @@ -309,39 +190,35 @@ fi # parse-runner if [ -x "$PARSE_RUNNER" ]; then - printf ' %s parse-runner\n' "$(color_grn found)" + printf ' found parse-runner\n' else - printf ' %s parse-runner missing — run "make build/test/parse-runner"\n' \ - "$(color_red FATAL)" >&2 + printf ' FATAL parse-runner missing — run "make build/test/parse-runner"\n' >&2 exit 1 fi # cfree-roundtrip — for path R. if [ -x "$ROUNDTRIP_BIN" ]; then have_roundtrip=1 - printf ' %s cfree-roundtrip\n' "$(color_grn found)" + printf ' found cfree-roundtrip\n' else - printf ' %s cfree-roundtrip missing — path R will skip\n' \ - "$(color_yel warn)" >&2 + printf ' warn cfree-roundtrip missing — path R will skip\n' >&2 fi # link-exe-runner — for path E. if [ -x "$LINK_EXE_RUNNER" ]; then have_exe_runner=1 - printf ' %s link-exe-runner\n' "$(color_grn found)" + printf ' found link-exe-runner\n' else - printf ' %s link-exe-runner missing — path E will skip\n' \ - "$(color_yel warn)" >&2 + printf ' warn link-exe-runner missing — path E will skip\n' >&2 fi # jit-runner — for path J. Only when host arch matches the cross-target. if [ $is_native_target -eq 1 ]; then if [ -x "$JIT_RUNNER" ]; then have_jit_runner=1 - printf ' %s jit-runner\n' "$(color_grn found)" + printf ' found jit-runner\n' else - printf ' %s jit-runner missing — path J will skip\n' \ - "$(color_yel warn)" >&2 + printf ' warn jit-runner missing — path J will skip\n' >&2 fi fi @@ -372,9 +249,9 @@ EOF fi if $CC -std=c11 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" 2>/dev/null; then have_c_wrapper=1 - printf ' %s c-wrapper\n' "$(color_grn built)" + printf ' built c-wrapper\n' else - printf ' %s c-wrapper (host CC failed)\n' "$(color_yel warn)" >&2 + printf ' warn c-wrapper (host CC failed)\n' >&2 fi # Probe whether the host C compiler treats `long double` as 128-bit @@ -395,370 +272,287 @@ if $CC -std=c11 "$LDBL_PROBE_SRC" -o "$LDBL_PROBE_BIN" 2>/dev/null \ HOST_LDBL128=1 fi -# ---- per-case loop --------------------------------------------------------- +# ---- per-lane oracle hooks (CF_WORK-confined -> parallel-safe) ------------- +# Each hook records via cf_pass/cf_fail/cf_skip (or cf_queue_e for E). The +# expected exit code is CF_EXPECTED (the engine read <name>.expected); compared +# mod 256 to the program's exit code. -CASES=() -for src in "$TEST_DIR"/cases/*.c; do - [ -e "$src" ] || continue - CASES+=("$src") -done - -# Path E result bookkeeping — same shape as test/cg. -E_NAMES=() -E_WORK=() -E_LINK_MS=() -E_EXPECTED=() - -FILTERED_CASES=() -# Path C forces opt_level=0 internally — when it's the only enabled path, -# higher opt levels duplicate identical work and (because no other path -# emits anything) leave a zero-byte worker event file that the replay -# loop flags as "missing worker result". Restrict to opt=0 in that case. -case_opt_levels="$OPT_LEVELS" -if [ $RUN_D -eq 0 ] && [ $RUN_R -eq 0 ] && [ $RUN_E -eq 0 ] && \ - [ $RUN_J -eq 0 ] && { [ $RUN_C -eq 1 ] || [ $RUN_W -eq 1 ]; }; then - case_opt_levels="0" -fi -for src in "${CASES[@]}"; do - name="$(basename "$src" .c)" - [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]] && continue - for opt in $case_opt_levels; do - FILTERED_CASES+=("$opt:$src") - done -done +# Build the .o the R/E/J lanes share, once per (case,opt). Sets PARSE_OBJ. +# Returns 0 on success, 1 on failure. +# +# The original harness emitted the .o ONCE up front (before R/E/J) and, on a +# failure, recorded a single FAIL "<name>/emit" and skipped R/E/J entirely. To +# preserve that exact verdict (one FAIL, not one per lane), the first lane that +# needs the object reports "<name>/emit"; a failure marker makes every later +# lane in the same item return silently without re-reporting. +_parse_emit_obj() { + PARSE_OBJ="$CF_WORK/$CF_BASE.o" + [ -f "$PARSE_OBJ" ] && return 0 + [ -f "$CF_WORK/.emit.failed" ] && return 1 + if ! CFREE_OPT_LEVEL="$CF_OPT" "$PARSE_RUNNER" --emit "$CF_SRC" "$PARSE_OBJ" \ + 2>"$CF_WORK/emit.err"; then + : > "$CF_WORK/.emit.failed" + cf_fail "$CF_NAME/emit" "parse-runner --emit failed; see $CF_WORK/emit.err" + return 1 + fi + return 0 +} -run_parse_case() { - local _idx="$1" item="$2" event="$3" - local opt src base_name name work reason expected expected_byte obj t0 dt d_rc r_ok r_msg rt - local exe link_dt j_rc - local c_src c_bin c_rc missing run_c - : "$_idx" - - opt="${item%%:*}" - src="${item#*:}" - base_name="$(basename "$src" .c)" - name="$base_name/O$opt" - work="$BUILD_DIR/parse/$base_name.O$opt" - mkdir -p "$work" - - # Skip sidecar. `<name>.skip` skips on all arches; `<name>.<arch>.skip` - # (e.g. asm_01_grammar.rv64.skip) skips only when CFREE_TEST_ARCH matches. - if [ -e "$TEST_DIR/cases/$base_name.$TEST_ARCH.skip" ]; then - reason=$(head -n1 "$TEST_DIR/cases/$base_name.$TEST_ARCH.skip") - emit_event "$event" SKIP "$name" "$reason" - return 0 +cf_lane_D() { + if [ $is_native_target -eq 0 ]; then + cf_skip "$CF_NAME/D" "host arch != $TEST_ARCH (no native JIT)" + return fi - if [ -e "$TEST_DIR/cases/$base_name.skip" ]; then - reason=$(head -n1 "$TEST_DIR/cases/$base_name.skip") - emit_event "$event" SKIP "$name" "$reason" - return 0 + local exp_byte t0 dt d_rc + exp_byte=$(( CF_EXPECTED & 0xff )) + t0=$(cf_now_ms) + CFREE_OPT_LEVEL="$CF_OPT" "$PARSE_RUNNER" --jit "$CF_SRC" \ + >"$CF_WORK/d.out" 2>"$CF_WORK/d.err" + d_rc=$? + dt=$(( $(cf_now_ms) - t0 )) + cf_time D "$dt" + if [ "$d_rc" -eq "$exp_byte" ]; then + cf_pass "$CF_NAME/D (${dt}ms)" + else + cf_fail "$CF_NAME/D" "expected $exp_byte got $d_rc, ${dt}ms" fi +} - # Expected exit code (default 0) - expected=0 - if [ -e "$TEST_DIR/cases/$base_name.expected" ]; then - expected=$(head -n1 "$TEST_DIR/cases/$base_name.expected") +cf_lane_R() { + if [ $have_roundtrip -ne 1 ] || [ $have_readelf -ne 1 ] || [ $have_python3 -ne 1 ]; then + cf_skip "$CF_NAME/R" "missing roundtrip/readelf/python3" + return fi - expected_byte=$(( expected & 0xff )) - - # ---- Path D: in-process JIT (only when host arch == cross-target) ---- - if [ $RUN_D -eq 1 ]; then - if [ $is_native_target -eq 1 ]; then - t0=$(now_ms) - CFREE_OPT_LEVEL="$opt" "$PARSE_RUNNER" --jit "$src" \ - >"$work/d.out" 2>"$work/d.err" - d_rc=$? - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME D "$dt" - if [ "$d_rc" -eq "$expected_byte" ]; then - emit_event "$event" PASS "$name/D (${dt}ms)" - else - emit_event "$event" FAIL "$name/D (expected $expected_byte got $d_rc, ${dt}ms)" - fi - else - emit_event "$event" SKIP "$name/D" "host arch != $TEST_ARCH (no native JIT)" - fi + _parse_emit_obj || return + local t0 dt rt r_ok r_msg + t0=$(cf_now_ms) + rt="$CF_WORK/$CF_BASE.rt.o" + r_ok=1; r_msg="" + if ! "$ROUNDTRIP_BIN" "$PARSE_OBJ" "$rt" 2>"$CF_WORK/rt.err"; then + r_ok=0; r_msg="roundtrip failed" + else + "$READELF_BIN" -aW "$PARSE_OBJ" | python3 "$NORMALIZE" >"$CF_WORK/golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$CF_WORK/rt.norm" 2>/dev/null + diff -u "$CF_WORK/golden.norm" "$CF_WORK/rt.norm" >"$CF_WORK/r.diff" 2>&1 || r_ok=0 fi + dt=$(( $(cf_now_ms) - t0 )) + cf_time R "$dt" + if [ $r_ok -eq 1 ]; then cf_pass "$CF_NAME/R (${dt}ms)" + else cf_fail "$CF_NAME/R" "${r_msg} ${dt}ms"; fi +} - # ---- emit (needed by R/E/J) ------------------------------------------ - obj="$work/$base_name.o" - if [ $RUN_R -eq 1 ] || [ $RUN_E -eq 1 ] || [ $RUN_J -eq 1 ]; then - if ! CFREE_OPT_LEVEL="$opt" "$PARSE_RUNNER" --emit "$src" "$obj" \ - 2>"$work/emit.err"; then - emit_event "$event" FAIL "$name/emit (parse-runner --emit failed; see $work/emit.err)" - return 0 - fi +cf_lane_E() { + if [ $have_exe_runner -ne 1 ] || [ $have_clang_cross -ne 1 ] || [ $have_start_obj -ne 1 ]; then + cf_skip "$CF_NAME/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" + return fi - - # ---- Path R: ELF roundtrip ------------------------------------------- - if [ $RUN_R -eq 1 ]; then - if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then - t0=$(now_ms) - rt="$work/$base_name.rt.o" - r_ok=1; r_msg="" - if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt.err"; then - r_ok=0; r_msg=" (roundtrip failed)" - else - "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" >"$work/golden.norm" 2>/dev/null - "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" >"$work/rt.norm" 2>/dev/null - diff -u "$work/golden.norm" "$work/rt.norm" >"$work/r.diff" 2>&1 || r_ok=0 - fi - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME R "$dt" - if [ $r_ok -eq 1 ]; then emit_event "$event" PASS "$name/R (${dt}ms)" - else emit_event "$event" FAIL "$name/R${r_msg} (${dt}ms)"; fi - else - emit_event "$event" SKIP "$name/R" "missing roundtrip/readelf/python3" - fi + _parse_emit_obj || return + local t0 dt exe exp_byte + exp_byte=$(( CF_EXPECTED & 0xff )) + t0=$(cf_now_ms) + exe="$CF_WORK/linked.exe" + if ! "$LINK_EXE_RUNNER" -o "$exe" "$PARSE_OBJ" "$START_OBJ" "${RT_LINK_ARGS[@]}" \ + >"$CF_WORK/exec_link.out" 2>"$CF_WORK/exec_link.err"; then + dt=$(( $(cf_now_ms) - t0 )) + cf_time E "$dt" + cf_fail "$CF_NAME/E" "link failed, ${dt}ms" + elif exec_target_supported "$EXEC_ARCH"; then + dt=$(( $(cf_now_ms) - t0 )) + cf_time E "$dt" + # Deferred batched exec: the engine flush runs the exe and verifies + # rc == exp_byte (both masked & 255) at the end. + cf_queue_e "$CF_NAME/E (link ${dt}ms)" "$exe" \ + "$CF_WORK/exec.out" "$CF_WORK/exec.err" "$CF_WORK/exec.rc" \ + "$exp_byte" "$EXEC_ARCH" + else + dt=$(( $(cf_now_ms) - t0 )) + cf_time E "$dt" + cf_skip "$CF_NAME/E" "no runner for $EXEC_ARCH" fi +} - # ---- Path E: link + (batched) qemu/podman ---------------------------- - if [ $RUN_E -eq 1 ]; then - if [ $have_exe_runner -eq 1 ] && [ $have_clang_cross -eq 1 ] \ - && [ $have_start_obj -eq 1 ]; then - t0=$(now_ms) - exe="$work/linked.exe" - if ! "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$START_OBJ" "${RT_LINK_ARGS[@]}" \ - >"$work/exec_link.out" 2>"$work/exec_link.err"; then - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME E "$dt" - emit_event "$event" FAIL "$name/E (link failed, ${dt}ms)" - elif exec_target_supported "$EXEC_ARCH"; then - link_dt=$(( $(now_ms) - t0 )) - emit_event "$event" QUEUE_E "$name" "$work" "$link_dt" "$expected_byte" "$name" "$EXEC_ARCH" - else - emit_event "$event" SKIP "$name/E" "no runner for $EXEC_ARCH" - fi - else - emit_event "$event" SKIP "$name/E" "no link-exe-runner, $TEST_ARCH clang, or start.o" - fi +cf_lane_J() { + if [ $have_jit_runner -ne 1 ]; then + cf_skip "$CF_NAME/J" "no jit-runner (host arch != $TEST_ARCH)" + return fi - - # ---- Path J: jit-via-file -------------------------------------------- - if [ $RUN_J -eq 1 ]; then - if [ $have_jit_runner -eq 1 ]; then - t0=$(now_ms) - "$JIT_RUNNER" "$obj" "${RT_LINK_ARGS[@]}" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME J "$dt" - if [ "$j_rc" -eq "$expected_byte" ]; then - emit_event "$event" PASS "$name/J (${dt}ms)" - else - emit_event "$event" FAIL "$name/J (expected $expected_byte got $j_rc, ${dt}ms)" - fi - else - emit_event "$event" SKIP "$name/J" "no jit-runner (host arch != $TEST_ARCH)" - fi + _parse_emit_obj || return + local t0 dt j_rc exp_byte + exp_byte=$(( CF_EXPECTED & 0xff )) + t0=$(cf_now_ms) + "$JIT_RUNNER" "$PARSE_OBJ" "${RT_LINK_ARGS[@]}" >"$CF_WORK/jit.out" 2>"$CF_WORK/jit.err" + j_rc=$? + dt=$(( $(cf_now_ms) - t0 )) + cf_time J "$dt" + if [ "$j_rc" -eq "$exp_byte" ]; then + cf_pass "$CF_NAME/J (${dt}ms)" + else + cf_fail "$CF_NAME/J" "expected $exp_byte got $j_rc, ${dt}ms" fi +} - # ---- Path C: --emit=c + host cc + run -------------------------------- - # Phase 1 of the C-source backend only handles a small slice of the - # CGTarget vtable (see doc/CBACKEND.md). Cases that hit an - # unimplemented method produce a panic that we surface as SKIP, so - # the test pass/fail signal reflects the implemented surface rather - # than churning while phases land. - # - # A per-case `<name>.cbackend.skip` sidecar opts the case out of - # path C only (other paths still run), for surface gaps that don't - # show up as a panic — e.g. emitted-C link errors that need Phase 4 - # work (aliases, asm definitions) to fix. - run_c=$RUN_C - # Path C forces opt_level=0 internally; running it at every requested - # opt level would duplicate identical work. Mirror the toy runner. - if [ $run_c -eq 1 ] && [ "$opt" != "0" ]; then - run_c=0 - fi - if [ $run_c -eq 1 ] && [ -e "$TEST_DIR/cases/$base_name.cbackend.skip" ]; then - reason=$(head -n1 "$TEST_DIR/cases/$base_name.cbackend.skip") - emit_event "$event" SKIP "$name/C" "$reason" - run_c=0 +# Path C: --emit=c + host cc + run. Phase 1 of the C-source backend only +# handles a slice of the CGTarget vtable (doc/CBACKEND.md). Cases that hit an +# unimplemented method panic, surfaced here as SKIP so the pass/fail signal +# reflects the implemented surface. The .cbackend.skip sidecar (handled by the +# engine via the per-lane sidecar with LANE=cbackend) opts a case out of C only. +cf_lane_C() { + # Per-case opt-out for path C: <name>.cbackend.skip. The engine's per-lane + # sidecar check keys on the lane id ("C"), so the cbackend-named sidecar is + # handled here instead. + local reason + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" cbackend); then + cf_skip "$CF_NAME/C" "$reason" + return fi # ldbl128_* tests assert 128-bit long-double semantics. Skip them on # path C when the host C compiler doesn't provide 128-bit ldbl (the # test's `if (__LDBL_MANT_DIG__ != 113) return 0;` early-out can't be - # reconciled with a non-zero expected). - if [ $run_c -eq 1 ] && [ $HOST_LDBL128 -eq 0 ] && \ - [[ "$base_name" == ldbl128_* ]] && \ - [[ "$base_name" != ldbl128_01_* ]]; then - emit_event "$event" SKIP "$name/C" "host long double is not 128-bit" - run_c=0 + # reconciled with a non-zero expected). ldbl128_01_* are exempt. + if [ $HOST_LDBL128 -eq 0 ] && \ + [[ "$CF_BASE" == ldbl128_* ]] && \ + [[ "$CF_BASE" != ldbl128_01_* ]]; then + cf_skip "$CF_NAME/C" "host long double is not 128-bit" + return fi - # Mach-O's static linker rejects unresolved weak undef refs that - # aren't backed by a dylib. ELF lets them resolve to 0 at link time, - # which is what the test expects. - if [ $run_c -eq 1 ] && [ "$HOST_OBJ_FMT" = "macho" ] && \ - [[ "$base_name" == attr_p2_08_weak_undef ]]; then - emit_event "$event" SKIP "$name/C" \ - "Mach-O static link rejects weak undef ref without dylib" - run_c=0 + # Mach-O's static linker rejects unresolved weak undef refs that aren't + # backed by a dylib. ELF lets them resolve to 0 at link time, which is + # what the test expects. + if [ "$HOST_OBJ_FMT" = "macho" ] && [[ "$CF_BASE" == attr_p2_08_weak_undef ]]; then + cf_skip "$CF_NAME/C" "Mach-O static link rejects weak undef ref without dylib" + return + fi + if [ $have_c_wrapper -eq 0 ]; then + cf_skip "$CF_NAME/C" "no c-wrapper (host CC failed)" + return fi - if [ $run_c -eq 1 ]; then - if [ $have_c_wrapper -eq 1 ] && [ $is_native_target -eq 1 ]; then - t0=$(now_ms) - c_src="$work/$base_name.cfree.c" - c_bin="$work/$base_name.cbackend.bin" - # Emitted C is target-locked, so we override CFREE_TEST_OBJ to - # the host's object format for the --emit-c invocation — - # otherwise ELF-only constructs like - # __attribute__((alias("x"))) leak into source compiled by a - # Mach-O-targeting host cc and fail at compile time. - if ! CFREE_TEST_OBJ="$HOST_OBJ_FMT" "$PARSE_RUNNER" \ - --emit-c "$src" "$c_src" \ - >"$work/c.emit.out" 2>"$work/c.emit.err"; then - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME C "$dt" - # Recognize "C target: ... not implemented" and - # "C target: ... not yet supported" as phased-rollout - # skips rather than regressions. Anything else is a real - # failure that the harness flags so it can't hide. - missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \ - "$work/c.emit.err" 2>/dev/null | head -n1 || true) - if [ -n "$missing" ]; then - emit_event "$event" SKIP "$name/C" "$missing" - else - emit_event "$event" FAIL "$name/C (parse-runner --emit-c failed; see $work/c.emit.err)" - fi - elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \ - >"$work/c.cc.out" 2>"$work/c.cc.err"; then - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME C "$dt" - emit_event "$event" FAIL "$name/C (host cc rejected emitted source; see $work/c.cc.err)" - else - "$c_bin" >"$work/c.run.out" 2>"$work/c.run.err" - c_rc=$? - dt=$(( $(now_ms) - t0 )) - emit_event "$event" TIME C "$dt" - if [ "$c_rc" -eq "$expected_byte" ]; then - emit_event "$event" PASS "$name/C (${dt}ms)" - else - emit_event "$event" FAIL "$name/C (expected $expected_byte got $c_rc, ${dt}ms)" - fi - fi - elif [ $have_c_wrapper -eq 0 ]; then - emit_event "$event" SKIP "$name/C" "no c-wrapper (host CC failed)" + if [ $is_native_target -eq 0 ]; then + cf_skip "$CF_NAME/C" "host arch != $TEST_ARCH (C target is target-locked)" + return + fi + local t0 dt c_src c_bin c_rc missing exp_byte + exp_byte=$(( CF_EXPECTED & 0xff )) + t0=$(cf_now_ms) + c_src="$CF_WORK/$CF_BASE.cfree.c" + c_bin="$CF_WORK/$CF_BASE.cbackend.bin" + # Emitted C is target-locked, so we override CFREE_TEST_OBJ to the host's + # object format for the --emit-c invocation — otherwise ELF-only constructs + # like __attribute__((alias("x"))) leak into source compiled by a + # Mach-O-targeting host cc and fail at compile time. + if ! CFREE_TEST_OBJ="$HOST_OBJ_FMT" "$PARSE_RUNNER" \ + --emit-c "$CF_SRC" "$c_src" \ + >"$CF_WORK/c.emit.out" 2>"$CF_WORK/c.emit.err"; then + dt=$(( $(cf_now_ms) - t0 )) + cf_time C "$dt" + # Recognize "C target: ... not implemented" and "... not yet supported" + # as phased-rollout skips. Anything else is a real failure. + missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \ + "$CF_WORK/c.emit.err" 2>/dev/null | head -n1 || true) + if [ -n "$missing" ]; then + cf_skip "$CF_NAME/C" "$missing" + else + cf_fail "$CF_NAME/C" "parse-runner --emit-c failed; see $CF_WORK/c.emit.err" + fi + elif ! $CC -std=c11 -Wall -Wextra -Werror "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \ + >"$CF_WORK/c.cc.out" 2>"$CF_WORK/c.cc.err"; then + dt=$(( $(cf_now_ms) - t0 )) + cf_time C "$dt" + cf_fail "$CF_NAME/C" "host cc rejected emitted source; see $CF_WORK/c.cc.err" + else + "$c_bin" >"$CF_WORK/c.run.out" 2>"$CF_WORK/c.run.err" + c_rc=$? + dt=$(( $(cf_now_ms) - t0 )) + cf_time C "$dt" + if [ "$c_rc" -eq "$exp_byte" ]; then + cf_pass "$CF_NAME/C (${dt}ms)" else - emit_event "$event" SKIP "$name/C" "host arch != $TEST_ARCH (C target is target-locked)" + cf_fail "$CF_NAME/C" "expected $exp_byte got $c_rc, ${dt}ms" fi fi +} - # ---- Path W: cc -target wasm32-none + cfree run ---------------------- - # Compile the case straight to a .wasm via the Wasm CGTarget, then run it - # with `cfree run -e test_main` (the lang/wasm frontend re-lowers the - # module to native CG, JITs it, and calls test_main). Mirrors the toy - # runner's W path. Like path C it is target-agnostic, so it does not - # depend on the cross-target arch; but the re-lowering JITs for the host, - # so it only runs when the host arch matches the cross-target. - # - # The Wasm CGTarget ignores -O for opt purposes (the wasm frontend picks - # its own native opt level when re-lowering), so W runs at opt=0 only. - # Phased-rollout panics ("wasm: <feature> not yet implemented" and the - # "unsupported"/"supported in v1" variants) surface as SKIP, not FAIL, so - # the signal stays "real regressions". A `<name>.wasm.skip` sidecar opts a - # case out of path W with a reason. - run_w=$RUN_W - if [ $run_w -eq 1 ] && [ "$opt" != "0" ]; then - run_w=0 - fi - if [ $run_w -eq 1 ] && [ -e "$TEST_DIR/cases/$base_name.wasm.skip" ]; then - emit_event "$event" SKIP "$name/W" \ - "$(head -n1 "$TEST_DIR/cases/$base_name.wasm.skip")" - run_w=0 +# Path W: cc -target wasm32-none + cfree run. Compile the case straight to a +# .wasm via the Wasm CGTarget, then run it with `cfree run -e test_main` (the +# lang/wasm frontend re-lowers to native CG, JITs it, and calls test_main). +# Target-agnostic like C, but the re-lowering JITs for the host, so it only +# runs when the host arch matches the cross target. opt=0 only. Phased-rollout +# panics surface as SKIP. The .wasm.skip sidecar (engine per-lane, LANE=wasm) +# opts a case out of W only. +cf_lane_W() { + # Per-case opt-out for path W: <name>.wasm.skip. The engine's per-lane + # sidecar check keys on the lane id ("W"), so the wasm-named sidecar is + # handled here instead. + local reason + if reason=$(cf_skip_sidecar "$CF_SIDECAR_DIR" "$CF_BASE" "" wasm); then + cf_skip "$CF_NAME/W" "$reason" + return fi - if [ $run_w -eq 1 ] && [ $is_native_target -eq 0 ]; then - emit_event "$event" SKIP "$name/W" "host arch != $TEST_ARCH (no native JIT for re-lowering)" - run_w=0 + if [ $is_native_target -eq 0 ]; then + cf_skip "$CF_NAME/W" "host arch != $TEST_ARCH (no native JIT for re-lowering)" + return fi - if [ $run_w -eq 1 ]; then - local wasm w_cc_err w_run_err w_rc w_missing - wasm="$work/$base_name.wasm" - w_cc_err="$work/w.cc.err" - w_run_err="$work/w.run.err" - t0=$(now_ms) - if ! "$CFREE" cc -O0 -target wasm32-none -c "$src" -o "$wasm" \ - >"$work/w.cc.out" 2>"$w_cc_err"; then - dt=$(( $(now_ms) - t0 )); emit_event "$event" TIME W "$dt" - w_missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \ - "$w_cc_err" 2>/dev/null | head -n1 || true) - if [ -n "$w_missing" ]; then - emit_event "$event" SKIP "$name/W" "$w_missing" - else - emit_event "$event" FAIL "$name/W (cc -target wasm32-none failed; see $w_cc_err)" - fi + local t0 dt wasm w_cc_err w_run_err w_rc w_missing exp_byte + exp_byte=$(( CF_EXPECTED & 0xff )) + wasm="$CF_WORK/$CF_BASE.wasm" + w_cc_err="$CF_WORK/w.cc.err" + w_run_err="$CF_WORK/w.run.err" + t0=$(cf_now_ms) + if ! "$CFREE" cc -O0 -target wasm32-none -c "$CF_SRC" -o "$wasm" \ + >"$CF_WORK/w.cc.out" 2>"$w_cc_err"; then + dt=$(( $(cf_now_ms) - t0 )); cf_time W "$dt" + w_missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \ + "$w_cc_err" 2>/dev/null | head -n1 || true) + if [ -n "$w_missing" ]; then + cf_skip "$CF_NAME/W" "$w_missing" else - # Validate by exit code only (like paths D/J/C). cc stderr is not a - # failure on its own: legitimate non-fatal diagnostics such as - # `#warning` print there while compilation succeeds and the program - # still runs. A real compile failure already took the branch above; - # a real run failure shows up as an exit-code mismatch. - "$CFREE" run -e test_main "$wasm" >"$work/w.run.out" 2>"$w_run_err" - w_rc=$? - dt=$(( $(now_ms) - t0 )); emit_event "$event" TIME W "$dt" - if [ "$w_rc" -eq "$expected_byte" ]; then - emit_event "$event" PASS "$name/W (${dt}ms)" - else - emit_event "$event" FAIL "$name/W (expected $expected_byte got $w_rc, ${dt}ms)" - fi + cf_fail "$CF_NAME/W" "cc -target wasm32-none failed; see $w_cc_err" fi - fi - return 0 -} - -if [ ${#FILTERED_CASES[@]} -gt 0 ]; then - if [ ${#FILTERED_CASES[@]} -le 4 ]; then - printf 'Running cases (serial, opt levels: %s)...\n' "$OPT_LEVELS" - run_serial_items "cases" run_parse_case "${FILTERED_CASES[@]}" else - printf 'Running cases (%s jobs, opt levels: %s)...\n' "$TEST_JOBS" "$OPT_LEVELS" - run_parallel_items "cases" run_parse_case "${FILTERED_CASES[@]}" - fi -fi - -# ---- batched path-E flush + verification ----------------------------------- - -T_E_BATCH=0 -if [ "$(exec_target_queue_size)" -gt 0 ]; then - printf 'Running path E (%d cases batched)...\n' "$(exec_target_queue_size)" - t0=$(now_ms) - exec_target_flush - T_E_BATCH=$(( $(now_ms) - t0 )); T_E=$(( T_E + T_E_BATCH )) - - i=0 - while [ $i -lt ${#E_NAMES[@]} ]; do - name="${E_NAMES[$i]}" - work="${E_WORK[$i]}" - link_dt="${E_LINK_MS[$i]}" - expected_byte="${E_EXPECTED[$i]}" - if [ ! -f "$work/exec.rc" ]; then - note_fail "$name/E (no rc; podman batch did not produce results)" + # Validate by exit code only (like D/J/C). cc stderr is not a failure + # on its own: legitimate non-fatal diagnostics such as `#warning` print + # there while compilation succeeds. + "$CFREE" run -e test_main "$wasm" >"$CF_WORK/w.run.out" 2>"$w_run_err" + w_rc=$? + dt=$(( $(cf_now_ms) - t0 )); cf_time W "$dt" + if [ "$w_rc" -eq "$exp_byte" ]; then + cf_pass "$CF_NAME/W (${dt}ms)" else - RUN_RC="$(cat "$work/exec.rc")" - if [ "$RUN_RC" -eq "$expected_byte" ]; then - note_pass "$name/E (link ${link_dt}ms)" - else - note_fail "$name/E (expected $expected_byte got $RUN_RC, link ${link_dt}ms)" - fi + cf_fail "$CF_NAME/W" "expected $exp_byte got $w_rc, ${dt}ms" fi - i=$((i+1)) - done -fi - -# ---- summary --------------------------------------------------------------- + fi +} -if [ ${#FAIL_NAMES[@]} -gt 0 ]; then - printf '\nFailed:\n' - for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done +# ---- drive the corpus ------------------------------------------------------ + +# Active lanes in DREJCW order. +LANES= +[ $RUN_D -eq 1 ] && LANES="$LANES D" +[ $RUN_R -eq 1 ] && LANES="$LANES R" +[ $RUN_E -eq 1 ] && LANES="$LANES E" +[ $RUN_J -eq 1 ] && LANES="$LANES J" +[ $RUN_C -eq 1 ] && LANES="$LANES C" +[ $RUN_W -eq 1 ] && LANES="$LANES W" + +# Paths C/W force opt_level=0 internally; running them at every requested opt +# level would duplicate identical work. When C and/or W are the ONLY enabled +# lanes, collapse the opt axis to "0" so the matrix isn't padded with items +# that produce no records. (The engine already gates C/W to opt 0 via +# CF_OPT0ONLY; this just trims the redundant opt=1 expansion.) +CASE_OPT_LEVELS="$OPT_LEVELS" +if [ $RUN_D -eq 0 ] && [ $RUN_R -eq 0 ] && [ $RUN_E -eq 0 ] && [ $RUN_J -eq 0 ] \ + && { [ $RUN_C -eq 1 ] || [ $RUN_W -eq 1 ]; }; then + CASE_OPT_LEVELS="0" fi -if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then - printf '\nSkipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' - for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done -fi +PAR="${CFREE_PARSE_PARALLEL:-1}" + +printf 'test-parse-ok target=%s obj=%s arch=%s\n' "$CLANG_TRIPLE" "$HOST_OBJ_FMT" "$TEST_ARCH" -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -printf 'Time: D=%dms R=%dms E=%dms (batch %dms) J=%dms C=%dms W=%dms\n' \ - "$T_D" "$T_R" "$T_E" "$T_E_BATCH" "$T_J" "$T_C" "$T_W" +CF_LABEL=test-parse-ok CF_BUILD_DIR="$BUILD_DIR/parse" \ + CF_CORPUS_GLOBS="$TEST_DIR/cases/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$TEST_DIR/cases" \ + CF_LANES="$LANES" CF_OPT_LEVELS="$CASE_OPT_LEVELS" CF_TUPLES="$TEST_ARCH-noobj" \ + CF_OPT0ONLY="C W" CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-parse-ok +cf_exit diff --git a/test/parse/run_errors.sh b/test/parse/run_errors.sh @@ -1,26 +1,38 @@ -#!/bin/sh -# test/parse/run_errors.sh — file-driven negative test runner. +#!/usr/bin/env bash +# test/parse/run_errors.sh — file-driven negative C-parser runner, on the +# shared corpus harness (test/lib/cf_corpus.sh). Single negative lane. # -# For each test/parse/cases_err/*.c, runs `parse-runner --emit FILE.c -# /dev/null` and expects a nonzero exit (the parser must diagnose the -# constraint violation / syntax error). Mirrors test/pp/run_errors.sh. +# For each test/parse/cases_err/*.c, runs `parse-runner --emit FILE.c /dev/null` +# and expects a nonzero exit (the parser must diagnose the constraint violation +# / syntax error). Kept as a separate runner from the positive parser harness +# (test-parse-err vs test-parse-ok); reuses parse-runner, which test-parse-ok +# builds. # -# Optional sidecar: -# <name>.errpat — substring of stderr that must be present. If -# missing, only exit status is checked. +# Oracle (lane P): parse-runner must exit nonzero. Optional sidecar: +# <name>.errpat — first line is a substring of stderr that must be present +# (grep -qF). When absent, only exit status is checked. # -# Filtering: -# ./run_errors.sh [name_filter] -# name_filter substring match against case basename -# Equivalent env var: CFREE_TEST_FILTER. +# No opt axis. Filtering: +# ./run_errors.sh [name_filter] (or CFREE_TEST_FILTER); substring match +# against case basename. +# +# Parallelism: parallel by default; CFREE_TEST_JOBS=N caps concurrency; +# CFREE_PARSE_PARALLEL=0 forces serial. The lane hook keeps all output under +# $CF_WORK, so the summary is identical serial or parallel. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + TEST_DIR="$ROOT/test/parse" BUILD_DIR="$ROOT/build/test" +CASES_DIR="$TEST_DIR/cases_err" FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" PARSE_RUNNER="$BUILD_DIR/parse-runner" @@ -29,49 +41,48 @@ if [ ! -x "$PARSE_RUNNER" ]; then exit 2 fi -cd "$TEST_DIR/cases_err" 2>/dev/null || { +# No cases_err directory -> nothing to test (graceful, like the original). +if [ ! -d "$CASES_DIR" ]; then echo "parse-err: no cases_err directory; nothing to test" exit 0 -} - -pass=0 -fail=0 -failures= +fi -for src in *.c; do - [ -e "$src" ] || continue - name="${src%.c}" - case "$name" in *"$FILTER"*) ;; *) [ -n "$FILTER" ] && continue ;; esac - pat_file="$name.errpat" - err_log="$BUILD_DIR/parse/$name.err.log" - mkdir -p "$BUILD_DIR/parse" +# ----- negative lane P: parse-runner --emit must fail (+ optional errpat) ---- +# parse-runner emits to /dev/null (we only care about exit status + stderr). +# All logs stay under $CF_WORK for parallel safety. When <name>.errpat exists, +# stderr must contain its first line (grep -qF), else only exit status checks. +cf_lane_P() { + local name="$CF_BASE" + local err_log="$CF_WORK/parse.err" - if "$PARSE_RUNNER" --emit "$src" /dev/null >/dev/null 2>"$err_log"; then - printf 'FAIL %s (expected nonzero exit, got success)\n' "$name" - fail=$((fail + 1)) - failures="$failures $name" - continue + if "$PARSE_RUNNER" --emit "$CF_SRC" /dev/null \ + >"$CF_WORK/parse.out" 2>"$err_log"; then + cf_fail "$name" "expected nonzero exit, got success" + return fi + local pat_file="$CF_SIDECAR_DIR/$CF_BASE.errpat" if [ -e "$pat_file" ]; then + local pat pat=$(head -n1 "$pat_file") if ! grep -qF -- "$pat" "$err_log"; then - printf 'FAIL %s (stderr missing pattern: %s)\n' "$name" "$pat" - cat "$err_log" - fail=$((fail + 1)) - failures="$failures $name" - continue + cf_fail "$name" "stderr missing pattern: $pat" + sed 's/^/ | /' "$err_log" + return fi fi - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) -done + cf_pass "$name" +} -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nparse-err: failures:%s\n' "$failures" - printf 'parse-err: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\nparse-err: %d/%d passed\n' "$pass" "$total" +# ----- drive the corpus ----------------------------------------------------- + +PAR="${CFREE_PARSE_PARALLEL:-1}" + +CF_LABEL=test-parse-err CF_BUILD_DIR="$BUILD_DIR/parse-err" \ + CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \ + CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run + +cf_summary test-parse-err +cf_exit diff --git a/test/pkg/run.sh b/test/pkg/run.sh @@ -22,71 +22,15 @@ SOURCE_DATE_EPOCH=1 export HOME CFREE_TRUSTED_KEYS SOURCE_DATE_EPOCH mkdir -p "$HOME" "$work/in" "$work/pkg" "$work/unpack" "$work/cas" -pass=0 -fail=0 -skip=0 -failures= - artifacts="empty.dat one.bin dup-one.bin payload.txt chunk64.bin chunk64-copy.bin chunk64p1.bin chunk3.bin bin/tool.sh" tree_id= -ok() { - printf 'PASS %s\n' "$1" - pass=$((pass + 1)) -} - -not_ok() { - printf 'FAIL %s\n' "$1" - if [ "$#" -gt 1 ] && [ -s "$2" ]; then - sed 's/^/ | /' "$2" - fi - fail=$((fail + 1)) - failures="$failures $1" -} - -skip_test() { - printf 'SKIP %s\n' "$1" - skip=$((skip + 1)) -} - -run_ok() { - name=$1 - shift - if "$@" > "$work/$name.out" 2> "$work/$name.err"; then - ok "$name" - else - not_ok "$name" "$work/$name.err" - fi -} - -run_fail() { - name=$1 - shift - if "$@" > "$work/$name.out" 2> "$work/$name.err"; then - { - echo "command unexpectedly succeeded" - sed 's/^/stdout: /' "$work/$name.out" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - else - ok "$name" - fi -} - -contains() { - name=$1 - file=$2 - needle=$3 - if grep -F "$needle" "$file" >/dev/null 2>&1; then - ok "$name" - else - { - printf 'missing text: %s\n' "$needle" - sed 's/^/file: /' "$file" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} +# Type-K mode-P kit: shared assert verbs + CAS helpers, all recording through +# the unified cf_* counters over $work. pkg-specific helpers (not_contains, +# same_artifacts, not_same_file, have_cmd) stay below. +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" +cf_report_init not_contains() { name=$1 @@ -103,22 +47,6 @@ not_contains() { fi } -same_file() { - name=$1 - want=$2 - got=$3 - if cmp -s "$want" "$got"; then - ok "$name" - else - { - printf 'files differ:\n' - printf ' want: %s\n' "$want" - printf ' got: %s\n' "$got" - } > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - same_artifacts() { name=$1 dir=$2 @@ -154,64 +82,10 @@ not_same_file() { fi } -is_executable() { - name=$1 - file=$2 - if [ -x "$file" ]; then - ok "$name" - else - echo "not executable: $file" > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - have_cmd() { command -v "$1" >/dev/null 2>&1 } -first_hex_id() { - sed -n 's/.*\([0-9a-fA-F]\{64\}\).*/\1/p' "$1" | sed -n '1p' -} - -id_prefix() { - printf '%.2s' "$1" -} - -cas_object_path() { - root=$1 - kind=$2 - id=$3 - prefix=$(id_prefix "$id") - printf '%s/%s/%s/%s\n' "$root" "$kind" "$prefix" "$id" -} - -tree_blob_for_path() { - tree_file=$1 - want=$2 - awk -v want="$want" ' - $0 == "[file]" { in_file = 1; path = ""; blob = ""; next } - in_file && /^path = / { path = substr($0, 8); next } - in_file && /^blob = / { - blob = substr($0, 8); - if (path == want) { - print blob; - exit; - } - } - ' "$tree_file" -} - -assert_file_exists() { - name=$1 - file=$2 - if [ -f "$file" ]; then - ok "$name" - else - echo "missing file: $file" > "$work/$name.diag" - not_ok "$name" "$work/$name.diag" - fi -} - make_fixtures() { : > "$work/in/empty.dat" printf x > "$work/in/one.bin" @@ -787,10 +661,5 @@ run_fail "pkg-inspect-malformed-fails" "$CFREE" pkg inspect "$work/not-a-package run_fail "pkg-trust-add-missing-pubkey-fails" "$CFREE" pkg trust add run_fail "pkg-trust-remove-bad-keyid-fails" "$CFREE" pkg trust remove xyz -if [ "$fail" -ne 0 ]; then - printf 'pkg: failures:%s\n' "$failures" - printf 'pkg: %d passed, %d failed, %d skipped\n' "$pass" "$fail" "$skip" - exit 1 -fi - -printf 'pkg: %d passed, %d skipped\n' "$pass" "$skip" +cf_summary pkg +cf_exit diff --git a/test/pp/run.sh b/test/pp/run.sh @@ -1,21 +1,37 @@ -#!/bin/sh -# Data-driven preprocessor test runner. +#!/usr/bin/env bash +# test/pp/run.sh — data-driven preprocessor golden-diff runner, on the shared +# corpus harness (test/lib/cf_corpus.sh). Single positive lane. # -# For each test/pp/cases/*.c, runs `cfree cc -E` (with -I pointing at the -# cases dir so sibling headers resolve) and diffs the output against the -# matching .expected file. Leaves .actual files behind on failure so they -# can be reviewed and copied over the expected baseline once intentional +# For each test/pp/cases/*.c, runs `cfree cc -E` (with -I pointing at the cases +# dir so sibling headers resolve) and diffs the output against the matching +# .expected file. Leaves <name>.actual files behind in the cases dir on failure +# so they can be reviewed and copied over the expected baseline once intentional # output changes are validated. # -# To make __DATE__/__TIME__/__FILE__ deterministic across runs and -# checkouts, the runner: -# - exports SOURCE_DATE_EPOCH=0 (1970-01-01T00:00:00Z), which fixes -# __DATE__ to "Jan 1 1970" and __TIME__ to "00:00:00"; -# - cd's into the cases dir and passes the source as a basename, so -# __FILE__ is the file name itself with no checkout-path prefix. +# To make __DATE__/__TIME__/__FILE__ deterministic across runs and checkouts, +# the runner: +# - exports SOURCE_DATE_EPOCH=0 (1970-01-01T00:00:00Z), which fixes __DATE__ +# to "Jan 1 1970" and __TIME__ to "00:00:00"; +# - cd's into the cases dir and uses bare basenames as the corpus glob, so the +# source path the preprocessor sees (and thus __FILE__) is the file name +# itself with no checkout-path prefix. # -# Honors $CFREE for the binary path; defaults to build/cfree relative to the -# repo root inferred from this script's location. +# Oracle (lane P): cfree cc -E must succeed and its output, normalized to a +# token sequence (any run of whitespace incl. newlines collapses to one space; +# leading/trailing whitespace stripped), must equal the .expected file similarly +# normalized. Line-position preservation, leading-space padding before +# macro-expanded `#`, and embed-induced reflow are downstream/cosmetic concerns; +# this runner checks only the token sequence (matching the original harness). +# +# No opt axis. Honors $CFREE for the binary path; defaults to build/cfree. +# +# Filtering: ./run.sh [name_filter] (or CFREE_TEST_FILTER); substring match +# against case basename. +# +# Parallelism: parallel by default (capped CPU-count); CFREE_TEST_JOBS=N caps +# concurrency; CFREE_PP_PARALLEL=0 forces serial. The lane hook reads/writes the +# per-case .expected/.actual next to the case (distinct path per case) and keeps +# all scratch under $CF_WORK, so the summary is identical serial or parallel. set -u @@ -23,6 +39,10 @@ script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +export CF_LIB_DIR="$repo_root/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$repo_root/test/lib/cf_corpus.sh" + CFREE="${CFREE:-$repo_root/build/cfree}" if [ ! -x "$CFREE" ]; then @@ -33,62 +53,63 @@ fi # Reproducible-builds: pin __DATE__ and __TIME__ to the Unix epoch. export SOURCE_DATE_EPOCH=0 +# cd into the cases dir so the corpus glob yields bare basenames: the path the +# preprocessor sees becomes the file name itself (deterministic __FILE__), and +# the .expected/.actual sidecars resolve relative to the same dir. cd "$cases_dir" || exit 2 -pass=0 -fail=0 -failures= +BUILD_DIR="$repo_root/build/test/pp" +mkdir -p "$BUILD_DIR" + +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" -for src in *.c; do - [ -e "$src" ] || continue - expected="${src%.c}.expected" - actual="${src%.c}.actual" - name="${src%.c}" +# ----- positive lane P: cc -E + normalized golden diff ---------------------- +# CF_SRC is a bare basename (cwd == cases dir); the .expected/.actual sidecars +# live alongside it. Scratch (the two normalized token streams) stays in +# $CF_WORK; the reviewable .actual is left in the cases dir on failure, as the +# original harness did (a deliberate baseline-update workflow aid). +cf_lane_P() { + local name="$CF_BASE" + local expected="$CF_BASE.expected" + local actual="$CF_BASE.actual" if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing %s)\n' "$name" "$expected" - fail=$((fail + 1)) - failures="$failures $name" - continue + cf_fail "$name" "missing $expected" + return fi - if ! "$CFREE" cc -E -I . "$src" -o "$actual" >/dev/null 2>&1; then - printf 'FAIL %s (cfree exit nonzero; see %s)\n' "$name" "$actual" - fail=$((fail + 1)) - failures="$failures $name" - continue + if ! "$CFREE" cc -E -I . "$CF_SRC" -o "$actual" >"$CF_WORK/pp.out" 2>"$CF_WORK/pp.err"; then + cf_fail "$name" "cfree exit nonzero; see $actual" + return fi - # Compare token sequences only — any run of whitespace (including - # newlines) collapses to a single space, and leading/trailing - # whitespace is stripped. Line-position preservation across consumed - # directives, leading-space padding clang inserts before - # macro-expanded `#` tokens, and embed-induced reflow are downstream - # / cosmetic concerns; this runner currently only checks the token - # sequence. - exp_strip=$(mktemp) - act_strip=$(mktemp) + # Compare token sequences only — collapse every run of whitespace + # (including newlines) to a single space, strip leading/trailing space. + local exp_strip="$CF_WORK/exp.strip" + local act_strip="$CF_WORK/act.strip" tr '\n' ' ' < "$expected" | tr -s '[:space:]' ' ' \ | sed -e 's/^ //' -e 's/ $//' > "$exp_strip" || true tr '\n' ' ' < "$actual" | tr -s '[:space:]' ' ' \ | sed -e 's/^ //' -e 's/ $//' > "$act_strip" || true + if diff -u "$exp_strip" "$act_strip" >/dev/null 2>&1; then - printf 'PASS %s\n' "$name" + cf_pass "$name" rm -f "$actual" - pass=$((pass + 1)) else - printf 'FAIL %s\n' "$name" - diff -u "$exp_strip" "$act_strip" || true - fail=$((fail + 1)) - failures="$failures $name" + cf_fail "$name" + diff -u "$exp_strip" "$act_strip" | sed 's/^/ | /' || true fi - rm -f "$exp_strip" "$act_strip" -done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\npp: failures:%s\n' "$failures" - printf 'pp: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\npp: %d/%d passed\n' "$pass" "$total" +} + +# ----- drive the corpus ----------------------------------------------------- + +PAR="${CFREE_PP_PARALLEL:-1}" + +CF_LABEL=test-pp-ok CF_BUILD_DIR="$BUILD_DIR/cases" \ + CF_CORPUS_GLOBS="*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$cases_dir" \ + CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run + +cf_summary test-pp-ok +cf_exit diff --git a/test/pp/run_errors.sh b/test/pp/run_errors.sh @@ -1,13 +1,24 @@ -#!/bin/sh -# Data-driven preprocessor must-fail runner. +#!/usr/bin/env bash +# test/pp/run_errors.sh — data-driven preprocessor must-fail runner, on the +# shared corpus harness (test/lib/cf_corpus.sh). Single negative lane. # -# For each test/pp/cases_err/*.c, runs `cfree cc -E` and expects a -# nonzero exit (constraint violations from C11 §6.10 must be -# diagnosed). Counts only exit status; stderr is captured but not -# pattern-matched. +# For each test/pp/cases_err/*.c, runs `cfree cc -E` and expects a nonzero exit +# (constraint violations from C11 §6.10 must be diagnosed). The oracle checks +# exit status only; stderr is captured under $CF_WORK but not pattern-matched +# (these cases ship no diagnostic sidecar). Kept as a separate runner from the +# positive pp golden-diff runner (test-pp vs test-pp-err). # -# Honors $CFREE for the binary path; defaults to build/cfree relative -# to the repo root inferred from this script's location. +# Like run.sh, exports SOURCE_DATE_EPOCH=0 and cd's into the cases dir so +# __DATE__/__TIME__/__FILE__ are deterministic. +# +# No opt axis. Honors $CFREE for the binary path; defaults to build/cfree. +# +# Filtering: ./run_errors.sh [name_filter] (or CFREE_TEST_FILTER); substring +# match against case basename. +# +# Parallelism: parallel by default; CFREE_TEST_JOBS=N caps concurrency; +# CFREE_PP_PARALLEL=0 forces serial. The lane hook keeps all output under +# $CF_WORK, so the summary is identical serial or parallel. set -u @@ -15,6 +26,10 @@ script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases_err" +export CF_LIB_DIR="$repo_root/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$repo_root/test/lib/cf_corpus.sh" + CFREE="${CFREE:-$repo_root/build/cfree}" if [ ! -x "$CFREE" ]; then @@ -25,30 +40,38 @@ fi # Match run.sh so __DATE__/__TIME__/__FILE__ are deterministic. export SOURCE_DATE_EPOCH=0 +# cd into the cases dir so the corpus glob yields bare basenames (deterministic +# __FILE__) and -I . resolves sibling headers. cd "$cases_dir" || exit 2 -pass=0 -fail=0 -failures= +BUILD_DIR="$repo_root/build/test/pp" +mkdir -p "$BUILD_DIR" -for src in *.c; do - [ -e "$src" ] || continue - name="${src%.c}" +FILTER="${1:-${CFREE_TEST_FILTER:-}}" +export CFREE_TEST_FILTER="$FILTER" - if "$CFREE" cc -E -I . "$src" -o /dev/null >/dev/null 2>&1; then - printf 'FAIL %s (expected nonzero exit, got success)\n' "$name" - fail=$((fail + 1)) - failures="$failures $name" +# ----- negative lane P: cc -E must fail (exit status only) ------------------- +# CF_SRC is a bare basename (cwd == cases dir). Output goes to /dev/null per the +# original (the preprocessed text is irrelevant); stderr/stdout logs stay under +# $CF_WORK for parallel safety. +cf_lane_P() { + local name="$CF_BASE" + if "$CFREE" cc -E -I . "$CF_SRC" -o /dev/null \ + >"$CF_WORK/pp.out" 2>"$CF_WORK/pp.err"; then + cf_fail "$name" "expected nonzero exit, got success" else - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) + cf_pass "$name" fi -done +} -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\npp-err: failures:%s\n' "$failures" - printf 'pp-err: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\npp-err: %d/%d passed\n' "$pass" "$total" +# ----- drive the corpus ----------------------------------------------------- + +PAR="${CFREE_PP_PARALLEL:-1}" + +CF_LABEL=test-pp-err CF_BUILD_DIR="$BUILD_DIR/cases_err" \ + CF_CORPUS_GLOBS="*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$cases_dir" \ + CF_LANES="P" CF_OPT_LEVELS="" CF_TUPLES="none-none" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run + +cf_summary test-pp-err +cf_exit diff --git a/test/rt/run.sh b/test/rt/run.sh @@ -1,7 +1,23 @@ #!/usr/bin/env bash -# Runtime tests for rt/include headers and libcfree_rt.a. Each case is compiled -# with cfree cc against rt/include, linked with the freestanding test _start and -# the matching libcfree_rt.a, then executed on the target when a runner exists. +# test/rt/run.sh — runtime tests for rt/include headers and libcfree_rt.a, on +# the shared corpus harness (test/lib/cf_corpus.sh). +# +# Each case (test/rt/cases/*.c) is compiled with `cfree cc` against rt/include, +# linked with the freestanding test _start and the matching libcfree_rt.a, then +# executed on the target when a runner exists. A correct run exits 42. +# +# This is a single-lane (R), no-opt corpus swept across one tuple per arch. +# Arch selection: CFREE_RT_RUNTIME_ARCHES (default "aa64 x64 rv64"). Each arch +# maps to an <arch>-linux exec_target tag and a clang `--target=` triple; the +# per-arch rt archive / clang start.o / extra clang flags are resolved +# LANE-LOCAL in cf_lane_R (no shared sysroot driver), and every artifact is +# written under $CF_WORK so the lane is parallel-safe by construction. +# +# Skip conditions (each a SKIP, not a failure): +# - libcfree_rt.a for the arch missing +# - no execution runner for the arch (podman/qemu/native) +# - clang cannot build the freestanding start.o for the triple +# Set CFREE_TEST_ALLOW_SKIP=1 to let the run exit 0 with skips. set -u @@ -12,20 +28,11 @@ CFREE="$ROOT/build/cfree" LINK_EXE_RUNNER="$ROOT/build/test/link-exe-runner" START_SRC="$ROOT/test/link/harness/start.c" -mkdir -p "$BUILD_DIR" - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } - -PASS=0 -FAIL=0 -SKIP=0 -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" -note_pass() { PASS=$((PASS + 1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL + 1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP + 1)); printf ' %s %s -- %s\n' "$(color_yel SKIP)" "$1" "$2"; } +mkdir -p "$BUILD_DIR" if [ ! -x "$CFREE" ]; then printf 'cfree driver missing at %s -- run `make bin` first\n' "$CFREE" >&2 @@ -37,6 +44,8 @@ if [ ! -x "$LINK_EXE_RUNNER" ]; then exit 2 fi +# exec_target wiring. The aarch64 helper expects these names regardless of +# target arch — they describe host detection rather than the target. have_qemu=0 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" [ -n "$QEMU_BIN" ] && have_qemu=1 @@ -50,8 +59,11 @@ fi export have_qemu QEMU_BIN have_podman is_aarch64 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +. "$ROOT/test/lib/exec_target.sh" + +# ---- per-arch wiring (LANE-LOCAL) ------------------------------------------ arch_triple() { case "$1" in @@ -78,73 +90,64 @@ clang_extra_flags() { esac } -run_arch() { - local arch="$1" - local triple rtlib start_obj arch_dir extra - triple="$(arch_triple "$arch")" +# ---- lane R: compile (cfree cc) -> link (link-exe-runner) -> exec ----------- +# Synchronous exec via exec_target_run (matches the original per-case flow). +# CF_ARCH is the corpus tuple's arch ("aa64"/"x64"/"rv64"); CF_OBJ is "linux". +cf_lane_R() { + local arch="$CF_ARCH" name="$CF_ARCH/$CF_NAME" + local triple rtlib start_obj extra + triple="$(arch_triple "$arch")" || { cf_fail "$name" "unknown arch '$arch'"; return; } rtlib="$(rt_archive "$arch")" - arch_dir="$BUILD_DIR/$arch" - start_obj="$arch_dir/start.o" extra="$(clang_extra_flags "$arch")" - mkdir -p "$arch_dir" + start_obj="$CF_WORK/start.o" if [ ! -f "$rtlib" ]; then - note_skip "$arch" "runtime archive missing at $rtlib" - return 0 + cf_skip "$name" "runtime archive missing at $rtlib"; return fi if ! exec_target_supported "$arch"; then - note_skip "$arch" "no execution runner" - return 0 + cf_skip "$name" "no execution runner"; return fi if ! clang --target="$triple" $extra -O1 -ffreestanding -fno-stack-protector \ -fno-PIC -fno-pie -c "$START_SRC" -o "$start_obj" \ - >"$arch_dir/start.out" 2>"$arch_dir/start.err"; then - note_skip "$arch" "clang cannot build start.o for $triple" - return 0 + >"$CF_WORK/start.out" 2>"$CF_WORK/start.err"; then + cf_skip "$name" "clang cannot build start.o for $triple"; return fi - local src name case_dir obj exe out err rc - for src in "$CASES_DIR"/*.c; do - [ -e "$src" ] || continue - name="$(basename "$src" .c)" - case_dir="$arch_dir/$name" - obj="$case_dir/$name.o" - exe="$case_dir/$name.exe" - out="$case_dir/run.out" - err="$case_dir/run.err" - mkdir -p "$case_dir" - - if ! "$CFREE" cc -target "$triple" -Werror -c "$src" -o "$obj" \ - >"$case_dir/cc.out" 2>"$case_dir/cc.err"; then - note_fail "$arch/$name compile (see $case_dir/cc.err)" - continue - fi - - if ! CFREE_TEST_ARCH="$arch" "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ - --archive "$rtlib" >"$case_dir/link.out" 2>"$case_dir/link.err"; then - note_fail "$arch/$name link (see $case_dir/link.err)" - continue - fi - - exec_target_run "$arch" "$exe" "$out" "$err" - rc="$RUN_RC" - if [ "$rc" -eq 42 ]; then - note_pass "$arch/$name" - else - note_fail "$arch/$name run (expected 42 got $rc; see $err)" - fi - done + local obj="$CF_WORK/$CF_BASE.o" + local exe="$CF_WORK/$CF_BASE.exe" + if ! "$CFREE" cc -target "$triple" -Werror -c "$CF_SRC" -o "$obj" \ + >"$CF_WORK/cc.out" 2>"$CF_WORK/cc.err"; then + cf_fail "$name" "compile (see $CF_WORK/cc.err)"; return + fi + + if ! CFREE_TEST_ARCH="$arch" "$LINK_EXE_RUNNER" -o "$exe" "$obj" "$start_obj" \ + --archive "$rtlib" >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then + cf_fail "$name" "link (see $CF_WORK/link.err)"; return + fi + + exec_target_run "$arch" "$exe" "$CF_WORK/run.out" "$CF_WORK/run.err" + if [ "$RUN_RC" -eq 42 ]; then + cf_pass "$name" + else + cf_fail "$name" "run (expected 42 got $RUN_RC; see $CF_WORK/run.err)" + fi } +# ---- drive the corpus ------------------------------------------------------ +# One tuple per requested arch (<arch>-linux). No opt axis; single lane R. ARCHES="${CFREE_RT_RUNTIME_ARCHES:-aa64 x64 rv64}" +TUPLES= for arch in $ARCHES; do case "$arch" in - aa64|x64|rv64) run_arch "$arch" ;; - *) note_fail "unknown arch '$arch'" ;; + aa64|x64|rv64) TUPLES="$TUPLES $arch-linux" ;; + *) cf_fail "unknown arch '$arch'" ;; esac done -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -if [ "$FAIL" -gt 0 ]; then exit 1; fi -if [ "$SKIP" -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +CF_LABEL=test-rt-runtime CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$CASES_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$CASES_DIR" \ + CF_LANES="R" CF_OPT_LEVELS="" CF_TUPLES="$TUPLES" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="${CFREE_RT_PARALLEL:-1}" cf_corpus_run + +cf_summary test-rt-runtime +cf_exit diff --git a/test/smoke.c b/test/rt/smoke.c diff --git a/test/smoke/rv64.sh b/test/smoke/rv64.sh @@ -1,16 +1,18 @@ #!/usr/bin/env bash -# test/smoke/rv64.sh — end-to-end smoke test for the rv64 podman/qemu path. +# test/smoke/rv64.sh — end-to-end smoke test for the rv64 podman/qemu path, on +# the shared Type-K mode-P kit (test/lib/cfree_sh_kit.sh). # # Phase-2 of the multi-arch bring-up: prove the test/lib/exec_target.sh helper -# can build, queue, and run a riscv64-linux ELF before any cfree-emitted -# rv64 bytes exist. Builds a tiny freestanding static executable with +# can build, queue, and run a riscv64-linux ELF before any cfree-emitted rv64 +# bytes exist. Builds a tiny freestanding static executable with # clang --target=riscv64-linux-gnu and pushes it through -# exec_target_run / exec_target_queue+flush, asserting the expected -# exit code on both paths. +# exec_target_run / exec_target_queue+flush, asserting exit code 42 on both +# paths. This is one inline program, so it is a single-scenario procedural +# suite (build, exec, assert rc) rather than a file-glob corpus. # -# Skipped if clang lacks the riscv64-linux-gnu target or no runner -# (podman or qemu-riscv64) is available. Mirrors test/cg's skip-vs-fail -# convention: skip is treated as failure unless CFREE_TEST_ALLOW_SKIP=1. +# Skipped if clang lacks the riscv64-linux-gnu target, ld.lld is missing, or no +# runner (podman or qemu-riscv64) is available. Skip is treated as failure +# unless CFREE_TEST_ALLOW_SKIP=1 (the shared cf_exit honors it). set -u @@ -18,11 +20,17 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)" BUILD_DIR="$ROOT/build/test/smoke-rv64" mkdir -p "$BUILD_DIR" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } +CF_KIT_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cfree_sh_kit.sh +. "$ROOT/test/lib/cfree_sh_kit.sh" +cf_report_init -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" +# This harness's convention (preserved from the original smoke runner): a SKIP +# is a failure unless CFREE_TEST_ALLOW_SKIP=1, so CI catches a degraded run. +CF_SKIP_IS_FAILURE=1 + +# Mode-P suites are SERIAL and share one $work sandbox. +work="$BUILD_DIR" # ---- detect prerequisites -------------------------------------------------- # @@ -32,7 +40,7 @@ ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" # READY/BLOCKED summary. The smoke script reuses those globals below # and never re-implements the detection. # shellcheck source=../lib/check_rv64_env.sh -source "$(cd "$(dirname "$0")/.." && pwd)/lib/check_rv64_env.sh" +. "$(cd "$(dirname "$0")/.." && pwd)/lib/check_rv64_env.sh" check_rv64_env have_clang_rv64="$RV64_HAVE_CLANG_TARGET" @@ -56,41 +64,31 @@ is_aarch64=0 export have_qemu QEMU_BIN have_podman is_aarch64 QEMU_RV64_BIN EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" - -PASS=0; FAIL=0; SKIP=0 -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } +. "$ROOT/test/lib/exec_target.sh" if [ "$have_clang_rv64" -eq 0 ]; then - note_skip "build" "clang --target=riscv64-linux-gnu unavailable — install: $(_rv64_hint_clang)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "build" "clang --target=riscv64-linux-gnu unavailable — install: $(_rv64_hint_clang)" + cf_summary test-smoke-rv64; cf_exit fi if [ "$have_lld" -eq 0 ]; then - note_skip "build" "ld.lld unavailable — install: $(_rv64_hint_lld)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "build" "ld.lld unavailable — install: $(_rv64_hint_lld)" + cf_summary test-smoke-rv64; cf_exit fi if ! exec_target_supported rv64; then # No runner: pick the most actionable hint. qemu is the lightest # to install on a contributor box; podman is the second-best. - note_skip "exec" "no rv64 runner — easiest fix: $(_rv64_hint_qemu); or set up podman ($(_rv64_hint_podman_riscv64))" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "exec" "no rv64 runner — easiest fix: $(_rv64_hint_qemu); or set up podman ($(_rv64_hint_podman_riscv64))" + cf_summary test-smoke-rv64; cf_exit fi # ---- build a tiny freestanding riscv64 ELF ---------------------------------- # Direct syscall in _start: SYS_exit_group on rv64 is 94 (a7), exit # code 42 (a0). No libc, no relocations, no PIE. The point is to -# exercise the harness pipeline (clang cross-compile → podman/qemu -# run → recorded rc), not to build a complete program. +# exercise the harness pipeline (clang cross-compile -> podman/qemu +# run -> recorded rc), not to build a complete program. SRC="$BUILD_DIR/smoke.c" cat >"$SRC" <<'EOF' __attribute__((noreturn)) void _start(void) { @@ -107,27 +105,33 @@ if ! clang $CLANG_TARGET -march=rv64gc -fuse-ld=lld \ -fno-PIC -fno-pie -nostdlib -static \ -Wl,-e,_start \ "$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then - note_fail "build (see $BUILD_DIR/build.err)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - exit 1 + not_ok "build" "$BUILD_DIR/build.err" + cf_summary test-smoke-rv64; cf_exit fi +ok "build" + +# Classify the recorded rc on a non-42 result. 125/126/127 are podman/shell +# "couldn't execute" rcs — treat those as setup failures and run the podman +# classifier so the contributor sees one line saying *which* podman issue it +# is. ERRFILE / NAME / RC are passed in. +note_run_fail() { + local name="$1" rc="$2" errfile="$3" diag="$BUILD_DIR/$4.diag" + if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \ + { [ "$rc" -eq 125 ] || [ "$rc" -eq 126 ] || [ "$rc" -eq 127 ]; }; then + printf '(rc=%s) — %s\n' "$rc" "$(classify_podman_rv64_error "$errfile")" > "$diag" + else + printf 'expected 42 got %s; see %s\n' "$rc" "$errfile" > "$diag" + fi + not_ok "$name" "$diag" +} # ---- exec_target_run --------------------------------------------------------- exec_target_run rv64 "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err" if [ "$RUN_RC" -eq 42 ]; then - note_pass "exec_target_run rv64 (rc=42)" + ok "exec_target_run rv64 (rc=42)" else - # 125/126/127 are podman/shell "couldn't execute" rcs — treat - # those as setup failures and run the podman classifier so the - # contributor sees one line saying *which* podman issue it is. - if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \ - { [ "$RUN_RC" -eq 125 ] || [ "$RUN_RC" -eq 126 ] || [ "$RUN_RC" -eq 127 ]; }; then - diag="$(classify_podman_rv64_error "$BUILD_DIR/run.err")" - note_fail "exec_target_run rv64 (rc=$RUN_RC) — $diag" - else - note_fail "exec_target_run rv64 (expected 42 got $RUN_RC; see $BUILD_DIR/run.err)" - fi + note_run_fail "exec_target_run rv64" "$RUN_RC" "$BUILD_DIR/run.err" "run" fi # ---- exec_target_queue + flush ---------------------------------------------- @@ -136,23 +140,16 @@ exec_target_queue rv64 smoke "$EXE" \ "$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc" exec_target_flush if [ ! -f "$BUILD_DIR/q.rc" ]; then - note_fail "exec_target_flush rv64 (no rc file produced)" + echo "no rc file produced" > "$BUILD_DIR/q.diag" + not_ok "exec_target_queue+flush rv64" "$BUILD_DIR/q.diag" else Q_RC="$(cat "$BUILD_DIR/q.rc")" if [ "$Q_RC" -eq 42 ]; then - note_pass "exec_target_queue+flush rv64 (rc=42)" + ok "exec_target_queue+flush rv64 (rc=42)" else - if [ "${RV64_HAVE_PODMAN:-0}" -eq 1 ] && \ - { [ "$Q_RC" -eq 125 ] || [ "$Q_RC" -eq 126 ] || [ "$Q_RC" -eq 127 ]; }; then - diag="$(classify_podman_rv64_error "$BUILD_DIR/q.err")" - note_fail "exec_target_queue+flush rv64 (rc=$Q_RC) — $diag" - else - note_fail "exec_target_queue+flush rv64 (expected 42 got $Q_RC; see $BUILD_DIR/q.err)" - fi + note_run_fail "exec_target_queue+flush rv64" "$Q_RC" "$BUILD_DIR/q.err" "q" fi fi -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-smoke-rv64 +cf_exit diff --git a/test/smoke/rv64_tls_link.sh b/test/smoke/rv64_tls_link.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# test/smoke/rv64_tls_link.sh — regression for rv64 TLS Local-Exec lowering. +# test/smoke/rv64_tls_link.sh — regression for rv64 TLS Local-Exec lowering, on +# the shared corpus harness (test/lib/cf_corpus.sh). # # rv64 used to emit R_RV_TLS_GOT_HI20 (Initial-Exec) for an extern # _Thread_local symbol under -fPIE (the hosted default). The linker has no @@ -8,42 +9,81 @@ # links whole-module/static, so the fix is to always emit Local-Exec # (R_RV_TPREL_HI20/LO12_I), matching the aa64 and x64 backends. # -# This is link-only (no execution), so it runs on any host without qemu. +# This is link-only (no execution), so it runs on any host without qemu. It is +# a single synthetic structural case: one lane (L) that compiles two sources, +# asserts the extern _Thread_local access lowered to TPREL (and NOT TLS_GOT) by +# grepping `objdump -r` BEFORE linking, then asserts the whole-module link +# succeeds. The L0 (reloc-presence) checks intentionally precede the link. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" CFREE="${CFREE:-$ROOT/build/cfree}" -WORK="$ROOT/build/test/rv64-tls-link" -mkdir -p "$WORK" +BUILD_DIR="$ROOT/build/test/rv64-tls-link" +mkdir -p "$BUILD_DIR" +# Preserve the original pass-through skip: when cfree is not built, this +# regression simply cannot run, and that is reported as a clean exit 0 (it is +# not a degraded run — the binary just isn't there yet). if [ ! -x "$CFREE" ]; then echo "SKIP rv64_tls_link: $CFREE not built" exit 0 fi -cat > "$WORK/tls_def.c" <<'EOF' +export CF_LIB_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cf_corpus.sh +. "$ROOT/test/lib/cf_corpus.sh" + +# Single synthetic case: a marker file the engine can discover so the lane +# fires exactly once. All real artifacts are generated under $CF_WORK. +CASE_DIR="$BUILD_DIR/case" +mkdir -p "$CASE_DIR" +: > "$CASE_DIR/rv64_tls_link.case" + +# ---- lane L: compile -> objdump -r reloc presence/absence -> link ---------- +# Keeps the L0-before-link ordering: the reloc oracle runs on the compiled .o +# before the whole-module link is attempted. +cf_lane_L() { + cat > "$CF_WORK/tls_def.c" <<'EOF' _Thread_local int g = 7; EOF -cat > "$WORK/tls_ie.c" <<'EOF' + cat > "$CF_WORK/tls_ie.c" <<'EOF' extern _Thread_local int g; int read_g(void) { return g; } int *addr_g(void) { return &g; } EOF -fail() { printf 'FAIL rv64_tls_link: %s\n' "$1"; exit 1; } + if ! "$CFREE" cc -c -target riscv64-linux "$CF_WORK/tls_ie.c" \ + -o "$CF_WORK/tls_ie.o" >"$CF_WORK/cc_ie.out" 2>"$CF_WORK/cc_ie.err"; then + cf_fail "$CF_NAME" "compile tls_ie.c"; return + fi + if ! "$CFREE" cc -c -target riscv64-linux "$CF_WORK/tls_def.c" \ + -o "$CF_WORK/tls_def.o" >"$CF_WORK/cc_def.out" 2>"$CF_WORK/cc_def.err"; then + cf_fail "$CF_NAME" "compile tls_def.c"; return + fi + + # The extern _Thread_local access must lower to TPREL, never TLS_GOT. + local relocs + relocs="$("$CFREE" objdump -r "$CF_WORK/tls_ie.o" 2>&1)" + if ! printf '%s\n' "$relocs" | grep -q 'RV_TPREL_HI20'; then + cf_fail "$CF_NAME" "expected RV_TPREL_HI20 reloc; got: $relocs"; return + fi + if printf '%s\n' "$relocs" | grep -q 'TLS_GOT'; then + cf_fail "$CF_NAME" "unexpected TLS_GOT reloc (Initial-Exec regressed): $relocs"; return + fi -"$CFREE" cc -c -target riscv64-linux "$WORK/tls_ie.c" -o "$WORK/tls_ie.o" \ - || fail "compile tls_ie.c" -"$CFREE" cc -c -target riscv64-linux "$WORK/tls_def.c" -o "$WORK/tls_def.o" \ - || fail "compile tls_def.c" + # And the whole-module link must succeed (previously: unsupported reloc 80). + if ! "$CFREE" ld --entry read_g "$CF_WORK/tls_ie.o" "$CF_WORK/tls_def.o" \ + -o "$CF_WORK/tls_out" >"$CF_WORK/ld.out" 2>"$CF_WORK/ld.err"; then + cf_fail "$CF_NAME" "link extern _Thread_local"; return + fi -# The extern _Thread_local access must lower to TPREL, never TLS_GOT. -relocs="$("$CFREE" objdump -r "$WORK/tls_ie.o" 2>&1)" -echo "$relocs" | grep -q 'RV_TPREL_HI20' || fail "expected RV_TPREL_HI20 reloc; got: $relocs" -if echo "$relocs" | grep -q 'TLS_GOT'; then fail "unexpected TLS_GOT reloc (Initial-Exec regressed): $relocs"; fi + cf_pass "$CF_NAME" +} -# And the whole-module link must succeed (previously: unsupported reloc kind 80). -"$CFREE" ld --entry read_g "$WORK/tls_ie.o" "$WORK/tls_def.o" -o "$WORK/tls_out" \ - || fail "link extern _Thread_local" +CF_LABEL=rv64_tls_link CF_BUILD_DIR="$BUILD_DIR/work" \ + CF_CORPUS_GLOBS="$CASE_DIR/*.case" CF_CORPUS_EXT=case CF_SIDECAR_DIR="$CASE_DIR" \ + CF_LANES="L" CF_OPT_LEVELS="" CF_TUPLES="rv64-elf" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE=0 cf_corpus_run -echo "OK rv64_tls_link" +cf_summary rv64_tls_link +cf_exit diff --git a/test/smoke/x64.sh b/test/smoke/x64.sh @@ -1,16 +1,18 @@ #!/usr/bin/env bash -# test/smoke/x64.sh — end-to-end smoke test for the x64 podman/qemu path. +# test/smoke/x64.sh — end-to-end smoke test for the x64 podman/qemu path, on +# the shared Type-K mode-P kit (test/lib/cfree_sh_kit.sh). # # Phase-1 of the multi-arch bring-up: prove the test/lib/exec_target.sh helper -# can build, queue, and run an x86_64-linux ELF before any cfree-emitted -# x64 bytes exist. Builds a tiny freestanding static executable with +# can build, queue, and run an x86_64-linux ELF before any cfree-emitted x64 +# bytes exist. Builds a tiny freestanding static executable with # clang --target=x86_64-linux-gnu and pushes it through -# exec_target_run / exec_target_queue+flush, asserting the expected -# exit code on both paths. +# exec_target_run / exec_target_queue+flush, asserting exit code 42 on both +# paths. This is one inline program, so it is a single-scenario procedural +# suite (build, exec, assert rc) rather than a file-glob corpus. # -# Skipped if clang lacks the x86_64-linux-gnu target or no runner -# (podman or qemu-x86_64) is available. Mirrors test/cg's skip-vs-fail -# convention: skip is treated as failure unless CFREE_TEST_ALLOW_SKIP=1. +# Skipped if clang lacks the x86_64-linux-gnu target, ld.lld is missing, or no +# runner (podman or qemu-x86_64) is available. Skip is treated as failure +# unless CFREE_TEST_ALLOW_SKIP=1 (the shared cf_exit honors it). set -u @@ -18,11 +20,17 @@ ROOT="$(cd "$(dirname "$0")/../.." && pwd)" BUILD_DIR="$ROOT/build/test/smoke-x64" mkdir -p "$BUILD_DIR" -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } -color_yel() { printf '\033[33m%s\033[0m' "$1"; } +CF_KIT_DIR="$ROOT/test/lib" +# shellcheck source=../lib/cfree_sh_kit.sh +. "$ROOT/test/lib/cfree_sh_kit.sh" +cf_report_init -ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" +# This harness's convention (preserved from the original smoke runner): a SKIP +# is a failure unless CFREE_TEST_ALLOW_SKIP=1, so CI catches a degraded run. +CF_SKIP_IS_FAILURE=1 + +# Mode-P suites are SERIAL and share one $work sandbox. +work="$BUILD_DIR" # ---- detect prerequisites -------------------------------------------------- @@ -51,38 +59,28 @@ is_aarch64=0 export have_qemu QEMU_BIN have_podman is_aarch64 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" +export EXEC_TARGET_MOUNT_ROOT # shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" - -PASS=0; FAIL=0; SKIP=0 -note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } -note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } -note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } +. "$ROOT/test/lib/exec_target.sh" if [ $have_clang_x64 -eq 0 ]; then - note_skip "build" "clang --target=x86_64-linux-gnu unavailable" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "build" "clang --target=x86_64-linux-gnu unavailable" + cf_summary test-smoke-x64; cf_exit fi if [ $have_lld -eq 0 ]; then - note_skip "build" "ld.lld unavailable (needed for ELF cross-link)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "build" "ld.lld unavailable (needed for ELF cross-link)" + cf_summary test-smoke-x64; cf_exit fi if ! exec_target_supported x64; then - note_skip "exec" "no runner for x64 (podman or qemu-x86_64)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - if [ "$ALLOW_SKIP" = "1" ]; then exit 0; fi - exit 1 + skip_test "exec" "no runner for x64 (podman or qemu-x86_64)" + cf_summary test-smoke-x64; cf_exit fi # ---- build a tiny freestanding x86_64 ELF ----------------------------------- # Direct syscall in _start: SYS_exit_group on x86_64 is 231, exit code # 42. No libc, no relocations, no PIE. The point is to exercise the -# harness pipeline (clang cross-compile → podman/qemu run → recorded +# harness pipeline (clang cross-compile -> podman/qemu run -> recorded # rc), not to build a complete program. SRC="$BUILD_DIR/smoke.c" cat >"$SRC" <<'EOF' @@ -100,18 +98,19 @@ if ! clang $CLANG_TARGET -fuse-ld=lld \ -fno-PIC -fno-pie -nostdlib -static \ -Wl,-e,_start \ "$SRC" -o "$EXE" 2>"$BUILD_DIR/build.err"; then - note_fail "build (see $BUILD_DIR/build.err)" - printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" - exit 1 + not_ok "build" "$BUILD_DIR/build.err" + cf_summary test-smoke-x64; cf_exit fi +ok "build" # ---- exec_target_run --------------------------------------------------------- exec_target_run x64 "$EXE" "$BUILD_DIR/run.out" "$BUILD_DIR/run.err" if [ "$RUN_RC" -eq 42 ]; then - note_pass "exec_target_run x64 (rc=42)" + ok "exec_target_run x64 (rc=42)" else - note_fail "exec_target_run x64 (expected 42 got $RUN_RC; see $BUILD_DIR/run.err)" + echo "expected 42 got $RUN_RC; see $BUILD_DIR/run.err" > "$BUILD_DIR/run.diag" + not_ok "exec_target_run x64" "$BUILD_DIR/run.diag" fi # ---- exec_target_queue + flush ---------------------------------------------- @@ -120,17 +119,17 @@ exec_target_queue x64 smoke "$EXE" \ "$BUILD_DIR/q.out" "$BUILD_DIR/q.err" "$BUILD_DIR/q.rc" exec_target_flush if [ ! -f "$BUILD_DIR/q.rc" ]; then - note_fail "exec_target_flush x64 (no rc file produced)" + echo "no rc file produced" > "$BUILD_DIR/q.diag" + not_ok "exec_target_queue+flush x64" "$BUILD_DIR/q.diag" else Q_RC="$(cat "$BUILD_DIR/q.rc")" if [ "$Q_RC" -eq 42 ]; then - note_pass "exec_target_queue+flush x64 (rc=42)" + ok "exec_target_queue+flush x64 (rc=42)" else - note_fail "exec_target_queue+flush x64 (expected 42 got $Q_RC; see $BUILD_DIR/q.err)" + echo "expected 42 got $Q_RC; see $BUILD_DIR/q.err" > "$BUILD_DIR/q.diag" + not_ok "exec_target_queue+flush x64" "$BUILD_DIR/q.diag" fi fi -printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" -if [ $FAIL -gt 0 ]; then exit 1; fi -if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi -exit 0 +cf_summary test-smoke-x64 +cf_exit diff --git a/test/strings/run.sh b/test/strings/run.sh @@ -1,69 +1,26 @@ #!/bin/sh -# Driver-level `cfree strings` test harness. Same shape as test/strip/run.sh. +# Driver-level `cfree strings` test harness. Shared loop/reporting in +# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and +# diff stdout+stderr against cases/<name>.expected. set -u script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree strings-driver -if [ ! -x "$CFREE" ]; then - echo "strings-driver: cfree binary not found at $CFREE" >&2 - exit 2 -fi - -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-strings-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT - -pass=0 -fail=0 -failures= - +CF_WORK=$(cf_workdir strings) +cf_report_init for sh in "$cases_dir"/*.sh; do [ -e "$sh" ] || continue name=$(basename "${sh%.sh}") - expected="${sh%.sh}.expected" - actual="$work_root/$name.actual" - - if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")" - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - sandbox="$work_root/$name" - mkdir -p "$sandbox" - ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1 - case_rc=$? - - if [ "$case_rc" -ne 0 ]; then - printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - if diff -u "$expected" "$actual" >/dev/null 2>&1; then - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "$name" - diff -u "$expected" "$actual" || true - cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true - fail=$((fail + 1)) - failures="$failures $name" - fi + cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir" done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nstrings-driver: failures:%s\n' "$failures" - printf 'strings-driver: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\nstrings-driver: %d/%d passed\n' "$pass" "$total" +cf_summary strings-driver +cf_exit diff --git a/test/strip/run.sh b/test/strip/run.sh @@ -1,69 +1,26 @@ #!/bin/sh -# Driver-level `cfree strip` test harness. Same shape as test/ar/run.sh. +# Driver-level `cfree strip` test harness. Shared loop/reporting in +# test/lib/cfree_sh_report.sh; per case: run cases/<name>.sh in a sandbox and +# diff stdout+stderr against cases/<name>.expected. set -u script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../.." && pwd) cases_dir="$script_dir/cases" +CF_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/cfree_sh_kit.sh" CFREE="${CFREE:-$repo_root/build/cfree}" export CFREE +cf_require_cfree strip-driver -if [ ! -x "$CFREE" ]; then - echo "strip-driver: cfree binary not found at $CFREE" >&2 - exit 2 -fi - -work_root=$(mktemp -d "${TMPDIR:-/tmp}/cfree-strip-test.XXXXXX") -trap 'rm -rf "$work_root"' EXIT - -pass=0 -fail=0 -failures= - +CF_WORK=$(cf_workdir strip) +cf_report_init for sh in "$cases_dir"/*.sh; do [ -e "$sh" ] || continue name=$(basename "${sh%.sh}") - expected="${sh%.sh}.expected" - actual="$work_root/$name.actual" - - if [ ! -e "$expected" ]; then - printf 'FAIL %s (missing %s)\n' "$name" "$(basename "$expected")" - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - sandbox="$work_root/$name" - mkdir -p "$sandbox" - ( cd "$sandbox" && sh "$sh" ) > "$actual" 2>&1 - case_rc=$? - - if [ "$case_rc" -ne 0 ]; then - printf 'FAIL %s (script exit=%d)\n' "$name" "$case_rc" - diff -u "$expected" "$actual" || true - fail=$((fail + 1)) - failures="$failures $name" - continue - fi - - if diff -u "$expected" "$actual" >/dev/null 2>&1; then - printf 'PASS %s\n' "$name" - pass=$((pass + 1)) - else - printf 'FAIL %s\n' "$name" - diff -u "$expected" "$actual" || true - cp "$actual" "$cases_dir/$name.actual" 2>/dev/null || true - fail=$((fail + 1)) - failures="$failures $name" - fi + cf_scenario_case "$name" "$sh" "${sh%.sh}.expected" "$cases_dir" done - -total=$((pass + fail)) -if [ "$fail" -gt 0 ]; then - printf '\nstrip-driver: failures:%s\n' "$failures" - printf 'strip-driver: %d/%d passed\n' "$pass" "$total" - exit 1 -fi -printf '\nstrip-driver: %d/%d passed\n' "$pass" "$total" +cf_summary strip-driver +cf_exit diff --git a/test/test.mk b/test/test.mk @@ -31,6 +31,7 @@ # and runs end-to-end so the wiring stays exercised. See doc/ASM.md. TEST_TARGETS = \ + test-cf-corpus-selftest \ test-aa64-inline \ test-abi-classify \ test-ar \ @@ -78,6 +79,7 @@ TEST_TARGETS = \ test-libc-musl \ test-libc-musl-rv64 \ test-link \ + test-link-reloc-uleb128 \ test-macho \ test-native-direct-target \ test-opt \ @@ -106,6 +108,7 @@ TEST_TARGETS = \ test-x64-inline DEFAULT_TEST_TARGETS = \ + test-cf-corpus-selftest \ test-driver \ test-pp \ test-elf \ @@ -132,12 +135,45 @@ DEFAULT_TEST_TARGETS = \ test-x64-inline \ test-x64-dbg \ test-rt-headers \ - test-lib-deps + test-lib-deps \ + test-cg-api \ + test-abi-classify \ + test-ir-recorder \ + test-native-direct-target \ + test-opt \ + test-asm-symmetry \ + test-link-reloc-uleb128 \ + test-dbg \ + test-disasm-complete \ + test-macho \ + test-interp-toy \ + test-wasm-front .PHONY: test $(TEST_TARGETS) test: $(DEFAULT_TEST_TARGETS) +# Unit-test binary build rules: two regimes (public vs internal interface). +include test/lib/unit.mk + +# Provision the pinned per-arch container rootfs images that exec_target.sh runs +# cfree-emitted binaries inside. This is the ONLY test step that touches the +# network: the cross-arch exec harnesses (toy/parse/link ... path X/E/L) run with +# `podman run --pull=never`, so without these images those paths SKIP. Run once, +# and again only after the pin in test/lib/test_images.sh changes. FORCE=1 +# re-pulls. The images are pinned per-arch by content digest, so they coexist in +# local storage and can never clobber one another. +.PHONY: test-images +test-images: + @bash test/lib/pull_test_images.sh + +# Hermetic self-test of the shared corpus harness engine (test/lib/cf_corpus.sh): +# asserts serial==parallel determinism, SKIP-NA, and the parallel-safety +# invariant (exec_target queued only on the parent, never in a worker). No +# cfree binary / podman / qemu needed. +test-cf-corpus-selftest: + @bash test/lib/cf_corpus_selftest.sh + test-driver: test-driver-cc test-driver-ar test-driver-cas test-driver-strip test-driver-objcopy test-driver-objdump test-driver-pkg test-driver-strings test-driver-cc: bin @@ -196,9 +232,6 @@ AR_TEST_BIN = build/test/ar_test test-ar: $(AR_TEST_BIN) $(AR_TEST_BIN) -$(AR_TEST_BIN): test/ar_test.c $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) test/ar_test.c $(LIB_AR) -o $@ test-driver-ar: bin @CFREE=$(abspath $(BIN)) test/ar/run.sh @@ -230,9 +263,6 @@ DWARF_TEST_BIN = build/test/dwarf_test test-dwarf: $(DWARF_TEST_BIN) $(DWARF_TEST_BIN) -$(DWARF_TEST_BIN): test/dwarf/dwarf_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/dwarf/dwarf_test.c $(LIB_OBJS) -o $@ # DWARF producer self-roundtrip unit test. Drives Debug directly, calls # debug_emit, asserts the produced sections have valid DWARF 5 structure @@ -246,15 +276,9 @@ test-debug: $(DEBUG_TEST_BIN) $(CFI_TEST_BIN) $(DEBUG_TEST_BIN) $(CFI_TEST_BIN) -$(DEBUG_TEST_BIN): test/debug/roundtrip_unit.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/roundtrip_unit.c $(LIB_OBJS) -o $@ # CFI/.eh_frame producer roundtrip for aa64/rv64/x64 (validates the per-arch # CIE template: code/data-align, return-address reg, CFA-init reg). -$(CFI_TEST_BIN): test/debug/cfi_unit.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/debug/cfi_unit.c $(LIB_OBJS) -o $@ test-dbg: bin @CFREE=$(abspath $(BIN)) sh test/dbg/run.sh @@ -274,21 +298,12 @@ test-isa: $(AA64_ISA_TEST_BIN) $(RV64_DECODE_TEST_BIN) $(AA64_ISA_TEST_BIN) $(RV64_DECODE_TEST_BIN) -$(AA64_ISA_TEST_BIN): test/arch/aa64_isa_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_isa_test.c $(LIB_OBJS) -o $@ -$(RV64_DECODE_TEST_BIN): test/arch/rv64_decode_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/rv64_decode_test.c $(LIB_OBJS) -o $@ # aa64_sweep_gen: emits one representative encoding per disasm-table row for the # asm<->disasm self-symmetry sweep (test/asm/symmetry.sh). Needs the internal # arch/aa64/isa.h surface, so -Isrc + LIB_OBJS like the ISA unit test. AA64_SWEEP_GEN = build/test/aa64_sweep_gen -$(AA64_SWEEP_GEN): test/arch/aa64_sweep_gen.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_sweep_gen.c $(LIB_OBJS) -o $@ # test-emu: emulator end-to-end integration test. Builds tiny in-memory rv64 # ELFs and runs them to their exit syscall, asserting the exit code — entirely @@ -300,9 +315,9 @@ EMU_RV64_TEST_BIN = build/test/emu_rv64_test test-emu: $(EMU_RV64_TEST_BIN) $(EMU_RV64_TEST_BIN) -$(EMU_RV64_TEST_BIN): test/emu/rv64_smoke_test.c $(LIB_AR) +$(EMU_RV64_TEST_BIN): test/emu/rv64_smoke_test.c $(UNIT_HDR_DEPS) $(LIB_AR) @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_smoke_test.c $(LIB_AR) -o $@ + $(CC) $(HOST_CFLAGS) -Iinclude -Isrc -Itest test/emu/rv64_smoke_test.c $(LIB_AR) -o $@ # RISC-V ULEB128 diff-reloc application unit test. link_reloc_apply is an # internal (hidden) symbol, so link the raw lib objects like the other @@ -312,9 +327,6 @@ RELOC_ULEB128_TEST_BIN = build/test/reloc_uleb128_unit test-link-reloc-uleb128: $(RELOC_ULEB128_TEST_BIN) $(RELOC_ULEB128_TEST_BIN) -$(RELOC_ULEB128_TEST_BIN): test/link/reloc_uleb128_unit.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/link/reloc_uleb128_unit.c $(LIB_OBJS) -o $@ # test-emu-unit: white-box unit tests for the emulator's INTERNAL units (rv64 # decoder, EmuAddrSpace, Linux syscall handler) that have no public API. Reaches # internal symbols -> links $(LIB_OBJS) (mirrors test-interp), not the archive. @@ -323,9 +335,6 @@ EMU_RV64_UNIT_TEST_BIN = build/test/emu_rv64_unit_test test-emu-unit: $(EMU_RV64_UNIT_TEST_BIN) $(EMU_RV64_UNIT_TEST_BIN) -$(EMU_RV64_UNIT_TEST_BIN): test/emu/rv64_vm_unit_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_vm_unit_test.c $(LIB_OBJS) -o $@ # test-interp: threaded-bytecode interpreter unit smoke test. Builds tiny CG IR # by hand, runs opt_run_o1_interp + interp_lower + the engine, asserts the @@ -336,9 +345,6 @@ INTERP_SMOKE_TEST_BIN = build/test/interp_smoke_test test-interp: $(INTERP_SMOKE_TEST_BIN) $(INTERP_SMOKE_TEST_BIN) -$(INTERP_SMOKE_TEST_BIN): test/interp/interp_smoke_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/interp/interp_smoke_test.c $(LIB_OBJS) -o $@ # test-interp-emu: differential test of the emulator's INTERP execution mode # (doc/INTERPRETER.md Phase 4). Builds a tiny rv64 ELF with SD/LD/ecall and runs @@ -350,9 +356,6 @@ INTERP_EMU_TEST_BIN = build/test/rv64_interp_smoke_test test-interp-emu: $(INTERP_EMU_TEST_BIN) $(INTERP_EMU_TEST_BIN) -$(INTERP_EMU_TEST_BIN): test/emu/rv64_interp_smoke_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/emu/rv64_interp_smoke_test.c $(LIB_OBJS) -o $@ # test-interp-toy: run the toy suite's interpreter (--no-jit) path only, # asserting it matches the golden exit codes (and SKIPping unimplemented ops). @@ -372,36 +375,20 @@ test-cg-api: $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) test-abi-classify: $(ABI_CLASSIFY_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) -$(CG_API_TEST_BIN): test/api/cg_type_test.c $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) test/api/cg_type_test.c $(LIB_AR) -o $@ -$(CG_SWITCH_TEST_BIN): test/api/cg_switch_test.c $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) test/api/cg_switch_test.c $(LIB_AR) -o $@ -$(ABI_CLASSIFY_TEST_BIN): test/api/abi_classify_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/api/abi_classify_test.c $(LIB_OBJS) -o $@ test-ir-recorder: $(IR_RECORDER_TEST_BIN) $(IR_RECORDER_TEST_BIN) -$(IR_RECORDER_TEST_BIN): test/cg/ir_recorder_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/cg/ir_recorder_test.c $(LIB_OBJS) -o $@ test-native-direct-target: $(NATIVE_DIRECT_TARGET_TEST_BIN) $(NATIVE_DIRECT_TARGET_TEST_BIN) -$(NATIVE_DIRECT_TARGET_TEST_BIN): test/cg/native_direct_target_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/cg/native_direct_target_test.c $(LIB_OBJS) -o $@ test-toy: bin @CFREE=$(abspath $(BIN)) test/toy/run.sh -INLINE_PUBLIC_TEST_HDR = test/arch/inline_public_test.h # Public-API inline-asm backend tests. These emit a tiny function through CG, # reopen the object through the public object reader, and assert the expected @@ -411,18 +398,12 @@ AA64_INLINE_TEST_BIN = build/test/aa64_inline_test test-aa64-inline: $(AA64_INLINE_TEST_BIN) $(AA64_INLINE_TEST_BIN) -$(AA64_INLINE_TEST_BIN): test/arch/aa64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/aa64_inline_test.c $(LIB_AR) -o $@ RV64_INLINE_TEST_BIN = build/test/rv64_inline_test test-rv64-inline: $(RV64_INLINE_TEST_BIN) $(RV64_INLINE_TEST_BIN) -$(RV64_INLINE_TEST_BIN): test/arch/rv64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/rv64_inline_test.c $(LIB_AR) -o $@ # rv64 JIT smoke test. Builds a tiny rv64 ELF .o in memory, runs it # through cfree_link_session in JIT-output mode, and skips native execution @@ -438,9 +419,6 @@ test-rv64-jit: $(RV64_JIT_TEST_BIN) exit $$rc; \ fi -$(RV64_JIT_TEST_BIN): test/link/rv64_jit_test.c $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) test/link/rv64_jit_test.c $(LIB_AR) -o $@ # Link-only regression for rv64 TLS Local-Exec lowering (runs on any host). test-rv64-tls-link: bin @@ -451,9 +429,6 @@ X64_INLINE_TEST_BIN = build/test/x64_inline_test test-x64-inline: $(X64_INLINE_TEST_BIN) $(X64_INLINE_TEST_BIN) -$(X64_INLINE_TEST_BIN): test/arch/x64_inline_test.c $(INLINE_PUBLIC_TEST_HDR) $(LIB_AR) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/x64_inline_test.c $(LIB_AR) -o $@ X64_DBG_TEST_BIN = build/test/x64_dbg_test @@ -463,9 +438,6 @@ test-x64-dbg: $(X64_DBG_TEST_BIN) # Reaches the internal arch/arch.h surface (arch_lookup, ArchDbgOps) -> links # $(LIB_OBJS), not the archive, whose relocatable merge localizes non-public # symbols (mirrors the aa64/rv64 arch unit tests above). -$(X64_DBG_TEST_BIN): test/arch/x64_dbg_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/arch/x64_dbg_test.c $(LIB_OBJS) -o $@ RT_HEADER_TEST_TARGETS = \ aarch64-linux-gnu \ @@ -479,7 +451,7 @@ test-rt-headers: bin for target in $(RT_HEADER_TEST_TARGETS); do \ out="build/test/rt-headers/$$target/smoke.o"; \ mkdir -p "$$(dirname "$$out")"; \ - $(BIN) cc -target "$$target" -Werror -c test/smoke.c -o "$$out"; \ + $(BIN) cc -target "$$target" -Werror -c test/rt/smoke.c -o "$$out"; \ done test-rt-runtime: bin rt $(LINK_EXE_RUNNER) @@ -637,19 +609,13 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R OPT_TEST_BIN = build/test/cg_ir_lower_test TINY_INLINE_TEST_BIN = build/test/tiny_inline_test -test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail +test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-prologue-tier $(OPT_TEST_BIN) -$(OPT_TEST_BIN): test/opt/cg_ir_lower_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/opt/cg_ir_lower_test.c $(LIB_OBJS) -o $@ test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN) $(TINY_INLINE_TEST_BIN) -$(TINY_INLINE_TEST_BIN): test/opt/tiny_inline_test.c $(LIB_OBJS) - @mkdir -p $(dir $@) - $(CC) $(TEST_HOST_CFLAGS) -Isrc test/opt/tiny_inline_test.c $(LIB_OBJS) -o $@ # Behavioral disasm check: tiny callee `bl` disappears from its caller at -O1. test-opt-inline: bin @@ -668,6 +634,12 @@ test-opt-static-prune-aa64: bin test-opt-aa64-tail: bin @CFREE=$(abspath $(BIN)) bash test/opt/aa64_tail_call.sh +# Structural disasm check: the -O1 known-frame prologue cost-model tiers +# (aa64 reference + ported x64 slim/red-zone and rv64 leaf shapes). +.PHONY: test-opt-prologue-tier +test-opt-prologue-tier: bin + @CFREE=$(abspath $(BIN)) bash test/opt/prologue_tier.sh + test-parse: test-parse-ok test-parse-err test-parse-ok: lib $(TEST_RT_DEP) $(PARSE_RUNNER) $(ROUNDTRIP_BIN) $(LINK_EXE_RUNNER) $(JIT_RUNNER) @@ -767,11 +739,11 @@ test-wasm-front: bin $(WASM_TOOL) $(LINK_EXE_RUNNER) $(JIT_RUNNER) bash test/wasm/run.sh # test-wasm-target: structural checks on `cfree cc -target wasm32-none` -# output. Each check_*.sh under test/wasm-target compiles a tiny C or -# toy fixture and asserts a property of the produced module bytes -# (memory.copy/fill opcodes, exported "memory", (import ...) decls). -# Opt-in: not in the default `test` target because the checks depend -# on the bulk-memory + (import ...) backend work landing first. +# output. test/wasm-target/run.sh is a Type C corpus harness whose lanes each +# compile a tiny C or toy fixture and assert a property of the produced module +# bytes (inline-asm opcodes, memory.copy/fill opcodes, exported "memory", +# (import ...) decls). Opt-in: not in the default `test` target because the +# checks depend on the bulk-memory + (import ...) backend work landing first. test-wasm-target: bin @CFREE=$(abspath $(BIN)) bash test/wasm-target/run.sh @@ -915,8 +887,8 @@ test-lib-deps: @$(MAKE) lib RELEASE=1 @mkdir -p $(dir $(LIB_DEPS_ACTUAL)) @python3 scripts/lib_external_deps.py $(LIB_DEPS_AR) > $(LIB_DEPS_ACTUAL) - @diff -u test/lib_deps.allowlist $(LIB_DEPS_ACTUAL) \ - || { echo "libcfree.a external symbol set drifted from test/lib_deps.allowlist"; exit 1; } + @diff -u scripts/lib_deps.allowlist $(LIB_DEPS_ACTUAL) \ + || { echo "libcfree.a external symbol set drifted from scripts/lib_deps.allowlist"; exit 1; } @python3 scripts/lib_reloc_defined_prefixes.py $(LIB_DEPS_AR) \ --output $(LIB_RELOC) --ar $(AR) --cc $(CC) > $(LIB_RELOC_BAD) @test ! -s $(LIB_RELOC_BAD) \ diff --git a/test/toy/run.sh b/test/toy/run.sh @@ -1,33 +1,32 @@ #!/usr/bin/env bash -# test/toy/run.sh — .toy frontend end-to-end tests. -# -# Paths per case: -# R cfree run -O{level} case.toy -# I cfree run --no-jit -O{level} case.toy -> execute via the IR interpreter -# instead of JIT native code; asserts the same exit code as the golden. +# test/toy/run.sh — .toy frontend end-to-end tests, on the shared corpus +# harness (test/lib/cf_corpus.sh). Lanes (CFREE_TEST_PATHS, default RLCWI): +# R cfree run -O{level} case.toy (JIT native) +# I cfree run --no-jit -O{level} case.toy (IR interpreter) # Ops the interpreter does not yet implement SKIP (greppable # "interp: <feature> not supported"), like paths C/W. -# L cfree cc -O{level} -c case.toy -> cfree ld case.o -> native executable -# X cfree cc -O{level} -target -> cfree ld -> exec_target for Linux cross targets +# L cfree cc -O{level} -c -> cfree ld -> native exec. Has a <name>.objdump +# sidecar substring check (separate :objdump verdict) and a +# <name>.link.skip sidecar. +# X cross-arch: cfree cc -O{level} -target -> cfree ld -> exec_target for the +# aa64/x64/rv64 Linux targets. Exec is deferred to the engine's batched +# exec_target flush (cf_queue_e). Opt-in (not in the default paths). # C cfree cc --emit=c case.toy -> host cc -> native exec. Exercises the -# --emit=c C-source backend driven by a non-C frontend (validates that -# the CGTarget seam is frontend-agnostic). Phased-rollout panics from -# the C target report as SKIP. Host cc runs under -# -Wall -Wextra -Werror; fixtures wrap their i64 main in a small i32 -# thunk so the emitted `int32_t main(void)` satisfies the standard. -# W cfree cc -target wasm32-none -c case.toy -> .wasm; then cfree run on -# the .wasm (routes through the lang/wasm frontend back to native CG, -# JITs, and invokes main). Exercises the Wasm backend (Toy -> wasm) and -# verifies the produced module still computes the expected result. -# Disabled by default because most cases hit unimplemented Wasm -# lowerings (aggregates, address-of, atomics, intrinsics, ...). +# --emit=c C-source backend driven by a non-C frontend (validates that the +# CGTarget seam is frontend-agnostic). Phased-rollout panics from the C +# target report as SKIP. Host cc runs under -Wall -Wextra -Werror; fixtures +# wrap their i64 main in a small i32 thunk so the emitted +# `int32_t main(void)` satisfies the standard. opt-0 only. +# W cfree cc -target wasm32-none -c case.toy -> .wasm; then cfree run on the +# .wasm (routes through the lang/wasm frontend back to native CG, JITs, and +# invokes main). Exercises the Wasm backend (Toy -> wasm). opt-0 only. +# Most cases hit unimplemented Wasm lowerings -> phased-rollout SKIP. # # Sidecars: # <name>.expected expected process exit code, default 0 # <name>.objdump fixed substrings expected in `cfree objdump -h -t` -# after the linked-object compile path -# <name>.cbackend.skip opts the case out of path C (with reason), -# without affecting other paths +# after the linked-object compile path (lane L) +# <name>.cbackend.skip opts the case out of path C (with reason) # <name>.wasm.skip opts the case out of path W (with reason) # <name>.link.skip opts the case out of path L (with reason) # err/<name>.expected expected diagnostic substring for compile-fail cases @@ -39,10 +38,16 @@ # C and W run only at O0 even when included with other opt levels. # Default paths are "RLCWI"; override with CFREE_TEST_PATHS. # CFREE_OPT_LEVELS selects optimization levels. +# +# Every lane hook writes only under CF_WORK and records via cf_*, so the runner +# is parallel-safe; CFREE_TOY_PARALLEL flips dispatch. set -u ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + TEST_DIR="$ROOT/test/toy" BUILD_DIR="$ROOT/build/test/toy" CFREE="${CFREE:-$ROOT/build/cfree}" @@ -58,112 +63,89 @@ case "$PATHS" in *I*) RUN_I=1;; *) RUN_I=0;; esac TOY_CROSS_ARCHS="${CFREE_TOY_CROSS_ARCHS:-aa64 x64 rv64}" TOY_OPT_LEVELS="${CFREE_OPT_LEVELS:-0 1}" HOST_CC="${CC:-cc}" +PAR="${CFREE_TOY_PARALLEL:-1}" -mkdir -p "$BUILD_DIR" +# The engine's CFREE_TEST_FILTER drives discovery; honor the positional filter. +export CFREE_TEST_FILTER="$FILTER" -PASS=0 -FAIL=0 -SKIP=0 -FAIL_NAMES=() - -color_red() { printf '\033[31m%s\033[0m' "$1"; } -color_grn() { printf '\033[32m%s\033[0m' "$1"; } - -note_pass() { - PASS=$((PASS + 1)) - printf ' %s %s\n' "$(color_grn PASS)" "$1" -} - -note_fail() { - FAIL=$((FAIL + 1)) - FAIL_NAMES+=("$1") - printf ' %s %s\n' "$(color_red FAIL)" "$1" -} - -note_skip() { - SKIP=$((SKIP + 1)) - printf ' SKIP %s (%s)\n' "$1" "$2" -} +shopt -s nullglob +mkdir -p "$BUILD_DIR" -expected_for() { - local src="$1" exp="${src%.toy}.expected" - if [ -f "$exp" ]; then - tr -d '[:space:]' < "$exp" - else - printf '0' - fi -} +if [ ! -x "$CFREE" ]; then + printf 'missing cfree binary: %s\n' "$CFREE" >&2 + exit 2 +fi -check_rc() { +# ---- shared rc oracle (CF_WORK-confined -> parallel-safe) ------------------- +# Toy's pass rule is stricter than wasm's: rc must match AND stderr must be +# empty. Diagnostics (expected/got + the stderr body) are surfaced on FAIL. +tf_check_rc() { # LABEL GOT EXPECTED STDERR_FILE local label="$1" got="$2" expected="$3" stderr_file="$4" expected=$((expected & 255)) if [ "$got" -eq "$expected" ] && [ ! -s "$stderr_file" ]; then - note_pass "$label" + cf_pass "$label" else - note_fail "$label" - printf ' expected rc %d, got %d\n' "$expected" "$got" + cf_fail "$label" "expected rc $expected, got $got" if [ -s "$stderr_file" ]; then sed 's/^/ | /' "$stderr_file" fi fi } -run_case_run() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" - local out="$work/run.out" err="$work/run.err" rc - "$CFREE" run "-O$opt" "$src" > "$out" 2> "$err" +# ---- lanes (cases corpus) -------------------------------------------------- +cf_lane_R() { + local label="$CF_BASE/R-O$CF_OPT" rc + "$CFREE" run "-O$CF_OPT" "$CF_SRC" > "$CF_WORK/run.out" 2> "$CF_WORK/run.err" rc=$? - check_rc "$name/R-O$opt" "$rc" "$expected" "$err" + tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/run.err" } # Path I: cfree run --no-jit — execute through the IR interpreter instead of -# JIT-compiled native code, and assert the same exit code as the golden (which -# the JIT R-path also matches). Ops the interpreter does not yet implement emit -# a greppable "interp: <feature> not supported" diagnostic and SKIP rather than -# FAIL, mirroring how paths C/W treat phased-rollout panics. -run_case_interp() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" - local out="$work/interp.out" err="$work/interp.err" rc missing - local label="$name/I-O$opt" - "$CFREE" run --no-jit "-O$opt" "$src" > "$out" 2> "$err" +# JIT-compiled native code, and assert the same exit code as the golden. Ops the +# interpreter does not yet implement emit a greppable +# "interp: <feature> not supported" diagnostic and SKIP rather than FAIL. +cf_lane_I() { + local label="$CF_BASE/I-O$CF_OPT" rc missing + "$CFREE" run --no-jit "-O$CF_OPT" "$CF_SRC" \ + > "$CF_WORK/interp.out" 2> "$CF_WORK/interp.err" rc=$? - missing=$(grep -oE 'interp: .*not supported' "$err" 2>/dev/null | head -n1 || true) + missing=$(grep -oE 'interp: .*not supported' "$CF_WORK/interp.err" 2>/dev/null | head -n1 || true) if [ -n "$missing" ]; then - note_skip "$label" "$missing" + cf_skip "$label" "$missing" return fi - check_rc "$label" "$rc" "$expected" "$err" + tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/interp.err" } -run_case_link() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" - local obj="$work/$name.o" exe="$work/$name.exe" - local cc_err="$work/cc.err" ld_err="$work/ld.err" out="$work/exe.out" - local err="$work/exe.err" dump="$work/objdump.out" dump_err="$work/objdump.err" rc - local dump_exp="${src%.toy}.objdump" - local link_skip="${src%.toy}.link.skip" +cf_lane_L() { + local label="$CF_BASE/L-O$CF_OPT" rc + local obj="$CF_WORK/$CF_BASE.o" exe="$CF_WORK/$CF_BASE.exe" + local cc_err="$CF_WORK/cc.err" ld_err="$CF_WORK/ld.err" + local dump="$CF_WORK/objdump.out" dump_err="$CF_WORK/objdump.err" + local dump_exp="${CF_SRC%.toy}.objdump" + local link_skip="${CF_SRC%.toy}.link.skip" + local pattern missing + if [ -e "$link_skip" ]; then - note_skip "$name/L-O$opt" "$(head -n1 "$link_skip")" + cf_skip "$label" "$(head -n1 "$link_skip")" return fi - if ! "$CFREE" cc "-O$opt" -c "$src" -o "$obj" > "$work/cc.out" 2> "$cc_err"; then - note_fail "$name/L-O$opt" - printf ' cfree cc -O%s -c failed\n' "$opt" + if ! "$CFREE" cc "-O$CF_OPT" -c "$CF_SRC" -o "$obj" \ + > "$CF_WORK/cc.out" 2> "$cc_err"; then + cf_fail "$label" "cfree cc -O$CF_OPT -c failed" sed 's/^/ | /' "$cc_err" return fi if [ -s "$cc_err" ]; then - note_fail "$name/L-O$opt" - printf ' cfree cc -O%s -c wrote stderr\n' "$opt" + cf_fail "$label" "cfree cc -O$CF_OPT -c wrote stderr" sed 's/^/ | /' "$cc_err" return fi if [ -f "$dump_exp" ]; then if ! "$CFREE" objdump -h -t "$obj" > "$dump" 2> "$dump_err"; then - note_fail "$name/L-O$opt:objdump" - printf ' cfree objdump failed\n' + cf_fail "$label:objdump" "cfree objdump failed" sed 's/^/ | /' "$dump_err" return fi @@ -172,39 +154,38 @@ run_case_link() { [ -z "$pattern" ] && continue if ! grep -F -q -- "$pattern" "$dump"; then missing=1 - printf '%s\n' "$pattern" >> "$work/objdump.missing" + printf '%s\n' "$pattern" >> "$CF_WORK/objdump.missing" fi done < "$dump_exp" if [ "$missing" -eq 0 ]; then - note_pass "$name/L-O$opt:objdump" + cf_pass "$label:objdump" else - note_fail "$name/L-O$opt:objdump" + cf_fail "$label:objdump" "missing objdump substring(s)" printf ' missing objdump substring(s):\n' - sed 's/^/ > /' "$work/objdump.missing" + sed 's/^/ > /' "$CF_WORK/objdump.missing" printf ' actual objdump:\n' sed 's/^/ | /' "$dump" fi fi - if ! "$CFREE" ld "$obj" -o "$exe" > "$work/ld.out" 2> "$ld_err"; then - note_fail "$name/L-O$opt" - printf ' cfree ld failed\n' + if ! "$CFREE" ld "$obj" -o "$exe" > "$CF_WORK/ld.out" 2> "$ld_err"; then + cf_fail "$label" "cfree ld failed" sed 's/^/ | /' "$ld_err" return fi if [ -s "$ld_err" ]; then - note_fail "$name/L-O$opt" - printf ' cfree ld wrote stderr\n' + cf_fail "$label" "cfree ld wrote stderr" sed 's/^/ | /' "$ld_err" return fi chmod +x "$exe" 2>/dev/null || true - "$exe" > "$out" 2> "$err" + "$exe" > "$CF_WORK/exe.out" 2> "$CF_WORK/exe.err" rc=$? - check_rc "$name/L-O$opt" "$rc" "$expected" "$err" + tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$CF_WORK/exe.err" } +# ---- cross-arch (path X) helpers ------------------------------------------- cross_triple_for() { case "$1" in aa64|aarch64) printf 'aarch64-linux-gnu' ;; @@ -263,185 +244,205 @@ EOF_START printf '%s' "$start_o" } -run_case_cross_one() { - local arch="$1" name="$2" src="$3" expected="$4" work="$5" opt="$6" - local triple tag obj exe start_obj cc_err ld_err out err rc label +cross_one() { + local arch="$1" + local triple tag obj exe start_obj cc_err ld_err out err label triple="$(cross_triple_for "$arch")" || { - note_skip "$name/X-O$opt:$arch" "unknown cross arch" + cf_skip "$CF_BASE/X-O$CF_OPT:$arch" "unknown cross arch" return } tag="$(cross_tag_for "$arch")" || { - note_skip "$name/X-O$opt:$arch" "unknown cross arch" + cf_skip "$CF_BASE/X-O$CF_OPT:$arch" "unknown cross arch" return } - label="$name/X-O$opt:$arch" + label="$CF_BASE/X-O$CF_OPT:$arch" if [ "$arch" != "aa64" ] && [ "$arch" != "aarch64" ] && - grep -q 'asmnop' "$src"; then - note_skip "$label" "asmnop is target-specific before toy asm selectors" + grep -q 'asmnop' "$CF_SRC"; then + cf_skip "$label" "asmnop is target-specific before toy asm selectors" return fi if ! exec_target_supported "$tag"; then - note_skip "$label" "no runner for $tag" + cf_skip "$label" "no runner for $tag" return fi - obj="$work/$name.O$opt.$arch.o" - exe="$work/$name.O$opt.$arch.exe" - cc_err="$work/$arch.cc.err" - ld_err="$work/$arch.ld.err" - out="$work/$arch.out" - err="$work/$arch.err" - - if ! "$CFREE" cc "-O$opt" -target "$triple" -c "$src" -o "$obj" \ - > "$work/$arch.cc.out" 2> "$cc_err"; then - note_fail "$label" - printf ' cfree cc -O%s -target %s -c failed\n' "$opt" "$triple" + obj="$CF_WORK/$CF_BASE.O$CF_OPT.$arch.o" + exe="$CF_WORK/$CF_BASE.O$CF_OPT.$arch.exe" + cc_err="$CF_WORK/$arch.cc.err" + ld_err="$CF_WORK/$arch.ld.err" + out="$CF_WORK/$arch.out" + err="$CF_WORK/$arch.err" + + if ! "$CFREE" cc "-O$CF_OPT" -target "$triple" -c "$CF_SRC" -o "$obj" \ + > "$CF_WORK/$arch.cc.out" 2> "$cc_err"; then + cf_fail "$label" "cfree cc -O$CF_OPT -target $triple -c failed" sed 's/^/ | /' "$cc_err" return fi if [ -s "$cc_err" ]; then - note_fail "$label" - printf ' cfree cc -O%s -target %s -c wrote stderr\n' "$opt" "$triple" + cf_fail "$label" "cfree cc -O$CF_OPT -target $triple -c wrote stderr" sed 's/^/ | /' "$cc_err" return fi - start_obj="$(cross_make_start_obj "$arch" "$triple" "$work")" || { - note_skip "$label" "clang --target=$triple unavailable for startup" + start_obj="$(cross_make_start_obj "$arch" "$triple" "$CF_WORK")" || { + cf_skip "$label" "clang --target=$triple unavailable for startup" return } if ! "$CFREE" ld "$obj" "$start_obj" -o "$exe" \ - > "$work/$arch.ld.out" 2> "$ld_err"; then - note_fail "$label" - printf ' cfree ld failed\n' + > "$CF_WORK/$arch.ld.out" 2> "$ld_err"; then + cf_fail "$label" "cfree ld failed" sed 's/^/ | /' "$ld_err" return fi if [ -s "$ld_err" ]; then - note_fail "$label" - printf ' cfree ld wrote stderr\n' + cf_fail "$label" "cfree ld wrote stderr" sed 's/^/ | /' "$ld_err" return fi chmod +x "$exe" 2>/dev/null || true - # Defer execution: queue for one batched container run per arch (drained - # by exec_target_flush after every case is compiled+linked). The - # platform-mismatch warning is filtered and the rc checked post-flush. - XQ_LABELS+=("$label") - XQ_EXPECTED+=("$expected") - XQ_ERRS+=("$err") - XQ_RCS+=("$work/$arch.rc") - exec_target_queue "$tag" "$name" "$exe" "$out" "$err" "$work/$arch.rc" + # Defer execution: the engine batches one container run per arch and checks + # rc == expected after exec_target_flush; tf_flush_verify (CF_FLUSH_VERIFY) + # adds the empty-stderr check after filtering the platform-mismatch warning. + cf_queue_e "$label" "$exe" "$out" "$err" "$CF_WORK/$arch.rc" "$CF_EXPECTED" "$tag" "$err" } -run_case_cross() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" arch +cf_lane_X() { + local arch for arch in $TOY_CROSS_ARCHS; do - run_case_cross_one "$arch" "$name" "$src" "$expected" "$work" "$opt" + cross_one "$arch" done } -run_case_wasm() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" - local label="$name/W-O$opt" - local wasm_skip="${src%.toy}.wasm.skip" - if [ -e "$wasm_skip" ]; then - note_skip "$label" "$(head -n1 "$wasm_skip")" +# CF_FLUSH_VERIFY: called per queued-X case after flush with (label, payload, +# rc). The engine already verified rc == expected; here we replicate the +# original's "stderr must be empty (after filtering the image-platform warning)" +# rule. payload is the err file path. Returns 0 to keep pass, 1 to fail. +tf_flush_verify() { + local errf="$2" + [ -n "$errf" ] || return 0 + if [ -s "$errf" ]; then + grep -v '^WARNING: image platform .* does not match the expected platform' \ + "$errf" > "$errf.clean" 2>/dev/null || true + mv "$errf.clean" "$errf" 2>/dev/null || true + fi + [ ! -s "$errf" ] +} + +# ---- C-source backend (path C, opt-0 only) --------------------------------- +cf_lane_C() { + local label="$CF_BASE/C-O$CF_OPT" rc missing + local cbackend_skip="${CF_SRC%.toy}.cbackend.skip" + if [ -e "$cbackend_skip" ]; then + cf_skip "$label" "$(head -n1 "$cbackend_skip")" return fi - local wasm="$work/$name.wasm" - local cc_err="$work/wasm.cc.err" cc_out="$work/wasm.cc.out" - local run_err="$work/wasm.run.err" run_out="$work/wasm.run.out" - local rc missing - if ! "$CFREE" cc "-O$opt" -target wasm32-none -c "$src" -o "$wasm" \ - > "$cc_out" 2> "$cc_err"; then - # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. Matches - # the C target's pattern so the suite signal stays "real regressions". - # The Wasm target emits "wasm: <feature> not yet implemented" plus a - # handful of "unsupported X" / "too many X (max N supported in v1)" - # / "max N supported in v1" variants — all of which mean the same - # thing operationally: this case needs more wasm CGTarget work. - missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \ - "$cc_err" 2>/dev/null | head -n1 || true) + local out_c="$CF_WORK/$CF_BASE.cfree.c" + local out_bin="$CF_WORK/$CF_BASE.cbackend.bin" + local emit_err="$CF_WORK/c.emit.err" + local cc_err="$CF_WORK/c.cc.err" + local run_err="$CF_WORK/c.run.err" + + # --emit=c forces opt_level=0 internally; pass -O$CF_OPT anyway so the + # driver flag parsing stays exercised. + if ! "$CFREE" cc "-O$CF_OPT" --emit=c "$CF_SRC" -o "$out_c" \ + > "$CF_WORK/c.emit.out" 2> "$emit_err"; then + # Phased-rollout panic from the C target -> SKIP, not FAIL. + missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \ + "$emit_err" 2>/dev/null | head -n1 || true) if [ -n "$missing" ]; then - note_skip "$label" "$missing" + cf_skip "$label" "$missing" return fi - note_fail "$label" - printf ' cfree cc -target wasm32-none failed\n' - sed 's/^/ | /' "$cc_err" + cf_fail "$label" "cfree cc --emit=c failed" + sed 's/^/ | /' "$emit_err" return fi - if [ -s "$cc_err" ]; then - note_fail "$label" - printf ' cfree cc -target wasm32-none wrote stderr\n' + # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the emitted + # `int32_t main(void)` satisfies the host C compiler's main-return-type check. + if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror "$out_c" -o "$out_bin" \ + > "$CF_WORK/c.cc.out" 2> "$cc_err"; then + cf_fail "$label" "host cc rejected emitted source" sed 's/^/ | /' "$cc_err" return fi - # cfree run on the .wasm routes through the lang/wasm frontend, lowers - # to native CG, links via the JIT, and invokes main. - "$CFREE" run "$wasm" > "$run_out" 2> "$run_err" + "$out_bin" > "$CF_WORK/c.run.out" 2> "$run_err" rc=$? - check_rc "$label" "$rc" "$expected" "$run_err" + tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$run_err" } -run_case_emit_c() { - local name="$1" src="$2" expected="$3" work="$4" opt="$5" - local label="$name/C-O$opt" - local cbackend_skip="${src%.toy}.cbackend.skip" - if [ -e "$cbackend_skip" ]; then - note_skip "$label" "$(head -n1 "$cbackend_skip")" +# ---- Wasm roundtrip (path W, opt-0 only) ----------------------------------- +cf_lane_W() { + local label="$CF_BASE/W-O$CF_OPT" rc missing + local wasm_skip="${CF_SRC%.toy}.wasm.skip" + if [ -e "$wasm_skip" ]; then + cf_skip "$label" "$(head -n1 "$wasm_skip")" return fi - local out_c="$work/$name.cfree.c" - local out_bin="$work/$name.cbackend.bin" - local emit_err="$work/c.emit.err" - local cc_err="$work/c.cc.err" - local run_out="$work/c.run.out" run_err="$work/c.run.err" - local rc missing - - # --emit=c forces opt_level=0 internally; pass -O$opt anyway so the - # driver flag parsing stays exercised. - if ! "$CFREE" cc "-O$opt" --emit=c "$src" -o "$out_c" \ - > "$work/c.emit.out" 2> "$emit_err"; then - # Phased-rollout panic from the C target → SKIP, not FAIL. - missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \ - "$emit_err" 2>/dev/null | head -n1 || true) + local wasm="$CF_WORK/$CF_BASE.wasm" + local cc_err="$CF_WORK/wasm.cc.err" + local run_err="$CF_WORK/wasm.run.err" + if ! "$CFREE" cc "-O$CF_OPT" -target wasm32-none -c "$CF_SRC" -o "$wasm" \ + > "$CF_WORK/wasm.cc.out" 2> "$cc_err"; then + # Phased-rollout panic from the Wasm target -> SKIP, not FAIL. The Wasm + # target emits "wasm: <feature> not yet implemented" plus a handful of + # "unsupported X" / "too many X (max N supported in v1)" / + # "max N supported in v1" variants — all meaning this case needs more + # wasm CGTarget work. + missing=$(grep -oE 'wasm(32 ABI| target)?: .*(not yet implemented|not (yet )?supported|unsupported [a-z_0-9]+|max [0-9]+ supported|supported in v1)' \ + "$cc_err" 2>/dev/null | head -n1 || true) if [ -n "$missing" ]; then - note_skip "$label" "$missing" + cf_skip "$label" "$missing" return fi - note_fail "$label" - printf ' cfree cc --emit=c failed\n' - sed 's/^/ | /' "$emit_err" + cf_fail "$label" "cfree cc -target wasm32-none failed" + sed 's/^/ | /' "$cc_err" return fi - # Fixtures wrap their i64 main in an `fn main(): i32` thunk so the - # emitted `int32_t main(void)` satisfies the host C compiler's - # main-return-type check. - if ! $HOST_CC -std=gnu99 -Wall -Wextra -Werror "$out_c" -o "$out_bin" \ - > "$work/c.cc.out" 2> "$cc_err"; then - note_fail "$label" - printf ' host cc rejected emitted source\n' + if [ -s "$cc_err" ]; then + cf_fail "$label" "cfree cc -target wasm32-none wrote stderr" sed 's/^/ | /' "$cc_err" return fi - "$out_bin" > "$run_out" 2> "$run_err" + # cfree run on the .wasm routes through the lang/wasm frontend, lowers to + # native CG, links via the JIT, and invokes main. + "$CFREE" run "$wasm" > "$CF_WORK/wasm.run.out" 2> "$run_err" rc=$? - check_rc "$label" "$rc" "$expected" "$run_err" + tf_check_rc "$label" "$rc" "$CF_EXPECTED" "$run_err" } -if [ ! -x "$CFREE" ]; then - printf 'missing cfree binary: %s\n' "$CFREE" >&2 - exit 2 -fi - -printf 'toy: CFREE=%s\n' "$CFREE" -printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS" +# ---- err corpus (compile-fail cases) --------------------------------------- +# cc MUST fail; the .expected file holds diagnostic substring(s) that must +# appear in stderr (grep -F -f). cc is invoked native (no -target), matching the +# original which used `cfree cc -c` on the host arch. +cf_lane_ERR() { + local label="$CF_BASE/E" + local obj="$CF_WORK/$CF_BASE.o" + local err="$CF_WORK/cc.err" + local exp="${CF_SRC%.toy}.expected" + if "$CFREE" cc -c "$CF_SRC" -o "$obj" > "$CF_WORK/cc.out" 2> "$err"; then + cf_fail "$label" "expected compile failure, got success" + return + fi + if [ ! -f "$exp" ]; then + cf_fail "$label" "missing expected diagnostic: $exp" + return + fi + if grep -F -q -f "$exp" "$err"; then + cf_pass "$label" + else + cf_fail "$label" "expected diagnostic substring not found" + printf ' expected diagnostic substring:\n' + sed 's/^/ > /' "$exp" + printf ' actual stderr:\n' + sed 's/^/ | /' "$err" + fi +} -if [ $RUN_X -eq 1 ]; then +# ---- exec_target wiring (path X uses the engine's deferred-exec flush) ------ +if [ "$RUN_X" -eq 1 ]; then have_podman=0 command -v podman >/dev/null 2>&1 && have_podman=1 QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}" @@ -452,137 +453,56 @@ if [ $RUN_X -eq 1 ]; then *) is_aarch64=0 ;; esac export have_qemu QEMU_BIN have_podman is_aarch64 - # shellcheck source=../lib/exec_target.sh - source "$ROOT/test/lib/exec_target.sh" - # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it - # once so a single batched container drains the whole path-X queue. + # Every queued exe/out/err/rc path lives under BUILD_DIR; bind-mount it once + # so a single batched container drains the whole path-X queue. EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" - # Deferred path-X bookkeeping, checked after exec_target_flush. - XQ_LABELS=() - XQ_EXPECTED=() - XQ_ERRS=() - XQ_RCS=() + export EXEC_TARGET_MOUNT_ROOT + # shellcheck source=../lib/exec_target.sh + . "$ROOT/test/lib/exec_target.sh" fi -shopt -s nullglob -cases=("$TEST_DIR"/cases/*.toy) -if [ ${#cases[@]} -eq 0 ]; then - printf 'no toy cases found under %s/cases\n' "$TEST_DIR" >&2 - exit 2 +# ---- drive the corpora ----------------------------------------------------- +printf 'toy: CFREE=%s\n' "$CFREE" +printf 'toy: PATHS=%s OPT_LEVELS=%s\n' "$PATHS" "$TOY_OPT_LEVELS" +if [ "$RUN_X" -eq 1 ]; then + printf 'toy: cross archs=%s (path X)\n' "$TOY_CROSS_ARCHS" fi -for src in "${cases[@]}"; do - name="$(basename "$src" .toy)" - if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then - continue +# cases corpus — active lanes in PATHS canonical order R I L X C W. +CASE_LANES= +[ "$RUN_R" -eq 1 ] && CASE_LANES="$CASE_LANES R" +[ "$RUN_I" -eq 1 ] && CASE_LANES="$CASE_LANES I" +[ "$RUN_L" -eq 1 ] && CASE_LANES="$CASE_LANES L" +[ "$RUN_X" -eq 1 ] && CASE_LANES="$CASE_LANES X" +[ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C" +[ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W" + +CF_LABEL=toy CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$TEST_DIR/cases/*.toy" CF_CORPUS_EXT=toy \ + CF_SIDECAR_DIR="$TEST_DIR/cases" \ + CF_LANES="$CASE_LANES" CF_OPT_LEVELS="$TOY_OPT_LEVELS" \ + CF_TUPLES="host-host" CF_TARGETS_EXT="" CF_OPT0ONLY="C W" \ + CF_PARALLELIZABLE="$PAR" CF_FLUSH_VERIFY=tf_flush_verify \ + cf_corpus_run + +# err cases exercise compile-failure paths; they aren't relevant to path C/W +# (which go through the same cc invocation). Only run them when at least one of +# the native compile paths (R/L/X) is enabled. +if [ "$RUN_R" -eq 1 ] || [ "$RUN_L" -eq 1 ] || [ "$RUN_X" -eq 1 ]; then + err_cases=("$TEST_DIR"/err/*.toy) + if [ "${#err_cases[@]}" -gt 0 ]; then + CF_LABEL=toy CF_BUILD_DIR="$BUILD_DIR/err" \ + CF_CORPUS_GLOBS="$TEST_DIR/err/*.toy" CF_CORPUS_EXT=toy \ + CF_SIDECAR_DIR="$TEST_DIR/err" \ + CF_LANES="ERR" CF_OPT_LEVELS="" CF_TUPLES="host-host" \ + CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run fi - expected="$(expected_for "$src")" - for opt in $TOY_OPT_LEVELS; do - work="$BUILD_DIR/$name/O$opt" - rm -rf "$work" - mkdir -p "$work" - if [ $RUN_R -eq 1 ]; then - run_case_run "$name" "$src" "$expected" "$work" "$opt" - fi - if [ $RUN_I -eq 1 ]; then - run_case_interp "$name" "$src" "$expected" "$work" "$opt" - fi - if [ $RUN_L -eq 1 ]; then - run_case_link "$name" "$src" "$expected" "$work" "$opt" - fi - if [ $RUN_X -eq 1 ]; then - run_case_cross "$name" "$src" "$expected" "$work" "$opt" - fi - # Path C forces opt_level=0 internally regardless of -O; running it - # at multiple opt levels would duplicate identical work. - if [ $RUN_C -eq 1 ] && [ "$opt" = "0" ]; then - run_case_emit_c "$name" "$src" "$expected" "$work" "$opt" - fi - # Path W (wasm roundtrip) only exercises -O0 today; the Wasm CGTarget - # ignores -O for opt-level purposes (the lang/wasm frontend chooses - # its own native opt level when re-lowering the .wasm). - if [ $RUN_W -eq 1 ] && [ "$opt" = "0" ]; then - run_case_wasm "$name" "$src" "$expected" "$work" "$opt" - fi - done -done - -# Drain the path-X queue in a single batched container per target arch, then -# check each deferred case's exit code (the cross-compile + link already -# passed/failed inline above; only the exec was deferred). -if [ $RUN_X -eq 1 ] && [ "$(exec_target_queue_size)" -gt 0 ]; then - printf 'Running path X (%d cases batched)...\n' "$(exec_target_queue_size)" - exec_target_flush - xi=0 - xn=${#XQ_LABELS[@]} - while [ $xi -lt "$xn" ]; do - xlabel="${XQ_LABELS[$xi]}" - xexp="${XQ_EXPECTED[$xi]}" - xerr="${XQ_ERRS[$xi]}" - xrcf="${XQ_RCS[$xi]}" - if [ -s "$xerr" ]; then - grep -v '^WARNING: image platform .* does not match the expected platform' \ - "$xerr" > "$xerr.clean" 2>/dev/null || true - mv "$xerr.clean" "$xerr" 2>/dev/null || true - fi - if [ -f "$xrcf" ]; then - xrc="$(cat "$xrcf")" - else - xrc=127 - fi - check_rc "$xlabel" "$xrc" "$xexp" "$xerr" - xi=$((xi+1)) - done fi -# err cases exercise compile-failure paths; they aren't relevant to path C -# (which goes through the same cc invocation). Only run them when at least -# one of the native compile paths (R/L/X) is enabled. -if [ $RUN_R -eq 1 ] || [ $RUN_L -eq 1 ] || [ $RUN_X -eq 1 ]; then - err_cases=("$TEST_DIR"/err/*.toy) -else - err_cases=() -fi -# Bash 3.x trips on `${arr[@]}` when arr is empty under `set -u`; guard. -for src in "${err_cases[@]:-}"; do - [ -n "$src" ] || continue - name="$(basename "$src" .toy)" - if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then - continue - fi - work="$BUILD_DIR/err/$name" - rm -rf "$work" - mkdir -p "$work" - obj="$work/$name.o" - out="$work/cc.out" - err="$work/cc.err" - exp="${src%.toy}.expected" - if "$CFREE" cc -c "$src" -o "$obj" > "$out" 2> "$err"; then - note_fail "$name/E" - printf ' expected compile failure, got success\n' - continue - fi - if [ ! -f "$exp" ]; then - note_fail "$name/E" - printf ' missing expected diagnostic: %s\n' "$exp" - continue - fi - if grep -F -q -f "$exp" "$err"; then - note_pass "$name/E" - else - note_fail "$name/E" - printf ' expected diagnostic substring:\n' - sed 's/^/ > /' "$exp" - printf ' actual stderr:\n' - sed 's/^/ | /' "$err" - fi -done - -printf '\nResults: %d pass, %d fail, %d skip\n' "$PASS" "$FAIL" "$SKIP" -if [ $FAIL -ne 0 ]; then - printf 'Failures:\n' - for name in "${FAIL_NAMES[@]}"; do - printf ' %s\n' "$name" - done - exit 1 -fi +# toy treats skips (interp-unsupported ops, no cross-arch runner, the +# intentionally-red musttail cases, ...) as non-fatal — matching the original +# runner, which only ever exited nonzero on a real FAIL. +CF_SKIP_IS_FAILURE=0 +cf_summary toy +cf_exit diff --git a/test/wasm-target/check_asm.sh b/test/wasm-target/check_asm.sh @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# Structural test: confirm the wasm backend can lower an inline-asm block -# whose template is a WAT instruction sequence. The fixture's snippet uses -# i32.popcnt, so the produced module must contain the popcnt opcode byte -# (0x69, the wasm-binary encoding of i32.popcnt). -# -# Exit status: -# 0 expectations hold. -# 1 fixture failed to compile, or popcnt opcode is missing. -# 2 toolchain unavailable. -set -u - -ROOT=$(cd "$(dirname "$0")/.." && pwd)/.. -ROOT=$(cd "$ROOT" && pwd) -CFREE="${CFREE:-$ROOT/build/cfree}" -SRC_DIR="$ROOT/test/wasm-target" -BUILD_DIR="$ROOT/build/test/wasm-target" -mkdir -p "$BUILD_DIR" - -if [ ! -x "$CFREE" ]; then - echo "SKIP: cfree driver missing at $CFREE" >&2 - exit 2 -fi - -OUT="$BUILD_DIR/asm_popcnt.wasm" -if ! "$CFREE" cc -target wasm32-none -c "$SRC_DIR/asm_popcnt.c" -o "$OUT" \ - >"$OUT.cc.out" 2>"$OUT.cc.err"; then - echo "FAIL: cfree cc -target wasm32-none failed for asm_popcnt.c" >&2 - sed 's/^/ | /' "$OUT.cc.err" >&2 - exit 1 -fi - -# i32.popcnt encodes as 0x69. Look for the byte in the produced module. -if ! od -An -tx1 -v "$OUT" | tr -d ' \n' | grep -q '69'; then - echo "FAIL: asm_popcnt.wasm missing i32.popcnt opcode (0x69)" >&2 - exit 1 -fi - -# Negative: a snippet whose top-level `br 0` would escape the synthetic -# frame must be rejected before emission. The driver should fail with a -# "wasm target:" diagnostic. -NEG="$BUILD_DIR/asm_branch_escape.wasm" -if "$CFREE" cc -target wasm32-none -c "$SRC_DIR/asm_branch_escape.c" -o "$NEG" \ - >"$NEG.cc.out" 2>"$NEG.cc.err"; then - echo "FAIL: asm_branch_escape.c compiled but should have been rejected" >&2 - exit 1 -fi -if ! grep -F "wasm target:" "$NEG.cc.err" >/dev/null 2>&1; then - echo "FAIL: asm_branch_escape rejection lacks 'wasm target:' diagnostic" >&2 - sed 's/^/ | /' "$NEG.cc.err" >&2 - exit 1 -fi - -echo "PASS: backend lowers WAT-template inline asm and rejects escaping br" -exit 0 diff --git a/test/wasm-target/check_imports.sh b/test/wasm-target/check_imports.sh @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -# Structural test: confirm that the wasm backend emits `(import ...)` -# declarations for `extern` C symbols, honors the default `env` module, -# and respects __attribute__((import_module, import_name)) overrides. -# -# Strategy: compile each fixture with -target wasm32-none -c and grep the -# raw module bytes for the expected (module, field) UTF-8 strings. The -# wasm import section encodes these as length-prefixed UTF-8 plain text -# so a simple `grep -F` is enough. -# -# Exit status: -# 0 every expectation holds. -# 1 some fixture is missing the expected import. -# 2 toolchain unavailable. -set -u - -ROOT=$(cd "$(dirname "$0")/.." && pwd)/.. -ROOT=$(cd "$ROOT" && pwd) -CFREE="${CFREE:-$ROOT/build/cfree}" -SRC_DIR="$ROOT/test/wasm-target" -BUILD_DIR="$ROOT/build/test/wasm-target" -mkdir -p "$BUILD_DIR" - -if [ ! -x "$CFREE" ]; then - echo "SKIP: cfree driver missing at $CFREE" >&2 - exit 2 -fi - -fail=0 - -compile() { - local src=$1 - local out=$2 - if ! "$CFREE" cc -target wasm32-none -c "$src" -o "$out" \ - >"$out.cc.out" 2>"$out.cc.err"; then - echo "FAIL: cfree cc -target wasm32-none failed for $src" >&2 - sed 's/^/ | /' "$out.cc.err" >&2 - return 1 - fi -} - -check_strings() { - local label=$1 - local wasm=$2 - shift 2 - local s - for s in "$@"; do - if ! grep -F "$s" "$wasm" >/dev/null 2>&1; then - echo "FAIL: $label: expected substring '$s' in $wasm" >&2 - fail=1 - fi - done -} - -# Default module/field: env / host_add. -OUT="$BUILD_DIR/import_decl.wasm" -if compile "$SRC_DIR/import_decl.c" "$OUT"; then - check_strings "import_decl" "$OUT" "env" "host_add" -else - fail=1 -fi - -# Attribute-override module/field: custom / add. -OUT="$BUILD_DIR/import_decl_attribute.wasm" -if compile "$SRC_DIR/import_decl_attribute.c" "$OUT"; then - check_strings "import_decl_attribute" "$OUT" "custom" "add" -else - fail=1 -fi - -if [ "$fail" -eq 0 ]; then - echo "PASS: backend emits expected (import ...) declarations" -fi -exit "$fail" diff --git a/test/wasm-target/check_memory_copy.sh b/test/wasm-target/check_memory_copy.sh @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Structural test: confirm that the wasm backend lowers `copy_bytes` / -# `set_bytes` to the bulk-memory opcodes `memory.copy` (0xfc 0x0a) and -# `memory.fill` (0xfc 0x0b) rather than to byte-loops. -# -# Invocation: -# test/wasm-target/check_memory_copy.sh -# -# Environment: -# CFREE: path to the cfree driver (default: ./build/cfree). -# TOY_FIXTURE: path to a .toy source whose lowering must use -# copy_bytes/set_bytes. Default: test/toy/cases/131_memcpy_uses_bulk.toy. -# -# Exit status: -# 0 both opcodes appear in the produced module. -# 1 either opcode is missing. -# 2 toolchain unavailable. -set -u - -ROOT=$(cd "$(dirname "$0")/.." && pwd)/.. -ROOT=$(cd "$ROOT" && pwd) -CFREE="${CFREE:-$ROOT/build/cfree}" -TOY_FIXTURE="${TOY_FIXTURE:-$ROOT/test/toy/cases/131_memcpy_uses_bulk.toy}" -BUILD_DIR="$ROOT/build/test/wasm-target" -mkdir -p "$BUILD_DIR" - -if [ ! -x "$CFREE" ]; then - echo "SKIP: cfree driver missing at $CFREE" >&2 - exit 2 -fi -if [ ! -f "$TOY_FIXTURE" ]; then - echo "SKIP: missing toy fixture $TOY_FIXTURE" >&2 - exit 2 -fi - -OUT="$BUILD_DIR/$(basename "$TOY_FIXTURE" .toy).wasm" -if ! "$CFREE" cc -target wasm32-none -c "$TOY_FIXTURE" -o "$OUT" \ - >"$BUILD_DIR/cc.out" 2>"$BUILD_DIR/cc.err"; then - echo "FAIL: cfree cc -target wasm32-none failed for $TOY_FIXTURE" >&2 - sed 's/^/ | /' "$BUILD_DIR/cc.err" >&2 - exit 1 -fi - -# Magic numbers in this test mirror the wasm spec opcode encoding: -# memory.copy = 0xfc 0x0a -# memory.fill = 0xfc 0x0b -# Keep these in sync with WASM_INSN_MEMORY_{COPY,FILL} in src/wasm/insn.c. -fail=0 -if ! xxd -p -c 256 "$OUT" | tr -d '\n' | grep -q 'fc0a'; then - echo "FAIL: memory.copy (0xfc 0x0a) not present in $OUT" >&2 - fail=1 -fi -if ! xxd -p -c 256 "$OUT" | tr -d '\n' | grep -q 'fc0b'; then - echo "FAIL: memory.fill (0xfc 0x0b) not present in $OUT" >&2 - fail=1 -fi - -if [ "$fail" -eq 0 ]; then - echo "PASS: $(basename "$OUT") contains memory.copy and memory.fill" -fi -exit "$fail" diff --git a/test/wasm-target/check_memory_export.sh b/test/wasm-target/check_memory_export.sh @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -# Structural test: confirm that the wasm backend exports the linear -# memory under the conventional name `"memory"`. This is required for -# browser, wasmtime, wasmer, and Node host runtimes. -# -# Strategy: compile a small toy fixture, then grep the produced module -# bytes for the UTF-8 string "memory" appearing as an export name. -# Combined with the test/wasm-target/import_decl.* fixtures, this gives -# the minimum signal that cfree's output is loadable by standard hosts. -set -u - -ROOT=$(cd "$(dirname "$0")/.." && pwd)/.. -ROOT=$(cd "$ROOT" && pwd) -CFREE="${CFREE:-$ROOT/build/cfree}" -TOY_FIXTURE="${TOY_FIXTURE:-$ROOT/test/toy/cases/01_return_const.toy}" -BUILD_DIR="$ROOT/build/test/wasm-target" -mkdir -p "$BUILD_DIR" - -if [ ! -x "$CFREE" ]; then - echo "SKIP: cfree driver missing at $CFREE" >&2 - exit 2 -fi -if [ ! -f "$TOY_FIXTURE" ]; then - echo "SKIP: missing toy fixture $TOY_FIXTURE" >&2 - exit 2 -fi - -OUT="$BUILD_DIR/memory_export.wasm" -if ! "$CFREE" cc -target wasm32-none -c "$TOY_FIXTURE" -o "$OUT" \ - >"$BUILD_DIR/me.cc.out" 2>"$BUILD_DIR/me.cc.err"; then - echo "FAIL: cfree cc -target wasm32-none failed for $TOY_FIXTURE" >&2 - sed 's/^/ | /' "$BUILD_DIR/me.cc.err" >&2 - exit 1 -fi - -if ! grep -F "memory" "$OUT" >/dev/null 2>&1; then - echo "FAIL: produced module does not contain export name 'memory'" >&2 - exit 1 -fi -echo "PASS: produced module exports 'memory'" diff --git a/test/wasm-target/run.sh b/test/wasm-target/run.sh @@ -1,30 +1,182 @@ #!/usr/bin/env bash -# Wasm-target structural test driver. Each check_*.sh script compiles a -# small fixture with `cfree cc -target wasm32-none -c` and asserts a -# structural property of the produced module (opcode bytes, export -# names, import declarations). +# test/wasm-target/run.sh — Wasm-target structural-grep tests, on the shared +# corpus harness (test/lib/cf_corpus.sh). # -# Invoked by `make test-wasm-target`; runs every check_*.sh under this -# directory and reports a pass/fail/skip summary. +# Each check compiles a tiny C or toy fixture with `cfree cc -target +# wasm32-none -c` and asserts a structural property of the produced module: +# opcode bytes, export names, or import declarations. The oracle is a +# structural grep over the raw module bytes; the opcode magic numbers and +# import (module, field) names live as metadata inside the lane bodies. +# +# Lanes (each is one structural check — there is no opt/tuple axis): +# ASM inline-asm WAT template lowers to i32.popcnt (0x69), and an +# escaping `br 0` is rejected with a "wasm target:" diagnostic. +# IMP extern C symbols emit (import ...) decls under the default `env` +# module and honor import_module/import_name attribute overrides. +# MCP copy_bytes/set_bytes lower to memory.copy (0xfc 0x0a) and +# memory.fill (0xfc 0x0b), not byte-loops. +# MEX the linear memory is exported under the conventional name "memory". +# +# All lane hooks write only under $CF_WORK and record via cf_*, so the runner +# is parallel-safe by construction; CFREE_WASM_TARGET_PARALLEL flips dispatch. set -u -ROOT=$(cd "$(dirname "$0")/.." && pwd)/.. -ROOT=$(cd "$ROOT" && pwd) -DIR="$ROOT/test/wasm-target" - -pass=0 -fail=0 -skip=0 -for script in "$DIR"/check_*.sh; do - [ -e "$script" ] || continue - name=$(basename "$script" .sh) - "$script" - rc=$? - case "$rc" in - 0) pass=$((pass + 1));; - 2) skip=$((skip + 1));; - *) fail=$((fail + 1));; - esac -done -printf 'test-wasm-target: pass=%d fail=%d skip=%d\n' "$pass" "$fail" "$skip" -[ "$fail" -eq 0 ] +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + +SRC_DIR="$ROOT/test/wasm-target" +BUILD_DIR="$ROOT/build/test/wasm-target" +CFREE_BIN="${CFREE:-$ROOT/build/cfree}" +mkdir -p "$BUILD_DIR" + +# Preserve the original per-script semantics: a missing-toolchain/missing-fixture +# SKIP (was a per-check exit 2 that the old run.sh counted as skip but did NOT +# let fail the run) must not gate this harness's exit. Override the corpus +# engine's default (CF_SKIP_IS_FAILURE=1) back to "skip is informational". +CF_SKIP_IS_FAILURE=0 + +# Toy fixtures live in the shared toy corpus; allow override (preserves the +# original TOY_FIXTURE knob from check_memory_{copy,export}.sh). +TOY_MEMCPY_FIXTURE="${TOY_FIXTURE_MEMCPY:-$ROOT/test/toy/cases/131_memcpy_uses_bulk.toy}" +TOY_EXPORT_FIXTURE="${TOY_FIXTURE_EXPORT:-$ROOT/test/toy/cases/01_return_const.toy}" + +PAR="${CFREE_WASM_TARGET_PARALLEL:-1}" + +# Driver presence gates every check: a missing driver SKIPs (was exit 2 in the +# per-script harnesses). Surfaced as a SKIP per lane via the hooks below. +have_cfree=0 +[ -x "$CFREE_BIN" ] && have_cfree=1 + +# wasm32-none -c <src> -> <out>; logs CF_WORK-confined. Returns nonzero on +# compile failure (lane decides pass/skip/fail). +wt_compile() { # SRC OUT TAG + "$CFREE_BIN" cc -target wasm32-none -c "$1" -o "$2" \ + >"$CF_WORK/$3.cc.out" 2>"$CF_WORK/$3.cc.err" +} + +# Hex-dump $1 with no whitespace, for opcode-byte greps. (od is universally +# available; matches the original check_asm.sh / check_memory_copy.sh dumps.) +wt_hex() { od -An -tx1 -v "$1" | tr -d ' \n'; } + +# ---- lane: inline-asm WAT template (check_asm.sh) -------------------------- +# i32.popcnt encodes as 0x69 in the wasm binary format; an escaping top-level +# `br 0` must be rejected before emission with a "wasm target:" diagnostic. +cf_lane_ASM() { + if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/asm" "cfree driver missing at $CFREE_BIN"; return; fi + + local out="$CF_WORK/asm_popcnt.wasm" + if ! wt_compile "$SRC_DIR/asm_popcnt.c" "$out" asm_popcnt; then + cf_fail "wasm-target/asm-popcnt" "cfree cc -target wasm32-none failed for asm_popcnt.c" + sed 's/^/ | /' "$CF_WORK/asm_popcnt.cc.err" >&2 + return + fi + if wt_hex "$out" | grep -q '69'; then cf_pass "wasm-target/asm-popcnt" + else cf_fail "wasm-target/asm-popcnt" "missing i32.popcnt opcode (0x69)"; fi + + # Negative oracle: escaping `br 0` must be rejected (nonzero) with a + # "wasm target:" stderr diagnostic. + local neg="$CF_WORK/asm_branch_escape.wasm" + if wt_compile "$SRC_DIR/asm_branch_escape.c" "$neg" asm_branch_escape; then + cf_fail "wasm-target/asm-branch-escape" "compiled but should have been rejected" + elif grep -F "wasm target:" "$CF_WORK/asm_branch_escape.cc.err" >/dev/null 2>&1; then + cf_pass "wasm-target/asm-branch-escape" + else + cf_fail "wasm-target/asm-branch-escape" "rejection lacks 'wasm target:' diagnostic" + sed 's/^/ | /' "$CF_WORK/asm_branch_escape.cc.err" >&2 + fi +} + +# ---- lane: import declarations (check_imports.sh) -------------------------- +# Wasm import sections encode (module, field) as length-prefixed UTF-8 plain +# text, so a `grep -F` over the raw bytes is enough. +_wt_check_strings() { # LABEL WASM STR... + local label=$1 wasm=$2; shift 2 + local s ok=1 + for s in "$@"; do + if ! grep -F "$s" "$wasm" >/dev/null 2>&1; then + ok=0 + printf 'expected substring %s in %s\n' "$s" "$wasm" >&2 + fi + done + [ "$ok" -eq 1 ] && cf_pass "$label" || cf_fail "$label" "missing expected import substring(s)" +} +cf_lane_IMP() { + if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/imports" "cfree driver missing at $CFREE_BIN"; return; fi + + # Default module/field: env / host_add. + local out="$CF_WORK/import_decl.wasm" + if wt_compile "$SRC_DIR/import_decl.c" "$out" import_decl; then + _wt_check_strings "wasm-target/import-default" "$out" "env" "host_add" + else + cf_fail "wasm-target/import-default" "cfree cc -target wasm32-none failed for import_decl.c" + sed 's/^/ | /' "$CF_WORK/import_decl.cc.err" >&2 + fi + + # Attribute-override module/field: custom / add. + out="$CF_WORK/import_decl_attribute.wasm" + if wt_compile "$SRC_DIR/import_decl_attribute.c" "$out" import_decl_attribute; then + _wt_check_strings "wasm-target/import-attribute" "$out" "custom" "add" + else + cf_fail "wasm-target/import-attribute" "cfree cc -target wasm32-none failed for import_decl_attribute.c" + sed 's/^/ | /' "$CF_WORK/import_decl_attribute.cc.err" >&2 + fi +} + +# ---- lane: bulk-memory opcodes (check_memory_copy.sh) ---------------------- +# Magic numbers mirror the wasm spec opcode encoding (keep in sync with +# WASM_INSN_MEMORY_{COPY,FILL} in src/wasm/insn.c): +# memory.copy = 0xfc 0x0a memory.fill = 0xfc 0x0b +cf_lane_MCP() { + if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/memory-copy" "cfree driver missing at $CFREE_BIN"; return; fi + if [ ! -f "$TOY_MEMCPY_FIXTURE" ]; then cf_skip "wasm-target/memory-copy" "missing toy fixture $TOY_MEMCPY_FIXTURE"; return; fi + + local out="$CF_WORK/$(basename "$TOY_MEMCPY_FIXTURE" .toy).wasm" + if ! wt_compile "$TOY_MEMCPY_FIXTURE" "$out" memory_copy; then + cf_fail "wasm-target/memory-copy" "cfree cc -target wasm32-none failed for $TOY_MEMCPY_FIXTURE" + sed 's/^/ | /' "$CF_WORK/memory_copy.cc.err" >&2 + return + fi + local hex; hex=$(wt_hex "$out") + local ok=1 + printf '%s' "$hex" | grep -q 'fc0a' || { ok=0; echo "memory.copy (0xfc 0x0a) not present in $out" >&2; } + printf '%s' "$hex" | grep -q 'fc0b' || { ok=0; echo "memory.fill (0xfc 0x0b) not present in $out" >&2; } + [ "$ok" -eq 1 ] && cf_pass "wasm-target/memory-copy" || cf_fail "wasm-target/memory-copy" "missing bulk-memory opcode(s)" +} + +# ---- lane: memory export (check_memory_export.sh) -------------------------- +# The linear memory must be exported under the conventional name "memory" for +# browser/wasmtime/wasmer/Node host runtimes. +cf_lane_MEX() { + if [ "$have_cfree" -eq 0 ]; then cf_skip "wasm-target/memory-export" "cfree driver missing at $CFREE_BIN"; return; fi + if [ ! -f "$TOY_EXPORT_FIXTURE" ]; then cf_skip "wasm-target/memory-export" "missing toy fixture $TOY_EXPORT_FIXTURE"; return; fi + + local out="$CF_WORK/memory_export.wasm" + if ! wt_compile "$TOY_EXPORT_FIXTURE" "$out" memory_export; then + cf_fail "wasm-target/memory-export" "cfree cc -target wasm32-none failed for $TOY_EXPORT_FIXTURE" + sed 's/^/ | /' "$CF_WORK/memory_export.cc.err" >&2 + return + fi + if grep -F "memory" "$out" >/dev/null 2>&1; then cf_pass "wasm-target/memory-export" + else cf_fail "wasm-target/memory-export" "produced module does not contain export name 'memory'"; fi +} + +# ---- drive the structural checks ------------------------------------------- +# A single synthetic case fans the checks out across lanes (no opt/tuple axis). +# Use this dir's fixture .c files as the corpus glob (the engine needs >=1 +# match); CF_READ_CASE collapses to one driving case so each lane fires once. +printf 'test-wasm-target target=wasm32-none\n' + +# Single-item corpus: pick exactly one fixture as the matrix driver so every +# lane runs exactly once. The lane bodies reference fixtures by absolute path, +# not via CF_SRC, since one check may touch several fixtures. +wt_pick_one() { [ "$CF_BASE" = "asm_popcnt" ] || CF_SKIP_NA_CASE=1; } + +CF_LABEL=test-wasm-target CF_BUILD_DIR="$BUILD_DIR" \ + CF_CORPUS_GLOBS="$SRC_DIR/*.c" CF_CORPUS_EXT=c CF_SIDECAR_DIR="$SRC_DIR" \ + CF_LANES="ASM IMP MCP MEX" CF_OPT_LEVELS="" CF_TUPLES="wasm32-none" \ + CF_TARGETS_EXT="" CF_READ_CASE=wt_pick_one CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +cf_summary test-wasm-target +cf_exit diff --git a/test/wasm/run.sh b/test/wasm/run.sh @@ -1,7 +1,22 @@ #!/usr/bin/env bash +# test/wasm/run.sh — Wasm frontend end-to-end tests, on the shared corpus +# harness (test/lib/cf_corpus.sh). Lanes (CFREE_TEST_PATHS, default WDNOJE): +# W wat2wasm (wasm-tool) +# D cfree run (JIT) — wat + wasm inputs +# N cfree run --no-jit (interp; SKIPs ops it does not implement) +# O cfree cc -c (object out) — wat + wasm inputs +# J jit-runner against the obj — wat + wasm inputs +# E link + native exec (synchronous exec_target_run) +# C --emit=c + host cc + exec (opt-in; C-source backend, doc/CBACKEND.md) +# Plus three diagnostic corpora (trap/err/meta) when any of W/D/O is enabled. +# All lane hooks write only under CF_WORK and record via cf_*, so the runner is +# parallel-safe; CFREE_WASM_PARALLEL flips dispatch. set -u ROOT=$(cd "$(dirname "$0")/../.." && pwd) +export CF_LIB_DIR="$ROOT/test/lib" +. "$ROOT/test/lib/cf_corpus.sh" + BUILD_DIR="$ROOT/build/test/wasm" CASES_DIR="$ROOT/test/wasm/cases" ERR_DIR="$ROOT/test/wasm/err" @@ -13,17 +28,9 @@ JIT_RUNNER="$ROOT/build/test/jit-runner" LINK_EXE_RUNNER="$ROOT/build/test/link-exe-runner" TEST_ARCH="${CFREE_TEST_ARCH:-aa64}" TEST_OBJ="${CFREE_TEST_OBJ:-macho}" +HOST_CC="${CC:-cc}" +mkdir -p "$BUILD_DIR" -# Path filtering. Default runs the legacy set (everything except C): -# W wat2wasm -# D cfree run (JIT) -# N cfree run --no-jit (IR interpreter; SKIPs ops it does not implement) -# O cfree cc -c (object output) -# J jit-runner against the produced obj -# E link + native exec -# C --emit=c + host cc + native exec (C-source backend; see doc/CBACKEND.md) -# C is opt-in because Phase 1 of the C backend skips most wasm cases; the -# combined `make test-cbackend` invokes this runner with CFREE_TEST_PATHS=C. PATHS="${CFREE_TEST_PATHS:-WDNOJE}" case "$PATHS" in *W*) RUN_W=1;; *) RUN_W=0;; esac case "$PATHS" in *D*) RUN_D=1;; *) RUN_D=0;; esac @@ -32,91 +39,57 @@ case "$PATHS" in *O*) RUN_O=1;; *) RUN_O=0;; esac case "$PATHS" in *J*) RUN_J=1;; *) RUN_J=0;; esac case "$PATHS" in *E*) RUN_E=1;; *) RUN_E=0;; esac case "$PATHS" in *C*) RUN_C=1;; *) RUN_C=0;; esac -HOST_CC="${CC:-cc}" - -mkdir -p "$BUILD_DIR" - -pass=0 -fail=0 -skip=0 - -note_pass() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); } -note_fail() { printf ' FAIL %s\n' "$1"; fail=$((fail + 1)); } -note_skip() { printf ' SKIP %s (%s)\n' "$1" "$2"; skip=$((skip + 1)); } case "$TEST_ARCH" in aa64|aarch64|arm64) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;; - x64|x86_64|amd64) TEST_ARCH=x64; EXEC_ARCH="x64" ;; - rv64|riscv64) TEST_ARCH=rv64; EXEC_ARCH="rv64" ;; - *) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;; + x64|x86_64|amd64) TEST_ARCH=x64; EXEC_ARCH="x64" ;; + rv64|riscv64) TEST_ARCH=rv64; EXEC_ARCH="rv64" ;; + *) TEST_ARCH=aa64; EXEC_ARCH="aarch64" ;; esac - -host_arch=$(uname -m) -host_matches=0 +host_arch=$(uname -m); host_matches=0 case "$TEST_ARCH:$host_arch" in aa64:arm64|aa64:aarch64) host_matches=1 ;; x64:x86_64) host_matches=1 ;; esac - case "$TEST_OBJ" in - macho) - EXEC_OS="macos" + macho) EXEC_OS="macos" case "$TEST_ARCH" in aa64) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;; - x64) target_triple="x86_64-macos"; clang_triple="x86_64-apple-macos" ;; - *) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;; - esac - ;; - elf) - EXEC_OS="linux" + x64) target_triple="x86_64-macos"; clang_triple="x86_64-apple-macos" ;; + *) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos" ;; + esac ;; + elf) EXEC_OS="linux" case "$TEST_ARCH" in aa64) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;; - x64) target_triple="x86_64-linux"; clang_triple="x86_64-linux-gnu" ;; + x64) target_triple="x86_64-linux"; clang_triple="x86_64-linux-gnu" ;; rv64) target_triple="riscv64-linux"; clang_triple="riscv64-linux-gnu" ;; - *) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;; - esac - ;; + *) target_triple="aarch64-linux"; clang_triple="aarch64-linux-gnu" ;; + esac ;; *) target_triple="aarch64-macos"; clang_triple="arm64-apple-macos"; EXEC_OS="macos" ;; esac EXEC_TAG="$EXEC_ARCH-$EXEC_OS" export CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ" -# Enable the runner's canned host-import resolver so fixtures like -# host_import_add can use `(import "env" "host_add" ...)`. -export CFREE_TEST_HOST_IMPORTS=1 +export CFREE_TEST_HOST_IMPORTS=1 # enable the runner's canned host-import resolver -have_wasm_tool=0 -if [ -x "$WASM_TOOL" ]; then - have_wasm_tool=1 -fi +have_wasm_tool=0; [ -x "$WASM_TOOL" ] && have_wasm_tool=1 +have_jit_runner=0; [ -x "$JIT_RUNNER" ] && [ "$host_matches" -eq 1 ] && have_jit_runner=1 +have_link_runner=0;[ -x "$LINK_EXE_RUNNER" ] && have_link_runner=1 -have_jit_runner=0 -if [ -x "$JIT_RUNNER" ] && [ "$host_matches" -eq 1 ]; then - have_jit_runner=1 -fi - -have_link_runner=0 -if [ -x "$LINK_EXE_RUNNER" ]; then - have_link_runner=1 -fi - -have_qemu=0 -have_podman=0 -is_aarch64=0 +# exec_target wiring (lane E runs SYNCHRONOUSLY via exec_target_run). +have_qemu=0; have_podman=0; is_aarch64=0 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" [ -n "$QEMU_BIN" ] && have_qemu=1 command -v podman >/dev/null 2>&1 && have_podman=1 -arch_raw="$(uname -m 2>/dev/null || true)" -{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 +{ [ "$host_arch" = "aarch64" ] || [ "$host_arch" = "arm64" ]; } && is_aarch64=1 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" -# shellcheck source=../lib/exec_target.sh -source "$ROOT/test/lib/exec_target.sh" +export have_qemu have_podman is_aarch64 QEMU_BIN EXEC_TARGET_MOUNT_ROOT +. "$ROOT/test/lib/exec_target.sh" WASM_START_OBJ="$BUILD_DIR/start-wasm.o" have_wasm_start_obj=0 if clang --target="$clang_triple" -ffreestanding -fno-stack-protector \ -fno-builtin -nostdlib -c "$ROOT/test/wasm/harness/start_wasm.c" \ - -o "$WASM_START_OBJ" >"$BUILD_DIR/start-wasm.out" \ - 2>"$BUILD_DIR/start-wasm.err"; then + -o "$WASM_START_OBJ" >"$BUILD_DIR/start-wasm.out" 2>"$BUILD_DIR/start-wasm.err"; then have_wasm_start_obj=1 fi @@ -128,329 +101,209 @@ if [ "$TEST_OBJ" = "macho" ] && command -v xcrun >/dev/null 2>&1; then fi fi -# Path C wrapper: emitted C exports `test_main` (i32 → maps to int32_t in -# the C target); bridge to `int main(void)` via a tiny host-cc-compiled -# stub linked alongside. +# Path C wrapper: bridges the emitted C's test_main(instance) to int main(void). C_WRAPPER_SRC="$BUILD_DIR/wasm_c_wrapper.c" C_WRAPPER_OBJ="$BUILD_DIR/wasm_c_wrapper.o" have_c_wrapper=0 if [ "$RUN_C" -eq 1 ]; then cat > "$C_WRAPPER_SRC" <<'EOF' -/* Generated by test/wasm/run.sh — bridges main() to test_main(). - * - * Wasm frontend exports use the internal instance ABI: - * __cfree_wasm_init(instance) - * test_main(instance) - * - * Mirrors test/wasm/harness/start_wasm.c but runs hosted (uses calloc), - * since the C target is compiled with the host toolchain. */ #include <stdint.h> #include <stdlib.h> - extern void __cfree_wasm_init(void *); extern int32_t test_main(void *); - -typedef struct { - unsigned char *data; - unsigned long long pages; - unsigned long long max_pages; - unsigned int flags; -} WasmStartMemoryPrefix; - +typedef struct { unsigned char *data; unsigned long long pages; + unsigned long long max_pages; unsigned int flags; } WasmStartMemoryPrefix; #define WASM_START_MEMORY_PREFIX_COUNT 8u #define WASM_START_INSTANCE_SIZE (64u * 1024u) #define WASM_START_MEMORY_SIZE (16u * 1024u * 1024u) - int main(void) { void *instance = calloc(1, WASM_START_INSTANCE_SIZE); unsigned char *memory = calloc(1, WASM_START_MEMORY_SIZE); if (!instance || !memory) return 1; - for (unsigned int i = 0; i < WASM_START_MEMORY_PREFIX_COUNT; ++i) { + for (unsigned int i = 0; i < WASM_START_MEMORY_PREFIX_COUNT; ++i) ((WasmStartMemoryPrefix *)instance)[i].data = memory + i * (WASM_START_MEMORY_SIZE / WASM_START_MEMORY_PREFIX_COUNT); - } __cfree_wasm_init(instance); return (int)test_main(instance); } EOF - if $HOST_CC -std=gnu99 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" \ - 2>"$BUILD_DIR/wasm_c_wrapper.err"; then - have_c_wrapper=1 - fi + $HOST_CC -std=gnu99 -c "$C_WRAPPER_SRC" -o "$C_WRAPPER_OBJ" \ + 2>"$BUILD_DIR/wasm_c_wrapper.err" && have_c_wrapper=1 fi -run_expect_rc() { - local label=$1 - local expected=$2 - shift 2 - "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err" - local rc=$? - if [ "$rc" -eq "$expected" ]; then - note_pass "$label" - else - note_fail "$label expected $expected got $rc" - fi +# ---- per-result oracle helpers (CF_WORK-confined -> parallel-safe) --------- +_wf_base() { printf '%s/%s' "$CF_WORK" "$(printf '%s' "$1" | tr '/ ' '__')"; } +wf_rc() { # LABEL EXPECTED CMD... + local label=$1 exp=$2 b; b=$(_wf_base "$1"); shift 2 + "$@" >"$b.out" 2>"$b.err"; local rc=$? + if [ "$rc" -eq "$exp" ]; then cf_pass "$label"; else cf_fail "$label" "expected $exp got $rc"; fi } - -# Like run_expect_rc, but for the --no-jit interpreter path: ops the interpreter -# does not yet implement emit a greppable "interp: <feature> not supported" -# diagnostic and SKIP rather than FAIL (mirrors the toy I-path). -run_expect_rc_interp() { - local label=$1 - local expected=$2 - shift 2 - local errf="$BUILD_DIR/${label//\//_}.err" - "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$errf" - local rc=$? - local missing - missing=$(grep -oE 'interp: .*not supported' "$errf" 2>/dev/null | head -n1 || true) - if [ -n "$missing" ]; then - note_skip "$label" "$missing" - elif [ "$rc" -eq "$expected" ]; then - note_pass "$label" - else - note_fail "$label expected $expected got $rc" - fi +wf_rc_interp() { # LABEL EXPECTED CMD... (--no-jit; SKIP on "interp: ..not supported") + local label=$1 exp=$2 b; b=$(_wf_base "$1"); shift 2 + "$@" >"$b.out" 2>"$b.err"; local rc=$? miss + miss=$(grep -oE 'interp: .*not supported' "$b.err" 2>/dev/null | head -n1 || true) + if [ -n "$miss" ]; then cf_skip "$label" "$miss" + elif [ "$rc" -eq "$exp" ]; then cf_pass "$label" + else cf_fail "$label" "expected $exp got $rc"; fi } - -run_expect_zero() { - local label=$1 - shift - if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err"; then - note_pass "$label" - else - note_fail "$label" - fi +wf_zero() { # LABEL CMD... + local label=$1 b; b=$(_wf_base "$1"); shift + if "$@" >"$b.out" 2>"$b.err"; then cf_pass "$label"; else cf_fail "$label"; fi +} +wf_fail() { # LABEL CMD... (expect nonzero) + local label=$1 b; b=$(_wf_base "$1"); shift + if "$@" >"$b.out" 2>"$b.err"; then cf_fail "$label" "expected failure"; else cf_pass "$label"; fi +} +wf_fail_grep() { # LABEL SUBSTR CMD... (expect failure + stderr contains SUBSTR) + local label=$1 want=$2 b; b=$(_wf_base "$1"); shift 2 + if "$@" >"$b.out" 2>"$b.err"; then cf_fail "$label" "expected failure" + elif grep -F "$want" "$b.err" >/dev/null 2>&1; then cf_pass "$label" + else cf_fail "$label" "missing diagnostic: $want"; fi } -run_expect_fail() { - local label=$1 - shift - if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$BUILD_DIR/${label//\//_}.err"; then - note_fail "$label expected failure" - else - note_pass "$label" +# Build the wat->wasm intermediate once per case (idempotent). Sets WASM. +wasm_build() { + WASM="$CF_WORK/$CF_BASE.wasm" + [ -f "$WASM" ] && return 0 + [ "$have_wasm_tool" -eq 1 ] || return 1 + "$WASM_TOOL" --wat2wasm "$CF_SRC" "$WASM" >"$CF_WORK/_w.out" 2>"$CF_WORK/_w.err" +} +# Ensure the wat/wasm .o exists (O lane builds it; J/E build on demand if absent). +_obj_wat() { local o="$CF_WORK/$CF_BASE.wat.o"; [ -f "$o" ] || "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$o" >/dev/null 2>&1; printf '%s' "$o"; } +_obj_wasm() { local o="$CF_WORK/$CF_BASE.wasm.o"; wasm_build || return 1; [ -f "$o" ] || "$CFREE_BIN" cc -target "$target_triple" -c "$WASM" -o "$o" >/dev/null 2>&1; printf '%s' "$o"; } + +# ---- cases-corpus lanes ---------------------------------------------------- +cf_lane_W() { + if [ "$have_wasm_tool" -eq 1 ]; then wf_zero "$CF_NAME/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm" + else cf_skip "$CF_NAME/W" "no wasm-tool"; fi +} +cf_lane_D() { + wf_rc "$CF_NAME/D-wat" "$CF_EXPECTED" "$CFREE_BIN" run -e test_main "$CF_SRC" + if wasm_build; then wf_rc "$CF_NAME/D-wasm" "$CF_EXPECTED" "$CFREE_BIN" run -e test_main "$WASM" + else cf_skip "$CF_NAME/D-wasm" "no wasm-tool"; fi +} +cf_lane_N() { + wf_rc_interp "$CF_NAME/N-wat" "$CF_EXPECTED" "$CFREE_BIN" run --no-jit -e test_main "$CF_SRC" + if wasm_build; then wf_rc_interp "$CF_NAME/N-wasm" "$CF_EXPECTED" "$CFREE_BIN" run --no-jit -e test_main "$WASM" + else cf_skip "$CF_NAME/N-wasm" "no wasm-tool"; fi +} +cf_lane_O() { + wf_zero "$CF_NAME/O-wat" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$CF_WORK/$CF_BASE.wat.o" + if wasm_build; then wf_zero "$CF_NAME/O-wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$WASM" -o "$CF_WORK/$CF_BASE.wasm.o" + else cf_skip "$CF_NAME/O-wasm" "no wasm-tool"; fi +} +cf_lane_J() { + if [ "$have_jit_runner" -eq 0 ]; then cf_skip "$CF_NAME/J" "host arch does not match target or no jit-runner"; return; fi + wf_rc "$CF_NAME/J-wat-obj" "$CF_EXPECTED" env CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$(_obj_wat)" + local wo; if wo=$(_obj_wasm); then wf_rc "$CF_NAME/J-wasm-obj" "$CF_EXPECTED" env CFREE_TEST_ARCH="$TEST_ARCH" CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wo" + else cf_skip "$CF_NAME/J-wasm-obj" "no wasm-tool"; fi +} +cf_lane_E() { + if [ "$have_link_runner" -eq 0 ] || [ "$have_wasm_start_obj" -eq 0 ]; then + cf_skip "$CF_NAME/E" "requires link runner and wasm start.o"; return fi + local exe="$CF_WORK/$CF_BASE.exe" + if "$LINK_EXE_RUNNER" -o "$exe" "${MACHO_DSO_ARGS[@]}" "$(_obj_wat)" "$WASM_START_OBJ" \ + >"$CF_WORK/link.out" 2>"$CF_WORK/link.err"; then + if exec_target_supported "$EXEC_TAG"; then + exec_target_run "$EXEC_TAG" "$exe" "$CF_WORK/exec.out" "$CF_WORK/exec.err" + if [ "$RUN_RC" -eq "$CF_EXPECTED" ]; then cf_pass "$CF_NAME/E" + else cf_fail "$CF_NAME/E" "expected $CF_EXPECTED got $RUN_RC"; fi + else cf_skip "$CF_NAME/E" "no execution support for $TEST_ARCH"; fi + else cf_fail "$CF_NAME/E" "link failed"; fi } - -run_expect_fail_stderr_contains() { - local label=$1 - local expected=$2 - local err="$BUILD_DIR/${label//\//_}.err" - shift 2 - if "$@" >"$BUILD_DIR/${label//\//_}.out" 2>"$err"; then - note_fail "$label expected failure" - elif grep -F "$expected" "$err" >/dev/null 2>&1; then - note_pass "$label" - else - note_fail "$label missing diagnostic: $expected" +cf_lane_C() { + if [ "$have_c_wrapper" -eq 0 ]; then cf_skip "$CF_NAME/C" "host CC wrapper unavailable"; return; fi + if [ "$host_matches" -eq 0 ]; then cf_skip "$CF_NAME/C" "host arch != $TEST_ARCH (C target is target-locked)"; return; fi + local c_src="$CF_WORK/$CF_BASE.cfree.c" c_bin="$CF_WORK/$CF_BASE.cbackend.bin" miss + if ! "$CFREE_BIN" cc -target "$target_triple" --emit=c "$CF_SRC" -o "$c_src" \ + >"$CF_WORK/c.emit.out" 2>"$CF_WORK/c.emit.err"; then + miss=$(grep -oE 'C target: .*(not implemented|not yet supported)' "$CF_WORK/c.emit.err" 2>/dev/null | head -n1 || true) + if [ -n "$miss" ]; then cf_skip "$CF_NAME/C" "$miss"; else cf_fail "$CF_NAME/C" "cfree cc --emit=c failed"; fi + return + fi + if ! $HOST_CC -std=gnu99 -Wno-main-return-type "$c_src" "$C_WRAPPER_OBJ" -o "$c_bin" \ + >"$CF_WORK/c.cc.out" 2>"$CF_WORK/c.cc.err"; then + cf_fail "$CF_NAME/C" "host cc rejected emitted source"; return fi + "$c_bin" >"$CF_WORK/c.run.out" 2>"$CF_WORK/c.run.err"; local rc=$? + if [ "$rc" -eq "$CF_EXPECTED" ]; then cf_pass "$CF_NAME/C"; else cf_fail "$CF_NAME/C" "expected $CF_EXPECTED got $rc"; fi } -run_expect_fail_quiet() { - local label=$1 - local out="$BUILD_DIR/${label//\//_}.out" - local err="$BUILD_DIR/${label//\//_}.err" - shift - if bash -c 'out=$1; err=$2; shift 2; "$@" >"$out" 2>"$err"' \ - bash "$out" "$err" "$@" >/dev/null 2>/dev/null; then - note_fail "$label expected failure" +# ---- diagnostic-corpus lanes ---------------------------------------------- +cf_lane_T() { # trap: build wasm (expect 0), then run expecting a trap (nonzero) + if [ "$have_wasm_tool" -eq 1 ]; then wf_zero "trap/$CF_BASE/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm" + else cf_skip "trap/$CF_BASE/W" "no wasm-tool"; return; fi + wf_fail "trap/$CF_BASE/D-wat" "$CFREE_BIN" run -e test_main "$CF_SRC" + wf_fail "trap/$CF_BASE/D-wasm" "$CFREE_BIN" run -e test_main "$CF_WORK/$CF_BASE.wasm" +} +cf_lane_ERR() { # err: cc must fail + wf_fail "err/$CF_BASE/cc" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_SRC" -o "$CF_WORK/$CF_BASE.o" +} +cf_lane_META() { # meta: build wasm, then native cc per the expect whitelist + wf_zero "meta/$CF_BASE/W" "$WASM_TOOL" --wat2wasm "$CF_SRC" "$CF_WORK/$CF_BASE.wasm" + if [ "$CF_BASE" = "type_custom" ] || [ "$CF_BASE" = "import_table_global_start" ]; then + wf_zero "meta/$CF_BASE/native" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/$CF_BASE.wasm" -o "$CF_WORK/$CF_BASE.o" else - note_pass "$label" + wf_fail "meta/$CF_BASE/native" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/$CF_BASE.wasm" -o "$CF_WORK/$CF_BASE.o" fi } +# ---- drive the corpora ----------------------------------------------------- printf 'test-wasm-front target=%s obj=%s\n' "$target_triple" "$TEST_OBJ" -for wat in "$CASES_DIR"/*.wat; do - [ -e "$wat" ] || continue - name=$(basename "$wat" .wat) - expected=$(tr -d '[:space:]' < "$CASES_DIR/$name.expect") - work="$BUILD_DIR/$name" - mkdir -p "$work" - wasm="$work/$name.wasm" - wat_obj="$work/$name.wat.o" - wasm_obj="$work/$name.wasm.o" - - if [ "$RUN_W" -eq 1 ]; then - if [ "$have_wasm_tool" -eq 1 ]; then - run_expect_zero "$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm" - else - note_skip "$name/W" "no wasm-tool" - continue - fi - elif [ "$have_wasm_tool" -eq 1 ]; then - # Path C needs the .wasm only if it consumes binary input; we use the - # .wat directly, so building .wasm is optional. Skip silently. - "$WASM_TOOL" --wat2wasm "$wat" "$wasm" >/dev/null 2>&1 || true - fi - - if [ "$RUN_D" -eq 1 ]; then - run_expect_rc "$name/D-wat" "$expected" "$CFREE_BIN" run -e test_main "$wat" - run_expect_rc "$name/D-wasm" "$expected" "$CFREE_BIN" run -e test_main "$wasm" - fi - - if [ "$RUN_N" -eq 1 ]; then - run_expect_rc_interp "$name/N-wat" "$expected" "$CFREE_BIN" run --no-jit \ - -e test_main "$wat" - run_expect_rc_interp "$name/N-wasm" "$expected" "$CFREE_BIN" run --no-jit \ - -e test_main "$wasm" - fi - - if [ "$RUN_O" -eq 1 ]; then - run_expect_zero "$name/O-wat" "$CFREE_BIN" cc -target "$target_triple" -c \ - "$wat" -o "$wat_obj" - run_expect_zero "$name/O-wasm" "$CFREE_BIN" cc -target "$target_triple" -c \ - "$wasm" -o "$wasm_obj" - fi - - if [ "$RUN_J" -eq 1 ]; then - if [ "$have_jit_runner" -eq 1 ]; then - run_expect_rc "$name/J-wat-obj" "$expected" env CFREE_TEST_ARCH="$TEST_ARCH" \ - CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wat_obj" - run_expect_rc "$name/J-wasm-obj" "$expected" env CFREE_TEST_ARCH="$TEST_ARCH" \ - CFREE_TEST_OBJ="$TEST_OBJ" "$JIT_RUNNER" "$wasm_obj" - else - note_skip "$name/J" "host arch does not match target or no jit-runner" - fi - fi - - if [ "$RUN_E" -eq 1 ]; then - if [ "$have_link_runner" -eq 1 ] && [ "$have_wasm_start_obj" -eq 1 ]; then - exe="$work/$name.exe" - if "$LINK_EXE_RUNNER" -o "$exe" "${MACHO_DSO_ARGS[@]}" "$wat_obj" "$WASM_START_OBJ" \ - >"$work/link.out" 2>"$work/link.err"; then - if exec_target_supported "$EXEC_TAG"; then - exec_target_run "$EXEC_TAG" "$exe" "$work/exec.out" "$work/exec.err" - rc=$RUN_RC - if [ "$rc" -eq "$expected" ]; then - note_pass "$name/E" - else - note_fail "$name/E expected $expected got $rc" - fi - else - note_skip "$name/E" "no execution support for $TEST_ARCH" - fi - else - note_fail "$name/E link failed" - fi - else - note_skip "$name/E" "requires link runner and wasm start.o" - fi - fi - - if [ "$RUN_C" -eq 1 ]; then - label="$name/C" - if [ "$have_c_wrapper" -eq 0 ]; then - note_skip "$label" "host CC wrapper unavailable" - elif [ "$host_matches" -eq 0 ]; then - note_skip "$label" "host arch != $TEST_ARCH (C target is target-locked)" - else - c_src="$work/$name.cfree.c" - c_bin="$work/$name.cbackend.bin" - c_emit_err="$work/c.emit.err" - c_cc_err="$work/c.cc.err" - # `cfree cc` uses the configured host triple by default. The wasm - # frontend doesn't require -target, but pass it explicitly so the - # emitted C is locked to the same arch as the host that will compile - # it. Skip on phased-rollout panics from the C target. - if ! "$CFREE_BIN" cc -target "$target_triple" --emit=c "$wat" \ - -o "$c_src" >"$work/c.emit.out" 2>"$c_emit_err"; then - missing=$(grep -oE 'C target: .*(not implemented|not yet supported)' \ - "$c_emit_err" 2>/dev/null | head -n1 || true) - if [ -n "$missing" ]; then - note_skip "$label" "$missing" - else - note_fail "$label cfree cc --emit=c failed" - sed 's/^/ | /' "$c_emit_err" - fi - elif ! $HOST_CC -std=gnu99 -Wno-main-return-type "$c_src" \ - "$C_WRAPPER_OBJ" -o "$c_bin" \ - >"$work/c.cc.out" 2>"$c_cc_err"; then - note_fail "$label host cc rejected emitted source" - sed 's/^/ | /' "$c_cc_err" - else - "$c_bin" >"$work/c.run.out" 2>"$work/c.run.err" - rc=$? - if [ "$rc" -eq "$expected" ]; then - note_pass "$label" - else - note_fail "$label expected $expected got $rc" - fi - fi - fi - fi -done - -# trap/err/meta corpora are diagnostics-focused and orthogonal to the -# C-source emit path; skip them when only RUN_C is requested. +# cases corpus — active lanes in PATHS order. +CASE_LANES= +[ "$RUN_W" -eq 1 ] && CASE_LANES="$CASE_LANES W" +[ "$RUN_D" -eq 1 ] && CASE_LANES="$CASE_LANES D" +[ "$RUN_N" -eq 1 ] && CASE_LANES="$CASE_LANES N" +[ "$RUN_O" -eq 1 ] && CASE_LANES="$CASE_LANES O" +[ "$RUN_J" -eq 1 ] && CASE_LANES="$CASE_LANES J" +[ "$RUN_E" -eq 1 ] && CASE_LANES="$CASE_LANES E" +[ "$RUN_C" -eq 1 ] && CASE_LANES="$CASE_LANES C" +PAR="${CFREE_WASM_PARALLEL:-1}" +CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/cases" \ + CF_CORPUS_GLOBS="$CASES_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$CASES_DIR" \ + CF_LANES="$CASE_LANES" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" \ + CF_EXPECTED_EXT=.expect CF_TARGETS_EXT="" CF_PARALLELIZABLE="$PAR" \ + cf_corpus_run + +# trap/err/meta are diagnostics corpora, orthogonal to the C-emit path. RUN_DIAG=0 -[ "$RUN_W" -eq 1 ] || [ "$RUN_D" -eq 1 ] || [ "$RUN_O" -eq 1 ] && RUN_DIAG=1 +{ [ "$RUN_W" -eq 1 ] || [ "$RUN_D" -eq 1 ] || [ "$RUN_O" -eq 1 ]; } && RUN_DIAG=1 -for wat in "$TRAP_DIR"/*.wat; do - [ "$RUN_DIAG" -eq 1 ] || break - [ -e "$wat" ] || continue - name=$(basename "$wat" .wat) - work="$BUILD_DIR/trap-$name" - mkdir -p "$work" - wasm="$work/$name.wasm" +if [ "$RUN_DIAG" -eq 1 ]; then + CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/trap" \ + CF_CORPUS_GLOBS="$TRAP_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$TRAP_DIR" \ + CF_LANES="T" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run + + CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/err" \ + CF_CORPUS_GLOBS="$ERR_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$ERR_DIR" \ + CF_LANES="ERR" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run + + # Inline diagnostic cases (not file-glob corpora). + CF_WORK="$BUILD_DIR/err-inline"; rm -rf "$CF_WORK"; mkdir -p "$CF_WORK" + CF_NAME=err CF_BASE=err + wf_fail_grep "err/stack_underflow/loc" \ + "$ERR_DIR/stack_underflow.wat:3:5: fatal: wasm: operand stack underflow" \ + "$CFREE_BIN" cc -target "$target_triple" -c "$ERR_DIR/stack_underflow.wat" -o "$CF_WORK/su-loc.o" if [ "$have_wasm_tool" -eq 1 ]; then - run_expect_zero "trap/$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm" + CF_LABEL=test-wasm-front CF_BUILD_DIR="$BUILD_DIR/meta" \ + CF_CORPUS_GLOBS="$META_DIR/*.wat" CF_CORPUS_EXT=wat CF_SIDECAR_DIR="$META_DIR" \ + CF_LANES="META" CF_OPT_LEVELS="" CF_TUPLES="$EXEC_TAG" CF_TARGETS_EXT="" \ + CF_PARALLELIZABLE="$PAR" cf_corpus_run else - note_skip "trap/$name/W" "no wasm-tool" - continue + cf_skip "meta" "no wasm-tool" fi - run_expect_fail_quiet "trap/$name/D-wat" "$CFREE_BIN" run -e test_main "$wat" - run_expect_fail_quiet "trap/$name/D-wasm" "$CFREE_BIN" run -e test_main "$wasm" -done - -for wat in "$ERR_DIR"/*.wat; do - [ "$RUN_DIAG" -eq 1 ] || break - [ -e "$wat" ] || continue - name=$(basename "$wat" .wat) - run_expect_fail "err/$name/cc" "$CFREE_BIN" cc -target "$target_triple" -c \ - "$wat" -o "$BUILD_DIR/err-$name.o" -done - -if [ "$RUN_DIAG" -eq 1 ]; then - run_expect_fail_stderr_contains "err/stack_underflow/loc" \ - "$ERR_DIR/stack_underflow.wat:3:5: fatal: wasm: operand stack underflow" \ - "$CFREE_BIN" cc -target "$target_triple" -c \ - "$ERR_DIR/stack_underflow.wat" -o "$BUILD_DIR/err-stack_underflow-loc.o" -fi - -if [ "$RUN_DIAG" -eq 1 ] && [ "$have_wasm_tool" -eq 1 ]; then - for wat in "$META_DIR"/*.wat; do - [ -e "$wat" ] || continue - name=$(basename "$wat" .wat) - wasm="$BUILD_DIR/meta-$name.wasm" - run_expect_zero "meta/$name/W" "$WASM_TOOL" --wat2wasm "$wat" "$wasm" - if [ "$name" = "type_custom" ] || - [ "$name" = "import_table_global_start" ]; then - run_expect_zero "meta/$name/native" "$CFREE_BIN" cc \ - -target "$target_triple" -c "$wasm" -o "$BUILD_DIR/meta-$name.o" - else - run_expect_fail "meta/$name/native" "$CFREE_BIN" cc \ - -target "$target_triple" -c "$wasm" -o "$BUILD_DIR/meta-$name.o" - fi - done -elif [ "$RUN_DIAG" -eq 1 ]; then - note_skip "meta" "no wasm-tool" -fi - -if [ "$RUN_DIAG" -eq 1 ]; then -bad_wasm="$BUILD_DIR/malformed-section.wasm" -printf '\000asm\001\000\000\000\001\005\001\140' > "$bad_wasm" -run_expect_fail "err/malformed-section/wasm" "$CFREE_BIN" cc \ - -target "$target_triple" -c "$bad_wasm" -o "$BUILD_DIR/bad-section.o" - -bad_leb="$BUILD_DIR/malformed-leb.wasm" -printf '\000asm\001\000\000\000\001\200\200\200\200\020' > "$bad_leb" -run_expect_fail "err/malformed-leb/wasm" "$CFREE_BIN" cc \ - -target "$target_triple" -c "$bad_leb" -o "$BUILD_DIR/bad-leb.o" + printf '\000asm\001\000\000\000\001\005\001\140' > "$CF_WORK/malformed-section.wasm" + wf_fail "err/malformed-section/wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/malformed-section.wasm" -o "$CF_WORK/bad-section.o" + printf '\000asm\001\000\000\000\001\200\200\200\200\020' > "$CF_WORK/malformed-leb.wasm" + wf_fail "err/malformed-leb/wasm" "$CFREE_BIN" cc -target "$target_triple" -c "$CF_WORK/malformed-leb.wasm" -o "$CF_WORK/bad-leb.o" fi -printf 'test-wasm-front: pass=%d fail=%d skip=%d\n' "$pass" "$fail" "$skip" -[ "$fail" -eq 0 ] +cf_summary test-wasm-front +cf_exit