#![cfg(feature = "host")]
#![allow(unsafe_code)]
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
use nautilus_model::types::fixed::FIXED_PRECISION;
use nautilus_plugin::{
NAUTILUS_PLUGIN_ABI_VERSION, PLUGIN_BUILD_ID_VERSION,
loader::{LoadError, PluginLoader},
manifest::compiled_precision_mode,
};
const PLUGIN_TEST_PROFILE: &str = "nextest";
const INVALID_MANIFEST_MESSAGES: &[&str] = &[
"plugin_name must not be empty",
"plugin_version has null pointer with non-zero length 1",
"custom_data[0].vtable must not be null",
"actors has null pointer with non-zero length 1",
"strategies[0].type_name is not valid UTF-8",
"strategies[0].vtable must not be null",
];
#[derive(Clone, Copy)]
enum LoadErrorExpectation {
MissingSymbol,
NullManifest,
AbiMismatch { actual: u32 },
InvalidManifest { messages: &'static [&'static str] },
}
fn cdylib_extension() -> &'static str {
if cfg!(target_os = "macos") {
"dylib"
} else if cfg!(target_os = "windows") {
"dll"
} else {
"so"
}
}
fn cdylib_prefix() -> &'static str {
if cfg!(target_os = "windows") {
""
} else {
"lib"
}
}
fn build_example_cdylib(example_name: &str) -> PathBuf {
let mut build_command = Command::new(env!("CARGO"));
build_command.args([
"build",
"-p",
"nautilus-plugin",
"--example",
example_name,
"--profile",
PLUGIN_TEST_PROFILE,
]);
if FIXED_PRECISION > 9 {
build_command.args(["--features", "nautilus-model/high-precision"]);
}
let status = build_command.status().expect("invoke cargo build");
assert!(status.success(), "cargo build --example failed");
let path = example_cdylib_path(example_name);
assert!(path.exists(), "expected cdylib at {}", path.display());
path
}
fn example_cdylib_path(example_name: &str) -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); path.pop(); path = cargo_target_dir(&path);
path.push(PLUGIN_TEST_PROFILE);
path.push("examples");
path.push(format!(
"{}{}.{}",
cdylib_prefix(),
example_name,
cdylib_extension()
));
path
}
fn cargo_target_dir(root: &Path) -> PathBuf {
let target_dir =
env::var_os("CARGO_TARGET_DIR").map_or_else(|| PathBuf::from("target"), PathBuf::from);
if target_dir.is_absolute() {
target_dir
} else {
root.join(target_dir)
}
}
#[rstest::rstest]
#[ignore]
fn loads_example_cdylib_and_walks_manifest() {
let path = build_example_cdylib("custom_data_plugin");
let mut loader = PluginLoader::new();
loader.load(&path).expect("load failed");
assert_eq!(loader.len(), 1);
let plugin = &loader.loaded()[0];
let manifest = plugin.manifest();
assert_eq!(manifest.abi_version, NAUTILUS_PLUGIN_ABI_VERSION);
manifest
.validate()
.expect("example cdylib manifest passes validation");
assert_eq!(
unsafe { manifest.plugin_name.as_str() },
"example-custom-data-plugin"
);
assert_eq!(manifest.build_id.schema_version, PLUGIN_BUILD_ID_VERSION);
assert_eq!(
unsafe { manifest.build_id.nautilus_plugin_version.as_str() },
env!("CARGO_PKG_VERSION")
);
assert!(!unsafe { manifest.build_id.target_triple.as_str() }.is_empty());
assert!(!unsafe { manifest.build_id.build_profile.as_str() }.is_empty());
assert_eq!(
unsafe { manifest.build_id.precision_mode.as_str() },
compiled_precision_mode()
);
assert_eq!(manifest.build_id.fixed_precision, FIXED_PRECISION);
let cd = unsafe { manifest.custom_data.as_slice() };
assert_eq!(cd.len(), 1, "one custom-data registration expected");
assert_eq!(unsafe { cd[0].type_name.as_str() }, "ExampleTick");
let actors = unsafe { manifest.actors.as_slice() };
assert_eq!(actors.len(), 1, "one actor registration expected");
assert_eq!(unsafe { actors[0].type_name.as_str() }, "ExampleActor");
let strategies = unsafe { manifest.strategies.as_slice() };
assert_eq!(strategies.len(), 1, "one strategy registration expected");
assert_eq!(
unsafe { strategies[0].type_name.as_str() },
"ExampleStrategy",
);
}
#[rstest::rstest]
#[ignore]
fn rejects_second_plugin_with_duplicate_custom_data_type() {
let path = build_example_cdylib("custom_data_plugin");
let mut loader = PluginLoader::new();
loader.load(&path).expect("first load succeeds");
let err = loader
.load(&path)
.expect_err("second load with a duplicate custom-data type should fail");
match err {
LoadError::DuplicateCustomDataType {
path: actual,
type_name,
existing_path,
} => {
assert_eq!(actual.as_path(), path.as_path());
assert_eq!(type_name, "ExampleTick");
assert_eq!(existing_path.as_path(), path.as_path());
}
other => panic!("expected DuplicateCustomDataType, was {other:?}"),
}
assert_eq!(loader.len(), 1);
}
#[rstest::rstest]
#[case::missing_init_symbol("bad_missing_init_plugin", LoadErrorExpectation::MissingSymbol)]
#[case::null_manifest("bad_null_manifest_plugin", LoadErrorExpectation::NullManifest)]
#[case::wrong_abi(
"bad_abi_manifest_plugin",
LoadErrorExpectation::AbiMismatch {
actual: NAUTILUS_PLUGIN_ABI_VERSION + 1,
}
)]
#[case::invalid_manifest(
"bad_invalid_manifest_plugin",
LoadErrorExpectation::InvalidManifest {
messages: INVALID_MANIFEST_MESSAGES,
}
)]
#[case::init_panic("bad_init_panic_plugin", LoadErrorExpectation::NullManifest)]
#[ignore]
fn rejects_malformed_cdylib_fixture(
#[case] example_name: &str,
#[case] expectation: LoadErrorExpectation,
) {
let path = build_example_cdylib(example_name);
let mut loader = PluginLoader::new();
let err = loader
.load(&path)
.expect_err("malformed fixture should fail to load");
assert_load_error(err, &path, expectation);
assert_eq!(loader.len(), 0);
}
fn assert_load_error(err: LoadError, path: &Path, expectation: LoadErrorExpectation) {
match (err, expectation) {
(LoadError::MissingSymbol { path: actual, .. }, LoadErrorExpectation::MissingSymbol) => {
assert_eq!(actual.as_path(), path);
}
(LoadError::NullManifest { path: actual }, LoadErrorExpectation::NullManifest) => {
assert_eq!(actual.as_path(), path);
}
(
LoadError::AbiMismatch {
path: actual_path,
expected,
actual,
diagnostics,
},
LoadErrorExpectation::AbiMismatch {
actual: expected_actual,
},
) => {
assert_eq!(actual_path.as_path(), path);
assert_eq!(expected, NAUTILUS_PLUGIN_ABI_VERSION);
assert_eq!(actual, expected_actual);
assert_eq!(diagnostics.plugin_name.as_str(), "bad-abi-plugin");
assert_eq!(
diagnostics.plugin_version.as_str(),
env!("CARGO_PKG_VERSION")
);
}
(
LoadError::InvalidManifest {
path: actual_path,
errors,
..
},
LoadErrorExpectation::InvalidManifest { messages },
) => {
assert_eq!(actual_path.as_path(), path);
let rendered = errors.to_string();
for message in messages {
assert!(
rendered.contains(message),
"expected manifest error containing {message:?}, was: {rendered}",
);
}
}
(err, _) => panic!("unexpected load error: {err:?}"),
}
}