1use crate::error::{PluginError, PluginResult};
8use crate::traits::PLUGIN_API_VERSION;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::Path;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SemVer {
18 pub major: u64,
20 pub minor: u64,
22 pub patch: u64,
24 pub pre: Option<String>,
26}
27
28impl SemVer {
29 pub fn parse(s: &str) -> Result<Self, String> {
35 let s = s.trim();
36 let (version_part, pre) = if let Some((v, p)) = s.split_once('-') {
37 (v, Some(p.to_string()))
38 } else {
39 (s, None)
40 };
41
42 let parts: Vec<&str> = version_part.split('.').collect();
43 if parts.len() < 2 || parts.len() > 3 {
44 return Err(format!("Expected 2-3 dot-separated numbers, got '{s}'"));
45 }
46
47 let major = parts[0]
48 .parse::<u64>()
49 .map_err(|e| format!("Invalid major: {e}"))?;
50 let minor = parts[1]
51 .parse::<u64>()
52 .map_err(|e| format!("Invalid minor: {e}"))?;
53 let patch = if parts.len() == 3 {
54 parts[2]
55 .parse::<u64>()
56 .map_err(|e| format!("Invalid patch: {e}"))?
57 } else {
58 0
59 };
60
61 Ok(Self {
62 major,
63 minor,
64 patch,
65 pre,
66 })
67 }
68
69 fn cmp_numeric(&self, other: &Self) -> std::cmp::Ordering {
71 self.major
72 .cmp(&other.major)
73 .then(self.minor.cmp(&other.minor))
74 .then(self.patch.cmp(&other.patch))
75 }
76
77 pub fn is_caret_compatible(&self, req: &Self) -> bool {
86 if self.cmp_numeric(req) == std::cmp::Ordering::Less {
87 return false;
88 }
89 if req.major > 0 {
90 self.major == req.major
91 } else if req.minor > 0 {
92 self.major == 0 && self.minor == req.minor
93 } else {
94 self.major == 0 && self.minor == 0 && self.patch == req.patch
95 }
96 }
97
98 pub fn is_tilde_compatible(&self, req: &Self) -> bool {
103 if self.cmp_numeric(req) == std::cmp::Ordering::Less {
104 return false;
105 }
106 self.major == req.major && self.minor == req.minor
107 }
108}
109
110impl std::fmt::Display for SemVer {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
113 if let Some(ref pre) = self.pre {
114 write!(f, "-{pre}")?;
115 }
116 Ok(())
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum SemVerOp {
123 Exact,
125 Gt,
127 Gte,
129 Lt,
131 Lte,
133 Caret,
135 Tilde,
137 Wildcard,
139}
140
141#[derive(Debug, Clone)]
143pub struct SemVerReq {
144 pub op: SemVerOp,
146 pub version: SemVer,
148}
149
150impl SemVerReq {
151 pub fn parse(s: &str) -> Result<Self, String> {
165 let s = s.trim();
166 if s == "*" {
167 return Ok(Self {
168 op: SemVerOp::Wildcard,
169 version: SemVer {
170 major: 0,
171 minor: 0,
172 patch: 0,
173 pre: None,
174 },
175 });
176 }
177
178 let (op, rest) = if let Some(r) = s.strip_prefix(">=") {
179 (SemVerOp::Gte, r)
180 } else if let Some(r) = s.strip_prefix("<=") {
181 (SemVerOp::Lte, r)
182 } else if let Some(r) = s.strip_prefix('>') {
183 (SemVerOp::Gt, r)
184 } else if let Some(r) = s.strip_prefix('<') {
185 (SemVerOp::Lt, r)
186 } else if let Some(r) = s.strip_prefix('^') {
187 (SemVerOp::Caret, r)
188 } else if let Some(r) = s.strip_prefix('~') {
189 (SemVerOp::Tilde, r)
190 } else if let Some(r) = s.strip_prefix('=') {
191 (SemVerOp::Exact, r)
192 } else {
193 (SemVerOp::Exact, s)
194 };
195
196 let version = SemVer::parse(rest.trim())?;
197 Ok(Self { op, version })
198 }
199
200 pub fn matches(&self, version: &SemVer) -> bool {
202 match self.op {
203 SemVerOp::Wildcard => true,
204 SemVerOp::Exact => version.cmp_numeric(&self.version) == std::cmp::Ordering::Equal,
205 SemVerOp::Gt => version.cmp_numeric(&self.version) == std::cmp::Ordering::Greater,
206 SemVerOp::Gte => version.cmp_numeric(&self.version) != std::cmp::Ordering::Less,
207 SemVerOp::Lt => version.cmp_numeric(&self.version) == std::cmp::Ordering::Less,
208 SemVerOp::Lte => version.cmp_numeric(&self.version) != std::cmp::Ordering::Greater,
209 SemVerOp::Caret => version.is_caret_compatible(&self.version),
210 SemVerOp::Tilde => version.is_tilde_compatible(&self.version),
211 }
212 }
213}
214
215impl std::fmt::Display for SemVerReq {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 match self.op {
218 SemVerOp::Wildcard => write!(f, "*"),
219 SemVerOp::Exact => write!(f, "={}", self.version),
220 SemVerOp::Gt => write!(f, ">{}", self.version),
221 SemVerOp::Gte => write!(f, ">={}", self.version),
222 SemVerOp::Lt => write!(f, "<{}", self.version),
223 SemVerOp::Lte => write!(f, "<={}", self.version),
224 SemVerOp::Caret => write!(f, "^{}", self.version),
225 SemVerOp::Tilde => write!(f, "~{}", self.version),
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
234pub struct DependencyResolution {
235 pub load_order: Vec<String>,
237 pub missing: Vec<(String, String, String)>,
239 pub conflicts: Vec<(String, String, String, String)>,
241}
242
243impl DependencyResolution {
244 pub fn is_satisfied(&self) -> bool {
246 self.missing.is_empty() && self.conflicts.is_empty()
247 }
248}
249
250pub fn resolve_dependencies(manifests: &[PluginManifest]) -> PluginResult<DependencyResolution> {
259 let mut index_by_name: HashMap<&str, usize> = HashMap::new();
261 for (i, m) in manifests.iter().enumerate() {
262 index_by_name.insert(&m.name, i);
263 }
264
265 let mut missing: Vec<(String, String, String)> = Vec::new();
266 let mut conflicts: Vec<(String, String, String, String)> = Vec::new();
267
268 for manifest in manifests {
270 for (dep_name, req_str) in &manifest.dependencies {
271 if let Some(&dep_idx) = index_by_name.get(dep_name.as_str()) {
272 let dep_manifest = &manifests[dep_idx];
273 let req = SemVerReq::parse(req_str).map_err(|e| {
274 PluginError::InvalidManifest(format!(
275 "Bad requirement for dep '{dep_name}' of '{}': {e}",
276 manifest.name
277 ))
278 })?;
279 let dep_ver = SemVer::parse(&dep_manifest.version).map_err(|e| {
280 PluginError::InvalidManifest(format!(
281 "Bad version in dep '{}': {e}",
282 dep_manifest.name
283 ))
284 })?;
285 if !req.matches(&dep_ver) {
286 conflicts.push((
287 manifest.name.clone(),
288 dep_name.clone(),
289 req_str.clone(),
290 dep_manifest.version.clone(),
291 ));
292 }
293 } else {
294 missing.push((manifest.name.clone(), dep_name.clone(), req_str.clone()));
295 }
296 }
297 }
298
299 let n = manifests.len();
301 let mut in_degree = vec![0usize; n];
302 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
303
304 for (i, manifest) in manifests.iter().enumerate() {
305 for dep_name in manifest.dependencies.keys() {
306 if let Some(&dep_idx) = index_by_name.get(dep_name.as_str()) {
307 adj[dep_idx].push(i);
308 in_degree[i] += 1;
309 }
310 }
311 }
312
313 let mut queue: std::collections::VecDeque<usize> = in_degree
314 .iter()
315 .enumerate()
316 .filter(|(_, &d)| d == 0)
317 .map(|(i, _)| i)
318 .collect();
319
320 let mut load_order: Vec<String> = Vec::with_capacity(n);
321
322 while let Some(idx) = queue.pop_front() {
323 load_order.push(manifests[idx].name.clone());
324 for &succ in &adj[idx] {
325 in_degree[succ] = in_degree[succ].saturating_sub(1);
326 if in_degree[succ] == 0 {
327 queue.push_back(succ);
328 }
329 }
330 }
331
332 if load_order.len() < n {
333 return Err(PluginError::InvalidManifest(
334 "Dependency cycle detected among plugins".to_string(),
335 ));
336 }
337
338 Ok(DependencyResolution {
339 load_order,
340 missing,
341 conflicts,
342 })
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct PluginManifest {
376 pub name: String,
378 pub version: String,
380 pub api_version: u32,
382 pub description: String,
384 pub author: String,
386 pub license: String,
388 pub patent_encumbered: bool,
390 pub library: String,
392 pub codecs: Vec<ManifestCodec>,
394 #[serde(default)]
396 pub dependencies: HashMap<String, String>,
397 #[serde(default)]
399 pub min_host_version: Option<String>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct ManifestCodec {
405 pub name: String,
407 pub decode: bool,
409 pub encode: bool,
411 pub description: String,
413}
414
415impl PluginManifest {
416 pub fn from_json(json: &str) -> PluginResult<Self> {
422 serde_json::from_str(json).map_err(|e| PluginError::InvalidManifest(e.to_string()))
423 }
424
425 pub fn to_json(&self) -> PluginResult<String> {
431 serde_json::to_string_pretty(self).map_err(PluginError::Json)
432 }
433
434 pub fn from_file(path: &Path) -> PluginResult<Self> {
441 let content = std::fs::read_to_string(path)?;
442 Self::from_json(&content)
443 }
444
445 pub fn validate(&self) -> PluginResult<()> {
461 if self.name.is_empty() {
462 return Err(PluginError::InvalidManifest(
463 "Plugin name must not be empty".to_string(),
464 ));
465 }
466
467 if self.version.is_empty() {
468 return Err(PluginError::InvalidManifest(
469 "Plugin version must not be empty".to_string(),
470 ));
471 }
472
473 if self.api_version != PLUGIN_API_VERSION {
474 return Err(PluginError::ApiIncompatible(format!(
475 "Manifest declares API v{}, host expects v{PLUGIN_API_VERSION}",
476 self.api_version
477 )));
478 }
479
480 if self.library.is_empty() {
481 return Err(PluginError::InvalidManifest(
482 "Library filename must not be empty".to_string(),
483 ));
484 }
485
486 if self.codecs.is_empty() {
487 return Err(PluginError::InvalidManifest(
488 "Plugin must declare at least one codec".to_string(),
489 ));
490 }
491
492 for (i, codec) in self.codecs.iter().enumerate() {
493 if codec.name.is_empty() {
494 return Err(PluginError::InvalidManifest(format!(
495 "Codec at index {i} has empty name"
496 )));
497 }
498
499 if !codec.decode && !codec.encode {
500 return Err(PluginError::InvalidManifest(format!(
501 "Codec '{}' must support at least decode or encode",
502 codec.name
503 )));
504 }
505 }
506
507 Ok(())
508 }
509
510 pub fn validate_version(&self) -> PluginResult<SemVer> {
517 SemVer::parse(&self.version).map_err(|e| {
518 PluginError::InvalidManifest(format!(
519 "Invalid plugin version '{}': {}",
520 self.version, e
521 ))
522 })
523 }
524
525 pub fn has_codec(&self, name: &str) -> bool {
527 self.codecs.iter().any(|c| c.name == name)
528 }
529
530 pub fn satisfies_requirement(&self, requirement: &str) -> PluginResult<bool> {
542 let version = self.validate_version()?;
543 let req = SemVerReq::parse(requirement).map_err(|e| {
544 PluginError::InvalidManifest(format!("Invalid requirement '{requirement}': {e}"))
545 })?;
546 Ok(req.matches(&version))
547 }
548
549 pub fn validate_dependencies(&self) -> PluginResult<()> {
555 for (dep_name, req_str) in &self.dependencies {
556 SemVerReq::parse(req_str).map_err(|e| {
557 PluginError::InvalidManifest(format!(
558 "Invalid dependency requirement for '{dep_name}': '{req_str}' — {e}"
559 ))
560 })?;
561 }
562 Ok(())
563 }
564
565 pub fn library_path(&self, manifest_path: &Path) -> Option<std::path::PathBuf> {
570 manifest_path.parent().map(|dir| dir.join(&self.library))
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 fn sample_manifest() -> PluginManifest {
579 PluginManifest {
580 name: "test-plugin".to_string(),
581 version: "1.0.0".to_string(),
582 api_version: PLUGIN_API_VERSION,
583 description: "A test plugin".to_string(),
584 author: "Test Author".to_string(),
585 license: "MIT".to_string(),
586 patent_encumbered: false,
587 library: "libtest_plugin.so".to_string(),
588 codecs: vec![ManifestCodec {
589 name: "test-codec".to_string(),
590 decode: true,
591 encode: false,
592 description: "A test codec".to_string(),
593 }],
594 dependencies: HashMap::new(),
595 min_host_version: None,
596 }
597 }
598
599 #[test]
600 fn test_manifest_roundtrip() {
601 let manifest = sample_manifest();
602 let json = manifest.to_json().expect("serialization should succeed");
603 let parsed = PluginManifest::from_json(&json).expect("deserialization should succeed");
604 assert_eq!(parsed.name, "test-plugin");
605 assert_eq!(parsed.version, "1.0.0");
606 assert_eq!(parsed.codecs.len(), 1);
607 assert_eq!(parsed.codecs[0].name, "test-codec");
608 }
609
610 #[test]
611 fn test_manifest_validate_success() {
612 let manifest = sample_manifest();
613 manifest.validate().expect("validation should succeed");
614 }
615
616 #[test]
617 fn test_manifest_validate_empty_name() {
618 let mut manifest = sample_manifest();
619 manifest.name = String::new();
620 let err = manifest.validate().expect_err("should fail");
621 assert!(err.to_string().contains("name must not be empty"));
622 }
623
624 #[test]
625 fn test_manifest_validate_empty_version() {
626 let mut manifest = sample_manifest();
627 manifest.version = String::new();
628 let err = manifest.validate().expect_err("should fail");
629 assert!(err.to_string().contains("version must not be empty"));
630 }
631
632 #[test]
633 fn test_manifest_validate_wrong_api_version() {
634 let mut manifest = sample_manifest();
635 manifest.api_version = 999;
636 let err = manifest.validate().expect_err("should fail");
637 assert!(err.to_string().contains("API"));
638 }
639
640 #[test]
641 fn test_manifest_validate_empty_library() {
642 let mut manifest = sample_manifest();
643 manifest.library = String::new();
644 let err = manifest.validate().expect_err("should fail");
645 assert!(err.to_string().contains("Library filename"));
646 }
647
648 #[test]
649 fn test_manifest_validate_no_codecs() {
650 let mut manifest = sample_manifest();
651 manifest.codecs.clear();
652 let err = manifest.validate().expect_err("should fail");
653 assert!(err.to_string().contains("at least one codec"));
654 }
655
656 #[test]
657 fn test_manifest_validate_codec_empty_name() {
658 let mut manifest = sample_manifest();
659 manifest.codecs[0].name = String::new();
660 let err = manifest.validate().expect_err("should fail");
661 assert!(err.to_string().contains("empty name"));
662 }
663
664 #[test]
665 fn test_manifest_validate_codec_no_capability() {
666 let mut manifest = sample_manifest();
667 manifest.codecs[0].decode = false;
668 manifest.codecs[0].encode = false;
669 let err = manifest.validate().expect_err("should fail");
670 assert!(err.to_string().contains("at least decode or encode"));
671 }
672
673 #[test]
674 fn test_manifest_has_codec() {
675 let manifest = sample_manifest();
676 assert!(manifest.has_codec("test-codec"));
677 assert!(!manifest.has_codec("nonexistent"));
678 }
679
680 #[test]
681 fn test_manifest_library_path() {
682 let manifest = sample_manifest();
683 let manifest_path = Path::new("/usr/lib/oximedia/plugins/test/plugin.json");
684 let lib_path = manifest.library_path(manifest_path);
685 assert_eq!(
686 lib_path,
687 Some(std::path::PathBuf::from(
688 "/usr/lib/oximedia/plugins/test/libtest_plugin.so"
689 ))
690 );
691 }
692
693 #[test]
694 fn test_manifest_from_invalid_json() {
695 let result = PluginManifest::from_json("not json");
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn test_manifest_from_file_not_found() {
701 let result = PluginManifest::from_file(Path::new("/nonexistent/plugin.json"));
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_manifest_from_file_roundtrip() {
707 let manifest = sample_manifest();
708 let json = manifest.to_json().expect("serialization should succeed");
709
710 let dir = std::env::temp_dir().join("oximedia-plugin-test-manifest");
711 std::fs::create_dir_all(&dir).expect("dir creation should succeed");
712 let path = dir.join("plugin.json");
713 std::fs::write(&path, &json).expect("write should succeed");
714
715 let loaded = PluginManifest::from_file(&path).expect("load should succeed");
716 assert_eq!(loaded.name, "test-plugin");
717
718 let _ = std::fs::remove_dir_all(&dir);
719 }
720
721 #[test]
724 fn test_semver_parse_full() {
725 let v = SemVer::parse("1.2.3").expect("parse should succeed");
726 assert_eq!(v.major, 1);
727 assert_eq!(v.minor, 2);
728 assert_eq!(v.patch, 3);
729 assert!(v.pre.is_none());
730 }
731
732 #[test]
733 fn test_semver_parse_with_pre() {
734 let v = SemVer::parse("0.1.0-alpha.1").expect("parse should succeed");
735 assert_eq!(v.major, 0);
736 assert_eq!(v.minor, 1);
737 assert_eq!(v.patch, 0);
738 assert_eq!(v.pre, Some("alpha.1".to_string()));
739 }
740
741 #[test]
742 fn test_semver_parse_two_parts() {
743 let v = SemVer::parse("2.5").expect("parse should succeed");
744 assert_eq!(v.major, 2);
745 assert_eq!(v.minor, 5);
746 assert_eq!(v.patch, 0);
747 }
748
749 #[test]
750 fn test_semver_parse_invalid() {
751 assert!(SemVer::parse("abc").is_err());
752 assert!(SemVer::parse("1.2.3.4").is_err());
753 }
754
755 #[test]
756 fn test_semver_display() {
757 let v = SemVer::parse("1.2.3").expect("parse should succeed");
758 assert_eq!(v.to_string(), "1.2.3");
759
760 let v2 = SemVer::parse("0.1.0-beta").expect("parse should succeed");
761 assert_eq!(v2.to_string(), "0.1.0-beta");
762 }
763
764 #[test]
765 fn test_semver_caret_compatibility() {
766 let v = SemVer::parse("1.5.2").expect("parse should succeed");
767 let req = SemVer::parse("1.0.0").expect("parse should succeed");
768 assert!(v.is_caret_compatible(&req));
769
770 let req2 = SemVer::parse("2.0.0").expect("parse should succeed");
771 assert!(!v.is_caret_compatible(&req2));
772
773 let v03 = SemVer::parse("0.2.5").expect("parse should succeed");
775 let req03 = SemVer::parse("0.2.0").expect("parse should succeed");
776 assert!(v03.is_caret_compatible(&req03));
777
778 let v04 = SemVer::parse("0.3.0").expect("parse should succeed");
779 assert!(!v04.is_caret_compatible(&req03));
780 }
781
782 #[test]
783 fn test_semver_tilde_compatibility() {
784 let v = SemVer::parse("1.2.5").expect("parse should succeed");
785 let req = SemVer::parse("1.2.0").expect("parse should succeed");
786 assert!(v.is_tilde_compatible(&req));
787
788 let v_bad = SemVer::parse("1.3.0").expect("parse should succeed");
789 assert!(!v_bad.is_tilde_compatible(&req));
790 }
791
792 #[test]
795 fn test_semver_req_exact() {
796 let req = SemVerReq::parse("1.0.0").expect("parse should succeed");
797 assert!(req.matches(&SemVer::parse("1.0.0").expect("parse")));
798 assert!(!req.matches(&SemVer::parse("1.0.1").expect("parse")));
799 }
800
801 #[test]
802 fn test_semver_req_gte() {
803 let req = SemVerReq::parse(">=1.0.0").expect("parse should succeed");
804 assert!(req.matches(&SemVer::parse("1.0.0").expect("parse")));
805 assert!(req.matches(&SemVer::parse("2.0.0").expect("parse")));
806 assert!(!req.matches(&SemVer::parse("0.9.9").expect("parse")));
807 }
808
809 #[test]
810 fn test_semver_req_caret() {
811 let req = SemVerReq::parse("^1.2.0").expect("parse should succeed");
812 assert!(req.matches(&SemVer::parse("1.5.0").expect("parse")));
813 assert!(!req.matches(&SemVer::parse("2.0.0").expect("parse")));
814 assert!(!req.matches(&SemVer::parse("1.1.0").expect("parse")));
815 }
816
817 #[test]
818 fn test_semver_req_tilde() {
819 let req = SemVerReq::parse("~1.2.0").expect("parse should succeed");
820 assert!(req.matches(&SemVer::parse("1.2.5").expect("parse")));
821 assert!(!req.matches(&SemVer::parse("1.3.0").expect("parse")));
822 }
823
824 #[test]
825 fn test_semver_req_wildcard() {
826 let req = SemVerReq::parse("*").expect("parse should succeed");
827 assert!(req.matches(&SemVer::parse("0.0.1").expect("parse")));
828 assert!(req.matches(&SemVer::parse("99.99.99").expect("parse")));
829 }
830
831 #[test]
832 fn test_semver_req_lt() {
833 let req = SemVerReq::parse("<2.0.0").expect("parse should succeed");
834 assert!(req.matches(&SemVer::parse("1.9.9").expect("parse")));
835 assert!(!req.matches(&SemVer::parse("2.0.0").expect("parse")));
836 }
837
838 #[test]
841 fn test_manifest_validate_version() {
842 let m = sample_manifest();
843 let v = m.validate_version().expect("should parse");
844 assert_eq!(v.major, 1);
845 }
846
847 #[test]
848 fn test_manifest_satisfies_requirement() {
849 let m = sample_manifest(); assert!(m.satisfies_requirement(">=0.5.0").expect("should parse"));
851 assert!(m.satisfies_requirement("^1.0.0").expect("should parse"));
852 assert!(!m.satisfies_requirement(">=2.0.0").expect("should parse"));
853 }
854
855 #[test]
856 fn test_manifest_validate_dependencies_ok() {
857 let mut m = sample_manifest();
858 m.dependencies
859 .insert("other-plugin".to_string(), "^1.0.0".to_string());
860 assert!(m.validate_dependencies().is_ok());
861 }
862
863 #[test]
864 fn test_manifest_validate_dependencies_bad_req() {
865 let mut m = sample_manifest();
866 m.dependencies
867 .insert("other".to_string(), "not-a-version!!!".to_string());
868 assert!(m.validate_dependencies().is_err());
869 }
870
871 #[test]
874 fn test_resolve_dependencies_no_deps() {
875 let m = sample_manifest();
876 let res = resolve_dependencies(&[m]).expect("should resolve");
877 assert!(res.is_satisfied());
878 assert_eq!(res.load_order.len(), 1);
879 }
880
881 #[test]
882 fn test_resolve_dependencies_linear_chain() {
883 let mut a = sample_manifest();
884 a.name = "plugin-a".to_string();
885 a.version = "1.0.0".to_string();
886 a.dependencies.clear();
887
888 let mut b = sample_manifest();
889 b.name = "plugin-b".to_string();
890 b.version = "2.0.0".to_string();
891 b.dependencies
892 .insert("plugin-a".to_string(), "^1.0.0".to_string());
893
894 let res = resolve_dependencies(&[a, b]).expect("should resolve");
895 assert!(res.is_satisfied());
896 let pos_a = res
898 .load_order
899 .iter()
900 .position(|n| n == "plugin-a")
901 .expect("should exist");
902 let pos_b = res
903 .load_order
904 .iter()
905 .position(|n| n == "plugin-b")
906 .expect("should exist");
907 assert!(pos_a < pos_b);
908 }
909
910 #[test]
911 fn test_resolve_dependencies_missing() {
912 let mut m = sample_manifest();
913 m.dependencies
914 .insert("nonexistent".to_string(), ">=1.0.0".to_string());
915
916 let res = resolve_dependencies(&[m]).expect("should resolve");
917 assert!(!res.is_satisfied());
918 assert_eq!(res.missing.len(), 1);
919 assert_eq!(res.missing[0].1, "nonexistent");
920 }
921
922 #[test]
923 fn test_resolve_dependencies_version_conflict() {
924 let mut a = sample_manifest();
925 a.name = "provider".to_string();
926 a.version = "1.0.0".to_string();
927
928 let mut b = sample_manifest();
929 b.name = "consumer".to_string();
930 b.dependencies
931 .insert("provider".to_string(), ">=2.0.0".to_string());
932
933 let res = resolve_dependencies(&[a, b]).expect("should resolve");
934 assert!(!res.is_satisfied());
935 assert_eq!(res.conflicts.len(), 1);
936 }
937
938 #[test]
939 fn test_resolve_dependencies_cycle() {
940 let mut a = sample_manifest();
941 a.name = "cycle-a".to_string();
942 a.dependencies
943 .insert("cycle-b".to_string(), "*".to_string());
944
945 let mut b = sample_manifest();
946 b.name = "cycle-b".to_string();
947 b.dependencies
948 .insert("cycle-a".to_string(), "*".to_string());
949
950 let res = resolve_dependencies(&[a, b]);
951 assert!(res.is_err());
952 assert!(res
953 .expect_err("should be an error")
954 .to_string()
955 .contains("cycle"));
956 }
957}