kit

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

commit 5cb0213fcda79c07c71fe59fa9aa10e792d32c67
parent 336cd946102c66acf0a2b6f75d6fd998af1ed51b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 12:09:04 -0700

test-link: move negative tests to bad/ dir; drop link_fail marker

link_fail was a per-case flag that converted a failing link into a PASS
when the failure was the contract being tested. That's a "fail → pass"
conversion, even if the intent is legitimate. Replace it with a
dedicated test/link/bad/ directory mirroring test/elf/bad/: the
location IS the marker, no special-cased flag in the harness.

Each bad/ case provides sources that compile cleanly plus an `expect`
substring; pass = runner exits non-zero with no signal AND stderr
contains expect. Both Path E (link-exe-runner) and Path J (jit-runner)
are exercised.

Moves cases/30_undef_strong → bad/30_undef_strong with an expect file
matching cfree's actual diagnostic ("undefined reference to
'missing_fn'").

Diffstat:
Mdoc/linker-status.md | 7+++++++
Mtest/link/CORPUS.md | 9+++++++--
Rtest/link/cases/30_undef_strong/a.c -> test/link/bad/30_undef_strong/a.c | 0
Atest/link/bad/30_undef_strong/expect | 1+
Dtest/link/cases/30_undef_strong/link_fail | 1-
Mtest/link/run.sh | 162++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
6 files changed, 130 insertions(+), 50 deletions(-)

