From 1497000be44e8bb1586755f21def1da74b511069 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 23 Jun 2026 04:23:20 +0200 Subject: [PATCH] test(dwarf): lock the --debug-line honest-fail contract on self-contained builds (VCR-DBG-001, #394, #242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.12.0 emits DWARF only on the ARM relocatable-object path; on a self-contained executable image (no imports, no --relocatable) there is no relocatable .text symbol to anchor the addresses, so --debug-line must LOUDLY no-op — warn, emit no .debug_*, and still compile — rather than silently mislead a consumer into expecting source lines that aren't there (the #383 honest-fail rule). PR C shipped this behavior but only the relocatable (positive) path was tested. Oracle D (debug_line_is_a_loud_noop_on_self_contained_build_394): compiles gust_kernel (import-free) WITHOUT --relocatable + --debug-line and asserts (1) it still compiles, (2) it warns naming the relocatable path (never silent), (3) the image is ET_EXEC with ZERO .debug_* sections. The previously-untested negative half of the contract. Test-only; zero production change; frozen fixtures bit-identical by construction. High-fidelity (exact warning + section absence), distinct from the spill/const measurement oracles. fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 --- .../tests/dwarf_debug_line_emit_394.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs b/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs index 4581cce4..793442f6 100644 --- a/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs +++ b/crates/synth-cli/tests/dwarf_debug_line_emit_394.rs @@ -68,6 +68,76 @@ fn compile(wasm: &Path, out: &str, debug_line: bool) -> Vec { std::fs::read(out).expect("read .o") } +/// ORACLE D — the `--debug-line` HONEST-FAIL contract (#383 / VCR-DBG-001 PR C). +/// DWARF is emitted ONLY on the ARM relocatable-object path. On a SELF-CONTAINED +/// build (no imports, no `--relocatable` ⇒ a standalone executable image) there +/// is no relocatable `.text` symbol to anchor the addresses, so `--debug-line` +/// MUST loudly no-op — warn, emit no `.debug_*`, and still compile — rather than +/// silently mislead a consumer (jess) into expecting source lines that aren't +/// there (the #383 overclaim shape). v0.12.0 ships this behavior; this locks it. +/// +/// (The relocatable path's positive behavior — `.debug_*` present and resolving +/// — is the additivity / resolves-to-source oracles above; this is its honest +/// negative complement, the previously-untested half of the contract.) +#[test] +fn debug_line_is_a_loud_noop_on_self_contained_build_394() { + // gust_kernel imports nothing, so WITHOUT `--relocatable` synth emits a + // self-contained executable image (not an ET_REL object). + let wasm = repro("gust_kernel.wasm"); + let out = "/tmp/dbg394_selfcontained.elf"; + let r = Command::new(synth()) + .args([ + "compile", + wasm.to_str().unwrap(), + "--target", + "cortex-m4", + "--all-exports", + "--debug-line", + "-o", + out, + ]) + .output() + .expect("run synth"); + + // (1) it still compiles — the flag is a no-op, not an error. + assert!( + r.status.success(), + "self-contained --debug-line must still compile: {}", + String::from_utf8_lossy(&r.stderr) + ); + + // (2) it WARNS loudly (honest-fail), naming the relocatable-object path as + // the way to actually get DWARF — never silent. The CLI's tracing subscriber + // writes to stdout, so search the combined output rather than assuming a + // stream. + let mut output = String::from_utf8_lossy(&r.stdout).into_owned(); + output.push_str(&String::from_utf8_lossy(&r.stderr)); + assert!( + output.contains("--debug-line has no effect") && output.contains("relocatable"), + "expected a loud no-effect warning naming the relocatable path; output:\n{output}" + ); + + // (3) the emitted image is a standalone EXECUTABLE (confirms we exercised the + // self-contained path, not ET_REL) and carries ZERO `.debug_*` sections. + let elf = std::fs::read(out).expect("read elf"); + let obj = ElfFile32::::parse(&*elf).expect("parse ELF"); + assert_eq!( + obj.kind(), + object::ObjectKind::Executable, + "self-contained build must be an executable image (ET_EXEC), not ET_REL" + ); + let debug_sections: Vec = obj + .sections() + .filter_map(|s| s.name().ok().map(str::to_string)) + .filter(|n| n.starts_with(".debug_")) + .collect(); + assert!( + debug_sections.is_empty(), + "self-contained --debug-line must emit NO .debug_* sections (would be \ + unrelocated, silently-wrong addresses); found {debug_sections:?}" + ); +} + /// Map section-name → section data bytes (skip the null/empty-named section). fn section_data(elf: &[u8]) -> HashMap> { let obj = ElfFile32::::parse(elf).expect("parse ELF");