#![allow(clippy::expect_used)]
use polyplug::error::GraphError;
use polyplug::error::LoaderError;
use polyplug::error::RuntimeError;
use polyplug::loader::BundleLoader;
use polyplug::loader::ManifestData;
use polyplug::runtime::Runtime;
use polyplug_abi::runtime::Compatibility;
use polyplug_abi::types::LogLevel;
use polyplug_utils::bundle_id;
use polyplug_utils::guest_contract_id;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use tempfile::TempDir;
struct NoopLoader;
impl BundleLoader for NoopLoader {
fn loader_name(&self) -> &'static str {
"test-noop"
}
fn loader_language(&self) -> polyplug_abi::SupportedLanguage {
polyplug_abi::SupportedLanguage::Rust
}
fn supports_hot_reload(&self) -> bool {
false
}
fn load(
&self,
_manifest: &ManifestData,
_source: &polyplug::loader::BundleSource,
_runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
Ok(())
}
fn reload(
&self,
_manifest: &ManifestData,
_runtime: &Runtime,
) -> Result<(), polyplug::error::LoaderError> {
Err(polyplug::error::LoaderError::HotReloadUnsupported {
loader_name: self.loader_name().to_owned(),
})
}
}
fn write_bundle_manifest(
dir: &TempDir,
bundle_name: &str,
version: &str,
provides: &[&str],
function_count_entries: &[(&str, u32)],
deps: &[(&str, u64, &str)],
) -> PathBuf {
let bundle_dir: PathBuf = dir.path().join(bundle_name);
std::fs::create_dir_all(&bundle_dir).expect("create bundle dir");
let so_name: String = format!("{bundle_name}.so");
std::fs::write(bundle_dir.join(&so_name), b"").expect("write stub so");
let mut manifest_toml: String = format!(
"id = {}\nname = \"{}\"\nloader = \"test-noop\"\nfile = \"{}\"\nversion = \"{}\"\n",
bundle_id(bundle_name),
bundle_name,
so_name,
version
);
if !provides.is_empty() {
manifest_toml.push_str("provides = [\n");
for p in provides {
manifest_toml.push_str(&format!(" \"{}\",\n", p));
}
manifest_toml.push_str("]\n");
}
if !function_count_entries.is_empty() {
manifest_toml.push_str("function_count = {\n");
for (k, v) in function_count_entries {
manifest_toml.push_str(&format!(" \"{}\" = {},\n", k, v));
}
manifest_toml.push_str("}\n");
}
if !deps.is_empty() {
manifest_toml.push_str("[[dependency]]\n");
for (contract, cid, min_ver) in deps {
manifest_toml.push_str(&format!(
"kind = \"contract\"\ncontract = \"{}\"\ncontract_id = {}\nmin_version = \"{}\"\n",
contract, cid, min_ver
));
}
}
fs::write(bundle_dir.join("manifest.toml"), manifest_toml).expect("write manifest.toml");
bundle_dir
}
#[test]
fn compatible_exact_version_strict_loads_ok() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn compatible_superset_version_strict_loads_ok() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.2",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn compatible_superset_version_relaxed_loads_ok() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.2",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Relaxed)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn compatible_superset_version_yolo_loads_ok() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.2",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Yolo)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn too_old_strict_returns_version_mismatch() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.2")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Loader(LoaderError::VersionMismatch { .. }))
),
"expected VersionMismatch but got an unexpected value"
);
}
#[test]
fn too_old_relaxed_warns_and_loads() {
let sink: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.2")],
);
let sink_clone: Arc<Mutex<Vec<String>>> = Arc::clone(&sink);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Relaxed)
.loader(NoopLoader)
.logger(move |_level: LogLevel, _scope: &str, msg: &str| {
sink_clone.lock().expect("lock").push(msg.to_owned());
})
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
assert!(
sink.lock()
.expect("lock")
.iter()
.any(|w: &String| w.to_lowercase().contains("version mismatch")),
"expected a version mismatch warning in sink"
);
}
#[test]
fn too_old_yolo_loads_silently() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.2")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Yolo)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
fn write_major_mismatch_bundles(tmp: &TempDir) {
let cid: u64 = guest_contract_id("test.contract", 2);
write_bundle_manifest(
tmp,
"provider",
"1.0",
&["test.contract"],
&[("test.contract@1", 2)],
&[],
);
write_bundle_manifest(
tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "2.0")],
);
}
#[test]
fn major_mismatch_strict_fails_unsatisfied_capability() {
let tmp: TempDir = TempDir::new().expect("tmp");
write_major_mismatch_bundles(&tmp);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Graph(
GraphError::UnsatisfiedCapability { .. }
))
),
"expected UnsatisfiedCapability, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn major_mismatch_relaxed_fails_unsatisfied_capability() {
let tmp: TempDir = TempDir::new().expect("tmp");
write_major_mismatch_bundles(&tmp);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Relaxed)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Graph(
GraphError::UnsatisfiedCapability { .. }
))
),
"expected UnsatisfiedCapability, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn major_mismatch_yolo_fails_unsatisfied_capability() {
let tmp: TempDir = TempDir::new().expect("tmp");
write_major_mismatch_bundles(&tmp);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Yolo)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Graph(
GraphError::UnsatisfiedCapability { .. }
))
),
"expected UnsatisfiedCapability, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn function_count_mismatch_strict_returns_error() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[], &[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Loader(
LoaderError::FunctionCountMismatch { .. }
))
),
"expected FunctionCountMismatch but got an unexpected value"
);
}
#[test]
fn function_count_mismatch_relaxed_warns_and_loads() {
let sink: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[], &[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let sink_clone: Arc<Mutex<Vec<String>>> = Arc::clone(&sink);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Relaxed)
.loader(NoopLoader)
.logger(move |_level: LogLevel, _scope: &str, msg: &str| {
sink_clone.lock().expect("lock").push(msg.to_owned());
})
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
assert!(
!sink.lock().expect("lock").is_empty(),
"expected at least one warning in sink for function count mismatch"
);
}
#[test]
fn function_count_mismatch_yolo_ignored() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"1.0",
&["test.contract"],
&[], &[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Yolo)
.loader(NoopLoader)
.build();
assert!(
result.is_ok(),
"expected Ok, got: {:?}",
result.as_ref().err()
);
}
#[test]
fn malformed_version_returns_manifest_parse_error() {
let tmp: TempDir = TempDir::new().expect("tmp");
let cid: u64 = guest_contract_id("test.contract", 1);
write_bundle_manifest(
&tmp,
"provider",
"not_a_version",
&["test.contract"],
&[("test.contract@0", 2)], &[],
);
write_bundle_manifest(
&tmp,
"consumer",
"1.0",
&[],
&[],
&[("test.contract", cid, "1.0")],
);
let result: Result<Arc<Runtime>, RuntimeError> = Runtime::builder()
.plugin_dir(tmp.path().to_path_buf())
.compatibility(Compatibility::Strict)
.loader(NoopLoader)
.build();
assert!(
matches!(
result,
Err(RuntimeError::Loader(LoaderError::ManifestParse { .. }))
),
"expected ManifestParse but got an unexpected value"
);
}