use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use pmpx_loader::{ContextSource, NoFiles, Plugin};
const ENTRY_SYMBOL: &[u8] = b"pmpx_plugin_entry_v3\0";
const ATTACHED_SYMBOL: &[u8] = b"pmpx_raw_plugin_attached\0";
fn workspace_target_dir() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(Path::to_path_buf))
.and_then(|deps| deps.parent().map(Path::to_path_buf))
.and_then(|profile| profile.parent().map(Path::to_path_buf))
.unwrap_or_else(std::env::temp_dir)
}
fn find_cdylib(dir: &Path, stem: &str) -> PathBuf {
#[cfg(target_os = "windows")]
let candidate = dir.join(format!("{stem}.dll"));
#[cfg(target_os = "macos")]
let candidate = dir.join(format!("lib{stem}.dylib"));
#[cfg(all(unix, not(target_os = "macos")))]
let candidate = dir.join(format!("lib{stem}.so"));
assert!(
candidate.is_file(),
"the fixture's cdylib should be at {}",
candidate.display()
);
candidate
}
fn fixture() -> &'static Path {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT
.get_or_init(|| {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("raw-plugin")
.join("Cargo.toml");
let target_dir = workspace_target_dir().join("fixture-raw-plugin");
let status = Command::new("cargo")
.args(["build", "--release", "--manifest-path"])
.arg(&manifest)
.arg("--target-dir")
.arg(&target_dir)
.status()
.expect("should be able to start cargo");
assert!(status.success(), "the fixture should compile");
find_cdylib(&target_dir.join("release"), "pmpx_plugin_raw")
})
.as_path()
}
struct Fixture {
root: PathBuf,
matched: Vec<String>,
config_files: Vec<PathBuf>,
pins: BTreeMap<String, String>,
args: Vec<OsString>,
}
impl Fixture {
fn new(args: &[&str]) -> Self {
Self {
root: PathBuf::from("/work/project"),
matched: Vec::new(),
config_files: Vec::new(),
pins: BTreeMap::new(),
args: args.iter().map(OsString::from).collect(),
}
}
fn source<'a>(&'a self, files: &'a dyn pmpx_loader::Files) -> ContextSource<'a> {
ContextSource {
root: &self.root,
start_dir: &self.root,
matched: &self.matched,
config_files: &self.config_files,
pins: &self.pins,
args: &self.args,
verb: pmpx_plugin_abi::PMPX_VERB_RUN,
reason: pmpx_plugin_abi::PMPX_REASON_SCORED,
score: 42,
files,
}
}
}
#[test]
fn a_real_library_loads_and_reports_its_identity() {
let plugin = unsafe { Plugin::open(fixture()) }.expect("the fixture should load");
assert_eq!(plugin.name(), "raw");
assert_eq!(plugin.family(), "rawfam");
let (rustc, target) = plugin.tables().build_info();
assert_eq!(rustc, "the fixture's rustc");
assert_eq!(target, "the fixture's target");
}
#[test]
fn a_call_crosses_in_both_directions() {
let plugin = unsafe { Plugin::open(fixture()) }.expect("the fixture should load");
let call = Fixture::new(&["left-pad", "--save-dev"]);
let command = plugin
.tables()
.call(&call.source(&NoFiles))
.expect("the fixture always answers ok");
assert_eq!(
command.program,
OsString::from(format!("raw-{}", pmpx_plugin_abi::PMPX_VERB_RUN)),
"the verb has to reach the plugin"
);
assert_eq!(
command.args,
vec![OsString::from("left-pad")],
"the arguments have to reach the plugin, and its answer has to come back"
);
assert_eq!(command.cwd, None, "no cwd means the project root");
}
#[test]
fn the_host_hooks_are_installed_when_the_plugin_offers_them() {
let plugin = unsafe { Plugin::open(fixture()) }.expect("the fixture should load");
static HOOKS: pmpx_plugin_abi::PmpxHost = pmpx_plugin_abi::PmpxHost {
abi_major: pmpx_plugin_abi::PMPX_ABI_MAJOR,
size: std::mem::size_of::<pmpx_plugin_abi::PmpxHost>(),
capability: no_capabilities,
};
unsafe extern "C" fn no_capabilities(
_name: pmpx_plugin_abi::PmpxStr,
) -> *const std::ffi::c_void {
std::ptr::null()
}
let before = attached_count();
plugin.tables().attach(&HOOKS);
assert_eq!(
attached_count(),
before + 1,
"the plugin's `attach` has to have been called"
);
}
fn attached_count() -> u32 {
unsafe {
let library = libloading::Library::new(fixture()).expect("the fixture should load");
let symbol: libloading::Symbol<unsafe extern "C" fn() -> u32> = library
.get(ATTACHED_SYMBOL)
.expect("the fixture exports its own counter");
symbol()
}
}
#[test]
fn a_library_without_the_entry_symbol_is_refused() {
let not_a_library = std::env::current_exe().expect("the test binary's path");
let error = match unsafe { Plugin::open(¬_a_library) } {
Ok(_) => panic!("a library with no entry symbol must be refused"),
Err(error) => error,
};
let text = error.to_string();
assert!(
text.contains("cannot load") || text.contains("exports no"),
"the refusal should say what happened: {text}"
);
}
#[test]
fn the_entry_symbol_is_the_one_the_abi_declares() {
let expected = pmpx_plugin_abi::PMPX_ENTRY_SYMBOL.as_bytes();
assert_eq!(
&ENTRY_SYMBOL[..ENTRY_SYMBOL.len() - 1],
expected,
"the ABI's entry symbol and the one this test looks for have to be the same"
);
assert!(
OsStr::new(pmpx_plugin_abi::PMPX_ENTRY_SYMBOL)
.to_string_lossy()
.ends_with("_v3"),
"the root structure's layout is what the symbol names"
);
}