use std::process::Command;
use object::{Object, ObjectSymbol};
mod artifact_guard;
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn fixture() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("scripts/repro")
.join("call_indirect_275_selfcontained.wat")
}
fn has_symbol(bytes: &[u8], name: &str) -> bool {
let obj = object::File::parse(bytes).expect("parse object");
obj.symbols().any(|s| s.name() == Ok(name))
}
#[test]
fn test_275_selfcontained_call_indirect_emits() {
let path = fixture();
let elf = artifact_guard::unique_artifact("ci275_selfcontained", "elf");
let mut cmd = Command::new(synth());
cmd.args([
"compile",
path.to_str().unwrap(),
"--cortex-m",
"--target",
"cortex-m3",
"-o",
elf.to_str().unwrap(),
]);
let (bytes, out) = artifact_guard::compile_artifact_with_output(&mut cmd, &elf)
.unwrap_or_else(|e| panic!("compile failed: {e}"));
let _ = std::fs::remove_file(&elf);
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stderr.contains("skipping function 'entry'"),
"entry must compile now (regression to the #717 loud-decline), got:\n{stderr}"
);
assert!(
format!("{stdout}{stderr}").contains("#275 funcref table"),
"the flash funcref table must be emitted (diagnostic missing), got:\n{stdout}\n{stderr}"
);
for f in ["entry", "func_1", "func_2", "func_3"] {
assert!(
has_symbol(&bytes, f),
"{f} must be in the self-contained image (#275)"
);
}
}
#[test]
fn test_275_selfcontained_a32_still_declines_loudly() {
let path = fixture();
let elf = artifact_guard::unique_artifact("ci275_selfcontained_r5", "elf");
let mut cmd = Command::new(synth());
cmd.args([
"compile",
path.to_str().unwrap(),
"--target",
"cortex-r5",
"--allow-skipped-exports",
"-o",
elf.to_str().unwrap(),
]);
let (bytes, out) = artifact_guard::compile_artifact_with_output(&mut cmd, &elf)
.unwrap_or_else(|e| panic!("compile should skip-and-continue: {e}"));
let _ = std::fs::remove_file(&elf);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipping function 'entry'"),
"the A32 self-contained call_indirect must stay a LOUD skip, got:\n{stderr}"
);
assert!(
stderr.contains("#275"),
"the decline diagnostic must name #275, got:\n{stderr}"
);
assert!(
stderr.contains("were skipped"),
"the skip must be surfaced in the summary count, got:\n{stderr}"
);
assert!(
!has_symbol(&bytes, "entry"),
"`entry` must NOT be emitted on the A32 self-contained path — a \
declined function is absent, not a silent colliding dispatch (#275)"
);
}
#[test]
fn test_275_selfcontained_with_imports_still_declines_loudly() {
let wat = r#"(module
(import "env" "ext" (func $ext (param i32) (result i32)))
(memory 1)
(type $un (func (param i32) (result i32)))
(func $easy (type $un) (i32.add (local.get 0) (i32.const 1)))
(func (export "go") (param i32 i32) (result i32)
(call_indirect (type $un) (local.get 0) (local.get 1)))
(table 1 funcref) (elem (i32.const 0) $easy))"#;
let src = "/tmp/ci275_imports.wat";
std::fs::write(src, wat).expect("write fixture");
let out = Command::new(synth())
.args([
"compile",
src,
"--cortex-m",
"--target",
"cortex-m3",
"-o",
"/tmp/ci275_imports.elf",
])
.output()
.expect("run synth");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("skipping function 'go'") && stderr.contains("#275"),
"a self-contained module WITH imports must keep the loud #275 \
call_indirect decline (ET_REL output owns the table region), got:\n{stderr}"
);
}
#[test]
fn test_275_skipped_table_target_fails_loudly() {
let wat = r#"(module
(memory 1)
(type $un (func (param i32) (result i32)))
(func $hard (type $un)
(i32.trunc_f64_s (f64.sqrt (f64.convert_i32_s (local.get 0)))))
(func $easy (type $un) (i32.add (local.get 0) (i32.const 1)))
(func (export "go") (param i32 i32) (result i32)
(call_indirect (type $un) (local.get 0) (local.get 1)))
(table 2 funcref) (elem (i32.const 0) $easy $hard))"#;
let src = "/tmp/ci275_skiptarget.wat";
std::fs::write(src, wat).expect("write fixture");
let out = Command::new(synth())
.args([
"compile",
src,
"--cortex-m",
"--target",
"cortex-m3", "-o",
"/tmp/ci275_skiptarget.elf",
])
.output()
.expect("run synth");
assert!(
!out.status.success(),
"a dispatch table with a skipped target must FAIL the compile, not \
link a broken table"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("refusing to link a broken dispatch table"),
"the failure must name the broken-table refusal (#275), got:\n{stderr}"
);
}
#[test]
fn test_275_relocatable_call_indirect_untouched() {
let path = fixture();
for target in ["cortex-m3", "cortex-r5"] {
let elf = artifact_guard::unique_artifact(&format!("ci275_reloc_{target}"), "o");
let mut cmd = Command::new(synth());
cmd.args([
"compile",
path.to_str().unwrap(),
"--target",
target,
"--all-exports",
"--relocatable",
"--no-optimize",
"-o",
elf.to_str().unwrap(),
]);
let (bytes, out) = artifact_guard::compile_artifact_with_output(&mut cmd, &elf)
.unwrap_or_else(|e| panic!("relocatable compile failed ({target}): {e}"));
let _ = std::fs::remove_file(&elf);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("skipping function 'entry'"),
"relocatable path must NOT decline entry ({target}):\n{stderr}"
);
assert!(
has_symbol(&bytes, "entry"),
"relocatable path must still emit `entry` with its dispatch ({target}) — #275 \
must not touch the host-linked path"
);
}
}