1use std::ffi::c_void;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use indexmap::IndexMap;
22use libloading::{Library, Symbol};
23
24use drasi_plugin_sdk::ffi::{
25 FfiPluginRegistration, LifecycleCallbackFn, LogCallbackFn, PluginMetadata,
26};
27
28use crate::proxies::bootstrap_provider::BootstrapPluginProxy;
29use crate::proxies::identity_provider::IdentityProviderPluginProxy;
30use crate::proxies::reaction::ReactionPluginProxy;
31use crate::proxies::source::SourcePluginProxy;
32
33#[derive(Debug, Clone)]
35pub struct PluginLoaderConfig {
36 pub plugin_dir: PathBuf,
38 pub file_patterns: Vec<String>,
40}
41
42pub struct LoadedPlugin {
44 pub source_plugins: Vec<SourcePluginProxy>,
46 pub reaction_plugins: Vec<ReactionPluginProxy>,
48 pub bootstrap_plugins: Vec<BootstrapPluginProxy>,
50 pub identity_provider_plugins: Vec<IdentityProviderPluginProxy>,
52 pub metadata_info: Option<String>,
54 pub file_path: PathBuf,
56 _library: Arc<Library>,
58}
59
60impl Drop for LoadedPlugin {
61 fn drop(&mut self) {
62 self.source_plugins.clear();
64 self.reaction_plugins.clear();
65 self.bootstrap_plugins.clear();
66
67 std::mem::forget(self._library.clone());
79 }
80}
81
82const CDYLIB_EXTENSIONS: &[&str] = &[".dylib", ".so", ".dll"];
84
85const ALL_KNOWN_EXTENSIONS: &[&str] = &[".dylib", ".so", ".dll", ".rlib", ".rmeta", ".d"];
87
88pub struct PluginLoader {
90 config: PluginLoaderConfig,
91}
92
93impl PluginLoader {
94 pub fn new(config: PluginLoaderConfig) -> Self {
95 Self { config }
96 }
97
98 pub fn load_all(
105 &self,
106 log_ctx: *mut c_void,
107 log_callback: LogCallbackFn,
108 lifecycle_ctx: *mut c_void,
109 lifecycle_callback: LifecycleCallbackFn,
110 ) -> anyhow::Result<Vec<LoadedPlugin>> {
111 let mut plugins = Vec::new();
112 let plugin_dir = &self.config.plugin_dir;
113
114 if !plugin_dir.exists() {
115 log::warn!("Plugin directory does not exist: {}", plugin_dir.display());
116 return Ok(plugins);
117 }
118
119 let candidates = discover_plugin_candidates(plugin_dir, &self.config.file_patterns);
121
122 for (plugin_name, files) in &candidates {
124 let cdylib_files: Vec<&PathBuf> = files
125 .iter()
126 .filter(|p| {
127 CDYLIB_EXTENSIONS
128 .iter()
129 .any(|ext| p.to_string_lossy().ends_with(ext))
130 })
131 .collect();
132
133 if cdylib_files.is_empty() {
134 log::debug!(
135 "Plugin '{}': no cdylib files found (skipping {} non-cdylib file(s))",
136 plugin_name,
137 files.len()
138 );
139 continue;
140 }
141
142 if cdylib_files.len() > 1 {
143 log::error!(
144 "Plugin '{}': found {} cdylib files — ambiguous. \
145 Remove duplicates and keep only one: {:?}",
146 plugin_name,
147 cdylib_files.len(),
148 cdylib_files
149 );
150 continue;
151 }
152
153 let path = cdylib_files[0];
155 match self.load_plugin(
156 path,
157 log_ctx,
158 log_callback,
159 lifecycle_ctx,
160 lifecycle_callback,
161 ) {
162 Ok(plugin) => {
163 log::info!(
164 "Loaded plugin: {} ({})",
165 path.display(),
166 plugin.metadata_info.as_deref().unwrap_or("no metadata")
167 );
168 plugins.push(plugin);
169 }
170 Err(e) => {
171 log::error!("Failed to load plugin {}: {}", path.display(), e);
172 }
173 }
174 }
175
176 Ok(plugins)
177 }
178
179 pub fn load_plugin(
181 &self,
182 path: &Path,
183 log_ctx: *mut c_void,
184 log_callback: LogCallbackFn,
185 lifecycle_ctx: *mut c_void,
186 lifecycle_callback: LifecycleCallbackFn,
187 ) -> anyhow::Result<LoadedPlugin> {
188 load_plugin_from_path(
189 path,
190 log_ctx,
191 log_callback,
192 lifecycle_ctx,
193 lifecycle_callback,
194 )
195 }
196}
197
198pub fn load_plugin_from_path(
207 path: &Path,
208 log_ctx: *mut c_void,
209 log_callback: LogCallbackFn,
210 lifecycle_ctx: *mut c_void,
211 lifecycle_callback: LifecycleCallbackFn,
212) -> anyhow::Result<LoadedPlugin> {
213 let lib = Arc::new(unsafe {
214 Library::new(path)
215 .map_err(|e| anyhow::anyhow!("Failed to load {}: {}", path.display(), e))?
216 });
217
218 let metadata_info = read_plugin_metadata(&lib);
220 validate_plugin_metadata(&lib, path)?;
221
222 let init_fn: Symbol<unsafe extern "C" fn() -> *mut FfiPluginRegistration> = unsafe {
224 lib.get(b"drasi_plugin_init").map_err(|e| {
225 anyhow::anyhow!("Missing drasi_plugin_init in {}: {}", path.display(), e)
226 })?
227 };
228
229 let reg_ptr = unsafe { init_fn() };
230 if reg_ptr.is_null() {
231 return Err(anyhow::anyhow!(
232 "drasi_plugin_init returned null (init panicked?) in {}",
233 path.display()
234 ));
235 }
236
237 let registration = unsafe { Box::from_raw(reg_ptr) };
238
239 (registration.set_log_callback)(log_ctx, log_callback);
241 (registration.set_lifecycle_callback)(lifecycle_ctx, lifecycle_callback);
242
243 let source_vtables =
247 if !registration.source_plugins.is_null() && registration.source_plugin_count > 0 {
248 Some(unsafe {
249 Vec::from_raw_parts(
250 registration.source_plugins,
251 registration.source_plugin_count,
252 registration.source_plugin_count,
253 )
254 })
255 } else {
256 None
257 };
258
259 let reaction_vtables =
260 if !registration.reaction_plugins.is_null() && registration.reaction_plugin_count > 0 {
261 Some(unsafe {
262 Vec::from_raw_parts(
263 registration.reaction_plugins,
264 registration.reaction_plugin_count,
265 registration.reaction_plugin_count,
266 )
267 })
268 } else {
269 None
270 };
271
272 let bootstrap_vtables =
273 if !registration.bootstrap_plugins.is_null() && registration.bootstrap_plugin_count > 0 {
274 Some(unsafe {
275 Vec::from_raw_parts(
276 registration.bootstrap_plugins,
277 registration.bootstrap_plugin_count,
278 registration.bootstrap_plugin_count,
279 )
280 })
281 } else {
282 None
283 };
284
285 let identity_provider_vtables: Option<
293 Vec<drasi_plugin_sdk::ffi::IdentityProviderPluginVtable>,
294 > = None;
295
296 std::mem::forget(registration);
298
299 let mut source_plugins = Vec::new();
301 let mut reaction_plugins = Vec::new();
302 let mut bootstrap_plugins = Vec::new();
303 let mut identity_provider_plugins = Vec::new();
304
305 for v in source_vtables.into_iter().flatten() {
306 source_plugins.push(SourcePluginProxy::new(v, lib.clone()));
307 }
308
309 for v in reaction_vtables.into_iter().flatten() {
310 reaction_plugins.push(ReactionPluginProxy::new(v, lib.clone()));
311 }
312
313 for v in bootstrap_vtables.into_iter().flatten() {
314 bootstrap_plugins.push(BootstrapPluginProxy::new(v, lib.clone()));
315 }
316
317 for v in identity_provider_vtables.into_iter().flatten() {
318 identity_provider_plugins.push(IdentityProviderPluginProxy::new(v, lib.clone()));
319 }
320
321 Ok(LoadedPlugin {
322 source_plugins,
323 reaction_plugins,
324 bootstrap_plugins,
325 identity_provider_plugins,
326 metadata_info,
327 file_path: path.to_path_buf(),
328 _library: lib,
329 })
330}
331
332fn read_plugin_metadata(lib: &Library) -> Option<String> {
334 unsafe {
335 if let Ok(meta_fn) =
336 lib.get::<unsafe extern "C" fn() -> *const PluginMetadata>(b"drasi_plugin_metadata")
337 {
338 let meta_ptr = meta_fn();
339 if !meta_ptr.is_null() {
340 let meta = &*meta_ptr;
341 let sdk_ver = meta.sdk_version.to_string();
342 let core_ver = meta.core_version.to_string();
343 let plugin_ver = meta.plugin_version.to_string();
344 let target = meta.target_triple.to_string();
345 let commit = meta.git_commit.to_string();
346 let built = meta.build_timestamp.to_string();
347 Some(format!(
348 "sdk={sdk_ver} core={core_ver} plugin={plugin_ver} target={target} commit={commit} built={built}"
349 ))
350 } else {
351 None
352 }
353 } else {
354 None
355 }
356 }
357}
358
359fn validate_plugin_metadata(lib: &Library, path: &Path) -> anyhow::Result<()> {
364 let meta_fn = unsafe {
365 match lib.get::<unsafe extern "C" fn() -> *const PluginMetadata>(b"drasi_plugin_metadata") {
366 Ok(f) => f,
367 Err(_) => {
368 log::warn!(
369 "Plugin '{}' does not export drasi_plugin_metadata — skipping version check",
370 path.display()
371 );
372 return Ok(());
373 }
374 }
375 };
376
377 let meta_ptr = unsafe { meta_fn() };
378 if meta_ptr.is_null() {
379 log::warn!(
380 "Plugin '{}' returned null metadata — skipping version check",
381 path.display()
382 );
383 return Ok(());
384 }
385
386 let meta = unsafe { &*meta_ptr };
387 let plugin_sdk_version = unsafe { meta.sdk_version.to_string() };
388 let host_sdk_version = drasi_plugin_sdk::ffi::metadata::FFI_SDK_VERSION;
389
390 let plugin_parts: Vec<&str> = plugin_sdk_version.split('.').collect();
392 let host_parts: Vec<&str> = host_sdk_version.split('.').collect();
393
394 let plugin_major_minor = format!(
395 "{}.{}",
396 plugin_parts.first().unwrap_or(&"0"),
397 plugin_parts.get(1).unwrap_or(&"0")
398 );
399 let host_major_minor = format!(
400 "{}.{}",
401 host_parts.first().unwrap_or(&"0"),
402 host_parts.get(1).unwrap_or(&"0")
403 );
404
405 if plugin_major_minor != host_major_minor {
406 anyhow::bail!(
407 "Plugin '{}' SDK version mismatch: plugin={}, host={}. \
408 Major.minor versions must match ({} != {}).",
409 path.display(),
410 plugin_sdk_version,
411 host_sdk_version,
412 plugin_major_minor,
413 host_major_minor,
414 );
415 }
416
417 let plugin_target = unsafe { meta.target_triple.to_string() };
419 let host_target = drasi_plugin_sdk::ffi::metadata::TARGET_TRIPLE;
420 if plugin_target != host_target {
421 anyhow::bail!(
422 "Plugin '{}' target mismatch: plugin={}, host={}. \
423 Plugins must be built for the same target platform.",
424 path.display(),
425 plugin_target,
426 host_target,
427 );
428 }
429
430 log::debug!(
431 "Plugin '{}' version check passed: sdk={} target={}",
432 path.display(),
433 plugin_sdk_version,
434 plugin_target
435 );
436
437 Ok(())
438}
439
440fn discover_plugin_candidates(dir: &Path, patterns: &[String]) -> IndexMap<String, Vec<PathBuf>> {
445 let mut groups: IndexMap<String, Vec<PathBuf>> = IndexMap::new();
446
447 let entries = match std::fs::read_dir(dir) {
448 Ok(e) => e,
449 Err(_) => return groups,
450 };
451
452 for entry in entries.flatten() {
453 let path = entry.path();
454 if !path.is_file() {
455 continue;
456 }
457 let file_name = path
458 .file_name()
459 .map(|n| n.to_string_lossy().to_string())
460 .unwrap_or_default();
461
462 let base_name = ALL_KNOWN_EXTENSIONS
464 .iter()
465 .find_map(|ext| file_name.strip_suffix(ext))
466 .unwrap_or(&file_name)
467 .to_string();
468
469 let matched = patterns.iter().any(|pattern| {
471 let pat = ALL_KNOWN_EXTENSIONS
472 .iter()
473 .find_map(|ext| pattern.strip_suffix(ext))
474 .unwrap_or(pattern);
475 matches_glob(pat, &base_name)
476 });
477
478 if matched {
479 groups.entry(base_name).or_default().push(path);
480 }
481 }
482
483 groups
484}
485
486fn matches_glob(pattern: &str, name: &str) -> bool {
488 if let Some(prefix) = pattern.strip_suffix('*') {
489 name.starts_with(prefix)
490 } else if let Some((prefix, suffix)) = pattern.split_once('*') {
491 name.starts_with(prefix) && name.ends_with(suffix)
492 } else {
493 name == pattern
494 }
495}
496
497pub fn plugin_path(dir: &Path, name: &str) -> PathBuf {
499 if cfg!(target_os = "macos") {
500 dir.join(format!("lib{name}.dylib"))
501 } else if cfg!(target_os = "windows") {
502 dir.join(format!("{name}.dll"))
503 } else {
504 dir.join(format!("lib{name}.so"))
505 }
506}
507
508pub const DEFAULT_PLUGIN_FILE_PATTERNS: &[&str] = &[
513 "libdrasi_source_*",
514 "libdrasi_reaction_*",
515 "libdrasi_bootstrap_*",
516 "drasi_source_*",
517 "drasi_reaction_*",
518 "drasi_bootstrap_*",
519];
520
521pub const PLUGIN_BINARY_EXTENSIONS: &[&str] = CDYLIB_EXTENSIONS;
523
524pub fn is_plugin_binary(name: &str) -> bool {
526 CDYLIB_EXTENSIONS.iter().any(|ext| name.ends_with(ext))
527}
528
529pub fn plugin_kind_from_filename(filename: &str) -> Option<String> {
538 let stem = if let Some(stem) = filename.strip_suffix(".so") {
539 stem.strip_prefix("lib")?
540 } else if let Some(stem) = filename.strip_suffix(".dll") {
541 stem
542 } else if let Some(stem) = filename.strip_suffix(".dylib") {
543 stem.strip_prefix("lib")?
544 } else {
545 return None;
546 };
547
548 let stem = stem.strip_prefix("drasi_")?;
549 let mut parts = stem.splitn(2, '_');
550 let ptype = parts.next()?;
551 let kind = parts.next()?.replace('_', "-");
552 Some(format!("{ptype}/{kind}"))
553}
554
555#[derive(Debug, Clone)]
560pub struct PluginMetadataSummary {
561 pub plugin_id: String,
562 pub version: String,
563 pub sdk_version: String,
564 pub core_version: String,
565 pub target_triple: String,
566 pub git_commit: String,
567 pub build_timestamp: String,
568 pub file_path: PathBuf,
569}
570
571pub fn scan_plugin_metadata(path: &Path) -> Option<PluginMetadataSummary> {
579 let lib = unsafe { Library::new(path).ok()? };
580 let meta_fn = unsafe {
581 lib.get::<unsafe extern "C" fn() -> *const PluginMetadata>(b"drasi_plugin_metadata")
582 .ok()?
583 };
584 let meta_ptr = unsafe { meta_fn() };
585 if meta_ptr.is_null() {
586 return None;
587 }
588 let meta = unsafe { &*meta_ptr };
589 let sdk_version = unsafe { meta.sdk_version.to_string() };
590 let core_version = unsafe { meta.core_version.to_string() };
591 let plugin_version = unsafe { meta.plugin_version.to_string() };
592 let target_triple = unsafe { meta.target_triple.to_string() };
593 let git_commit = unsafe { meta.git_commit.to_string() };
594 let build_timestamp = unsafe { meta.build_timestamp.to_string() };
595
596 let plugin_id = path
598 .file_name()
599 .and_then(|f| f.to_str())
600 .and_then(plugin_kind_from_filename)
601 .unwrap_or_default();
602
603 drop(lib);
607
608 Some(PluginMetadataSummary {
609 plugin_id,
610 version: plugin_version,
611 sdk_version,
612 core_version,
613 target_triple,
614 git_commit,
615 build_timestamp,
616 file_path: path.to_path_buf(),
617 })
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use std::fs;
624
625 fn setup_temp_dir(files: &[&str]) -> tempfile::TempDir {
627 let dir = tempfile::tempdir().unwrap();
628 for f in files {
629 fs::write(dir.path().join(f), b"").unwrap();
630 }
631 dir
632 }
633
634 #[test]
637 fn test_matches_glob_prefix_wildcard() {
638 assert!(matches_glob("libdrasi_source_*", "libdrasi_source_mock"));
639 assert!(matches_glob("libdrasi_source_*", "libdrasi_source_http"));
640 assert!(!matches_glob("libdrasi_source_*", "libdrasi_reaction_log"));
641 }
642
643 #[test]
644 fn test_matches_glob_exact() {
645 assert!(matches_glob("libdrasi_source_mock", "libdrasi_source_mock"));
646 assert!(!matches_glob(
647 "libdrasi_source_mock",
648 "libdrasi_source_http"
649 ));
650 }
651
652 #[test]
653 fn test_matches_glob_middle_wildcard() {
654 assert!(matches_glob("lib*mock", "libdrasi_source_mock"));
655 assert!(!matches_glob("lib*mock", "libdrasi_source_http"));
656 }
657
658 #[test]
661 fn test_discover_groups_by_base_name() {
662 let dir = setup_temp_dir(&[
663 "libdrasi_source_mock.dylib",
664 "libdrasi_source_mock.rlib",
665 "libdrasi_source_mock.d",
666 ]);
667 let patterns = vec!["libdrasi_source_*".to_string()];
668 let groups = discover_plugin_candidates(dir.path(), &patterns);
669
670 assert_eq!(groups.len(), 1);
671 assert!(groups.contains_key("libdrasi_source_mock"));
672 assert_eq!(groups["libdrasi_source_mock"].len(), 3);
673 }
674
675 #[test]
676 fn test_discover_ignores_non_matching_files() {
677 let dir = setup_temp_dir(&[
678 "libdrasi_source_mock.dylib",
679 "unrelated_file.txt",
680 "libfoo.so",
681 ]);
682 let patterns = vec!["libdrasi_source_*".to_string()];
683 let groups = discover_plugin_candidates(dir.path(), &patterns);
684
685 assert_eq!(groups.len(), 1);
686 assert!(groups.contains_key("libdrasi_source_mock"));
687 }
688
689 #[test]
690 fn test_discover_multiple_plugins() {
691 let dir = setup_temp_dir(&[
692 "libdrasi_source_mock.dylib",
693 "libdrasi_source_mock.rlib",
694 "libdrasi_source_http.so",
695 "libdrasi_source_http.rmeta",
696 ]);
697 let patterns = vec!["libdrasi_source_*".to_string()];
698 let groups = discover_plugin_candidates(dir.path(), &patterns);
699
700 assert_eq!(groups.len(), 2);
701 assert!(groups.contains_key("libdrasi_source_mock"));
702 assert!(groups.contains_key("libdrasi_source_http"));
703 }
704
705 #[test]
706 fn test_discover_empty_dir() {
707 let dir = tempfile::tempdir().unwrap();
708 let patterns = vec!["libdrasi_source_*".to_string()];
709 let groups = discover_plugin_candidates(dir.path(), &patterns);
710
711 assert!(groups.is_empty());
712 }
713
714 #[test]
715 fn test_discover_nonexistent_dir() {
716 let groups = discover_plugin_candidates(Path::new("/nonexistent"), &["libdrasi_*".into()]);
717 assert!(groups.is_empty());
718 }
719
720 #[test]
723 fn test_cdylib_only_filtering() {
724 let dir = setup_temp_dir(&[
725 "libdrasi_source_mock.dylib",
726 "libdrasi_source_mock.rlib",
727 "libdrasi_source_mock.d",
728 ]);
729 let patterns = vec!["libdrasi_source_*".to_string()];
730 let groups = discover_plugin_candidates(dir.path(), &patterns);
731 let files = &groups["libdrasi_source_mock"];
732
733 let cdylib_files: Vec<&PathBuf> = files
734 .iter()
735 .filter(|p| {
736 CDYLIB_EXTENSIONS
737 .iter()
738 .any(|ext| p.to_string_lossy().ends_with(ext))
739 })
740 .collect();
741
742 assert_eq!(cdylib_files.len(), 1);
743 assert!(cdylib_files[0]
744 .to_string_lossy()
745 .ends_with("libdrasi_source_mock.dylib"));
746 }
747
748 #[test]
749 fn test_ambiguous_cdylib_detected() {
750 let dir = setup_temp_dir(&["libdrasi_source_mock.dylib", "libdrasi_source_mock.so"]);
751 let patterns = vec!["libdrasi_source_*".to_string()];
752 let groups = discover_plugin_candidates(dir.path(), &patterns);
753 let files = &groups["libdrasi_source_mock"];
754
755 let cdylib_files: Vec<&PathBuf> = files
756 .iter()
757 .filter(|p| {
758 CDYLIB_EXTENSIONS
759 .iter()
760 .any(|ext| p.to_string_lossy().ends_with(ext))
761 })
762 .collect();
763
764 assert_eq!(
765 cdylib_files.len(),
766 2,
767 "Should detect 2 ambiguous cdylib files"
768 );
769 }
770
771 #[test]
772 fn test_no_cdylib_skips_silently() {
773 let dir = setup_temp_dir(&["libdrasi_source_mock.rlib", "libdrasi_source_mock.d"]);
774 let patterns = vec!["libdrasi_source_*".to_string()];
775 let groups = discover_plugin_candidates(dir.path(), &patterns);
776 let files = &groups["libdrasi_source_mock"];
777
778 let cdylib_files: Vec<&PathBuf> = files
779 .iter()
780 .filter(|p| {
781 CDYLIB_EXTENSIONS
782 .iter()
783 .any(|ext| p.to_string_lossy().ends_with(ext))
784 })
785 .collect();
786
787 assert!(
788 cdylib_files.is_empty(),
789 "Should find no cdylib files when only .rlib and .d exist"
790 );
791 }
792
793 #[test]
794 fn test_discover_with_pattern_including_extension() {
795 let dir = setup_temp_dir(&["libdrasi_source_mock.dylib", "libdrasi_source_mock.rlib"]);
796 let patterns = vec!["libdrasi_source_*.dylib".to_string()];
798 let groups = discover_plugin_candidates(dir.path(), &patterns);
799
800 assert_eq!(groups.len(), 1);
801 assert!(groups.contains_key("libdrasi_source_mock"));
802 assert_eq!(groups["libdrasi_source_mock"].len(), 2);
803 }
804
805 #[test]
806 fn test_discover_multiple_patterns() {
807 let dir = setup_temp_dir(&[
808 "libdrasi_source_mock.dylib",
809 "libdrasi_reaction_log.so",
810 "libdrasi_bootstrap_mock.dylib",
811 ]);
812 let patterns = vec![
813 "libdrasi_source_*".to_string(),
814 "libdrasi_reaction_*".to_string(),
815 ];
816 let groups = discover_plugin_candidates(dir.path(), &patterns);
817
818 assert_eq!(groups.len(), 2);
819 assert!(groups.contains_key("libdrasi_source_mock"));
820 assert!(groups.contains_key("libdrasi_reaction_log"));
821 assert!(!groups.contains_key("libdrasi_bootstrap_mock"));
822 }
823
824 #[test]
825 fn test_file_without_known_extension_matched_by_base() {
826 let dir = setup_temp_dir(&["libdrasi_source_mock"]);
827 let patterns = vec!["libdrasi_source_*".to_string()];
828 let groups = discover_plugin_candidates(dir.path(), &patterns);
829
830 assert_eq!(groups.len(), 1);
832 assert!(groups.contains_key("libdrasi_source_mock"));
833 }
834
835 #[test]
838 fn test_plugin_kind_from_filename_unix() {
839 assert_eq!(
840 plugin_kind_from_filename("libdrasi_source_postgres.so"),
841 Some("source/postgres".to_string())
842 );
843 assert_eq!(
844 plugin_kind_from_filename("libdrasi_reaction_log.dylib"),
845 Some("reaction/log".to_string())
846 );
847 assert_eq!(
848 plugin_kind_from_filename("libdrasi_bootstrap_postgres.so"),
849 Some("bootstrap/postgres".to_string())
850 );
851 }
852
853 #[test]
854 fn test_plugin_kind_from_filename_windows() {
855 assert_eq!(
856 plugin_kind_from_filename("drasi_source_postgres.dll"),
857 Some("source/postgres".to_string())
858 );
859 }
860
861 #[test]
862 fn test_plugin_kind_from_filename_underscore_to_hyphen() {
863 assert_eq!(
864 plugin_kind_from_filename("libdrasi_source_postgres_replication.so"),
865 Some("source/postgres-replication".to_string())
866 );
867 }
868
869 #[test]
870 fn test_plugin_kind_from_filename_not_a_plugin() {
871 assert_eq!(plugin_kind_from_filename("random_lib.so"), None);
872 assert_eq!(plugin_kind_from_filename("not_a_plugin.txt"), None);
873 }
874
875 #[test]
876 fn test_is_plugin_binary() {
877 assert!(is_plugin_binary("libdrasi_source_mock.so"));
878 assert!(is_plugin_binary("drasi_reaction_log.dll"));
879 assert!(is_plugin_binary("libdrasi_bootstrap_postgres.dylib"));
880 assert!(!is_plugin_binary("plugin.rlib"));
881 assert!(!is_plugin_binary("readme.md"));
882 }
883
884 #[test]
885 #[allow(clippy::const_is_empty)]
886 fn test_default_patterns_not_empty() {
887 assert!(!DEFAULT_PLUGIN_FILE_PATTERNS.is_empty());
888 }
889}