use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use crate_plugin_kit::{CratePluginKit, KitConfig};
use pmpx_plugin::abi::{self, PmpxCommand, PmpxPluginV1, PmpxStr};
use pmpx_plugin::Verb;
const CRATE_NAME: &str = "pmpx-plugin-toy";
const MANIFEST_NAME: &str = "pmpx-plugin.toml";
const MANIFEST: &str = r#"
[plugin]
name = "toy"
version = "0.1.0"
abi = 1
family = "node"
# The host's own section -- this crate does not know about it and must not touch it
[detect]
strong = [".yarnrc.yml"]
weak = ["package.json"]
"#;
fn build_fixture() -> (tempfile::TempDir, PathBuf) {
if let Ok(prebuilt) = std::env::var("PMPX_TEST_PREBUILT_LIB") {
let path = PathBuf::from(&prebuilt);
assert!(
path.is_file(),
"the file PMPX_TEST_PREBUILT_LIB points at does not exist: {prebuilt}"
);
println!(
"using the prebuilt plugin artifact (cross-toolchain mode): {}",
path.display()
);
return (
tempfile::tempdir().expect("should be able to create a temp dir"),
path,
);
}
let tmp = tempfile::tempdir().expect("should be able to create a temp dir");
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("toy-plugin");
let target_dir = tmp.path().join("target");
let status = Command::new("cargo")
.args(["build", "--release", "--manifest-path"])
.arg(fixture.join("Cargo.toml"))
.arg("--target-dir")
.arg(&target_dir)
.status()
.expect("should be able to start cargo");
assert!(status.success(), "the fixture should compile");
let lib = crate_plugin_kit::find_library(&target_dir.join("release"), "pmpx_plugin_toy")
.expect("the fixture's cdylib should be among the artifacts");
(tmp, lib)
}
fn store_with_fixture(lib: &Path) -> (tempfile::TempDir, CratePluginKit<PmpxPluginV1>) {
let tmp = tempfile::tempdir().expect("should be able to create a temp dir");
let root = tmp.path().join("store");
let plugin_dir = root.join("plugins").join(CRATE_NAME);
std::fs::create_dir_all(&plugin_dir).expect("should be able to create the plugin dir");
std::fs::write(plugin_dir.join(MANIFEST_NAME), MANIFEST)
.expect("should be able to write the manifest");
std::fs::copy(lib, plugin_dir.join(lib.file_name().unwrap()))
.expect("should be able to copy the cdylib");
let cfg = KitConfig::new("pmpx")
.with_data_dir(&root)
.with_lock_timeout(Duration::from_millis(500));
let kit = CratePluginKit::<PmpxPluginV1>::new(cfg).expect("should be able to build the kit");
(tmp, kit)
}
fn call(
entry: &PmpxPluginV1,
project_root: &Path,
matched: &[&str],
verb: Verb,
args: &[&str],
) -> Result<(OsString, Vec<OsString>, Option<PathBuf>), u32> {
let root_bytes = project_root.to_string_lossy().into_owned().into_bytes();
let root_s = PmpxStr {
ptr: root_bytes.as_ptr(),
len: root_bytes.len(),
};
let matched_raw: Vec<PmpxStr> = matched
.iter()
.map(|s| PmpxStr {
ptr: s.as_ptr(),
len: s.len(),
})
.collect();
let args_raw: Vec<PmpxStr> = args
.iter()
.map(|s| PmpxStr {
ptr: s.as_ptr(),
len: s.len(),
})
.collect();
let mut out = std::mem::MaybeUninit::<PmpxCommand>::uninit();
let code = unsafe {
(entry.command)(
root_s,
matched_raw.as_ptr(),
matched_raw.len(),
verb.to_abi(),
args_raw.as_ptr(),
args_raw.len(),
out.as_mut_ptr(),
)
};
if code != abi::PMPX_OK {
return Err(code);
}
let mut cmd = unsafe { out.assume_init() };
let result = unsafe {
let read = |s: PmpxStr| -> String {
if s.is_empty() {
return String::new();
}
let bytes = std::slice::from_raw_parts(s.ptr, s.len);
String::from_utf8_lossy(bytes).into_owned()
};
let program: OsString = read(cmd.program).into();
let cwd = if cmd.cwd.is_empty() {
None
} else {
Some(PathBuf::from(read(cmd.cwd)))
};
let args: Vec<OsString> = (0..cmd.args_len)
.map(|i| OsString::from(read(*cmd.args.add(i))))
.collect();
(program, args, cwd)
};
unsafe { (entry.free_command)(&mut cmd as *mut _) };
Ok(result)
}
#[test]
fn loads_a_real_cdylib_and_drives_the_vtable() {
let (_build_tmp, lib) = build_fixture();
let (_store_tmp, kit) = store_with_fixture(&lib);
let all = kit.list().expect("list should succeed");
assert_eq!(all.len(), 1, "{all:?}");
assert_eq!(all[0].name, "toy");
assert_eq!(all[0].crate_name, CRATE_NAME);
let loaded = kit.load("toy").expect("should be able to load");
let entry = loaded.entry();
assert!(!entry.is_null());
let entry = unsafe { &*entry };
assert_eq!(
entry.abi_version,
abi::ABI_VERSION,
"the ABI version the plugin reports must match the host's"
);
unsafe {
let name = (entry.name)();
let bytes = std::slice::from_raw_parts(name.ptr, name.len);
assert_eq!(bytes, b"toy");
(entry.free_str)(name);
let family = (entry.family)();
let bytes = std::slice::from_raw_parts(family.ptr, family.len);
assert_eq!(bytes, b"node");
(entry.free_str)(family);
}
let root = PathBuf::from("/tmp/project");
let (program, args, cwd) = call(entry, &root, &[], Verb::Install, &["serde"]).expect("install");
assert_eq!(program, OsString::from("toy-bin"));
assert_eq!(args, vec![OsString::from("add"), OsString::from("serde")]);
assert_eq!(cwd, None);
let (_, args, _) = call(entry, &root, &[".yarnrc.yml"], Verb::Install, &[]).expect("berry");
assert!(
args.iter().any(|a| a == "--berry"),
"the plugin should see a matched .yarnrc.yml: {args:?}"
);
let (_, args, _) = call(entry, &root, &[], Verb::Run, &[]).expect("run");
assert_eq!(args, vec![root.clone().into_os_string()]);
}
#[test]
fn unsupported_verb_keeps_its_code_across_the_boundary() {
let (_build_tmp, lib) = build_fixture();
let (_store_tmp, kit) = store_with_fixture(&lib);
let loaded = kit.load("toy").expect("should be able to load");
let entry = unsafe { &*loaded.entry() };
let code = call(entry, Path::new("/tmp/p"), &[], Verb::Exec, &[]).unwrap_err();
assert_eq!(code, abi::PMPX_ERR_UNSUPPORTED_VERB);
}
#[test]
fn a_panicking_plugin_does_not_take_the_host_down() {
let (_build_tmp, lib) = build_fixture();
let (_store_tmp, kit) = store_with_fixture(&lib);
let loaded = kit.load("toy").expect("should be able to load");
let entry = unsafe { &*loaded.entry() };
let code = call(entry, Path::new("/tmp/p"), &[], Verb::Test, &[]).unwrap_err();
assert_eq!(code, abi::PMPX_ERR_INTERNAL);
}