1use anyhow::Result;
8use semver::{Op, Version, VersionReq};
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11
12use crate::core::AgpmError;
13use pubgrub::Ranges;
14
15#[derive(Debug, Clone)]
17pub struct VersionConflict {
18 pub resource: String,
19 pub conflicting_requirements: Vec<ConflictingRequirement>,
20}
21
22#[derive(Debug, Clone)]
23pub struct ConflictingRequirement {
24 pub required_by: String,
25 pub requirement: String,
26 pub resolved_version: Option<Version>,
27}
28
29impl fmt::Display for VersionConflict {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 writeln!(f, "Version conflict for '{}':", self.resource)?;
32 for req in &self.conflicting_requirements {
33 writeln!(f, " - {} requires {}", req.required_by, req.requirement)?;
34 if let Some(v) = &req.resolved_version {
35 writeln!(f, " (resolved to {v})")?;
36 }
37 }
38 Ok(())
39 }
40}
41
42pub struct ConflictDetector {
44 requirements: HashMap<String, Vec<(String, String)>>, }
46
47impl Default for ConflictDetector {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl ConflictDetector {
54 pub fn new() -> Self {
55 Self {
56 requirements: HashMap::new(),
57 }
58 }
59
60 pub fn add_requirement(&mut self, resource: &str, required_by: &str, requirement: &str) {
62 self.requirements
63 .entry(resource.to_string())
64 .or_default()
65 .push((required_by.to_string(), requirement.to_string()));
66 }
67
68 pub fn detect_conflicts(&self) -> Vec<VersionConflict> {
70 let mut conflicts = Vec::new();
71
72 for (resource, requirements) in &self.requirements {
73 if requirements.len() <= 1 {
74 continue; }
76
77 let compatible = self.are_requirements_compatible(requirements);
79 if !compatible {
80 let conflict = VersionConflict {
81 resource: resource.clone(),
82 conflicting_requirements: requirements
83 .iter()
84 .map(|(requirer, req)| ConflictingRequirement {
85 required_by: requirer.clone(),
86 requirement: req.clone(),
87 resolved_version: None,
88 })
89 .collect(),
90 };
91 conflicts.push(conflict);
92 }
93 }
94
95 conflicts
96 }
97
98 fn are_requirements_compatible(&self, requirements: &[(String, String)]) -> bool {
100 let has_head = requirements.iter().any(|(_, req)| req == "HEAD");
102 let has_specific = requirements.iter().any(|(_, req)| req != "HEAD");
103
104 if has_head && has_specific {
105 return false;
107 }
108
109 let parsed_reqs: Vec<_> = requirements
111 .iter()
112 .filter_map(|(_, req)| {
113 if req == "*" {
114 Some(VersionReq::parse("*").unwrap())
115 } else if req == "HEAD" {
116 None
118 } else {
119 crate::version::parse_version_req(req).ok()
120 }
121 })
122 .collect();
123
124 if parsed_reqs.len() != requirements.len() {
125 let has_semver = !parsed_reqs.is_empty();
128 let has_git_refs = parsed_reqs.len() < requirements.len();
129
130 if has_semver && has_git_refs {
131 return false;
133 }
134
135 return self.check_git_ref_compatibility(requirements);
137 }
138
139 self.can_satisfy_all(&parsed_reqs)
141 }
142
143 fn check_git_ref_compatibility(&self, requirements: &[(String, String)]) -> bool {
148 let refs: HashSet<_> = requirements
149 .iter()
150 .filter_map(|(_, req)| {
151 if !req.starts_with('^')
152 && !req.starts_with('~')
153 && !req.starts_with('>')
154 && !req.starts_with('<')
155 && !req.starts_with('=')
156 && req != "HEAD"
157 && req != "*"
158 {
159 Some(req.to_lowercase())
162 } else {
163 None
164 }
165 })
166 .collect();
167
168 refs.len() <= 1
170 }
171
172 fn can_satisfy_all(&self, requirements: &[VersionReq]) -> bool {
176 if requirements.is_empty() {
177 return true;
178 }
179
180 let mut intersection: Option<Ranges<Version>> = None;
182
183 for req in requirements {
184 let range = self.version_req_to_ranges(req);
185
186 intersection = match intersection {
187 None => Some(range),
188 Some(current) => Some(current.intersection(&range)),
189 };
190
191 if let Some(ref i) = intersection
193 && i.is_empty()
194 {
195 return false;
196 }
197 }
198
199 intersection.is_none_or(|i| !i.is_empty())
201 }
202
203 fn version_req_to_ranges(&self, req: &VersionReq) -> Ranges<Version> {
208 let comparators = &req.comparators;
209
210 if comparators.is_empty() {
212 return Ranges::full();
213 }
214
215 let mut ranges = Ranges::full();
217
218 for comp in comparators {
219 let base_version = if comp.pre.is_empty() {
221 Version::new(comp.major, comp.minor.unwrap_or(0), comp.patch.unwrap_or(0))
222 } else {
223 Version {
224 major: comp.major,
225 minor: comp.minor.unwrap_or(0),
226 patch: comp.patch.unwrap_or(0),
227 pre: comp.pre.clone(),
228 build: Default::default(),
229 }
230 };
231
232 let comp_range = match comp.op {
233 Op::Exact => {
234 Ranges::singleton(base_version)
236 }
237 Op::Greater => {
238 Ranges::strictly_higher_than(base_version)
240 }
241 Op::GreaterEq => {
242 Ranges::higher_than(base_version)
244 }
245 Op::Less => {
246 Ranges::strictly_lower_than(base_version)
248 }
249 Op::LessEq => {
250 Ranges::lower_than(base_version)
252 }
253 Op::Tilde => {
254 let upper = if comp.minor.is_none() {
259 Version::new(comp.major + 1, 0, 0)
261 } else {
262 Version::new(comp.major, comp.minor.unwrap() + 1, 0)
264 };
265 Ranges::between(base_version, upper)
266 }
267 Op::Caret => {
268 if base_version.major > 0 {
276 let upper = Version::new(base_version.major + 1, 0, 0);
278 Ranges::between(base_version, upper)
279 } else if base_version.minor > 0 {
280 let upper = Version::new(0, base_version.minor + 1, 0);
282 Ranges::between(base_version, upper)
283 } else if comp.patch.is_some() && base_version.patch > 0 {
284 let upper = Version::new(0, 0, base_version.patch + 1);
286 Ranges::between(base_version, upper)
287 } else if comp.patch.is_none() && comp.minor.is_some() {
288 let upper = Version::new(0, 1, 0);
290 Ranges::between(base_version, upper)
291 } else if comp.minor.is_none() {
292 let upper = Version::new(1, 0, 0);
294 Ranges::between(base_version, upper)
295 } else {
296 let upper = Version::new(0, 0, 1);
298 Ranges::between(base_version, upper)
299 }
300 }
301 Op::Wildcard => {
302 if comp.minor.is_none() {
304 let lower = Version::new(comp.major, 0, 0);
306 let upper = Version::new(comp.major + 1, 0, 0);
307 Ranges::between(lower, upper)
308 } else if comp.patch.is_none() {
309 let lower = Version::new(comp.major, comp.minor.unwrap(), 0);
311 let upper = Version::new(comp.major, comp.minor.unwrap() + 1, 0);
312 Ranges::between(lower, upper)
313 } else {
314 Ranges::singleton(base_version)
316 }
317 }
318 _ => {
319 Ranges::full()
321 }
322 };
323
324 ranges = ranges.intersection(&comp_range);
326 }
327
328 ranges
329 }
330
331 pub fn resolve_conflicts(
333 &self,
334 available_versions: &HashMap<String, Vec<Version>>,
335 ) -> Result<HashMap<String, Version>> {
336 let mut resolved = HashMap::new();
337 let conflicts = self.detect_conflicts();
338
339 if !conflicts.is_empty() {
340 let conflict_messages: Vec<String> =
341 conflicts.iter().map(std::string::ToString::to_string).collect();
342
343 return Err(AgpmError::Other {
344 message: format!(
345 "Unable to resolve version conflicts:\n{}",
346 conflict_messages.join("\n")
347 ),
348 }
349 .into());
350 }
351
352 for (resource, requirements) in &self.requirements {
354 let versions = available_versions.get(resource).ok_or_else(|| AgpmError::Other {
355 message: format!("No versions available for resource: {resource}"),
356 })?;
357
358 let best_version = self.find_best_version(versions, requirements)?;
359 resolved.insert(resource.clone(), best_version);
360 }
361
362 Ok(resolved)
363 }
364
365 fn find_best_version(
367 &self,
368 available: &[Version],
369 requirements: &[(String, String)],
370 ) -> Result<Version> {
371 let mut candidates = available.to_vec();
372
373 for (_, req_str) in requirements {
375 if req_str == "latest" || req_str == "*" {
376 continue; }
378
379 if let Ok(req) = crate::version::parse_version_req(req_str) {
380 candidates.retain(|v| req.matches(v));
381 }
382 }
383
384 if candidates.is_empty() {
385 return Err(AgpmError::Other {
386 message: format!("No version satisfies all requirements: {requirements:?}"),
387 }
388 .into());
389 }
390
391 candidates.sort_by(|a, b| b.cmp(a));
393 Ok(candidates[0].clone())
394 }
395}
396
397pub struct CircularDependencyDetector {
399 graph: HashMap<String, HashSet<String>>,
400}
401
402impl Default for CircularDependencyDetector {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408impl CircularDependencyDetector {
409 pub fn new() -> Self {
410 Self {
411 graph: HashMap::new(),
412 }
413 }
414
415 pub fn add_dependency(&mut self, from: &str, to: &str) {
417 self.graph.entry(from.to_string()).or_default().insert(to.to_string());
418 }
419
420 pub fn detect_cycles(&self) -> Vec<Vec<String>> {
422 let mut cycles = Vec::new();
423 let mut visited = HashSet::new();
424 let mut rec_stack = HashSet::new();
425 let mut path = Vec::new();
426
427 for node in self.graph.keys() {
428 if !visited.contains(node) {
429 self.dfs_detect_cycle(node, &mut visited, &mut rec_stack, &mut path, &mut cycles);
430 }
431 }
432
433 cycles
434 }
435
436 fn dfs_detect_cycle(
437 &self,
438 node: &str,
439 visited: &mut HashSet<String>,
440 rec_stack: &mut HashSet<String>,
441 path: &mut Vec<String>,
442 cycles: &mut Vec<Vec<String>>,
443 ) {
444 visited.insert(node.to_string());
445 rec_stack.insert(node.to_string());
446 path.push(node.to_string());
447
448 if let Some(neighbors) = self.graph.get(node) {
449 for neighbor in neighbors {
450 if !visited.contains(neighbor) {
451 self.dfs_detect_cycle(neighbor, visited, rec_stack, path, cycles);
452 } else if rec_stack.contains(neighbor) {
453 let cycle_start = path.iter().position(|n| n == neighbor).unwrap();
455 let cycle = path[cycle_start..].to_vec();
456 cycles.push(cycle);
457 }
458 }
459 }
460
461 path.pop();
462 rec_stack.remove(node);
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_conflict_detection() {
472 let mut detector = ConflictDetector::new();
473
474 detector.add_requirement("lib1", "app1", "^1.0.0");
476 detector.add_requirement("lib1", "app2", "^1.2.0");
477
478 let conflicts = detector.detect_conflicts();
479 assert_eq!(conflicts.len(), 0); detector.add_requirement("lib2", "app1", "^1.0.0");
483 detector.add_requirement("lib2", "app2", "^2.0.0");
484
485 let conflicts = detector.detect_conflicts();
486 assert_eq!(conflicts.len(), 1);
487 assert_eq!(conflicts[0].resource, "lib2");
488 }
489
490 #[test]
491 fn test_git_ref_compatibility() {
492 let mut detector = ConflictDetector::new();
493
494 detector.add_requirement("lib1", "app1", "main");
496 detector.add_requirement("lib1", "app2", "main");
497
498 let conflicts = detector.detect_conflicts();
499 assert_eq!(conflicts.len(), 0);
500
501 detector.add_requirement("lib2", "app1", "main");
503 detector.add_requirement("lib2", "app2", "develop");
504
505 let conflicts = detector.detect_conflicts();
506 assert_eq!(conflicts.len(), 1);
507 }
508
509 #[test]
510 fn test_git_ref_case_insensitive() {
511 let mut detector = ConflictDetector::new();
512
513 detector.add_requirement("lib1", "app1", "main");
516 detector.add_requirement("lib1", "app2", "Main");
517 detector.add_requirement("lib1", "app3", "MAIN");
518
519 let conflicts = detector.detect_conflicts();
520 assert_eq!(
521 conflicts.len(),
522 0,
523 "Git refs differing only by case should be compatible (case-insensitive filesystems)"
524 );
525
526 let mut detector2 = ConflictDetector::new();
528 detector2.add_requirement("lib2", "app1", "Main");
529 detector2.add_requirement("lib2", "app2", "Develop");
530
531 let conflicts2 = detector2.detect_conflicts();
532 assert_eq!(
533 conflicts2.len(),
534 1,
535 "Different branch names should conflict regardless of case"
536 );
537 }
538
539 #[test]
540 fn test_resolve_conflicts() {
541 let mut detector = ConflictDetector::new();
542 detector.add_requirement("lib1", "app1", "^1.0.0");
543 detector.add_requirement("lib1", "app2", "^1.2.0");
544
545 let mut available = HashMap::new();
546 available.insert(
547 "lib1".to_string(),
548 vec![
549 Version::parse("1.0.0").unwrap(),
550 Version::parse("1.2.0").unwrap(),
551 Version::parse("1.5.0").unwrap(),
552 Version::parse("2.0.0").unwrap(),
553 ],
554 );
555
556 let resolved = detector.resolve_conflicts(&available).unwrap();
557 assert_eq!(resolved.get("lib1"), Some(&Version::parse("1.5.0").unwrap()));
558 }
559
560 #[test]
561 fn test_circular_dependency_detection() {
562 let mut detector = CircularDependencyDetector::new();
563
564 detector.add_dependency("A", "B");
566 detector.add_dependency("B", "C");
567 detector.add_dependency("C", "A");
568
569 let cycles = detector.detect_cycles();
570 assert_eq!(cycles.len(), 1);
571 assert!(cycles[0].contains(&"A".to_string()));
572 assert!(cycles[0].contains(&"B".to_string()));
573 assert!(cycles[0].contains(&"C".to_string()));
574 }
575
576 #[test]
577 fn test_no_circular_dependencies() {
578 let mut detector = CircularDependencyDetector::new();
579
580 detector.add_dependency("A", "B");
582 detector.add_dependency("B", "C");
583 detector.add_dependency("A", "C");
584
585 let cycles = detector.detect_cycles();
586 assert_eq!(cycles.len(), 0);
587 }
588
589 #[test]
590 fn test_conflict_display() {
591 let conflict = VersionConflict {
592 resource: "test-lib".to_string(),
593 conflicting_requirements: vec![
594 ConflictingRequirement {
595 required_by: "app1".to_string(),
596 requirement: "^1.0.0".to_string(),
597 resolved_version: Some(Version::parse("1.5.0").unwrap()),
598 },
599 ConflictingRequirement {
600 required_by: "app2".to_string(),
601 requirement: "^2.0.0".to_string(),
602 resolved_version: None,
603 },
604 ],
605 };
606
607 let display = format!("{}", conflict);
608 assert!(display.contains("test-lib"));
609 assert!(display.contains("app1"));
610 assert!(display.contains("^1.0.0"));
611 assert!(display.contains("1.5.0"));
612 }
613
614 #[test]
615 fn test_head_with_specific_version_conflict() {
616 let mut detector = ConflictDetector::new();
617
618 detector.add_requirement("lib1", "app1", "HEAD");
620 detector.add_requirement("lib1", "app2", "^1.0.0");
621
622 let conflicts = detector.detect_conflicts();
623 assert_eq!(conflicts.len(), 1, "HEAD mixed with specific version should conflict");
624
625 let mut detector2 = ConflictDetector::new();
627 detector2.add_requirement("lib2", "app1", "*");
628 detector2.add_requirement("lib2", "app2", "^1.0.0");
629
630 let conflicts = detector2.detect_conflicts();
631 assert_eq!(
632 conflicts.len(),
633 0,
634 "* should be compatible with ^1.0.0 (intersection is [1.0.0, 2.0.0))"
635 );
636
637 let mut detector3 = ConflictDetector::new();
639 detector3.add_requirement("lib3", "app1", "*");
640 detector3.add_requirement("lib3", "app2", "~2.1.0");
641
642 let conflicts = detector3.detect_conflicts();
643 assert_eq!(
644 conflicts.len(),
645 0,
646 "* should be compatible with ~2.1.0 (intersection is [2.1.0, 2.2.0))"
647 );
648 }
649
650 #[test]
651 fn test_mixed_semver_and_git_refs() {
652 let mut detector = ConflictDetector::new();
653
654 detector.add_requirement("lib1", "app1", "^1.0.0");
656 detector.add_requirement("lib1", "app2", "main");
657
658 let conflicts = detector.detect_conflicts();
659 assert_eq!(conflicts.len(), 1, "Mixed semver and git ref should be detected as conflict");
660
661 let mut detector2 = ConflictDetector::new();
663 detector2.add_requirement("lib2", "app1", "v1.0.0");
664 detector2.add_requirement("lib2", "app2", "develop");
665
666 let conflicts2 = detector2.detect_conflicts();
667 assert_eq!(conflicts2.len(), 1, "Exact version with git branch should conflict");
668 }
669
670 #[test]
671 fn test_duplicate_requirements_same_version() {
672 let mut detector = ConflictDetector::new();
673
674 detector.add_requirement("lib1", "app1", "v1.0.0");
676 detector.add_requirement("lib1", "app2", "v1.0.0");
677 detector.add_requirement("lib1", "app3", "v1.0.0");
678
679 let conflicts = detector.detect_conflicts();
680 assert_eq!(conflicts.len(), 0, "Same version requirements should not conflict");
681 }
682
683 #[test]
684 fn test_exact_version_conflicts() {
685 let mut detector = ConflictDetector::new();
686
687 detector.add_requirement("lib1", "app1", "v1.0.0");
689 detector.add_requirement("lib1", "app2", "v2.0.0");
690
691 let conflicts = detector.detect_conflicts();
692 assert_eq!(conflicts.len(), 1, "Different exact versions must conflict");
693 assert_eq!(conflicts[0].conflicting_requirements.len(), 2);
694 }
695
696 #[test]
697 fn test_resolve_conflicts_missing_resource() {
698 let mut detector = ConflictDetector::new();
699 detector.add_requirement("lib1", "app1", "^1.0.0");
700
701 let available = HashMap::new(); let result = detector.resolve_conflicts(&available);
704 assert!(result.is_err(), "Should error when resource not in available versions");
705 let err_msg = result.unwrap_err().to_string();
706 assert!(err_msg.contains("No versions available"), "Error should mention missing versions");
707 }
708
709 #[test]
710 fn test_resolve_conflicts_with_incompatible_ranges() {
711 let mut detector = ConflictDetector::new();
712 detector.add_requirement("lib1", "app1", "^1.0.0");
713 detector.add_requirement("lib1", "app2", "^2.0.0");
714
715 let mut available = HashMap::new();
716 available.insert(
717 "lib1".to_string(),
718 vec![Version::parse("1.5.0").unwrap(), Version::parse("2.3.0").unwrap()],
719 );
720
721 let result = detector.resolve_conflicts(&available);
722 assert!(result.is_err(), "Should error when requirements are incompatible");
723 let err_msg = result.unwrap_err().to_string();
724 assert!(
725 err_msg.contains("Unable to resolve version conflicts"),
726 "Error should mention conflict resolution failure"
727 );
728 }
729
730 #[test]
731 fn test_resolve_conflicts_no_matching_version() {
732 let mut detector = ConflictDetector::new();
733 detector.add_requirement("lib1", "app1", "^3.0.0"); let mut available = HashMap::new();
736 available.insert(
737 "lib1".to_string(),
738 vec![Version::parse("1.0.0").unwrap(), Version::parse("2.0.0").unwrap()],
739 );
740
741 let result = detector.resolve_conflicts(&available);
742 assert!(result.is_err(), "Should error when no version satisfies requirement");
743 let err_msg = result.unwrap_err().to_string();
744 assert!(
745 err_msg.contains("No version satisfies"),
746 "Error should mention no matching version: {}",
747 err_msg
748 );
749 }
750
751 #[test]
752 fn test_conflict_aggregated_error_message() {
753 let mut detector = ConflictDetector::new();
754 detector.add_requirement("lib1", "app1", "^1.0.0");
755 detector.add_requirement("lib1", "app2", "^2.0.0");
756 detector.add_requirement("lib2", "app1", "main");
757 detector.add_requirement("lib2", "app3", "develop");
758
759 let conflicts = detector.detect_conflicts();
760 assert_eq!(conflicts.len(), 2, "Should detect both conflicts");
761
762 let lib1_conflict = conflicts.iter().find(|c| c.resource == "lib1");
764 assert!(lib1_conflict.is_some(), "Should have lib1 conflict");
765 assert_eq!(
766 lib1_conflict.unwrap().conflicting_requirements.len(),
767 2,
768 "lib1 should have 2 conflicting requirements"
769 );
770
771 let lib2_conflict = conflicts.iter().find(|c| c.resource == "lib2");
772 assert!(lib2_conflict.is_some(), "Should have lib2 conflict");
773 assert_eq!(
774 lib2_conflict.unwrap().conflicting_requirements.len(),
775 2,
776 "lib2 should have 2 conflicting requirements"
777 );
778 }
779
780 #[test]
781 fn test_multi_comparator_compatible() {
782 let mut detector = ConflictDetector::new();
783
784 detector.add_requirement("lib1", "app1", ">=5.0.0, <6.0.0");
787 detector.add_requirement("lib1", "app2", ">=5.5.0");
788
789 let conflicts = detector.detect_conflicts();
790 assert_eq!(
791 conflicts.len(),
792 0,
793 "Multi-comparator ranges with non-empty intersection should be compatible"
794 );
795 }
796
797 #[test]
798 fn test_multi_comparator_incompatible() {
799 let mut detector = ConflictDetector::new();
800
801 detector.add_requirement("lib1", "app1", ">=5.0.0, <6.0.0");
804 detector.add_requirement("lib1", "app2", ">=7.0.0");
805
806 let conflicts = detector.detect_conflicts();
807 assert_eq!(
808 conflicts.len(),
809 1,
810 "Multi-comparator ranges with empty intersection should conflict"
811 );
812 }
813
814 #[test]
815 fn test_tilde_operator_variants() {
816 let mut detector1 = ConflictDetector::new();
817
818 detector1.add_requirement("lib1", "app1", "~1");
820 detector1.add_requirement("lib1", "app2", "^1.5.0");
821
822 let conflicts1 = detector1.detect_conflicts();
823 assert_eq!(
824 conflicts1.len(),
825 0,
826 "~1 should be compatible with ^1.5.0 (intersection is [1.5.0, 2.0.0))"
827 );
828
829 let mut detector2 = ConflictDetector::new();
830
831 detector2.add_requirement("lib2", "app1", "~1.2");
833 detector2.add_requirement("lib2", "app2", "^1.5.0");
834
835 let conflicts2 = detector2.detect_conflicts();
836 assert_eq!(conflicts2.len(), 1, "~1.2 should conflict with ^1.5.0 (disjoint ranges)");
837
838 let mut detector3 = ConflictDetector::new();
839
840 detector3.add_requirement("lib3", "app1", "~1.2.3");
842 detector3.add_requirement("lib3", "app2", ">=1.2.0");
843
844 let conflicts3 = detector3.detect_conflicts();
845 assert_eq!(conflicts3.len(), 0, "~1.2.3 should be compatible with >=1.2.0");
846 }
847
848 #[test]
849 fn test_caret_zero_zero_patch() {
850 let mut detector1 = ConflictDetector::new();
851
852 detector1.add_requirement("lib1", "app1", "^0.0.3");
854 detector1.add_requirement("lib1", "app2", ">=0.0.3, <0.0.5");
855
856 let conflicts1 = detector1.detect_conflicts();
857 assert_eq!(
858 conflicts1.len(),
859 0,
860 "^0.0.3 should be compatible with >=0.0.3, <0.0.5 (intersection is [0.0.3, 0.0.4))"
861 );
862
863 let mut detector2 = ConflictDetector::new();
864
865 detector2.add_requirement("lib2", "app1", "^0.0.3");
867 detector2.add_requirement("lib2", "app2", "^0.0.5");
868
869 let conflicts2 = detector2.detect_conflicts();
870 assert_eq!(conflicts2.len(), 1, "^0.0.3 should conflict with ^0.0.5 (disjoint ranges)");
871 }
872
873 #[test]
874 fn test_caret_zero_variants() {
875 let mut detector1 = ConflictDetector::new();
876
877 detector1.add_requirement("lib1", "app1", "^0");
879 detector1.add_requirement("lib1", "app2", "^0.5.0");
880
881 let conflicts1 = detector1.detect_conflicts();
882 assert_eq!(
883 conflicts1.len(),
884 0,
885 "^0 should be compatible with ^0.5.0 (intersection is [0.5.0, 0.6.0))"
886 );
887
888 let mut detector2 = ConflictDetector::new();
889
890 detector2.add_requirement("lib2", "app1", "^0.0");
892 detector2.add_requirement("lib2", "app2", "^0.5.0");
893
894 let conflicts2 = detector2.detect_conflicts();
895 assert_eq!(conflicts2.len(), 1, "^0.0 should conflict with ^0.5.0 (disjoint ranges)");
896 }
897
898 #[test]
899 fn test_prerelease_versions() {
900 let mut detector1 = ConflictDetector::new();
901
902 detector1.add_requirement("lib1", "app1", "=1.0.0-beta.1");
904 detector1.add_requirement("lib1", "app2", "=1.0.0");
905
906 let conflicts1 = detector1.detect_conflicts();
907 assert_eq!(
908 conflicts1.len(),
909 1,
910 "=1.0.0-beta.1 should conflict with =1.0.0 (different prerelease)"
911 );
912
913 let mut detector2 = ConflictDetector::new();
914
915 detector2.add_requirement("lib2", "app1", "=1.0.0-beta.1");
917 detector2.add_requirement("lib2", "app2", "=1.0.0-beta.1");
918
919 let conflicts2 = detector2.detect_conflicts();
920 assert_eq!(conflicts2.len(), 0, "Same prerelease version should be compatible");
921
922 let mut detector3 = ConflictDetector::new();
923
924 detector3.add_requirement("lib3", "app1", ">=1.0.0-beta");
926 detector3.add_requirement("lib3", "app2", ">=1.0.0-alpha");
927
928 let conflicts3 = detector3.detect_conflicts();
929 assert_eq!(conflicts3.len(), 0, ">=1.0.0-beta should be compatible with >=1.0.0-alpha");
930 }
931
932 #[test]
933 fn test_high_version_ranges() {
934 let mut detector = ConflictDetector::new();
935
936 detector.add_requirement("lib1", "app1", ">=5.0.0, <10.0.0");
938 detector.add_requirement("lib1", "app2", "^7.5.0");
939
940 let conflicts = detector.detect_conflicts();
941 assert_eq!(
942 conflicts.len(),
943 0,
944 "High version ranges should work correctly (intersection is [7.5.0, 8.0.0))"
945 );
946
947 let mut detector2 = ConflictDetector::new();
948
949 detector2.add_requirement("lib2", "app1", ">=100.0.0");
951 detector2.add_requirement("lib2", "app2", "<50.0.0");
952
953 let conflicts2 = detector2.detect_conflicts();
954 assert_eq!(conflicts2.len(), 1, "Disjoint high version ranges should conflict");
955 }
956}