From 4c28c32168e0820dfad0ff1726fe30176c0a2791 Mon Sep 17 00:00:00 2001 From: ostylk Date: Fri, 13 Mar 2026 15:53:55 +0100 Subject: [PATCH] test: integration tests can now call dynamic libraries --- Cargo.lock | 1 + wild/Cargo.toml | 1 + wild/tests/integration_tests.rs | 59 ++++++++++++++++++++++++++++++--- wild/tests/sources/shared.c | 5 +-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fceb81015..271e16c8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2257,6 +2257,7 @@ dependencies = [ "dhat", "foldhash 0.2.0", "itertools", + "libloading", "libwild", "linker-diff", "mimalloc", diff --git a/wild/Cargo.toml b/wild/Cargo.toml index cc5bbcd03..ce92ab20b 100644 --- a/wild/Cargo.toml +++ b/wild/Cargo.toml @@ -26,6 +26,7 @@ ar = { workspace = true } itertools = { workspace = true } foldhash = { workspace = true } linker-diff = { path = "../linker-diff" } +libloading = { workspace = true } object = { workspace = true } os_info = { workspace = true } regex = { workspace = true } diff --git a/wild/tests/integration_tests.rs b/wild/tests/integration_tests.rs index 46e2e324a..f0f0a8155 100644 --- a/wild/tests/integration_tests.rs +++ b/wild/tests/integration_tests.rs @@ -67,6 +67,11 @@ //! //! RunEnabled:{bool} Defaults to true. Set to false to disable execution of the resulting binary. //! +//! RunDynSym:{string} If set and RunEnabled:true, then, instead of executing the binary normally, +//! the binary is loaded as a shared library and the function specified by the string is called. +//! The function must return an integer to indicate status (status != 42 is an error). +//! Such run is obviously skipped if the shared library is cross compiled. +//! //! SkipLinker:{linker-name} Don't link with the specified linker. Mostly useful if testing a flag //! that isn't supported by GNU ld. //! @@ -165,6 +170,7 @@ mod external_tests; use itertools::Itertools; +use libloading::Library; use libwild::bail; use libwild::ensure; use libwild::error; @@ -510,6 +516,7 @@ struct Config { compiler: String, should_diff: bool, should_run: bool, + run_dyn_sym: Option, should_error: bool, expect_messages: Vec, support_architectures: Vec, @@ -983,6 +990,7 @@ impl Config { compiler: "gcc".to_owned(), should_diff: true, should_run: true, + run_dyn_sym: None, should_error: false, expect_messages: Default::default(), cross_enabled: true, @@ -1158,6 +1166,9 @@ fn parse_configs(src_filename: &Path, default_config: &Config) -> Result { config.should_run = arg.parse().context("Invalid bool for RunEnabled")? } + "RunDynSym" => { + config.run_dyn_sym = Some(arg.parse().context("Invalid string for RunDynSym")?) + } "SkipLinker" => { config.skip_linkers.insert(arg.trim().to_owned()); } @@ -1550,6 +1561,7 @@ impl Debug for SectionDiff { /// the system, which can mean that the test binaries take longer to start, so we need to be /// somewhat generous here to avoid flakes. const TEST_BINARY_TIMEOUT: Duration = std::time::Duration::from_millis(2000); +const EXIT_SUCCESS: i32 = 42; impl Program<'_> { fn run(&self, cross_arch: Option) -> Result { @@ -1610,12 +1622,36 @@ impl Program<'_> { error!("Binary exited{possible_core_dumped_msg} with signal {signal}: {output}") })?; - if exit_code != 42 { + if exit_code != EXIT_SUCCESS { bail!("Binary exited with unexpected exit code {exit_code}: {output}"); } Ok(()) } + + fn run_as_dynlib(&self, entry_sym: &str) -> Result { + // SAFETY: It is not safe. However, we assume that our test cases do not break anything. + // In particular: All initialization, termination and entry routines of the shared library + // need to be safe and entry_sym has to be of type `extern "C" fn() -> i32`. + let exit_code = unsafe { + let lib = Library::new(&self.link_output.binary).with_context(|| { + format!( + "Cannot load shared library {}", + self.link_output.binary.to_string_lossy() + ) + })?; + let entry = lib + .get:: i32>(entry_sym) + .with_context(|| format!("Cannot find entry point symbol {entry_sym}"))?; + entry() + }; + + if exit_code != EXIT_SUCCESS { + bail!("Function {entry_sym} exited with unexpected status code {exit_code}."); + } + + Ok(()) + } } /// Attempts to spawn `command`. If that fails due to ETXTBSY, then retries until we've tried @@ -3488,9 +3524,24 @@ fn run_with_config( } if config.should_run { - program - .run(cross_arch) - .with_context(|| format!("Failed to run program. {program}"))?; + // If RunDynSym is set, execute our binary by loading it dynamically and calling the + // configured function. As we are loading the library directly into our + // process, our binary cannot be cross compiled. + if let Some(func) = config.run_dyn_sym.as_ref() + && cross_arch.is_none() + { + program + .run_as_dynlib(func) + .with_context(|| format!("Failed to load shared library. {program}"))?; + } else { + program + .run(cross_arch) + .with_context(|| format!("Failed to run program. {program}"))?; + } + } else if config.run_dyn_sym.is_some() { + // RunEnabled is false but RunDynSym is set. + // That is definitely not intended, so just bail. + bail!("RunDynSym is set but RunEnabled:false. {program}"); } } diff --git a/wild/tests/sources/shared.c b/wild/tests/sources/shared.c index c6ef14fc9..a0385f45b 100644 --- a/wild/tests/sources/shared.c +++ b/wild/tests/sources/shared.c @@ -1,6 +1,3 @@ -// We don't currently run this, we just make sure that we can produce a shared -// object and that it passes the diff test. -// // One notable scenario that this test tests is having a non-weak undefined // symbol (baz) in a shared object and having that symbol be defined by an // archive entry that we don't load. @@ -8,7 +5,7 @@ //#Config:default //#LinkArgs:-shared -z now //#Mode:dynamic -//#RunEnabled:false +//#RunDynSym:foo //#Shared:shared-s1.c //#Archive:shared-a1.c,shared-a2.c //#DiffIgnore:.dynamic.DT_RELA