1#![allow(clippy::needless_doctest_main)]
2
3pub mod build_support;
499pub mod plugin_types;
500pub mod connection_factory;
502
503#[cfg(test)]
504use async_trait::async_trait;
505use genja_core::task::{TaskProcessor, TaskProcessorResolver};
506use libloading::{Library, Symbol};
507use plugin_types::{
508 AsyncPluginInventory, GroupOrName, PluginConnection, PluginCreatePlugins, PluginEntry,
509 PluginInventory, PluginName, PluginProcessor, PluginResultPlugins, PluginRunner,
510 PluginTransformFunction, Plugins,
511};
512use serde::Deserialize;
513use std::collections::{HashMap, hash_map};
514use std::fs;
515use std::path::{Path, PathBuf};
516use std::sync::Arc;
517use std::io::{Error, ErrorKind};
519
520#[derive(Deserialize, Debug)]
521pub struct Metadata {
522 pub plugins: Option<HashMap<GroupOrName, PluginEntry>>,
523}
524
525#[derive(Debug)]
534pub struct PluginManager {
535 plugins: HashMap<PluginName, Plugins>,
536 plugin_path: Vec<HashMap<GroupOrName, PluginEntry>>,
537 libraries: Vec<libloading::Library>, }
539
540impl Default for PluginManager {
541 fn default() -> Self {
542 Self::new()
543 }
544}
545macro_rules! get_plugins_by_variant {
551 ($self:expr, $variant:path, $trait_type:ty) => {
552 $self
553 .plugins
554 .iter()
555 .filter_map(|(name, plugin)| match plugin {
556 $variant(inner) => Some((name, inner as $trait_type)),
557 _ => None,
558 })
559 .collect()
560 };
561}
562
563impl PluginManager {
564 pub fn new() -> Self {
565 PluginManager {
566 plugins: HashMap::new(),
567 plugin_path: Vec::new(),
568 libraries: Vec::new(),
569 }
570 }
571
572 pub fn activate_plugins(mut self) -> Result<PluginManager, Box<dyn std::error::Error>> {
578 let meta_data = self.get_plugin_metadata();
579 log::debug!("Plugin metadata: {:?}", meta_data);
580 let mut registrations = Vec::new();
581 if let Some(plugin_config) = meta_data.plugins {
582 for (group_or_name, plugin_entry) in plugin_config {
583 registrations.push((group_or_name, plugin_entry));
584 }
585 } else {
586 log::error!("No plugin metadata found in manifest");
587 return Err("No plugin metadata found in manifest".into());
588 }
589 if !self.plugin_path.is_empty() {
590 for entry in &self.plugin_path {
591 for (group_or_name, plugin_entry) in entry {
592 registrations.push((group_or_name.clone(), plugin_entry.clone()));
593 }
594 }
595 }
596 for (group_or_name, plugin_entry) in registrations {
597 self.activation_registration(group_or_name.clone(), &plugin_entry)?;
598 }
599 Ok(self)
600 }
601
602 pub fn get_plugin_metadata(&self) -> Metadata {
614 let plugin_path = std::env::var("CARGO_MANIFEST_PATH").unwrap_or_else(|_| ".".to_string());
615
616 let file_string = std::fs::read_to_string(plugin_path);
617 let manifest = match file_string {
618 Ok(manifest) => manifest,
619 Err(msg) => {
620 eprintln!("Error reading manifest file {}", msg);
621 return Metadata { plugins: None };
622 }
623 };
624 let value: toml::Value = match toml::from_str(&manifest) {
625 Ok(value) => value,
626 Err(err) => {
627 eprintln!("Error parsing manifest file: {err}");
628 return Metadata { plugins: None };
629 }
630 };
631 if let Some(meta_data) = value
633 .get("package")
634 .and_then(|p| p.get("metadata"))
635 .and_then(|m| m.as_table())
636 {
637 let meta: Result<Metadata, toml::de::Error> =
638 toml::from_str(&toml::to_string(meta_data).unwrap());
639 meta.unwrap()
640 } else {
641 Metadata { plugins: None }
642 }
643 }
645
646 fn activation_registration(
651 &mut self,
652 group_or_name: String,
653 plugin_entry: &PluginEntry,
654 ) -> Result<(), Box<dyn std::error::Error>> {
655 match plugin_entry {
656 PluginEntry::Individual(path) => {
657 log::debug!("Loading individual plugin: {group_or_name} {path}");
658 let (library, plugins) = self.load_plugin(path)?;
659 self.libraries.push(library);
660 for plugin in plugins {
661 self.register_plugin(plugin);
662 }
663 }
664 PluginEntry::Group(group_plugins) => {
665 for (name, path) in group_plugins {
666 log::debug!("Loading plugin group: {group_or_name}, {name} {path}");
667 let (library, plugins) = self.load_plugin(path)?;
668 self.libraries.push(library);
669 for plugin in plugins {
670 self.register_plugin(plugin);
671 }
672 }
673 }
674 }
675 Ok(())
676 }
677
678 pub fn load_plugin(&self, filename: &str) -> PluginResultPlugins {
683 let path = Path::new(filename);
684
685 if !path.exists() {
686 let msg = format!("Plugin file does not exist: {}", filename);
687 log::error!("{msg}");
688 return Err(msg.into());
689 } else {
690 log::debug!("Attempting to load plugin: {}", filename);
691 }
692
693 let library = unsafe { Library::new(path)? };
694 log::debug!("Library loaded successfully");
695
696 let create_plugin: Symbol<PluginCreatePlugins> = unsafe { library.get(b"create_plugins")? };
697 log::debug!("Found create_plugins symbol");
698
699 let plugins = unsafe { create_plugin() };
700 log::debug!("Plugin created successfully");
701
702 Ok((library, plugins))
703 }
704
705 pub fn load_plugins_from_directory(
710 mut self,
711 directory: impl AsRef<Path>,
712 ) -> Result<Self, Box<dyn std::error::Error>> {
713 let directory = directory.as_ref();
714
715 if !directory.exists() {
716 return Ok(self);
717 }
718
719 if !directory.is_dir() {
720 return Err(format!("plugin path is not a directory: {}", directory.display()).into());
721 }
722
723 let extension = std::env::consts::DLL_EXTENSION;
724 let mut entries: Vec<PathBuf> = fs::read_dir(directory)?
725 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
726 .filter(|path| {
727 path.is_file()
728 && path
729 .extension()
730 .and_then(|value| value.to_str())
731 .map(|value| value == extension)
732 .unwrap_or(false)
733 })
734 .collect();
735 entries.sort();
736
737 for path in entries {
738 let filename = path
739 .to_str()
740 .ok_or_else(|| format!("path contains invalid Unicode: {}", path.display()))?;
741 let (library, plugins) = self.load_plugin(filename)?;
742 self.libraries.push(library);
743 for plugin in plugins {
744 self.register_plugin(plugin);
745 }
746 }
747
748 Ok(self)
749 }
750
751 pub fn register_plugin(&mut self, plugin: Plugins) {
755 let name = plugin.name();
756 log::info!("Registering plugin: {:?}", name);
757
758 if let hash_map::Entry::Vacant(entry) = self.plugins.entry(name.clone()) {
759 entry.insert(plugin);
760 } else {
761 let msg = format!("Plugin '{}' already registered", &name);
762 log::error!("{msg}");
763 panic!("{msg}");
764 }
765 }
766
767 pub fn get_plugin(&self, name: &str) -> Option<&Plugins> {
769 self.plugins.get(name)
770 }
771
772 #[allow(clippy::borrowed_box)]
774 pub fn get_connection_plugin(&self, name: &str) -> Option<&Box<dyn PluginConnection>> {
775 self.plugins.get(name).and_then(|plugin| match plugin {
776 Plugins::Connection(base) => Some(base),
777 _ => None,
778 })
779 }
780
781 #[allow(clippy::borrowed_box)]
782 pub fn get_inventory_plugin(&self, name: &str) -> Option<&Box<dyn PluginInventory>> {
784 self.plugins.get(name).and_then(|plugin| match plugin {
785 Plugins::Inventory(inventory) => Some(inventory),
786 _ => None,
787 })
788 }
789
790 #[allow(clippy::borrowed_box)]
791 pub fn get_async_inventory_plugin(&self, name: &str) -> Option<&Box<dyn AsyncPluginInventory>> {
793 self.plugins.get(name).and_then(|plugin| match plugin {
794 Plugins::AsyncInventory(inventory) => Some(inventory),
795 _ => None,
796 })
797 }
798
799 #[allow(clippy::borrowed_box)]
800 pub fn get_transform_function_plugin(
802 &self,
803 name: &str,
804 ) -> Option<&Box<dyn PluginTransformFunction>> {
805 self.plugins.get(name).and_then(|plugin| match plugin {
806 Plugins::TransformFunction(transform) => Some(transform),
807 _ => None,
808 })
809 }
810
811 #[allow(clippy::borrowed_box)]
812 pub fn get_processor_plugin(&self, name: &str) -> Option<&Box<dyn PluginProcessor>> {
814 self.plugins.get(name).and_then(|plugin| match plugin {
815 Plugins::Processor(processor) => Some(processor),
816 _ => None,
817 })
818 }
819
820 pub fn get_plugins_by_variant<'a, T>(
822 &'a self,
823 mapper: impl Fn(&'a Plugins) -> Option<T>,
824 ) -> Vec<(&'a String, T)> {
825 self.plugins
826 .iter()
827 .filter_map(|(name, plugin)| mapper(plugin).map(|p| (name, p)))
828 .collect()
829 }
830
831 #[allow(clippy::borrowed_box)]
846 pub fn get_plugins_by_type_connection(&self) -> Vec<(&String, &Box<dyn PluginConnection>)> {
847 get_plugins_by_variant!(self, Plugins::Connection, &Box<dyn PluginConnection>)
848 }
849
850 #[allow(clippy::borrowed_box)]
852 pub fn get_plugins_by_type_inventory(&self) -> Vec<(&String, &Box<dyn PluginInventory>)> {
853 get_plugins_by_variant!(self, Plugins::Inventory, &Box<dyn PluginInventory>)
854 }
855
856 #[allow(clippy::borrowed_box)]
858 pub fn get_plugins_by_type_async_inventory(
859 &self,
860 ) -> Vec<(&String, &Box<dyn AsyncPluginInventory>)> {
861 get_plugins_by_variant!(
862 self,
863 Plugins::AsyncInventory,
864 &Box<dyn AsyncPluginInventory>
865 )
866 }
867
868 #[allow(clippy::borrowed_box)]
870 pub fn get_plugins_by_type_processor(&self) -> Vec<(&String, &Box<dyn PluginProcessor>)> {
871 get_plugins_by_variant!(self, Plugins::Processor, &Box<dyn PluginProcessor>)
872 }
873
874 #[allow(clippy::borrowed_box)]
876 pub fn get_plugins_by_type_transform_function(
877 &self,
878 ) -> Vec<(&String, &Box<dyn PluginTransformFunction>)> {
879 get_plugins_by_variant!(
880 self,
881 Plugins::TransformFunction,
882 &Box<dyn PluginTransformFunction>
883 )
884 }
885
886 pub fn deregister_plugin(&mut self, name: &str) -> Option<String> {
888 if let Some(plugin) = self.plugins.remove(name) {
889 log::info!("De-registering plugin: {}", name);
890 Some(plugin.name())
891 } else {
892 None
893 }
894 }
895
896 pub fn deregister_all_plugins(&mut self) -> Vec<String> {
898 let mut deregistered_plugins = Vec::new();
899 for (name, plugin) in self.plugins.drain() {
900 log::info!("De-registering plugin: {}", name);
901 deregistered_plugins.push(plugin.name());
902 }
903 deregistered_plugins
904 }
905
906 pub fn merge(&mut self, other: PluginManager) {
912 self.plugin_path.extend(other.plugin_path);
913 self.libraries.extend(other.libraries);
914
915 for (name, plugin) in other.plugins {
916 if self.plugins.insert(name.clone(), plugin).is_some() {
917 log::info!("Overriding plugin: {}", name);
918 } else {
919 log::info!("Registering merged plugin: {}", name);
920 }
921 }
922 }
923
924 pub fn get_all_plugin_names(&self) -> Vec<&String> {
926 self.plugins.keys().collect()
927 }
928
929 pub fn get_all_plugin_names_and_groups(&self) -> Vec<(String, String)> {
931 self.plugins
932 .iter()
933 .map(|(name, plugin)| (name.clone(), plugin.group_name()))
934 .collect()
935 }
936
937 #[allow(clippy::borrowed_box)]
939 pub fn get_runner_plugin(&self, name: &str) -> Option<&Box<dyn PluginRunner>> {
940 self.plugins.get(name).and_then(|plugin| match plugin {
941 Plugins::Runner(runner) => Some(runner),
942 _ => None,
943 })
944 }
945 pub fn with_path(mut self, path: &str, group: Option<&str>) -> Result<Self, Error> {
946 let path = Path::new(&path);
947 if path.exists() {
948 let path_string = if let Some(path_str) = path.to_str() {
949 path_str.to_string()
950 } else {
951 return Err(Error::new(
952 ErrorKind::InvalidData,
953 "Path contains invalid Unicode",
954 ));
955 };
956 if let Some(group_string) = group {
957 let group_info = HashMap::from([(
958 group_string.to_string(),
959 PluginEntry::Group(HashMap::from([(group_string.to_string(), path_string)])),
960 )]);
961 self.plugin_path.push(group_info);
962 } else {
963 let individual_info =
964 HashMap::from([("base".to_string(), PluginEntry::Individual(path_string))]);
965 self.plugin_path.push(individual_info);
966 };
967 Ok(self)
968 } else {
969 Err(Error::new(
970 ErrorKind::NotFound,
971 format!("FileNotFoundError: {:?}", path.as_os_str()),
972 ))
973 }
974 }
975}
976
977impl TaskProcessorResolver for PluginManager {
978 fn resolve_task_processor(&self, name: &str) -> Option<Arc<dyn TaskProcessor>> {
979 self.get_processor_plugin(name)
980 .map(|processor| processor.processor())
981 }
982}
983
984#[cfg(test)]
985mod tests {
986 use std::path::{Path, PathBuf};
987 use std::process::Command;
988 use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
989 use std::time::{SystemTime, UNIX_EPOCH};
990
991 use super::*;
992 use crate::plugin_types::{
993 AsyncPluginInventory, Plugin, PluginConnection, PluginInventory, PluginProcessor,
994 PluginRunner, PluginTransformFunction,
995 };
996 use genja_core::inventory::{
997 ConnectionKey, Inventory, ResolvedConnectionParams, TransformFunction,
998 };
999 use genja_core::task::{TaskProcessor, Tasks};
1000 use genja_core::{InventoryLoadError, Settings};
1001
1002 fn env_lock() -> MutexGuard<'static, ()> {
1003 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1004 let lock = LOCK.get_or_init(|| Mutex::new(()));
1005 lock.lock().unwrap_or_else(|err| err.into_inner())
1006 }
1007
1008 fn workspace_root() -> PathBuf {
1009 Path::new(env!("CARGO_MANIFEST_DIR"))
1010 .parent()
1011 .unwrap()
1012 .to_path_buf()
1013 }
1014
1015 fn ensure_test_plugins_built() {
1016 static BUILT: OnceLock<()> = OnceLock::new();
1017 BUILT.get_or_init(|| {
1018 let status = Command::new("cargo")
1019 .current_dir(workspace_root())
1020 .args([
1021 "build",
1022 "--quiet",
1023 "-p",
1024 "plugin-mods",
1025 "-p",
1026 "plugin_inventory",
1027 "-p",
1028 "plugin_connection",
1029 ])
1030 .status()
1031 .expect("Failed to run cargo build for test plugins");
1032 assert!(status.success(), "Failed to build test plugins");
1033 });
1034 }
1035
1036 fn set_env_var() -> MutexGuard<'static, ()> {
1037 let guard = env_lock();
1038 ensure_test_plugins_built();
1039 let file_name = match std::env::consts::OS {
1040 "linux" => "Cargo.toml",
1041 "windows" => "Cargo-windows.toml",
1042 "macos" => "Cargo-macos.toml",
1043 _ => "Cargo.toml",
1044 };
1045 let file = format!("../genja-plugin-manager/tests/plugin_mods/{}", file_name);
1046 unsafe {
1047 std::env::set_var("CARGO_MANIFEST_PATH", file);
1048 }
1049 guard
1050 }
1051
1052 fn make_file_path(module_name: &str) -> String {
1053 ensure_test_plugins_built();
1054 let mut path_name = PathBuf::new();
1055 let mut module_name_prefix = String::from(std::env::consts::DLL_PREFIX);
1056 module_name_prefix.push_str(module_name);
1057 path_name.push("..");
1058 path_name.push("target");
1059 path_name.push("debug");
1060 path_name.push(module_name_prefix);
1061 path_name.set_extension(std::env::consts::DLL_EXTENSION);
1062 path_name.to_string_lossy().to_string()
1063 }
1064
1065 fn temp_manifest_path(filename: &str) -> std::path::PathBuf {
1066 let now = SystemTime::now()
1067 .duration_since(UNIX_EPOCH)
1068 .unwrap_or_default()
1069 .as_nanos();
1070 let mut path = std::env::temp_dir();
1071 path.push(format!("genja_plugin_manager_{now}_{filename}"));
1072 path
1073 }
1074
1075 fn temp_file_path(filename: &str) -> std::path::PathBuf {
1076 let now = SystemTime::now()
1077 .duration_since(UNIX_EPOCH)
1078 .unwrap_or_default()
1079 .as_nanos();
1080 let mut path = std::env::temp_dir();
1081 path.push(format!("genja_plugin_manager_{now}_{filename}"));
1082 path
1083 }
1084
1085 #[cfg(target_os = "linux")]
1086 fn system_library_path() -> Option<&'static str> {
1087 let candidates = [
1088 "/lib/x86_64-linux-gnu/libc.so.6",
1089 "/lib64/libc.so.6",
1090 "/usr/lib/x86_64-linux-gnu/libc.so.6",
1091 ];
1092 candidates.iter().copied().find(|p| Path::new(p).exists())
1093 }
1094
1095 #[cfg(target_os = "macos")]
1096 fn system_library_path() -> Option<&'static str> {
1097 let p = "/usr/lib/libSystem.B.dylib";
1098 if Path::new(p).exists() { Some(p) } else { None }
1099 }
1100
1101 #[cfg(target_os = "windows")]
1102 fn system_library_path() -> Option<&'static str> {
1103 let p = "C:\\Windows\\System32\\kernel32.dll";
1104 if Path::new(p).exists() { Some(p) } else { None }
1105 }
1106
1107 #[test]
1108 fn get_plugin_path_test() {
1109 let _env = set_env_var();
1110 let plugin_manager = PluginManager::new();
1111 let metadata = plugin_manager.get_plugin_metadata();
1112 let plugins = metadata.plugins;
1113 match plugins {
1114 Some(plug_entry) => {
1115 for (group, entry) in plug_entry {
1116 match entry {
1117 PluginEntry::Individual(path) => {
1118 assert_eq!(path, make_file_path("plugin_mods"));
1119 }
1120 PluginEntry::Group(path) => {
1121 path.iter().for_each(|(metadata_name, path)| {
1122 assert_eq!(path, &make_file_path("plugin_inventory"));
1123 assert_eq!(metadata_name, "inventory_a");
1124 assert_eq!(group, "inventory");
1125 });
1126 }
1127 }
1128 }
1129 }
1130 None => {
1131 panic!("No plugins found in metadata");
1132 }
1133 }
1134 }
1135
1136 #[test]
1137 fn get_plugin_metadata_test() {
1138 let _env = set_env_var();
1139 let plugin_manager = PluginManager::new();
1140 let metadata = plugin_manager.get_plugin_metadata();
1141 assert!(metadata.plugins.is_some());
1142 assert_eq!(metadata.plugins.clone().unwrap().len(), 2);
1144 }
1145
1146 #[test]
1147 fn get_plugin_metadata_missing_manifest_test() {
1148 let _env = env_lock();
1149 let missing = temp_manifest_path("missing_manifest.toml");
1150 unsafe {
1151 std::env::set_var("CARGO_MANIFEST_PATH", missing.to_string_lossy().to_string());
1152 }
1153 let plugin_manager = PluginManager::new();
1154 let metadata = plugin_manager.get_plugin_metadata();
1155 assert!(metadata.plugins.is_none());
1156 }
1157
1158 #[test]
1159 fn get_plugin_metadata_missing_metadata_section_test() {
1160 let _env = env_lock();
1161 let manifest = temp_manifest_path("no_metadata.toml");
1162 std::fs::write(
1163 &manifest,
1164 "[package]\nname = \"no_metadata\"\nversion = \"0.1.0\"\n",
1165 )
1166 .unwrap();
1167 unsafe {
1168 std::env::set_var(
1169 "CARGO_MANIFEST_PATH",
1170 manifest.to_string_lossy().to_string(),
1171 );
1172 }
1173 let plugin_manager = PluginManager::new();
1174 let metadata = plugin_manager.get_plugin_metadata();
1175 assert!(metadata.plugins.is_none());
1176 let _ = std::fs::remove_file(&manifest);
1177 }
1178
1179 #[test]
1180 fn get_plugin_metadata_invalid_toml_test() {
1181 let _env = env_lock();
1182 let manifest = temp_manifest_path("invalid_toml.toml");
1183 std::fs::write(&manifest, "[package]\nname = \"invalid\"\nversion =\n").unwrap();
1184 unsafe {
1185 std::env::set_var(
1186 "CARGO_MANIFEST_PATH",
1187 manifest.to_string_lossy().to_string(),
1188 );
1189 }
1190 let plugin_manager = PluginManager::new();
1191 let metadata = plugin_manager.get_plugin_metadata();
1192 assert!(metadata.plugins.is_none());
1193 let _ = std::fs::remove_file(&manifest);
1194 }
1195
1196 #[test]
1197 fn activate_plugins_group_invalid_path_returns_error_test() {
1198 let _env = env_lock();
1199 let manifest = temp_manifest_path("group_invalid_path.toml");
1200 std::fs::write(
1201 &manifest,
1202 r#"[package]
1203name = "invalid_group"
1204version = "0.1.0"
1205
1206[package.metadata.plugins.inventory]
1207inventory_a = "../this/path/does/not/exist.so"
1208"#,
1209 )
1210 .unwrap();
1211 unsafe {
1212 std::env::set_var(
1213 "CARGO_MANIFEST_PATH",
1214 manifest.to_string_lossy().to_string(),
1215 );
1216 }
1217 let plugin_manager = PluginManager::new();
1218 let result = plugin_manager.activate_plugins();
1219 assert!(result.is_err());
1220 let _ = std::fs::remove_file(&manifest);
1221 }
1222
1223 #[test]
1224 fn activate_plugins_test() {
1225 let _env = set_env_var();
1226 let mut plugin_manager = PluginManager::new();
1227 plugin_manager = plugin_manager.activate_plugins().unwrap();
1228 assert!(plugin_manager.get_plugin("plugin_a").is_some());
1229 assert_eq!(plugin_manager.plugins.len(), 3);
1230 }
1231
1232 #[test]
1233 #[should_panic]
1234 fn activate_plugins_and_panic_test() {
1236 let _env = set_env_var();
1237 let mut plugin_manager = PluginManager::new();
1238 plugin_manager = plugin_manager.activate_plugins().unwrap();
1239 _ = plugin_manager.activate_plugins().unwrap();
1240 }
1241
1242 #[test]
1243 fn load_plugin_test() {
1244 let plugin_manager = PluginManager::new();
1245 let filename = make_file_path("plugin_mods");
1246 let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
1247 assert_eq!(plugins.len(), 2);
1248 assert_eq!(plugins[0].name(), "plugin_a");
1249 }
1250
1251 #[test]
1252 fn load_plugin_and_panic_test() {
1253 let plugin_manager = PluginManager::new();
1254 let filename = make_file_path("plugin_mods");
1255 let (_library, _) = plugin_manager.load_plugin(&filename).unwrap();
1256 let filename = make_file_path("plugin_mods");
1257 let (_library, plugins) = plugin_manager.load_plugin(&filename).unwrap();
1258 assert_eq!(plugins.len(), 2);
1259 assert_eq!(plugins[0].name(), "plugin_a");
1260 }
1261
1262 #[test]
1263 fn load_plugin_missing_file_test() {
1264 let plugin_manager = PluginManager::new();
1265 let missing = temp_file_path("missing_plugin_file.so");
1266 let result = plugin_manager.load_plugin(&missing.to_string_lossy());
1267 assert!(result.is_err());
1268 }
1269
1270 #[test]
1271 fn load_plugin_invalid_library_test() {
1272 let plugin_manager = PluginManager::new();
1273 let file = temp_file_path("not_a_library.so");
1274 std::fs::write(&file, "not a library").unwrap();
1275 let result = plugin_manager.load_plugin(&file.to_string_lossy());
1276 assert!(result.is_err());
1277 let _ = std::fs::remove_file(&file);
1278 }
1279
1280 #[test]
1281 fn load_plugin_missing_symbol_test() {
1282 let plugin_manager = PluginManager::new();
1283 let Some(path) = system_library_path() else {
1284 return;
1285 };
1286 let result = plugin_manager.load_plugin(path);
1287 assert!(result.is_err());
1288 }
1289
1290 #[test]
1291 fn activate_plugins_with_groups_test() {
1292 let _env = set_env_var();
1293 let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1294
1295 let inventory_plugins = plugin_manager.get_plugins_by_type_connection();
1297 assert_eq!(inventory_plugins.len(), 2);
1298
1299 let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
1301 assert_eq!(inventory_plugins.len(), 1);
1302 assert_eq!(inventory_plugins[0].1.name(), "inventory_a");
1303
1304 assert_eq!(plugin_manager.plugins.len(), 3);
1305 }
1306
1307 #[test]
1308 fn get_all_plugin_names_and_groups_test() {
1309 let _env = set_env_var();
1310 let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1311 let all_plugins = plugin_manager.get_all_plugin_names_and_groups();
1312 assert_eq!(all_plugins.len(), 3);
1313 all_plugins
1314 .iter()
1315 .for_each(|(name, group)| match name.as_str() {
1316 "plugin_a" => assert_eq!(group, "Connection"),
1317 "plugin_b" => assert_eq!(group, "Connection"),
1318 "inventory_a" => assert_eq!(group, "Inventory"),
1319 _ => panic!("Unexpected plugin name"),
1320 });
1321 }
1322
1323 #[test]
1324 fn deregister_plugin_test() {
1325 let _env = set_env_var();
1326 let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
1327 assert_eq!(plugin_manager.plugins.len(), 3);
1328
1329 let plugin_name = plugin_manager.deregister_plugin("plugin_a");
1331 if let Some(plugin) = plugin_name {
1332 assert_eq!(plugin, "plugin_a");
1333 assert_eq!(plugin_manager.plugins.len(), 2);
1334 }
1335
1336 let plugin_name = plugin_manager.deregister_plugin("inventory_a");
1338 if let Some(plugin) = plugin_name {
1339 assert_eq!(plugin, "inventory_a");
1340 assert_eq!(plugin_manager.plugins.len(), 1);
1341 }
1342
1343 let plugin_name = plugin_manager.deregister_plugin("non_existent_plugin");
1345 assert_eq!(plugin_name, None);
1346 }
1347
1348 #[test]
1349 fn deregister_all_plugins_test() {
1350 let _env = set_env_var();
1351 let mut plugin_manager = PluginManager::new().activate_plugins().unwrap();
1352 assert_eq!(plugin_manager.plugins.len(), 3);
1353
1354 let num_plugins_deregistered = plugin_manager.deregister_all_plugins();
1356 assert_eq!(num_plugins_deregistered.len(), 3);
1357 assert_eq!(plugin_manager.plugins.len(), 0);
1358 }
1359
1360 #[test]
1361 fn plugin_manager_new_test() {
1362 let _env = set_env_var();
1363 let mut plugin_manager = PluginManager::new();
1364 assert_eq!(plugin_manager.plugins.len(), 0);
1365 plugin_manager = plugin_manager.activate_plugins().unwrap();
1366 assert_eq!(plugin_manager.plugins.len(), 3);
1367 }
1368
1369 #[test]
1370 fn get_plugins_by_type_test() {
1371 let _env = set_env_var();
1372 let plugin_manager = PluginManager::new().activate_plugins().unwrap();
1373 let connection_plugins = plugin_manager.get_plugins_by_type_connection();
1374 assert_eq!(connection_plugins.len(), 2);
1375
1376 let base_plugin_names: Vec<&str> = connection_plugins
1378 .iter()
1379 .map(|(name, _)| name.as_str())
1380 .collect();
1381 assert!(base_plugin_names.contains(&"plugin_a"));
1382 assert!(base_plugin_names.contains(&"plugin_b"));
1383
1384 for (name, plugin) in connection_plugins {
1386 let debug_output = format!("{:?}", plugin);
1387 assert!(debug_output.contains("ConnectionPlugin"));
1388 assert!(debug_output.contains(name));
1389 }
1390
1391 let inventory_plugins = plugin_manager.get_plugins_by_type_inventory();
1392 assert_eq!(inventory_plugins.len(), 1);
1393 }
1394
1395 #[test]
1396 fn with_path_test() {
1397 let _env = set_env_var();
1398 let path = make_file_path("plugin_connection");
1399 let plugin_manager = PluginManager::new()
1400 .with_path(&path, None)
1401 .unwrap()
1402 .activate_plugins()
1403 .unwrap();
1404 assert_eq!(plugin_manager.plugins.len(), 4);
1405 }
1406
1407 #[test]
1408 fn with_path_group_loads_plugins() {
1409 let _env = set_env_var();
1410 let path = make_file_path("plugin_connection");
1411 let plugin_manager = PluginManager::new()
1412 .with_path(&path, Some("extra"))
1413 .unwrap()
1414 .activate_plugins()
1415 .unwrap();
1416 assert_eq!(plugin_manager.plugins.len(), 4);
1417 }
1418
1419 #[test]
1420 fn with_path_not_found_test() {
1421 let missing = temp_file_path("missing_with_path_plugin.so");
1422 let result = PluginManager::new().with_path(&missing.to_string_lossy(), None);
1423 assert!(result.is_err());
1424 if let Err(err) = result {
1425 assert_eq!(err.kind(), ErrorKind::NotFound);
1426 }
1427 }
1428
1429 #[test]
1430 #[should_panic]
1431 fn with_path_duplicate_plugin_panics_test() {
1432 let _env = set_env_var();
1433 let duplicate = make_file_path("plugin_mods");
1434 let _ = PluginManager::new()
1435 .with_path(&duplicate, None)
1436 .unwrap()
1437 .activate_plugins()
1438 .unwrap();
1439 }
1440
1441 #[derive(Debug)]
1442 struct DummyConnection {
1443 name: &'static str,
1444 }
1445
1446 impl Plugin for DummyConnection {
1447 fn name(&self) -> String {
1448 self.name.to_string()
1449 }
1450 }
1451
1452 #[async_trait]
1453 impl PluginConnection for DummyConnection {
1454 fn create(&self, _key: &ConnectionKey) -> Box<dyn PluginConnection> {
1455 Box::new(Self { name: self.name })
1456 }
1457
1458 async fn open(&mut self, _params: &ResolvedConnectionParams) -> Result<(), String> {
1459 Ok(())
1460 }
1461
1462 fn close(&mut self) -> ConnectionKey {
1463 ConnectionKey::new("dummy", "conn")
1464 }
1465
1466 fn is_alive(&self) -> bool {
1467 false
1468 }
1469 }
1470
1471 #[derive(Debug)]
1472 struct DummyInventory {
1473 name: &'static str,
1474 }
1475
1476 impl Plugin for DummyInventory {
1477 fn name(&self) -> String {
1478 self.name.to_string()
1479 }
1480 }
1481
1482 impl PluginInventory for DummyInventory {
1483 fn load(
1484 &self,
1485 _settings: &Settings,
1486 _plugins: &PluginManager,
1487 ) -> Result<Inventory, InventoryLoadError> {
1488 Ok(Inventory::builder().build())
1489 }
1490 }
1491
1492 #[derive(Debug)]
1493 struct DummyAsyncInventory {
1494 name: &'static str,
1495 }
1496
1497 impl Plugin for DummyAsyncInventory {
1498 fn name(&self) -> String {
1499 self.name.to_string()
1500 }
1501 }
1502
1503 #[async_trait]
1504 impl AsyncPluginInventory for DummyAsyncInventory {
1505 async fn load_async(
1506 &self,
1507 _settings: &Settings,
1508 _plugins: &PluginManager,
1509 ) -> Result<Inventory, InventoryLoadError> {
1510 Ok(Inventory::builder().build())
1511 }
1512 }
1513
1514 #[derive(Debug)]
1515 struct DummyRunner {
1516 name: &'static str,
1517 }
1518
1519 impl Plugin for DummyRunner {
1520 fn name(&self) -> String {
1521 self.name.to_string()
1522 }
1523 }
1524
1525 #[async_trait]
1526 impl PluginRunner for DummyRunner {
1527 async fn run_task(
1528 &self,
1529 _task: &genja_core::task::TaskDefinition,
1530 _hosts: &genja_core::inventory::Hosts,
1531 _connection_resolver: Option<
1532 std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
1533 >,
1534 _runner_config: &genja_core::settings::RunnerConfig,
1535 _max_depth: usize,
1536 ) -> Result<genja_core::task::TaskResults, genja_core::GenjaError> {
1537 Ok(genja_core::task::TaskResults::new(self.name))
1538 }
1539
1540 async fn run_tasks(
1541 &self,
1542 _tasks: &Tasks,
1543 _hosts: &genja_core::inventory::Hosts,
1544 _connection_resolver: Option<
1545 std::sync::Arc<dyn genja_core::task::TaskConnectionResolver>,
1546 >,
1547 _runner_config: &genja_core::settings::RunnerConfig,
1548 _max_depth: usize,
1549 ) -> Result<Vec<genja_core::task::TaskResults>, genja_core::GenjaError> {
1550 Ok(Vec::new())
1551 }
1552 }
1553
1554 #[derive(Debug)]
1555 struct DummyTransform {
1556 name: &'static str,
1557 }
1558
1559 impl Plugin for DummyTransform {
1560 fn name(&self) -> String {
1561 self.name.to_string()
1562 }
1563 }
1564
1565 impl PluginTransformFunction for DummyTransform {
1566 fn transform_function(&self) -> TransformFunction {
1567 TransformFunction::new(|host, _| host.clone())
1568 }
1569 }
1570
1571 #[derive(Debug)]
1572 struct DummyProcessorPlugin {
1573 name: &'static str,
1574 }
1575
1576 impl Plugin for DummyProcessorPlugin {
1577 fn name(&self) -> String {
1578 self.name.to_string()
1579 }
1580 }
1581
1582 impl PluginProcessor for DummyProcessorPlugin {
1583 fn processor(&self) -> Arc<dyn TaskProcessor> {
1584 Arc::new(DummyProcessor)
1585 }
1586 }
1587
1588 struct DummyProcessor;
1589
1590 impl TaskProcessor for DummyProcessor {}
1591
1592 #[test]
1593 fn get_plugin_and_typed_getters_match_variants() {
1594 let mut manager = PluginManager::new();
1595 manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1596 name: "conn",
1597 })));
1598 manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
1599 manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
1600 name: "ainv",
1601 })));
1602 manager.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
1603 manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
1604 name: "tf",
1605 })));
1606
1607 assert!(manager.get_plugin("conn").is_some());
1608 assert!(manager.get_plugin("inv").is_some());
1609 assert!(manager.get_plugin("ainv").is_some());
1610 assert!(manager.get_plugin("run").is_some());
1611 assert!(manager.get_plugin("tf").is_some());
1612 assert!(manager.get_plugin("missing").is_none());
1613
1614 assert!(manager.get_connection_plugin("conn").is_some());
1615 assert!(manager.get_connection_plugin("inv").is_none());
1616 assert!(manager.get_connection_plugin("ainv").is_none());
1617 assert!(manager.get_connection_plugin("run").is_none());
1618 assert!(manager.get_connection_plugin("tf").is_none());
1619
1620 assert!(manager.get_inventory_plugin("inv").is_some());
1621 assert!(manager.get_inventory_plugin("conn").is_none());
1622 assert!(manager.get_inventory_plugin("ainv").is_none());
1623 assert!(manager.get_inventory_plugin("run").is_none());
1624 assert!(manager.get_inventory_plugin("tf").is_none());
1625
1626 assert!(manager.get_async_inventory_plugin("ainv").is_some());
1627 assert!(manager.get_async_inventory_plugin("inv").is_none());
1628 assert!(manager.get_async_inventory_plugin("conn").is_none());
1629
1630 assert!(manager.get_runner_plugin("run").is_some());
1631 assert!(manager.get_runner_plugin("conn").is_none());
1632 assert!(manager.get_runner_plugin("inv").is_none());
1633 assert!(manager.get_runner_plugin("ainv").is_none());
1634 assert!(manager.get_runner_plugin("tf").is_none());
1635
1636 assert!(manager.get_transform_function_plugin("tf").is_some());
1637 assert!(manager.get_transform_function_plugin("conn").is_none());
1638 assert!(manager.get_transform_function_plugin("inv").is_none());
1639 assert!(manager.get_transform_function_plugin("ainv").is_none());
1640 assert!(manager.get_transform_function_plugin("run").is_none());
1641 }
1642
1643 #[test]
1644 fn processor_plugin_getters_and_resolver_match_processor_variant() {
1645 let mut manager = PluginManager::new();
1646 manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1647 name: "conn",
1648 })));
1649 manager.register_plugin(Plugins::Processor(Box::new(DummyProcessorPlugin {
1650 name: "audit",
1651 })));
1652
1653 assert!(manager.get_processor_plugin("audit").is_some());
1654 assert!(manager.get_processor_plugin("conn").is_none());
1655 assert!(manager.get_processor_plugin("missing").is_none());
1656
1657 let processors = manager.get_plugins_by_type_processor();
1658 assert_eq!(processors.len(), 1);
1659 assert_eq!(processors[0].0.as_str(), "audit");
1660 assert_eq!(processors[0].1.name(), "audit");
1661
1662 assert!(manager.resolve_task_processor("audit").is_some());
1663 assert!(manager.resolve_task_processor("conn").is_none());
1664 assert!(manager.resolve_task_processor("missing").is_none());
1665 }
1666
1667 #[test]
1668 #[should_panic(expected = "Plugin 'dup' already registered")]
1669 fn register_plugin_duplicate_name_panics() {
1670 let mut manager = PluginManager::new();
1671 manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1672 name: "dup",
1673 })));
1674 manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1675 name: "dup",
1676 })));
1677 }
1678
1679 #[test]
1680 fn get_plugins_by_type_transform_function_and_all_names() {
1681 let mut manager = PluginManager::new();
1682 manager.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1683 name: "conn",
1684 })));
1685 manager.register_plugin(Plugins::Inventory(Box::new(DummyInventory { name: "inv" })));
1686 manager.register_plugin(Plugins::AsyncInventory(Box::new(DummyAsyncInventory {
1687 name: "ainv",
1688 })));
1689 manager.register_plugin(Plugins::TransformFunction(Box::new(DummyTransform {
1690 name: "tf",
1691 })));
1692
1693 let transforms = manager.get_plugins_by_type_transform_function();
1694 assert_eq!(transforms.len(), 1);
1695 assert_eq!(transforms[0].0.as_str(), "tf");
1696
1697 let async_inventory_plugins = manager.get_plugins_by_type_async_inventory();
1698 assert_eq!(async_inventory_plugins.len(), 1);
1699 assert_eq!(async_inventory_plugins[0].0.as_str(), "ainv");
1700
1701 let names = manager.get_all_plugin_names();
1702 assert_eq!(names.len(), 4);
1703 assert!(names.contains(&&"conn".to_string()));
1704 assert!(names.contains(&&"inv".to_string()));
1705 assert!(names.contains(&&"ainv".to_string()));
1706 assert!(names.contains(&&"tf".to_string()));
1707 }
1708
1709 #[test]
1710 fn merge_overrides_existing_plugins_by_name() {
1711 let mut base = PluginManager::new();
1712 base.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1713 name: "conn",
1714 })));
1715
1716 let mut custom = PluginManager::new();
1717 custom.register_plugin(Plugins::Runner(Box::new(DummyRunner { name: "run" })));
1718 custom.register_plugin(Plugins::Connection(Box::new(DummyConnection {
1719 name: "conn",
1720 })));
1721
1722 base.merge(custom);
1723
1724 assert!(base.get_connection_plugin("conn").is_some());
1725 assert!(base.get_runner_plugin("run").is_some());
1726 assert_eq!(base.get_all_plugin_names().len(), 2);
1727 }
1728}