diff --git a/doc/linker-status.md b/doc/linker-status.md @@ -53,6 +53,13 @@ enforces is byte-equality of the *normalized* readelf dumps. There is **no xfail mechanism** — failures fail. If a case is broken, fix the bug or remove the case. +The same rule applies to `test/link/`: there is no `link_fail` marker +in `cases/`. Negative tests live in `test/link/bad/<name>/`, which +mirrors `test/elf/bad/`. Each `bad/` case ships sources that compile +cleanly plus an `expect` substring that must appear in the runner's +stderr; pass = runner exits non-zero with no signal AND substring +matches. The directory location *is* the marker. + --- ## test-link / JIT — Apple Silicon J-path diff --git a/test/link/CORPUS.md b/test/link/CORPUS.md @@ -33,12 +33,17 @@ expects from the combined sequence. |------|---------| | `expected` | expected return value (default 0) | | `jit_only` | skip R and E; run J only | -| `link_fail` | E and J paths expect the linker to fail cleanly (non-zero, no signal) | | `use_resolver` | pass `--use-resolver` to jit_runner | | `linker_flags` | one flag per line passed to link-exe-runner and jit-runner | | `gc_absent` | one symbol per line that must be absent post --gc-sections | | `archive_b` | package b.o as b.a; content `demand` (normal) or `whole` (--whole-archive) | +Negative tests live in `test/link/bad/<name>/` instead of `cases/`. Each +bad-case directory contains source files (compile cleanly) plus an +`expect` file: a substring that must appear in the runner's stderr. +Pass = runner exits non-zero with no signal AND stderr contains +`expect`. The `bad/` location *is* the marker — no `link_fail` flag. + ## Case index ### Group A — single-TU, reloc coverage @@ -97,7 +102,7 @@ Cases 02–09 all pair ADR_PREL_PG_HI21 with their primary LDST reloc. | 28 | `extern_resolver` | `CfreeExternResolver` provides address for undefined symbol | | 29 | `jit_lookup_miss` | `cfree_jit_lookup` returns NULL for unknown name | -### Group F — error cases +### bad/ — negative tests | # | Name | Exercises | |---|------|-----------| diff --git a/test/link/cases/30_undef_strong/a.c b/test/link/bad/30_undef_strong/a.c diff --git a/test/link/bad/30_undef_strong/expect b/test/link/bad/30_undef_strong/expect @@ -0,0 +1 @@ +undefined reference to 'missing_fn' diff --git a/test/link/cases/30_undef_strong/link_fail b/test/link/cases/30_undef_strong/link_fail @@ -1 +0,0 @@ -1 diff --git a/test/link/run.sh b/test/link/run.sh @@ -12,11 +12,15 @@ # → check return value. # Validates JIT path (runs on aarch64 host only). # +# 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. +# # Case markers (files in the case directory): # expected — expected exit/return value (default 0) # jit_only — skip R and E paths; run J only -# link_fail — E and J paths expect the link step to fail (non-zero, -# signal-free exit from the runner) # use_resolver — pass --use-resolver to jit-runner (case 28) # linker_flags — one flag per line; passed to link-exe-runner and jit-runner # gc_absent — one symbol per line; jit-runner verifies each is absent @@ -147,7 +151,6 @@ for case_dir in "$TEST_DIR/cases"/*/; do # 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 - link_fail=0; [ -f "$case_dir/link_fail" ] && link_fail=1 use_resolver=0; [ -f "$case_dir/use_resolver" ] && use_resolver=1 archive_mode="none" if [ -f "$case_dir/archive_b" ]; then @@ -266,25 +269,14 @@ for case_dir in "$TEST_DIR/cases"/*/; do link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" -o "$exe" \ "${link_obj_files[@]}" "$start_obj" "${link_arc_flags[@]}") - if [ $link_fail -eq 1 ]; then - if "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then - note_fail "$name/E (expected link failure but link succeeded)" - else - e_rc=$? - # Clean fail: non-zero and no signal (exit < 128) - if [ $e_rc -lt 128 ]; then note_pass "$name/E" - else note_fail "$name/E (link failed via signal $e_rc)"; fi - fi + if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then + note_fail "$name/E (link failed)" + elif [ $have_runner -eq 1 ]; then + run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" + if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E" + else note_fail "$name/E (expected $expected, got $RUN_RC)"; fi else - if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then - note_fail "$name/E (link failed)" - elif [ $have_runner -eq 1 ]; then - run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" - if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E" - else note_fail "$name/E (expected $expected, got $RUN_RC)"; fi - else - note_skip "$name/E" "no runner (qemu/podman)" - fi + note_skip "$name/E" "no runner (qemu/podman)" fi else if [ $jit_only -eq 0 ]; then @@ -298,41 +290,117 @@ for case_dir in "$TEST_DIR/cases"/*/; do [ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver) jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}") - if [ $link_fail -eq 1 ]; then - if "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err"; then - note_fail "$name/J (expected link failure but link succeeded)" + "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err" + j_rc=$? + + # For gc_sections cases, additionally verify absent symbols. + # The jit_runner returns 0 on test_main pass; absent-symbol + # verification is handled by the jit_runner reading gc_absent + # via a side channel. Since the runner doesn't read case markers + # itself, we instead check the lookup from a thin wrapper here: + # re-run jit_runner with --check-absent SYM for each gc_absent sym. + 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 else j_rc=$? - if [ $j_rc -lt 128 ]; then note_pass "$name/J" - else note_fail "$name/J (link failed via signal $j_rc)"; fi + break fi + done + + if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J" + else note_fail "$name/J (expected $expected, got $j_rc)"; fi + else + note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" + fi + +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")" + 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 + fi + expect="$(cat "$expect_file")" + + if [ $have_clang_cross -eq 0 ]; then note_skip "$name" "no aarch64 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") + done + + obj_files=(); compile_ok=1 + for src in "${tu_srcs[@]}"; do + base="$(basename "$src" .c)" + obj="$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 + fi + 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 + fi + + # Path E + if [ $have_exe_runner -eq 1 ]; then + start_obj="$work/start.o" + clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -fno-PIC -fno-pie \ + -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null + 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 - "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err" - j_rc=$? - - # For gc_sections cases, additionally verify absent symbols. - # The jit_runner returns 0 on test_main pass; absent-symbol - # verification is handled by the jit_runner reading gc_absent - # via a side channel. Since the runner doesn't read case markers - # itself, we instead check the lookup from a thin wrapper here: - # re-run jit_runner with --check-absent SYM for each gc_absent sym. - 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 - else - j_rc=$? - break - fi - done + 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 + else + note_skip "$name/E" "no link-exe-runner" + fi - if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J" - else note_fail "$name/J (expected $expected, got $j_rc)"; fi + # Path J + if [ $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)" + 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 fi else note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" fi - done # ---- summary ---------------------------------------------------------------