#![allow(clippy::needless_doctest_main)]
pub mod build_support;
pub mod plugin_types;
pub mod connection_factory;
#[cfg(test)]
use async_trait::async_trait;
use genja_core::task::{TaskProcessor, TaskProcessorResolver};
use libloading::{Library, Symbol};
use plugin_types::{
AsyncPluginInventory, GroupOrName, PluginConnection, PluginCreatePlugins, PluginEntry,
PluginInventory, PluginName, PluginProcessor, PluginResultPlugins, PluginRunner,
PluginTransformFunction, Plugins,
};
use serde::Deserialize;
use std::collections::{HashMap, hash_map};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::io::{Error, ErrorKind};
#[derive(Deserialize, Debug)]
pub struct Metadata {
pub plugins: Option<HashMap<GroupOrName, PluginEntry>>,
}
#[derive(Debug)]
pub struct PluginManager {
plugins: HashMap<PluginName, Plugins>,
plugin_path: Vec<HashMap<GroupOrName, PluginEntry>>,
libraries: Vec<libloading::Library>, }
impl Default for PluginManager {
fn default() -> Self {
Self::new()
}
}
macro_rules! get_plugins_by_variant {
($self:expr, $variant:path, $trait_type:ty) => {
$self
.plugins
.iter()
.filter_map(|(name, plugin)| match plugin {
$variant(inner) => Some((name, inner as $trait_type)),
_ => None,
})
.collect()
};
}
impl PluginManager {
pub fn new() -> Self {
PluginManager {
plugins: HashMap::new(),
plugin_path: Vec::new(),
libraries: Vec::new(),
}
}
pub fn activate_plugins(mut self) -> Result<PluginManager, Box<dyn std::error::Error>> {
let meta_data = self.get_plugin_metadata();
log::debug!("Plugin metadata: {:?}", meta_data);
let mut registrations = Vec::new();
if let Some(plugin_config) = meta_data.plugins {
for (group_or_name, plugin_entry) in plugin_config {
registrations.push((group_or_name, plugin_entry));
}
} else {
log::error!("No plugin metadata found in manifest");
return Err("No plugin metadata found in manifest".into());
}
if !self.plugin_path.is_empty() {
for entry in &self.plugin_path {
for (group_or_name, plugin_entry) in entry {
registrations.push((group_or_name.clone(), plugin_entry.clone()));
}
}
}
for (group_or_name, plugin_entry) in registrations {
self.activation_registration(group_or_name.clone(), &plugin_entry)?;
}
Ok(self)
}
pub fn get_plugin_metadata(&self) -> Metadata {
let plugin_path = std::env::var("CARGO_MANIFEST_PATH").unwrap_or_else(|_| ".".to_string());
let file_string = std::fs::read_to_string(plugin_path);
let manifest = match file_string {
Ok(manifest) => manifest,
Err(msg) => {
eprintln!("Error reading manifest file {}", msg);
return Metadata { plugins: None };
}
};
let value: toml::Value = match toml::from_str(&manifest) {
Ok(value) => value,
Err(err) => {
eprintln!("Error parsing manifest file: {err}");
return Metadata { plugins: None };
}
};
if let Some(meta_data) = value
.get("package")
.and_then(|p| p.get("metadata"))
.and_then(|m| m.as_table())
{
let meta: Result<Metadata, toml::de::Error> =
toml::from_str(&toml::to_string(meta_data).unwrap());
meta.unwrap()
} else {
Metadata { plugins: None }
}
}
fn activation_registration(
&mut self,
group_or_name: String,
plugin_entry: &PluginEntry,
) -> Result<(), Box<dyn std::error::Error>> {
match plugin_entry {
PluginEntry::Individual(path) => {
log::debug!("Loading individual plugin: {group_or_name} {path}");
let (library, plugins) = self.load_plugin(path)?;
self.libraries.push(library);
for plugin in plugins {
self.register_plugin(plugin);
}
}
PluginEntry::Group(group_plugins) => {
for (name, path) in group_plugins {
log::debug!("Loading plugin group: {group_or_name}, {name} {path}");
let (library, plugins) = self.load_plugin(path)?;
self.libraries.push(library);
for plugin in plugins {
self.register_plugin(plugin);
}
}
}
}
Ok(())
}
pub fn load_plugin(&self, filename: &str) -> PluginResultPlugins {
let path = Path::new(filename);
if !path.exists() {
let msg = format!("Plugin file does not exist: {}", filename);
log::error!("{msg}");
return Err(msg.into());
} else {
log::debug!("Attempting to load plugin: {}", filename);
}
let library = unsafe { Library::new(path)? };
log::debug!("Library loaded successfully");
let create_plugin: Symbol<PluginCreatePlugins> = unsafe { library.get(b"create_plugins")? };
log::debug!("Found create_plugins symbol");
let plugins = unsafe { create_plugin() };
log::debug!("Plugin created successfully");
Ok((library, plugins))
}
pub fn load_plugins_from_directory(
mut self,
directory: impl AsRef<Path>,
) -> Result<Self, Box<dyn std::error::Error>> {
let directory = directory.as_ref();
if !directory.exists() {
return Ok(self);
}
if !directory.is_dir() {
return Err(format!("plugin path is not a directory: {}", directory.display()).into());
}
let extension = std::env::consts::DLL_EXTENSION;
let mut entries: Vec<PathBuf> = fs::read_dir(directory)?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.is_file()
&& path
.extension()
.and_then(|value| value.to_str())
.map(|value| value == extension)
.unwrap_or(false)
})
.collect();
entries.sort();
for path in entries {
let filename = path
.to_str()
.ok_or_else(|| format!("path contains invalid Unicode: {}", path.display()))?;
let (library, plugins) = self.load_plugin(filename)?;
self.libraries.push(library);
for plugin in plugins {
self.register_plugin(plugin);
}
}
Ok(self)
}
pub fn register_plugin(&mut self, plugin: Plugins) {
let name = plugin.name();
log::info!("Registering plugin: {:?}", name);
if let hash_map::Entry::Vacant(entry) = self.plugins.entry(name.clone()) {
entry.insert(plugin);
} else {
let msg = format!("Plugin '{}' already registered", &name);
log::error!("{msg}");
panic!("{msg}");
}
}
pub fn get_plugin(&self, name: &str) -> Option<&Plugins> {
self.plugins.get(name)
}
#[allow(clippy::borrowed_box)]
pub fn get_connection_plugin(&self, name: &str) -> Option<&Box<dyn PluginConnection>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::Connection(base) => Some(base),
_ => None,
})
}
#[allow(clippy::borrowed_box)]
pub fn get_inventory_plugin(&self, name: &str) -> Option<&Box<dyn PluginInventory>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::Inventory(inventory) => Some(inventory),
_ => None,
})
}
#[allow(clippy::borrowed_box)]
pub fn get_async_inventory_plugin(&self, name: &str) -> Option<&Box<dyn AsyncPluginInventory>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::AsyncInventory(inventory) => Some(inventory),
_ => None,
})
}
#[allow(clippy::borrowed_box)]
pub fn get_transform_function_plugin(
&self,
name: &str,
) -> Option<&Box<dyn PluginTransformFunction>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::TransformFunction(transform) => Some(transform),
_ => None,
})
}
#[allow(clippy::borrowed_box)]
pub fn get_processor_plugin(&self, name: &str) -> Option<&Box<dyn PluginProcessor>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::Processor(processor) => Some(processor),
_ => None,
})
}
pub fn get_plugins_by_variant<'a, T>(
&'a self,
mapper: impl Fn(&'a Plugins) -> Option<T>,
) -> Vec<(&'a String, T)> {
self.plugins
.iter()
.filter_map(|(name, plugin)| mapper(plugin).map(|p| (name, p)))
.collect()
}
#[allow(clippy::borrowed_box)]
pub fn get_plugins_by_type_connection(&self) -> Vec<(&String, &Box<dyn PluginConnection>)> {
get_plugins_by_variant!(self, Plugins::Connection, &Box<dyn PluginConnection>)
}
#[allow(clippy::borrowed_box)]
pub fn get_plugins_by_type_inventory(&self) -> Vec<(&String, &Box<dyn PluginInventory>)> {
get_plugins_by_variant!(self, Plugins::Inventory, &Box<dyn PluginInventory>)
}
#[allow(clippy::borrowed_box)]
pub fn get_plugins_by_type_async_inventory(
&self,
) -> Vec<(&String, &Box<dyn AsyncPluginInventory>)> {
get_plugins_by_variant!(
self,
Plugins::AsyncInventory,
&Box<dyn AsyncPluginInventory>
)
}
#[allow(clippy::borrowed_box)]
pub fn get_plugins_by_type_processor(&self) -> Vec<(&String, &Box<dyn PluginProcessor>)> {
get_plugins_by_variant!(self, Plugins::Processor, &Box<dyn PluginProcessor>)
}
#[allow(clippy::borrowed_box)]
pub fn get_plugins_by_type_transform_function(
&self,
) -> Vec<(&String, &Box<dyn PluginTransformFunction>)> {
get_plugins_by_variant!(
self,
Plugins::TransformFunction,
&Box<dyn PluginTransformFunction>
)
}
pub fn deregister_plugin(&mut self, name: &str) -> Option<String> {
if let Some(plugin) = self.plugins.remove(name) {
log::info!("De-registering plugin: {}", name);
Some(plugin.name())
} else {
None
}
}
pub fn deregister_all_plugins(&mut self) -> Vec<String> {
let mut deregistered_plugins = Vec::new();
for (name, plugin) in self.plugins.drain() {
log::info!("De-registering plugin: {}", name);
deregistered_plugins.push(plugin.name());
}
deregistered_plugins
}
pub fn merge(&mut self, other: PluginManager) {
self.plugin_path.extend(other.plugin_path);
self.libraries.extend(other.libraries);
for (name, plugin) in other.plugins {
if self.plugins.insert(name.clone(), plugin).is_some() {
log::info!("Overriding plugin: {}", name);
} else {
log::info!("Registering merged plugin: {}", name);
}
}
}
pub fn get_all_plugin_names(&self) -> Vec<&String> {
self.plugins.keys().collect()
}
pub fn get_all_plugin_names_and_groups(&self) -> Vec<(String, String)> {
self.plugins
.iter()
.map(|(name, plugin)| (name.clone(), plugin.group_name()))
.collect()
}
#[allow(clippy::borrowed_box)]
pub fn get_runner_plugin(&self, name: &str) -> Option<&Box<dyn PluginRunner>> {
self.plugins.get(name).and_then(|plugin| match plugin {
Plugins::Runner(runner) => Some(runner),
_ => None,
})
}
pub fn with_path(mut self, path: &str, group: Option<&str>) -> Result<Self, Error> {
let path = Path::new(&path);
if path.exists() {
let path_string = if let Some(path_str) = path.to_str() {
path_str.to_string()
} else {
return Err(Error::new(
ErrorKind::InvalidData,
"Path contains invalid Unicode",
));
};
if let Some(group_string) = group {
let group_info = HashMap::from([(
group_string.to_string(),
PluginEntry::Group(HashMap::from([(group_string.to_string(), path_string)])),
)]);
self.plugin_path.push(group_info);
} else {
let individual_info =
HashMap::from([("base".to_string(), PluginEntry::Individual(path_string))]);
self.plugin_path.push(individual_info);
};
Ok(self)
} else {
Err(Error::new(
ErrorKind::NotFound,
format!("FileNotFoundError: {:?}", path.as_os_str()),
))
}
}
}
impl TaskProcessorResolver for PluginManager {
fn resolve_task_processor(&self, name: &str) -> Option<Arc<dyn TaskProcessor>> {
self.get_processor_plugin(name)
.map(|processor| processor.processor())
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
use crate::plugin_types::{
AsyncPluginInventory, Plugin, PluginConnection, PluginInventory, PluginProcessor,
PluginRunner, PluginTransformFunction,
};
use genja_core::inventory::{
ConnectionKey, Inventory, ResolvedConnectionParams, TransformFunction,
};
use genja_core::task::{TaskProcessor, Tasks};
use genja_core::{InventoryLoadError, Settings};
fn env_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let lock = LOCK.get_or_init(|| Mutex::new(()));
lock.lock().unwrap_or_else(|err| err.into_inner())
}
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf()
}
fn ensure_test_plugins_built() {
static BUILT: OnceLock<()> = OnceLock::new();
BUILT.get_or_init(|| {
let status = Command::new("cargo")
.current_dir(workspace_root())
.args([
"build",
"--quiet",
"-p",
"plugin-mods",
"-p",
"plugin_inventory",
"-p",
"plugin_connection",
])
.status()
.expect("Failed to run cargo build for test plugins");
assert!(status.success(), "Failed to build test plugins");
});
}
fn set_env_var() -> MutexGuard<'static, ()> {
let guard = env_lock();
ensure_test_plugins_built();
let file_name = match std::env::consts::OS {
"linux" => "Cargo.toml",
"windows" => "Cargo-windows.toml",
"macos" => "Cargo-macos.toml",
_ => "Cargo.toml",
};
let file = format!("../genja-plugin-manager/tests/plugin_mods/{}", file_name);
unsafe {
std::env::set_var("CARGO_MANIFEST_PATH", file);
}
guard
}
fn make_file_path(module_name: &str) -> String {
ensure_test_plugins_built();
let mut path_name = PathBuf::new();
let mut module_name_prefix = String::from(std::env::consts::DLL_PREFIX);
module_name_prefix.push_str(module_name);
path_name.push("..");
path_name.push("target");
path_name.push("debug");
path_name.push(module_name_prefix);
path_name.set_extension(std::env::consts::DLL_EXTENSION);
path_name.to_string_lossy().to_string()
}
fn temp_manifest_path(filename: &str) -> std::path::PathBuf {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = std::env::temp_dir();
path.push(format!("genja_plugin_manager_{now}_{filename}"));
path
}
fn temp_file_path(filename: &str) -> std::path::PathBuf {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = std::env::temp_dir();
path.push(format!("genja_plugin_manager_{now}_{filename}"));
path
}
#[cfg(target_os = "linux")]
fn system_library_path() -> Option<&'static str> {
let candidates = [
"/lib/x86_64-linux-gnu/libc.so.6",
"/lib64/libc.so.6",
"/usr/lib/x86_64-linux-gnu/libc.so.6",
];
candidates.iter().copied().find(|p| Path::new(p).exists())
}
#[cfg(target_os = "macos")]
fn system_library_path() -> Option<&'static str> {
let p = "/usr/lib/libSystem.B.dylib";
if Path::new(p).exists() { Some(p) } else { None }
}
#[cfg(target_os = "windows")]
fn system_library_path() -> Option<&'static str> {
let p = "C:\\Windows\\System32\\kernel32.dll";
if Path::new(p).exists() { Some(p) } else { None }
}
#[test]
fn get_plugin_path_test() {
let _env = set_env_var();
let plugin_manager = PluginManager::new();
let metadata = plugin_manager.get_plugin_metadata();
let plugins = metadata.plugins;
match plugins {
Some(plug_entry) => {
for (group, entry) in plug_entry {
match entry {
PluginEntry::Individual(path) => {
assert_eq!(path, make_file_path("plugin_mods"));
}
PluginEntry::Group(path) => {
path.iter().for_each(|(metadata_name, path)| {
assert_eq!(path, &make_file_path("plugin_inventory"));
assert_eq!(metadata_name, "inventory_a");
assert_eq!(group, "inventory");
});
}
}
}
}
None => {
panic!("No plugins found in metadata");
}
}
}
#[test]
fn get_plugin_metadata_test() {
let _env = set_env_var();
let plugin_manager = PluginManager::new();
let metadata = plugin_manager.get_plugin_metadata();
assert!(metadata.plugins.is_some());
assert_eq!(metadata.plugins.clone().unwrap().len(), 2);
}
#[test]
fn get_plugin_metadata_missing_manifest_test() {
let _env = env_lock();
let missing = temp_manifest_path("missing_manifest.toml");
unsafe {
std::env::set_var("CARGO_MANIFEST_PATH", missing.to_string_lossy().to_string());
}
let plugin_manager = PluginManager::new();
let metadata = plugin_manager.get_plugin_metadata();
assert!(metadata.plugins.is_none());
}
#[test]
fn get_plugin_metadata_missing_metadata_section_test() {
let _env = env_lock();
let manifest = temp_manifest_path("no_metadata.toml");
std::fs::write(
&manifest,
"[package]\nname = \"no_metadata\"\nversion = \"0.1.0\"\n",
)
.unwrap();
unsafe {
std::env::set_var(
"CARGO_MANIFEST_PATH",
manifest.to_string_lossy().to_string(),
);
}
let plugin_manager = PluginManager::new();
let metadata = plugin_manager.get_plugin_metadata();
assert!(metadata.plugins.is_none());
let _ = std::fs::remove_file(&manifest);
}
#[test]
fn get_plugin_metadata_invalid_toml_test() {
let _env = env_lock();
let manifest = temp_manifest_path("invalid_toml.toml");
std::fs::write(&manifest, "[package]\nname = \"invalid\"\nversion =\n").unwrap();
unsafe {
std::env::set_var(
"CARGO_MANIFEST_PATH",
manifest.to_string_lossy().to_string(),
);
}
let plugin_manager = PluginManager::new();
let metadata = plugin_manager.get_plugin_metadata();
assert!(metadata.plugins.is_none());
let _ = std::fs::remove_file(&manifest);
}
#[test]
fn activate_plugins_group_invalid_path_returns_error_test() {
let _env = env_lock();
let manifest = temp_manifest_path("group_invalid_path.toml");
std::fs::write(
&manifest,
r#"[package]
name = "invalid_group"
version = "0.1.0"
[package.metadata.plugins.inventory]
inventory_a = "../this/path/does/not/exist.so"
"#,
)
.unwrap();
unsafe {
std::env::set_var(
"CARGO_MANIFEST_PATH",
manifest.to_string_lossy().to_string(),
);
}
let plugin_manager = PluginManager::new();
let result = plugin_manager.activate_plugins();
assert!(result.is_err());
let _ = std::fs::remove_file(&manifest);
}
#[test]
fn activate_plugins_test() {
let _env = set_env_var();
let mut plugin_manager = PluginManager::new();
plugin_manager = plugin_manager.activate_plugins().unwrap();
assert!(plugin_manager.get_plugin("plugin_a").is_some());
assert_eq!(plugin_manager.plugins.len(), 3);
}
#[test]
#[should_panic]
fn activate_plugins_and_panic_test() {
let _env = set_env_var();
let mut plugin_manager = PluginManager::new();
plugin_manager = plugin_manager.activate_plugins().unwrap();
_ = plugin_manager.activate_plugins().unwrap();
}
#[test]
fn load_plugin_test() {
let plugin_manager = PluginManager::new();
let filename = make_file_path("plugin_mods");
let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
assert_eq!(plugins.len(), 2);
assert_eq!(plugins[0].name(), "plugin_a");
}
#[test]
fn load_plugin_and_panic_test() {
let plugin_manager = PluginManager::new();
let filename = make_file_path("plugin_mods");
let (_library, _) = plugin_manager.load_plugin(&filename).unwrap();
let filename = make_file_path("plugin_mods");
let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
assert_eq!(plugins.len(), 2);
assert_eq!(plugins[0].name(), "plugin_a");
}
#[test]
fn load_plugin_missing_file_test() {
let plugin_manager = PluginManager::new();
let missing = temp_file_path("missing_plugin_file.so");
let result = plugin_manager.load_plugin(&missing.to_string_lossy());
assert!(result.is_err());
}
#[test]
fn load_plugin_invalid_library_test() {
let plugin_manager = PluginManager::new();
let file = temp_file_path("not_a_library.so");
std::fs::write(&file, "not a library").unwrap();
let result = plugin_manager.load_plugin(&file.to_string_lossy());
assert!(result.is_err());
let _ = std::fs::remove_file(&file);
}
#[test]
fn load_plugin_missing_symbol_test() {
let plugin_manager = PluginManager::new();
let Some(path) = system_library_path() else {
return;
};
let result = plugin_manager.load_plugin(path);
assert!(result.is_err());
}
#[test]
fn activate_plugins_with_groups_test() {
let _env = set_env_var();
let plugin_manager = PluginManager::new().activate_plugins().unwrap();
let inventory_plugins = plugin_manager.get_plugins_by_type_connection();
assert_eq!(inventory_plugins.len(), 2);
let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
assert_eq!(inventory_plugins.len(), 1);
assert_eq!(inventory_plugins[0].1.name(), "inventory_a");
assert_eq!(plugin_manager.plugins.len(), 3);
}
#[test]
fn get_all_plugin_names_and_groups_test() {
let _env = set_env_var();
let plugin_manager = PluginManager::new().activate_plugins().unwrap();
let all_plugins = plugin_manager.get_all_plugin_names_and_groups();
assert_eq!(all_plugins.len(), 3);
all_plugins
.iter()
.for_each(|(name, group)| match name.as_str() {
"plugin_a" => assert_eq!(group, "Connection"),
"plugin_b" => assert_eq!(group, "Connection"),
"inventory_a" => assert_eq!(group, "Inventory"),
_ => panic!("Unexpected plugin name"),
});
}
#[test]
fn deregister_plugin_test() {
let _env = set_env_var();
let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
assert_eq!(plugin_manager.plugins.len(), 3);
let plugin_name = plugin_manager.deregister_plugin("plugin_a");
if let Some(plugin) = plugin_name {
assert_eq!(plugin, "plugin_a");
assert_eq!(plugin_manager.plugins.len(), 2);
}
let plugin_name = plugin_manager.deregister_plugin("inventory_a");
if let Some(plugin) = plugin_name {
assert_eq!(plugin, "inventory_a");
assert_eq!(plugin_manager.plugins.len(), 1);
}
let plugin_name = plugin_manager.deregister_plugin("non_existent_plugin");
assert_eq!(plugin_name, None);
}
#[test]
fn deregister_all_plugins_test() {
let _env = set_env_var();
let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
assert_eq!(plugin_manager.plugins.len(), 3);
let num_plugins_deregistered = plugin_manager.deregister_all_plugins();
assert_eq!(num_plugins_deregistered.len(), 3);
assert_eq!(plugin_manager.plugins.len(), 0);
}
#[test]
fn plugin_manager_new_test() {
let _env = set_env_var();
let mut plugin_manager = PluginManager::new();
assert_eq!(plugin_manager.plugins.len(), 0);
plugin_manager = plugin_manager.activate_plugins().unwrap();
assert_eq!(plugin_manager.plugins.len(), 3);
}
#[test]
fn get_plugins_by_type_test() {
let _env = set_env_var();
let plugin_manager = PluginManager::new().activate_plugins().unwrap();
let connection_plugins = plugin_manager.get_plugins_by_type_connection();
assert_eq!(connection_plugins.len(), 2);
let base_plugin_names: Vec<&str> = connection_plugins
.iter()
.map(|(name, _)| name.as_str())
.collect();
assert!(base_plugin_names.contains(&"plugin_a"));
assert!(base_plugin_names.contains(&"plugin_b"));
for (name, plugin) in connection_plugins {
let debug_output = format!("{:?}", plugin);
assert!(debug_output.contains("ConnectionPlugin"));
assert!(debug_output.contains(name));
}
let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
assert_eq!(inventory_plugins.len(), 1);
}
#[test]
fn with_path_test() {
let _env = set_env_var();
let path = make_file_path("plugin_connection");
let plugin_manager = PluginManager::new()
.with_path(&path, None)
.unwrap()
.activate_plugins()
.unwrap();
assert_eq!(plugin_manager.plugins.len(), 4);
}
#[test]
fn with_path_group_loads_plugins() {
let _env = set_env_var();
let path = make_file_path("plugin_connection");
let plugin_manager = PluginManager::new()
.with_path(&path, Some("extra"))
.unwrap()
.activate_plugins()
.unwrap();
assert_eq!(plugin_manager.plugins.len(), 4);
}
#[test]
fn with_path_not_found_test() {
let missing = temp_file_path("missing_with_path_plugin.so");
let result = PluginManager::new().with_path(&missing.to_string_lossy(), None);
assert!(result.is_err());
if let Err(err) = result {
assert_eq!(err.kind(), ErrorKind::NotFound);
}
}
#[test]
#[should_panic]
fn with_path_duplicate_plugin_panics_test() {
let _env = set_env_var();
let duplicate = make_file_path("plugin_mods");
let _ = PluginManager::new()
.with_path(&duplicate, None)
.unwrap()
.activate_plugins()
.unwrap();
}
#[derive(Debug)]
struct DummyConnection {
name: &'static str,
}
impl Plugin for DummyConnection {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl PluginConnection for DummyConnection {
fn create(&self, _key: &ConnectionKey) -> Box<dyn PluginConnection> {
Box::new(Self { name: self.name })
}
async fn open(&mut self, _params: &ResolvedConnectionParams) -> Result<(), String> {
Ok(())
}
fn close(&mut self) -> ConnectionKey {
ConnectionKey::new("dummy", "conn")
}
fn is_alive(&self) -> bool {
false
}
}
#[derive(Debug)]
struct DummyInventory {
name: &'static str,
}
impl Plugin for DummyInventory {
fn name(&self) -> String {
self.name.to_string()
}
}
impl PluginInventory for DummyInventory {
fn load(
&self,
_settings: &Settings,
_plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
Ok(Inventory::builder().build())
}
}
#[derive(Debug)]
struct DummyAsyncInventory {
name: &'static str,
}
impl Plugin for DummyAsyncInventory {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl AsyncPluginInventory for DummyAsyncInventory {
async fn load_async(
&self,
_settings: &Settings,
_plugins: &PluginManager,
) -> Result<Inventory, InventoryLoadError> {
Ok(Inventory::builder().build())
}
}
#[derive(Debug)]
struct DummyRunner {
name: &'static str,
}
impl Plugin for DummyRunner {
fn name(&self) -> String {
self.name.to_string()
}
}
#[async_trait]
impl PluginRunner for DummyRunner {
async fn run_task(
&self,
_task: &genja_core::task::TaskDefinition,
_hosts: &genja_core::inventory::Hosts,
_connection_resolver: Option<
std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
>,
_runner_config: &genja_core::settings::RunnerConfig,
_max_depth: usize,
) -> Result<genja_core::task::TaskResults, genja_core::GenjaError> {
Ok(genja_core::task::TaskResults::new(self.name))
}
async fn run_tasks(
&self,
_tasks: &Tasks,
_hosts: &genja_core::inventory::Hosts,
_connection_resolver: Option<
std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
>,
_runner_config: &genja_core::settings::RunnerConfig,
_max_depth: usize,
) -> Result<Vec<genja_core::task::TaskResults>, genja_core::GenjaError> {
Ok(Vec::new())
}
}
#[derive(Debug)]
struct DummyTransform {
name: &'static str,
}
impl Plugin for DummyTransform {
fn name(&self) -> String {
self.name.to_string()
}
}
impl PluginTransformFunction for DummyTransform {
fn transform_function(&self) -> TransformFunction {
TransformFunction::new(|host, _| host.clone())
}
}
#[derive(Debug)]
struct DummyProcessorPlugin {
name: &'static str,
}
impl Plugin for DummyProcessorPlugin {
fn name(&self) -> String {
self.name.to_string()
}
}
impl PluginProcessor for DummyProcessorPlugin {
fn processor(&self) -> Arc<dyn TaskProcessor> {
Arc::new(DummyProcessor)
}
}
struct DummyProcessor;
impl TaskProcessor for DummyProcessor {}
#[test]
fn get_plugin_and_typed_getters_match_variants() {
let mut manager = PluginManager::new();
manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "conn",
})));
manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
name: "ainv",
})));
manager.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
name: "tf",
})));
assert!(manager.get_plugin("conn").is_some());
assert!(manager.get_plugin("inv").is_some());
assert!(manager.get_plugin("ainv").is_some());
assert!(manager.get_plugin("run").is_some());
assert!(manager.get_plugin("tf").is_some());
assert!(manager.get_plugin("missing").is_none());
assert!(manager.get_connection_plugin("conn").is_some());
assert!(manager.get_connection_plugin("inv").is_none());
assert!(manager.get_connection_plugin("ainv").is_none());
assert!(manager.get_connection_plugin("run").is_none());
assert!(manager.get_connection_plugin("tf").is_none());
assert!(manager.get_inventory_plugin("inv").is_some());
assert!(manager.get_inventory_plugin("conn").is_none());
assert!(manager.get_inventory_plugin("ainv").is_none());
assert!(manager.get_inventory_plugin("run").is_none());
assert!(manager.get_inventory_plugin("tf").is_none());
assert!(manager.get_async_inventory_plugin("ainv").is_some());
assert!(manager.get_async_inventory_plugin("inv").is_none());
assert!(manager.get_async_inventory_plugin("conn").is_none());
assert!(manager.get_runner_plugin("run").is_some());
assert!(manager.get_runner_plugin("conn").is_none());
assert!(manager.get_runner_plugin("inv").is_none());
assert!(manager.get_runner_plugin("ainv").is_none());
assert!(manager.get_runner_plugin("tf").is_none());
assert!(manager.get_transform_function_plugin("tf").is_some());
assert!(manager.get_transform_function_plugin("conn").is_none());
assert!(manager.get_transform_function_plugin("inv").is_none());
assert!(manager.get_transform_function_plugin("ainv").is_none());
assert!(manager.get_transform_function_plugin("run").is_none());
}
#[test]
fn processor_plugin_getters_and_resolver_match_processor_variant() {
let mut manager = PluginManager::new();
manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "conn",
})));
manager.register_plugin(Plugins::Processor(Box::new(DummyProcessorPlugin {
name: "audit",
})));
assert!(manager.get_processor_plugin("audit").is_some());
assert!(manager.get_processor_plugin("conn").is_none());
assert!(manager.get_processor_plugin("missing").is_none());
let processors = manager.get_plugins_by_type_processor();
assert_eq!(processors.len(), 1);
assert_eq!(processors[0].0.as_str(), "audit");
assert_eq!(processors[0].1.name(), "audit");
assert!(manager.resolve_task_processor("audit").is_some());
assert!(manager.resolve_task_processor("conn").is_none());
assert!(manager.resolve_task_processor("missing").is_none());
}
#[test]
#[should_panic(expected = "Plugin 'dup' already registered")]
fn register_plugin_duplicate_name_panics() {
let mut manager = PluginManager::new();
manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "dup",
})));
manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "dup",
})));
}
#[test]
fn get_plugins_by_type_transform_function_and_all_names() {
let mut manager = PluginManager::new();
manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "conn",
})));
manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
name: "ainv",
})));
manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
name: "tf",
})));
let transforms = manager.get_plugins_by_type_transform_function();
assert_eq!(transforms.len(), 1);
assert_eq!(transforms[0].0.as_str(), "tf");
let async_inventory_plugins = manager.get_plugins_by_type_async_inventory();
assert_eq!(async_inventory_plugins.len(), 1);
assert_eq!(async_inventory_plugins[0].0.as_str(), "ainv");
let names = manager.get_all_plugin_names();
assert_eq!(names.len(), 4);
assert!(names.contains(&&"conn".to_string()));
assert!(names.contains(&&"inv".to_string()));
assert!(names.contains(&&"ainv".to_string()));
assert!(names.contains(&&"tf".to_string()));
}
#[test]
fn merge_overrides_existing_plugins_by_name() {
let mut base = PluginManager::new();
base.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "conn",
})));
let mut custom = PluginManager::new();
custom.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
custom.register_plugin(Plugins::Connection(Box::new(DummyConnection {
name: "conn",
})));
base.merge(custom);
assert!(base.get_connection_plugin("conn").is_some());
assert!(base.get_runner_plugin("run").is_some());
assert_eq!(base.get_all_plugin_names().len(), 2);
}
}