1use crate::error::{PluginError, PluginResult};
21use crate::traits::{CodecPlugin, CodecPluginInfo, PluginCapability, PLUGIN_API_VERSION};
22use oximedia_codec::{CodecResult, EncoderConfig, VideoDecoder, VideoEncoder};
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25use std::sync::{Arc, RwLock};
26
27struct PluginEntry {
31 plugin: Arc<dyn CodecPlugin>,
32 priority: i32,
35}
36
37#[derive(Default)]
44struct CapabilityCache {
45 decoder_index: HashMap<String, usize>,
47 encoder_index: HashMap<String, usize>,
49}
50
51impl CapabilityCache {
52 fn new() -> Self {
53 Self {
54 decoder_index: HashMap::new(),
55 encoder_index: HashMap::new(),
56 }
57 }
58
59 fn invalidate(&mut self) {
60 self.decoder_index.clear();
61 self.encoder_index.clear();
62 }
63
64 fn rebuild(&mut self, plugins: &[PluginEntry]) {
66 self.invalidate();
67 for (idx, entry) in plugins.iter().enumerate() {
68 for cap in entry.plugin.capabilities() {
69 if cap.can_decode {
70 self.decoder_index
71 .entry(cap.codec_name.clone())
72 .or_insert(idx);
73 }
74 if cap.can_encode {
75 self.encoder_index
76 .entry(cap.codec_name.clone())
77 .or_insert(idx);
78 }
79 }
80 }
81 }
82}
83
84pub struct PluginRegistry {
136 plugins: RwLock<Vec<PluginEntry>>,
138 cache: RwLock<CapabilityCache>,
140 search_paths: Vec<PathBuf>,
141}
142
143impl PluginRegistry {
144 #[must_use]
146 pub fn new() -> Self {
147 Self {
148 plugins: RwLock::new(Vec::new()),
149 cache: RwLock::new(CapabilityCache::new()),
150 search_paths: Self::default_search_paths(),
151 }
152 }
153
154 #[must_use]
156 pub fn empty() -> Self {
157 Self {
158 plugins: RwLock::new(Vec::new()),
159 cache: RwLock::new(CapabilityCache::new()),
160 search_paths: Vec::new(),
161 }
162 }
163
164 pub fn add_search_path(&mut self, path: PathBuf) {
166 if !self.search_paths.contains(&path) {
167 self.search_paths.push(path);
168 }
169 }
170
171 pub fn search_paths(&self) -> &[PathBuf] {
173 &self.search_paths
174 }
175
176 #[must_use]
184 pub fn default_search_paths() -> Vec<PathBuf> {
185 let mut paths = Vec::new();
186
187 if let Ok(env_paths) = std::env::var("OXIMEDIA_PLUGIN_PATH") {
189 let separator = if cfg!(windows) { ';' } else { ':' };
190 for p in env_paths.split(separator) {
191 let path = PathBuf::from(p);
192 if !paths.contains(&path) {
193 paths.push(path);
194 }
195 }
196 }
197
198 if let Some(home) = home_dir() {
200 let user_plugins = home.join(".oximedia").join("plugins");
201 if !paths.contains(&user_plugins) {
202 paths.push(user_plugins);
203 }
204 }
205
206 #[cfg(unix)]
208 {
209 let sys_paths = [
210 PathBuf::from("/usr/lib/oximedia/plugins"),
211 PathBuf::from("/usr/local/lib/oximedia/plugins"),
212 ];
213 for p in sys_paths {
214 if !paths.contains(&p) {
215 paths.push(p);
216 }
217 }
218 }
219
220 paths
221 }
222
223 pub fn register(&self, plugin: Arc<dyn CodecPlugin>) -> PluginResult<()> {
234 self.register_with_priority(plugin, 0)
235 }
236
237 pub fn register_with_priority(
246 &self,
247 plugin: Arc<dyn CodecPlugin>,
248 priority: i32,
249 ) -> PluginResult<()> {
250 let info = plugin.info();
251
252 if info.api_version != PLUGIN_API_VERSION {
254 return Err(PluginError::ApiIncompatible(format!(
255 "Plugin '{}' has API v{}, host expects v{PLUGIN_API_VERSION}",
256 info.name, info.api_version
257 )));
258 }
259
260 let mut plugins = self
261 .plugins
262 .write()
263 .map_err(|e| PluginError::InitFailed(format!("Lock poisoned: {e}")))?;
264
265 for existing in plugins.iter() {
267 if existing.plugin.info().name == info.name {
268 return Err(PluginError::AlreadyRegistered(info.name));
269 }
270 }
271
272 tracing::info!(
273 "Registered plugin: {} v{} (priority={}, {} codec(s))",
274 info.name,
275 info.version,
276 priority,
277 plugin.capabilities().len()
278 );
279
280 if info.patent_encumbered {
281 tracing::warn!(
282 "Plugin '{}' contains patent-encumbered codecs. Use at your own risk.",
283 info.name
284 );
285 }
286
287 plugins.push(PluginEntry { plugin, priority });
288
289 plugins.sort_by(|a, b| b.priority.cmp(&a.priority));
291
292 drop(plugins);
294 self.rebuild_cache()?;
295 Ok(())
296 }
297
298 pub fn unregister(&self, name: &str) -> PluginResult<()> {
304 let mut plugins = self
305 .plugins
306 .write()
307 .map_err(|e| PluginError::InitFailed(format!("Lock poisoned: {e}")))?;
308
309 let before = plugins.len();
310 plugins.retain(|e| e.plugin.info().name != name);
311
312 if plugins.len() == before {
313 return Err(PluginError::NotFound(name.to_string()));
314 }
315
316 drop(plugins);
317 self.rebuild_cache()?;
318 Ok(())
319 }
320
321 #[cfg(feature = "dynamic-loading")]
335 pub fn load_plugin(&self, path: &Path) -> PluginResult<()> {
336 let loaded = crate::loader::LoadedPlugin::load(path)?;
337 self.register(loaded.into_plugin())
338 }
339
340 #[cfg(not(feature = "dynamic-loading"))]
345 pub fn load_plugin(&self, _path: &Path) -> PluginResult<()> {
346 Err(PluginError::DynamicLoadingDisabled)
347 }
348
349 #[cfg(feature = "dynamic-loading")]
360 pub fn discover_plugins(&self) -> PluginResult<Vec<CodecPluginInfo>> {
361 let mut loaded = Vec::new();
362
363 for search_path in &self.search_paths {
364 if !search_path.is_dir() {
365 tracing::debug!(
366 "Plugin search path does not exist: {}",
367 search_path.display()
368 );
369 continue;
370 }
371
372 let entries = std::fs::read_dir(search_path)?;
373 for entry in entries {
374 let entry = entry?;
375 let path = entry.path();
376
377 let manifest_path = if path.is_dir() {
379 path.join("plugin.json")
380 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
381 path.clone()
382 } else {
383 continue;
384 };
385
386 if !manifest_path.exists() {
387 continue;
388 }
389
390 match self.load_from_manifest(&manifest_path) {
391 Ok(info) => loaded.push(info),
392 Err(e) => {
393 tracing::warn!(
394 "Failed to load plugin from {}: {e}",
395 manifest_path.display()
396 );
397 }
398 }
399 }
400 }
401
402 Ok(loaded)
403 }
404
405 #[cfg(not(feature = "dynamic-loading"))]
407 pub fn discover_plugins(&self) -> PluginResult<Vec<CodecPluginInfo>> {
408 Err(PluginError::DynamicLoadingDisabled)
409 }
410
411 #[cfg(feature = "dynamic-loading")]
413 fn load_from_manifest(&self, manifest_path: &Path) -> PluginResult<CodecPluginInfo> {
414 let manifest = crate::manifest::PluginManifest::from_file(manifest_path)?;
415 manifest.validate()?;
416
417 let lib_path = manifest.library_path(manifest_path).ok_or_else(|| {
418 PluginError::LoadFailed("Cannot determine library path from manifest".to_string())
419 })?;
420
421 self.load_plugin(&lib_path)?;
422
423 let plugins = self
425 .plugins
426 .read()
427 .map_err(|e| PluginError::InitFailed(format!("Lock poisoned: {e}")))?;
428
429 plugins.last().map(|e| e.plugin.info()).ok_or_else(|| {
430 PluginError::InitFailed("Plugin was not added after loading".to_string())
431 })
432 }
433
434 pub fn list_plugins(&self) -> Vec<CodecPluginInfo> {
436 let plugins = match self.plugins.read() {
437 Ok(p) => p,
438 Err(_) => return Vec::new(),
439 };
440 plugins.iter().map(|e| e.plugin.info()).collect()
441 }
442
443 pub fn list_codecs(&self) -> Vec<PluginCapability> {
445 let plugins = match self.plugins.read() {
446 Ok(p) => p,
447 Err(_) => return Vec::new(),
448 };
449 plugins
450 .iter()
451 .flat_map(|e| e.plugin.capabilities())
452 .collect()
453 }
454
455 pub fn find_decoder(&self, codec_name: &str) -> CodecResult<Box<dyn VideoDecoder>> {
466 if let Some(plugin) = self.cached_decoder_plugin(codec_name) {
468 return plugin.create_decoder(codec_name);
469 }
470
471 let plugins = self
473 .plugins
474 .read()
475 .map_err(|e| oximedia_codec::CodecError::Internal(format!("Lock poisoned: {e}")))?;
476
477 for entry in plugins.iter() {
478 if entry.plugin.can_decode(codec_name) {
479 return entry.plugin.create_decoder(codec_name);
480 }
481 }
482
483 Err(oximedia_codec::CodecError::UnsupportedFeature(format!(
484 "No plugin provides decoder for '{codec_name}'"
485 )))
486 }
487
488 pub fn find_encoder(
499 &self,
500 codec_name: &str,
501 config: EncoderConfig,
502 ) -> CodecResult<Box<dyn VideoEncoder>> {
503 if let Some(plugin) = self.cached_encoder_plugin(codec_name) {
505 return plugin.create_encoder(codec_name, config);
506 }
507
508 let plugins = self
510 .plugins
511 .read()
512 .map_err(|e| oximedia_codec::CodecError::Internal(format!("Lock poisoned: {e}")))?;
513
514 for entry in plugins.iter() {
515 if entry.plugin.can_encode(codec_name) {
516 return entry.plugin.create_encoder(codec_name, config);
517 }
518 }
519
520 Err(oximedia_codec::CodecError::UnsupportedFeature(format!(
521 "No plugin provides encoder for '{codec_name}'"
522 )))
523 }
524
525 pub fn has_codec(&self, codec_name: &str) -> bool {
527 if let Ok(cache) = self.cache.read() {
529 if cache.decoder_index.contains_key(codec_name)
530 || cache.encoder_index.contains_key(codec_name)
531 {
532 return true;
533 }
534 }
535 let plugins = match self.plugins.read() {
536 Ok(p) => p,
537 Err(_) => return false,
538 };
539 plugins.iter().any(|e| e.plugin.supports_codec(codec_name))
540 }
541
542 pub fn has_decoder(&self, codec_name: &str) -> bool {
544 if let Ok(cache) = self.cache.read() {
545 if cache.decoder_index.contains_key(codec_name) {
546 return true;
547 }
548 }
549 let plugins = match self.plugins.read() {
550 Ok(p) => p,
551 Err(_) => return false,
552 };
553 plugins.iter().any(|e| e.plugin.can_decode(codec_name))
554 }
555
556 pub fn has_encoder(&self, codec_name: &str) -> bool {
558 if let Ok(cache) = self.cache.read() {
559 if cache.encoder_index.contains_key(codec_name) {
560 return true;
561 }
562 }
563 let plugins = match self.plugins.read() {
564 Ok(p) => p,
565 Err(_) => return false,
566 };
567 plugins.iter().any(|e| e.plugin.can_encode(codec_name))
568 }
569
570 pub fn plugin_count(&self) -> usize {
572 match self.plugins.read() {
573 Ok(p) => p.len(),
574 Err(_) => 0,
575 }
576 }
577
578 pub fn clear(&self) {
580 if let Ok(mut plugins) = self.plugins.write() {
581 plugins.clear();
582 }
583 if let Ok(mut cache) = self.cache.write() {
584 cache.invalidate();
585 }
586 }
587
588 pub fn find_plugin_for_codec(&self, codec_name: &str) -> Option<CodecPluginInfo> {
590 if let Some(plugin) = self.cached_decoder_plugin(codec_name) {
592 return Some(plugin.info());
593 }
594 if let Some(plugin) = self.cached_encoder_plugin(codec_name) {
595 return Some(plugin.info());
596 }
597 let plugins = self.plugins.read().ok()?;
598 plugins
599 .iter()
600 .find(|e| e.plugin.supports_codec(codec_name))
601 .map(|e| e.plugin.info())
602 }
603
604 pub fn plugin_priority(&self, name: &str) -> Option<i32> {
608 let plugins = self.plugins.read().ok()?;
609 plugins
610 .iter()
611 .find(|e| e.plugin.info().name == name)
612 .map(|e| e.priority)
613 }
614
615 fn rebuild_cache(&self) -> PluginResult<()> {
619 let plugins = self
620 .plugins
621 .read()
622 .map_err(|e| PluginError::InitFailed(format!("Lock poisoned: {e}")))?;
623 let mut cache = self
624 .cache
625 .write()
626 .map_err(|e| PluginError::InitFailed(format!("Cache lock poisoned: {e}")))?;
627 cache.rebuild(&plugins);
628 Ok(())
629 }
630
631 fn cached_decoder_plugin(&self, codec_name: &str) -> Option<Arc<dyn CodecPlugin>> {
633 let cache = self.cache.read().ok()?;
634 let idx = *cache.decoder_index.get(codec_name)?;
635 drop(cache);
636 let plugins = self.plugins.read().ok()?;
637 plugins.get(idx).map(|e| Arc::clone(&e.plugin))
638 }
639
640 fn cached_encoder_plugin(&self, codec_name: &str) -> Option<Arc<dyn CodecPlugin>> {
642 let cache = self.cache.read().ok()?;
643 let idx = *cache.encoder_index.get(codec_name)?;
644 drop(cache);
645 let plugins = self.plugins.read().ok()?;
646 plugins.get(idx).map(|e| Arc::clone(&e.plugin))
647 }
648}
649
650impl Default for PluginRegistry {
651 fn default() -> Self {
652 Self::new()
653 }
654}
655
656fn home_dir() -> Option<PathBuf> {
658 #[cfg(unix)]
659 {
660 std::env::var("HOME").ok().map(PathBuf::from)
661 }
662 #[cfg(windows)]
663 {
664 std::env::var("USERPROFILE").ok().map(PathBuf::from)
665 }
666 #[cfg(not(any(unix, windows)))]
667 {
668 None
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use crate::static_plugin::StaticPlugin;
676 use std::collections::HashMap;
677
678 fn make_test_plugin(name: &str, codecs: &[(&str, bool, bool)]) -> Arc<dyn CodecPlugin> {
679 let info = CodecPluginInfo {
680 name: name.to_string(),
681 version: "1.0.0".to_string(),
682 author: "Test".to_string(),
683 description: format!("Test plugin: {name}"),
684 api_version: PLUGIN_API_VERSION,
685 license: "MIT".to_string(),
686 patent_encumbered: false,
687 };
688
689 let mut plugin = StaticPlugin::new(info);
690 for (codec_name, decode, encode) in codecs {
691 plugin = plugin.add_capability(PluginCapability {
692 codec_name: (*codec_name).to_string(),
693 can_decode: *decode,
694 can_encode: *encode,
695 pixel_formats: vec!["yuv420p".to_string()],
696 properties: HashMap::new(),
697 });
698 }
699 Arc::new(plugin)
700 }
701
702 #[test]
703 fn test_registry_new() {
704 let registry = PluginRegistry::empty();
705 assert_eq!(registry.plugin_count(), 0);
706 assert!(registry.list_plugins().is_empty());
707 assert!(registry.list_codecs().is_empty());
708 }
709
710 #[test]
711 fn test_register_plugin() {
712 let registry = PluginRegistry::empty();
713 let plugin = make_test_plugin("test-1", &[("h264", true, true)]);
714 registry.register(plugin).expect("should register");
715 assert_eq!(registry.plugin_count(), 1);
716 }
717
718 #[test]
719 fn test_register_duplicate_rejected() {
720 let registry = PluginRegistry::empty();
721 let p1 = make_test_plugin("same-name", &[("h264", true, false)]);
722 let p2 = make_test_plugin("same-name", &[("h265", true, false)]);
723 registry.register(p1).expect("first should succeed");
724 let err = registry.register(p2).expect_err("second should fail");
725 assert!(err.to_string().contains("already registered"));
726 }
727
728 #[test]
729 fn test_register_wrong_api_version() {
730 let registry = PluginRegistry::empty();
731 let info = CodecPluginInfo {
732 name: "bad-api".to_string(),
733 version: "1.0.0".to_string(),
734 author: "Test".to_string(),
735 description: "Bad API plugin".to_string(),
736 api_version: 999,
737 license: "MIT".to_string(),
738 patent_encumbered: false,
739 };
740 let plugin = Arc::new(StaticPlugin::new(info));
741 let err = registry.register(plugin).expect_err("should fail");
742 assert!(err.to_string().contains("API"));
743 }
744
745 #[test]
746 fn test_has_codec() {
747 let registry = PluginRegistry::empty();
748 let plugin = make_test_plugin("test", &[("h264", true, true), ("h265", true, false)]);
749 registry.register(plugin).expect("should register");
750
751 assert!(registry.has_codec("h264"));
752 assert!(registry.has_codec("h265"));
753 assert!(!registry.has_codec("vp9"));
754 }
755
756 #[test]
757 fn test_has_decoder_encoder() {
758 let registry = PluginRegistry::empty();
759 let plugin = make_test_plugin("test", &[("h264", true, true), ("h265", true, false)]);
760 registry.register(plugin).expect("should register");
761
762 assert!(registry.has_decoder("h264"));
763 assert!(registry.has_encoder("h264"));
764 assert!(registry.has_decoder("h265"));
765 assert!(!registry.has_encoder("h265"));
766 assert!(!registry.has_decoder("nonexistent"));
767 }
768
769 #[test]
770 fn test_list_plugins() {
771 let registry = PluginRegistry::empty();
772 let p1 = make_test_plugin("alpha", &[("h264", true, false)]);
773 let p2 = make_test_plugin("beta", &[("h265", true, false)]);
774 registry.register(p1).expect("should register alpha");
775 registry.register(p2).expect("should register beta");
776
777 let list = registry.list_plugins();
778 assert_eq!(list.len(), 2);
779 let names: Vec<&str> = list.iter().map(|i| i.name.as_str()).collect();
781 assert!(names.contains(&"alpha"));
782 assert!(names.contains(&"beta"));
783 }
784
785 #[test]
786 fn test_list_codecs() {
787 let registry = PluginRegistry::empty();
788 let p1 = make_test_plugin("p1", &[("h264", true, true)]);
789 let p2 = make_test_plugin("p2", &[("h265", true, false), ("aac", false, true)]);
790 registry.register(p1).expect("should register");
791 registry.register(p2).expect("should register");
792
793 let codecs = registry.list_codecs();
794 assert_eq!(codecs.len(), 3);
795 let names: Vec<&str> = codecs.iter().map(|c| c.codec_name.as_str()).collect();
796 assert!(names.contains(&"h264"));
797 assert!(names.contains(&"h265"));
798 assert!(names.contains(&"aac"));
799 }
800
801 #[test]
802 fn test_find_decoder_not_found() {
803 let registry = PluginRegistry::empty();
804 let result = registry.find_decoder("nonexistent");
805 assert!(result.is_err());
806 }
807
808 #[test]
809 fn test_find_encoder_not_found() {
810 let registry = PluginRegistry::empty();
811 let config = EncoderConfig::default();
812 let result = registry.find_encoder("nonexistent", config);
813 assert!(result.is_err());
814 }
815
816 #[test]
817 fn test_clear() {
818 let registry = PluginRegistry::empty();
819 let plugin = make_test_plugin("test", &[("h264", true, false)]);
820 registry.register(plugin).expect("should register");
821 assert_eq!(registry.plugin_count(), 1);
822 registry.clear();
823 assert_eq!(registry.plugin_count(), 0);
824 assert!(!registry.has_codec("h264"));
826 }
827
828 #[test]
829 fn test_find_plugin_for_codec() {
830 let registry = PluginRegistry::empty();
831 let plugin = make_test_plugin("h264-provider", &[("h264", true, true)]);
832 registry.register(plugin).expect("should register");
833
834 let found = registry.find_plugin_for_codec("h264");
835 assert!(found.is_some());
836 assert_eq!(
837 found.as_ref().map(|i| i.name.as_str()),
838 Some("h264-provider")
839 );
840
841 let not_found = registry.find_plugin_for_codec("aac");
842 assert!(not_found.is_none());
843 }
844
845 #[test]
846 fn test_multiple_plugins_first_wins_same_priority() {
847 let registry = PluginRegistry::empty();
848 let p1 = make_test_plugin("first", &[("h264", true, false)]);
850 let p2 = make_test_plugin("second", &[("h264", true, true)]);
851 registry.register(p1).expect("should register first");
852 registry.register(p2).expect("should register second");
853
854 let found = registry.find_plugin_for_codec("h264");
855 assert_eq!(found.as_ref().map(|i| i.name.as_str()), Some("first"));
856 }
857
858 #[test]
859 fn test_priority_ordering() {
860 let registry = PluginRegistry::empty();
861 let low = make_test_plugin("low-priority", &[("h264", true, false)]);
863 let high = make_test_plugin("high-priority", &[("h264", true, true)]);
864
865 registry
866 .register_with_priority(low, 0)
867 .expect("register low");
868 registry
869 .register_with_priority(high, 10)
870 .expect("register high");
871
872 let found = registry.find_plugin_for_codec("h264");
874 assert_eq!(
875 found.as_ref().map(|i| i.name.as_str()),
876 Some("high-priority")
877 );
878 }
879
880 #[test]
881 fn test_priority_negative() {
882 let registry = PluginRegistry::empty();
883 let normal = make_test_plugin("normal", &[("vp9", true, true)]);
884 let fallback = make_test_plugin("fallback", &[("vp9", true, false)]);
885
886 registry
887 .register_with_priority(normal, 0)
888 .expect("register normal");
889 registry
890 .register_with_priority(fallback, -5)
891 .expect("register fallback");
892
893 let found = registry.find_plugin_for_codec("vp9");
895 assert_eq!(found.as_ref().map(|i| i.name.as_str()), Some("normal"));
896 }
897
898 #[test]
899 fn test_priority_accessor() {
900 let registry = PluginRegistry::empty();
901 let p = make_test_plugin("prio-test", &[]);
902 registry.register_with_priority(p, 42).expect("register");
903 assert_eq!(registry.plugin_priority("prio-test"), Some(42));
904 assert_eq!(registry.plugin_priority("nonexistent"), None);
905 }
906
907 #[test]
908 fn test_unregister() {
909 let registry = PluginRegistry::empty();
910 let p = make_test_plugin("to-remove", &[("aac", true, false)]);
911 registry.register(p).expect("register");
912 assert_eq!(registry.plugin_count(), 1);
913 assert!(registry.has_codec("aac"));
914
915 registry.unregister("to-remove").expect("unregister");
916 assert_eq!(registry.plugin_count(), 0);
917 assert!(!registry.has_codec("aac"));
918 }
919
920 #[test]
921 fn test_unregister_not_found() {
922 let registry = PluginRegistry::empty();
923 assert!(matches!(
924 registry.unregister("ghost"),
925 Err(PluginError::NotFound(_))
926 ));
927 }
928
929 #[test]
930 fn test_capability_cache_after_clear() {
931 let registry = PluginRegistry::empty();
932 let p = make_test_plugin("cached", &[("opus", true, true)]);
933 registry.register(p).expect("register");
934 assert!(registry.has_decoder("opus"));
935 registry.clear();
936 assert!(!registry.has_decoder("opus"));
937 assert!(!registry.has_encoder("opus"));
938 }
939
940 #[test]
941 fn test_cache_invalidated_on_unregister() {
942 let registry = PluginRegistry::empty();
943 let p1 = make_test_plugin("provider-a", &[("vorbis", true, false)]);
944 let p2 = make_test_plugin("provider-b", &[("flac", true, true)]);
945 registry.register(p1).expect("register a");
946 registry.register(p2).expect("register b");
947
948 assert!(registry.has_decoder("vorbis"));
949 registry.unregister("provider-a").expect("unregister");
950 assert!(!registry.has_decoder("vorbis"));
951 assert!(registry.has_codec("flac")); }
953
954 #[test]
955 fn test_default_search_paths() {
956 let paths = PluginRegistry::default_search_paths();
957 assert!(!paths.is_empty());
959 }
960
961 #[test]
962 fn test_add_search_path() {
963 let mut registry = PluginRegistry::empty();
964 let path = PathBuf::from("/tmp/test-plugins");
965 registry.add_search_path(path.clone());
966 assert!(registry.search_paths().contains(&path));
967
968 registry.add_search_path(path.clone());
970 assert_eq!(
971 registry
972 .search_paths()
973 .iter()
974 .filter(|p| **p == path)
975 .count(),
976 1
977 );
978 }
979
980 #[test]
981 fn test_load_plugin_without_dynamic_loading() {
982 let registry = PluginRegistry::empty();
983 let result = registry.load_plugin(Path::new("/nonexistent.so"));
984
985 #[cfg(not(feature = "dynamic-loading"))]
986 {
987 assert!(result.is_err());
988 assert!(result
989 .unwrap_err()
990 .to_string()
991 .contains("Dynamic loading not enabled"));
992 }
993
994 #[cfg(feature = "dynamic-loading")]
995 {
996 assert!(result.is_err());
998 }
999 }
1000}