#![cfg(feature = "python")]
use std::path::PathBuf;
use fidius_core::python_descriptor::PythonInterfaceDescriptor;
use fidius_host::{PluginHost, PluginRuntimeKind};
fn stage_plugin(tmp: &tempfile::TempDir) -> PathBuf {
let plugins_root = tmp.path().to_path_buf();
let dest = plugins_root.join("py-byte-pipe");
let src = repo_root().join("tests/test-plugin-py-greeter");
copy_dir(&src, &dest);
let vendor = dest.join("vendor");
std::fs::create_dir_all(&vendor).unwrap();
copy_dir(&repo_root().join("python/fidius"), &vendor.join("fidius"));
plugins_root
}
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf()
}
fn copy_dir(src: &std::path::Path, dst: &std::path::Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
copy_dir(&from, &to);
} else {
std::fs::copy(&from, &to).unwrap();
}
}
}
fn byte_pipe_descriptor() -> &'static PythonInterfaceDescriptor {
&test_plugin_smoke::__fidius_BytePipe::BytePipe_PYTHON_DESCRIPTOR
}
#[test]
fn discover_lists_python_plugin_with_python_runtime() {
let tmp = tempfile::TempDir::new().unwrap();
let plugins = stage_plugin(&tmp);
let host = PluginHost::builder().search_path(&plugins).build().unwrap();
let infos = host.discover().unwrap();
let info = infos
.iter()
.find(|i| i.name == "py-byte-pipe")
.expect("py-byte-pipe should appear in discovery results");
assert!(matches!(info.runtime, PluginRuntimeKind::Python));
assert_eq!(info.interface_name, "BytePipe");
assert_eq!(info.interface_version, 1);
}
#[test]
fn typed_method_round_trips() {
let tmp = tempfile::TempDir::new().unwrap();
let plugins = stage_plugin(&tmp);
let host = PluginHost::builder().search_path(&plugins).build().unwrap();
let handle = host
.load_python("py-byte-pipe", byte_pipe_descriptor())
.expect("load_python should succeed");
let input = serde_json::to_vec(&()).unwrap();
let out = handle.call_typed_json(1, &input).expect("name");
let result: String = serde_json::from_slice(&out).unwrap();
assert_eq!(result, "py-byte-pipe");
}
#[test]
fn raw_wire_method_round_trips_2mb() {
let tmp = tempfile::TempDir::new().unwrap();
let plugins = stage_plugin(&tmp);
let host = PluginHost::builder().search_path(&plugins).build().unwrap();
let handle = host
.load_python("py-byte-pipe", byte_pipe_descriptor())
.expect("load_python should succeed");
let payload: Vec<u8> = (0..(2 * 1024 * 1024u32))
.map(|i| (i & 0xFF) as u8)
.collect();
let result = handle
.call_raw(0, &payload)
.expect("reverse_bytes should round-trip");
assert_eq!(result.len(), payload.len());
assert_eq!(result.first(), payload.last());
assert_eq!(result.last(), payload.first());
}
#[test]
fn tampered_interface_hash_is_rejected_at_load() {
let tmp = tempfile::TempDir::new().unwrap();
let plugins = stage_plugin(&tmp);
let pkg_dir = plugins.join("py-byte-pipe");
let original = pkg_dir.join("byte_pipe.py");
let tampered_path = pkg_dir.join("byte_pipe_tampered.py");
let src = std::fs::read_to_string(&original).unwrap();
let tampered = src.replace("0xDF233D1A5936EB5C", "0xBADBADBADBADBADB");
std::fs::write(&tampered_path, tampered).unwrap();
std::fs::remove_file(&original).unwrap();
let manifest_path = pkg_dir.join("package.toml");
let manifest = std::fs::read_to_string(&manifest_path).unwrap();
let manifest = manifest.replace("byte_pipe", "byte_pipe_tampered");
std::fs::write(&manifest_path, manifest).unwrap();
let host = PluginHost::builder().search_path(&plugins).build().unwrap();
let err = host
.load_python("py-byte-pipe", byte_pipe_descriptor())
.unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("interface hash mismatch") || msg.contains("python load failed"),
"expected hash-mismatch error, got: {msg}"
);
}