#![cfg(feature = "codegen")]
use std::path::{Path, PathBuf};
use std::process::Command;
use dlsym::codegen::{Builder, parse};
fn skip(why: &str) {
if std::env::var("DLSYM_REQUIRE_E2E").is_ok_and(|v| !v.is_empty() && v != "0") {
panic!("end-to-end test required but {why}");
}
eprintln!("skipping end-to-end test: {why}");
}
fn compiler() -> Option<String> {
let candidates = std::env::var("CC")
.into_iter()
.chain(["cc", "clang", "gcc"].map(String::from));
candidates.into_iter().find(|cc| {
Command::new(cc)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
})
}
fn fixtures() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo")
}
#[test]
fn demo_library() {
if cfg!(target_pointer_width = "32") {
eprintln!("skipping end-to-end test on a 32-bit target");
return;
}
let Some(cc) = compiler() else {
return skip("no C compiler found");
};
let work = Path::new(env!("CARGO_TARGET_TMPDIR")).join("e2e-demo");
let _ = std::fs::remove_dir_all(&work);
std::fs::create_dir_all(work.join("src")).unwrap();
let lib = work.join(dlsym::library_filename("demo"));
let mut cmd = Command::new(&cc);
if cfg!(target_vendor = "apple") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
if cfg!(unix) {
cmd.arg("-fPIC");
}
let status = cmd
.arg("-o")
.arg(&lib)
.arg(fixtures().join("demo.c"))
.status()
.expect("run C compiler");
assert!(status.success(), "C compiler failed");
let generated = match Builder::new()
.header(fixtures().join("demo.h"))
.struct_name("Demo")
.safe("demo_add")
.safe("demo_is_even")
.library_name(lib.to_str().unwrap())
.generate()
{
Ok(g) => g,
Err(parse::Error::LibClang(e)) => return skip(&e),
Err(e) => panic!("{e}"),
};
assert!(
generated
.warnings
.iter()
.any(|w| w.contains("demo_long_double")),
"{:?}",
generated.warnings
);
assert!(
generated
.warnings
.iter()
.any(|w| w.contains("`demo_not_implemented` is not exported")),
"{:?}",
generated.warnings
);
let code = generated.code;
for needle in [
"#[repr(C, packed)]\n#[derive(Clone, Copy, Debug)]\npub struct demo_packed",
"#[repr(C, align(16))]",
"#[repr(C, packed(2))]",
"pub union demo_number",
"pub struct demo_handle {\n _private: [u8; 0],",
"pub const DEMO_VERSION: &::core::ffi::CStr = c\"1.2.3\";",
"pub fn demo_sum(count: ::core::ffi::c_int, ...) -> ::core::ffi::c_int;",
"pub safe fn demo_add(",
"#[optional]\n pub fn demo_not_implemented()",
] {
assert!(code.contains(needle), "missing {needle:?}");
}
assert!(
!code.contains("demo_long_double"),
"long double is unsupported"
);
assert!(
!code.contains("demo_inline"),
"static inline functions have no symbol"
);
assert!(!code.contains("DEMO_EMPTY") && !code.contains("DEMO_FUNC_LIKE"));
std::fs::write(work.join("src/bindings.rs"), &code).unwrap();
std::fs::copy(fixtures().join("main.rs"), work.join("src/main.rs")).unwrap();
std::fs::write(
work.join("Cargo.toml"),
format!(
"[package]\nname = \"demo-e2e\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\ndlsym = {{ path = {:?} }}\n\n[workspace]\n",
env!("CARGO_MANIFEST_DIR")
),
)
.unwrap();
let out = Command::new(env!("CARGO"))
.args(["run", "--quiet", "--manifest-path"])
.arg(work.join("Cargo.toml"))
.arg("--")
.arg(&lib)
.env(
"CARGO_TARGET_DIR",
Path::new(env!("CARGO_TARGET_TMPDIR")).join("e2e-target"),
)
.env_remove("RUSTFLAGS")
.output()
.expect("run cargo");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success() && stdout.contains("demo ok"),
"generated bindings failed:\n--- stdout\n{stdout}\n--- stderr\n{stderr}\n--- bindings\n{code}"
);
}