1use std::collections::HashMap;
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SemVerError {
17 ParseError(String),
19 ArtifactNotFound(String),
21 ArtifactAlreadyExists(String),
23 InvalidVersion(String),
25}
26
27impl fmt::Display for SemVerError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 SemVerError::ParseError(s) => write!(f, "SemVer parse error: {s}"),
31 SemVerError::ArtifactNotFound(s) => write!(f, "artifact not found: {s}"),
32 SemVerError::ArtifactAlreadyExists(s) => write!(f, "artifact already exists: {s}"),
33 SemVerError::InvalidVersion(s) => write!(f, "invalid version: {s}"),
34 }
35 }
36}
37
38impl std::error::Error for SemVerError {}
39
40#[derive(Debug, Clone, Eq)]
49pub struct SemVer {
50 pub major: u32,
52 pub minor: u32,
54 pub patch: u32,
56 pub pre_release: Option<String>,
58 pub build_metadata: Option<String>,
60}
61
62impl SemVer {
63 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
65 Self {
66 major,
67 minor,
68 patch,
69 pre_release: None,
70 build_metadata: None,
71 }
72 }
73
74 pub fn parse(s: &str) -> Result<SemVer, SemVerError> {
77 let (version_and_pre, build_metadata) = match s.split_once('+') {
79 Some((left, right)) => (left, Some(right.to_string())),
80 None => (s, None),
81 };
82
83 let (version_part, pre_release) = match version_and_pre.split_once('-') {
85 Some((left, right)) => (left, Some(right.to_string())),
86 None => (version_and_pre, None),
87 };
88
89 let parts: Vec<&str> = version_part.split('.').collect();
91 if parts.len() != 3 {
92 return Err(SemVerError::ParseError(format!(
93 "expected X.Y.Z, got {version_part:?}"
94 )));
95 }
96
97 let parse_u32 = |raw: &str| -> Result<u32, SemVerError> {
98 raw.parse::<u32>().map_err(|_| {
99 SemVerError::ParseError(format!("non-numeric version component: {raw:?}"))
100 })
101 };
102
103 Ok(SemVer {
104 major: parse_u32(parts[0])?,
105 minor: parse_u32(parts[1])?,
106 patch: parse_u32(parts[2])?,
107 pre_release,
108 build_metadata,
109 })
110 }
111
112 pub fn bump_major(&self) -> SemVer {
115 SemVer::new(self.major + 1, 0, 0)
116 }
117
118 pub fn bump_minor(&self) -> SemVer {
121 SemVer::new(self.major, self.minor + 1, 0)
122 }
123
124 pub fn bump_patch(&self) -> SemVer {
127 SemVer::new(self.major, self.minor, self.patch + 1)
128 }
129
130 pub fn is_compatible_with(&self, other: &SemVer) -> bool {
133 self.major == other.major && self >= other
134 }
135}
136
137impl PartialEq for SemVer {
138 fn eq(&self, other: &Self) -> bool {
139 (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
140 }
141}
142
143impl PartialOrd for SemVer {
144 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
145 Some(self.cmp(other))
146 }
147}
148
149impl Ord for SemVer {
150 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
151 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
152 }
153}
154
155impl fmt::Display for SemVer {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
158 if let Some(pre) = &self.pre_release {
159 write!(f, "-{pre}")?;
160 }
161 if let Some(build) = &self.build_metadata {
162 write!(f, "+{build}")?;
163 }
164 Ok(())
165 }
166}
167
168impl Default for SemVer {
169 fn default() -> Self {
170 SemVer::new(0, 1, 0)
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum ChangeType {
181 Breaking,
183 Feature,
185 Fix,
187 Documentation,
189 Refactor,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum BumpType {
196 Major,
198 Minor,
200 Patch,
202}
203
204impl ChangeType {
205 pub fn required_bump(self) -> BumpType {
207 match self {
208 ChangeType::Breaking => BumpType::Major,
209 ChangeType::Feature => BumpType::Minor,
210 ChangeType::Fix | ChangeType::Documentation | ChangeType::Refactor => BumpType::Patch,
211 }
212 }
213}
214
215#[derive(Debug, Clone)]
221pub struct ChangeRecord {
222 pub version: SemVer,
224 pub change_type: ChangeType,
226 pub description: String,
228 pub timestamp: u64,
230}
231
232#[derive(Debug, Clone)]
239pub struct VersionedArtifact {
240 pub id: String,
242 pub version: SemVer,
244 pub content_hash: u64,
246 pub embedding_dim: Option<usize>,
248 pub created_at: u64,
250 pub changelog: Vec<ChangeRecord>,
252}
253
254impl VersionedArtifact {
255 pub fn fnv1a(data: &[u8]) -> u64 {
257 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
258 const FNV_PRIME: u64 = 1_099_511_628_211;
259 data.iter().fold(FNV_OFFSET, |acc, &b| {
260 (acc ^ (b as u64)).wrapping_mul(FNV_PRIME)
261 })
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum CompatibilityLevel {
272 FullyCompatible,
274 BackwardCompatible,
276 ForwardCompatible,
278 Incompatible,
280}
281
282#[derive(Debug, Clone, Default)]
284pub struct CompatibilityMatrix {
285 pub entries: HashMap<(u32, u32), CompatibilityLevel>,
287}
288
289impl CompatibilityMatrix {
290 pub fn get(&self, from_major: u32, to_major: u32) -> CompatibilityLevel {
293 *self
294 .entries
295 .get(&(from_major, to_major))
296 .unwrap_or(&CompatibilityLevel::Incompatible)
297 }
298
299 pub fn set(&mut self, from_major: u32, to_major: u32, level: CompatibilityLevel) {
301 self.entries.insert((from_major, to_major), level);
302 }
303}
304
305#[derive(Debug, Clone)]
311pub struct VersioningStats {
312 pub total_artifacts: usize,
314 pub total_changes: usize,
316 pub breaking_changes: usize,
318 pub avg_version: String,
320 pub latest_version: Option<SemVer>,
322}
323
324pub struct SemanticVersioningEngine {
331 pub artifacts: HashMap<String, VersionedArtifact>,
333 pub compatibility_rules: CompatibilityMatrix,
335}
336
337impl SemanticVersioningEngine {
338 pub fn new() -> Self {
346 let mut matrix = CompatibilityMatrix::default();
347 for m in 0u32..=9 {
349 matrix.set(m, m, CompatibilityLevel::FullyCompatible);
351 if m > 0 {
353 matrix.set(m - 1, m, CompatibilityLevel::BackwardCompatible);
354 matrix.set(m, m - 1, CompatibilityLevel::BackwardCompatible);
355 }
356 }
357 Self {
358 artifacts: HashMap::new(),
359 compatibility_rules: matrix,
360 }
361 }
362
363 pub fn register_artifact(&mut self, artifact: VersionedArtifact) -> Result<(), SemVerError> {
372 if self.artifacts.contains_key(&artifact.id) {
373 return Err(SemVerError::ArtifactAlreadyExists(artifact.id.clone()));
374 }
375 self.artifacts.insert(artifact.id.clone(), artifact);
376 Ok(())
377 }
378
379 pub fn publish_change(
390 &mut self,
391 artifact_id: &str,
392 change_type: ChangeType,
393 description: String,
394 now: u64,
395 ) -> Result<SemVer, SemVerError> {
396 let artifact = self
397 .artifacts
398 .get_mut(artifact_id)
399 .ok_or_else(|| SemVerError::ArtifactNotFound(artifact_id.to_string()))?;
400
401 let new_version = match change_type.required_bump() {
402 BumpType::Major => artifact.version.bump_major(),
403 BumpType::Minor => artifact.version.bump_minor(),
404 BumpType::Patch => artifact.version.bump_patch(),
405 };
406
407 let record = ChangeRecord {
408 version: new_version.clone(),
409 change_type,
410 description,
411 timestamp: now,
412 };
413
414 artifact.version = new_version.clone();
415 artifact.changelog.push(record);
416 Ok(new_version)
417 }
418
419 pub fn get_version(&self, artifact_id: &str) -> Option<&SemVer> {
425 self.artifacts.get(artifact_id).map(|a| &a.version)
426 }
427
428 pub fn version_history(&self, artifact_id: &str) -> Vec<&ChangeRecord> {
430 self.artifacts
431 .get(artifact_id)
432 .map(|a| a.changelog.iter().collect())
433 .unwrap_or_default()
434 }
435
436 pub fn check_compatibility(
441 &self,
442 from_id: &str,
443 to_id: &str,
444 ) -> Result<CompatibilityLevel, SemVerError> {
445 let from = self
446 .artifacts
447 .get(from_id)
448 .ok_or_else(|| SemVerError::ArtifactNotFound(from_id.to_string()))?;
449 let to = self
450 .artifacts
451 .get(to_id)
452 .ok_or_else(|| SemVerError::ArtifactNotFound(to_id.to_string()))?;
453
454 Ok(self
455 .compatibility_rules
456 .get(from.version.major, to.version.major))
457 }
458
459 pub fn find_breaking_changes(&self, artifact_id: &str, since: &SemVer) -> Vec<&ChangeRecord> {
462 self.artifacts
463 .get(artifact_id)
464 .map(|a| {
465 a.changelog
466 .iter()
467 .filter(|r| r.change_type == ChangeType::Breaking && &r.version > since)
468 .collect()
469 })
470 .unwrap_or_default()
471 }
472
473 pub fn migration_path(&self, from: &SemVer, to: &SemVer) -> Vec<SemVer> {
483 if from == to {
484 return vec![from.clone()];
485 }
486
487 if from.major == to.major {
488 if from.minor == to.minor {
489 return vec![from.clone(), to.clone()];
491 }
492 let intermediate = SemVer::new(from.major, to.minor, 0);
494 return vec![from.clone(), intermediate, to.clone()];
495 }
496
497 let major_delta = (to.major as i64 - from.major as i64).unsigned_abs();
498 if major_delta == 1 {
499 return vec![from.clone(), to.clone()];
500 }
501
502 vec![]
504 }
505
506 pub fn artifacts_at_version(&self, version: &SemVer) -> Vec<&str> {
508 self.artifacts
509 .values()
510 .filter(|a| &a.version == version)
511 .map(|a| a.id.as_str())
512 .collect()
513 }
514
515 pub fn stats(&self) -> VersioningStats {
521 let total_artifacts = self.artifacts.len();
522
523 let mut total_changes = 0usize;
524 let mut breaking_changes = 0usize;
525 let mut latest_version: Option<SemVer> = None;
526
527 let mut sum_major = 0u64;
529 let mut sum_minor = 0u64;
530 let mut sum_patch = 0u64;
531
532 for artifact in self.artifacts.values() {
533 let cl = artifact.changelog.len();
534 total_changes += cl;
535 breaking_changes += artifact
536 .changelog
537 .iter()
538 .filter(|r| r.change_type == ChangeType::Breaking)
539 .count();
540
541 sum_major += artifact.version.major as u64;
542 sum_minor += artifact.version.minor as u64;
543 sum_patch += artifact.version.patch as u64;
544
545 match &latest_version {
546 None => latest_version = Some(artifact.version.clone()),
547 Some(current) if artifact.version > *current => {
548 latest_version = Some(artifact.version.clone());
549 }
550 _ => {}
551 }
552 }
553
554 let avg_version = if total_artifacts == 0 {
555 "0.0.0".to_string()
556 } else {
557 let n = total_artifacts as u64;
558 let avg_maj = (sum_major / n) as u32;
559 let avg_min = (sum_minor / n) as u32;
560 let avg_pat = (sum_patch / n) as u32;
561 SemVer::new(avg_maj, avg_min, avg_pat).to_string()
562 };
563
564 VersioningStats {
565 total_artifacts,
566 total_changes,
567 breaking_changes,
568 avg_version,
569 latest_version,
570 }
571 }
572}
573
574impl Default for SemanticVersioningEngine {
575 fn default() -> Self {
576 Self::new()
577 }
578}
579
580#[cfg(test)]
585mod tests {
586 use crate::semantic_versioning::{
587 BumpType, ChangeRecord, ChangeType, CompatibilityLevel, CompatibilityMatrix, SemVer,
588 SemVerError, SemanticVersioningEngine, VersionedArtifact,
589 };
590
591 #[test]
596 fn parse_simple() {
597 let v = SemVer::parse("1.2.3").expect("valid");
598 assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
599 assert!(v.pre_release.is_none());
600 assert!(v.build_metadata.is_none());
601 }
602
603 #[test]
604 fn parse_with_pre_release() {
605 let v = SemVer::parse("1.2.3-alpha").expect("valid");
606 assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
607 assert_eq!(v.pre_release.as_deref(), Some("alpha"));
608 assert!(v.build_metadata.is_none());
609 }
610
611 #[test]
612 fn parse_with_build_metadata() {
613 let v = SemVer::parse("1.2.3+build.1").expect("valid");
614 assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
615 assert!(v.pre_release.is_none());
616 assert_eq!(v.build_metadata.as_deref(), Some("build.1"));
617 }
618
619 #[test]
620 fn parse_with_pre_and_build() {
621 let v = SemVer::parse("1.2.3-alpha+build").expect("valid");
622 assert_eq!((v.major, v.minor, v.patch), (1, 2, 3));
623 assert_eq!(v.pre_release.as_deref(), Some("alpha"));
624 assert_eq!(v.build_metadata.as_deref(), Some("build"));
625 }
626
627 #[test]
628 fn parse_zero_version() {
629 let v = SemVer::parse("0.0.0").expect("valid");
630 assert_eq!((v.major, v.minor, v.patch), (0, 0, 0));
631 }
632
633 #[test]
634 fn parse_error_missing_parts() {
635 assert!(matches!(
636 SemVer::parse("1.2"),
637 Err(SemVerError::ParseError(_))
638 ));
639 }
640
641 #[test]
642 fn parse_error_non_numeric() {
643 assert!(matches!(
644 SemVer::parse("a.b.c"),
645 Err(SemVerError::ParseError(_))
646 ));
647 }
648
649 #[test]
650 fn parse_error_extra_dots() {
651 assert!(matches!(
652 SemVer::parse("1.2.3.4"),
653 Err(SemVerError::ParseError(_))
654 ));
655 }
656
657 #[test]
662 fn display_simple() {
663 assert_eq!(SemVer::new(1, 2, 3).to_string(), "1.2.3");
664 }
665
666 #[test]
667 fn display_with_pre() {
668 let v = SemVer::parse("2.0.0-beta").expect("valid");
669 assert_eq!(v.to_string(), "2.0.0-beta");
670 }
671
672 #[test]
673 fn display_with_build() {
674 let v = SemVer::parse("2.0.0+sha.abc").expect("valid");
675 assert_eq!(v.to_string(), "2.0.0+sha.abc");
676 }
677
678 #[test]
679 fn display_with_pre_and_build() {
680 let v = SemVer::parse("2.0.0-rc.1+exp.sha.5114f85").expect("valid");
681 assert_eq!(v.to_string(), "2.0.0-rc.1+exp.sha.5114f85");
682 }
683
684 #[test]
689 fn ordering_major() {
690 assert!(SemVer::new(2, 0, 0) > SemVer::new(1, 9, 9));
691 }
692
693 #[test]
694 fn ordering_minor() {
695 assert!(SemVer::new(1, 5, 0) > SemVer::new(1, 4, 99));
696 }
697
698 #[test]
699 fn ordering_patch() {
700 assert!(SemVer::new(1, 0, 2) > SemVer::new(1, 0, 1));
701 }
702
703 #[test]
704 fn ordering_equal() {
705 let a = SemVer::parse("1.2.3-alpha").expect("valid");
706 let b = SemVer::parse("1.2.3+build").expect("valid");
707 assert_eq!(a, b); }
709
710 #[test]
715 fn bump_major_resets_minor_patch() {
716 let v = SemVer::new(1, 5, 3).bump_major();
717 assert_eq!((v.major, v.minor, v.patch), (2, 0, 0));
718 }
719
720 #[test]
721 fn bump_minor_resets_patch() {
722 let v = SemVer::new(1, 5, 3).bump_minor();
723 assert_eq!((v.major, v.minor, v.patch), (1, 6, 0));
724 }
725
726 #[test]
727 fn bump_patch_increments() {
728 let v = SemVer::new(1, 5, 3).bump_patch();
729 assert_eq!((v.major, v.minor, v.patch), (1, 5, 4));
730 }
731
732 #[test]
733 fn bump_clears_pre_release() {
734 let v = SemVer::parse("1.0.0-alpha").expect("valid");
735 let bumped = v.bump_patch();
736 assert!(bumped.pre_release.is_none());
737 }
738
739 #[test]
744 fn compatible_same_major_newer() {
745 let a = SemVer::new(1, 5, 0);
746 let b = SemVer::new(1, 3, 0);
747 assert!(a.is_compatible_with(&b));
748 }
749
750 #[test]
751 fn not_compatible_different_major() {
752 let a = SemVer::new(2, 0, 0);
753 let b = SemVer::new(1, 9, 0);
754 assert!(!a.is_compatible_with(&b));
755 }
756
757 #[test]
758 fn not_compatible_older_than_target() {
759 let a = SemVer::new(1, 1, 0);
760 let b = SemVer::new(1, 5, 0);
761 assert!(!a.is_compatible_with(&b));
762 }
763
764 #[test]
769 fn change_type_bump_breaking() {
770 assert_eq!(ChangeType::Breaking.required_bump(), BumpType::Major);
771 }
772
773 #[test]
774 fn change_type_bump_feature() {
775 assert_eq!(ChangeType::Feature.required_bump(), BumpType::Minor);
776 }
777
778 #[test]
779 fn change_type_bump_fix() {
780 assert_eq!(ChangeType::Fix.required_bump(), BumpType::Patch);
781 }
782
783 #[test]
784 fn change_type_bump_documentation() {
785 assert_eq!(ChangeType::Documentation.required_bump(), BumpType::Patch);
786 }
787
788 #[test]
789 fn change_type_bump_refactor() {
790 assert_eq!(ChangeType::Refactor.required_bump(), BumpType::Patch);
791 }
792
793 #[test]
798 fn fnv1a_empty() {
799 assert_eq!(VersionedArtifact::fnv1a(b""), 14_695_981_039_346_656_037);
801 }
802
803 #[test]
804 fn fnv1a_deterministic() {
805 let h1 = VersionedArtifact::fnv1a(b"hello world");
806 let h2 = VersionedArtifact::fnv1a(b"hello world");
807 assert_eq!(h1, h2);
808 }
809
810 #[test]
811 fn fnv1a_different_inputs() {
812 let h1 = VersionedArtifact::fnv1a(b"foo");
813 let h2 = VersionedArtifact::fnv1a(b"bar");
814 assert_ne!(h1, h2);
815 }
816
817 #[test]
822 fn matrix_default_incompatible() {
823 let m = CompatibilityMatrix::default();
824 assert_eq!(m.get(0, 5), CompatibilityLevel::Incompatible);
825 }
826
827 #[test]
828 fn matrix_set_and_get() {
829 let mut m = CompatibilityMatrix::default();
830 m.set(1, 2, CompatibilityLevel::BackwardCompatible);
831 assert_eq!(m.get(1, 2), CompatibilityLevel::BackwardCompatible);
832 }
833
834 fn make_artifact(id: &str) -> VersionedArtifact {
839 VersionedArtifact {
840 id: id.to_string(),
841 version: SemVer::new(0, 1, 0),
842 content_hash: 0,
843 embedding_dim: None,
844 created_at: 1_000,
845 changelog: vec![],
846 }
847 }
848
849 #[test]
850 fn register_new_artifact() {
851 let mut engine = SemanticVersioningEngine::new();
852 assert!(engine.register_artifact(make_artifact("doc-a")).is_ok());
853 assert!(engine.get_version("doc-a").is_some());
854 }
855
856 #[test]
857 fn register_duplicate_returns_error() {
858 let mut engine = SemanticVersioningEngine::new();
859 engine
860 .register_artifact(make_artifact("doc-a"))
861 .expect("first");
862 let err = engine
863 .register_artifact(make_artifact("doc-a"))
864 .unwrap_err();
865 assert!(matches!(err, SemVerError::ArtifactAlreadyExists(_)));
866 }
867
868 #[test]
873 fn publish_breaking_bumps_major() {
874 let mut engine = SemanticVersioningEngine::new();
875 engine.register_artifact(make_artifact("a")).expect("ok");
876 let v = engine
877 .publish_change("a", ChangeType::Breaking, "Removed old API".into(), 2000)
878 .expect("ok");
879 assert_eq!(v, SemVer::new(1, 0, 0));
880 }
881
882 #[test]
883 fn publish_feature_bumps_minor() {
884 let mut engine = SemanticVersioningEngine::new();
885 engine.register_artifact(make_artifact("a")).expect("ok");
886 let v = engine
887 .publish_change("a", ChangeType::Feature, "New endpoint".into(), 2000)
888 .expect("ok");
889 assert_eq!(v, SemVer::new(0, 2, 0));
890 }
891
892 #[test]
893 fn publish_fix_bumps_patch() {
894 let mut engine = SemanticVersioningEngine::new();
895 engine.register_artifact(make_artifact("a")).expect("ok");
896 let v = engine
897 .publish_change("a", ChangeType::Fix, "Null ptr fix".into(), 2000)
898 .expect("ok");
899 assert_eq!(v, SemVer::new(0, 1, 1));
900 }
901
902 #[test]
903 fn publish_unknown_artifact_errors() {
904 let mut engine = SemanticVersioningEngine::new();
905 let err = engine
906 .publish_change("missing", ChangeType::Fix, "x".into(), 0)
907 .unwrap_err();
908 assert!(matches!(err, SemVerError::ArtifactNotFound(_)));
909 }
910
911 #[test]
912 fn publish_appends_changelog() {
913 let mut engine = SemanticVersioningEngine::new();
914 engine.register_artifact(make_artifact("a")).expect("ok");
915 engine
916 .publish_change("a", ChangeType::Feature, "feat 1".into(), 1000)
917 .expect("ok");
918 engine
919 .publish_change("a", ChangeType::Fix, "fix 1".into(), 2000)
920 .expect("ok");
921 let history = engine.version_history("a");
922 assert_eq!(history.len(), 2);
923 }
924
925 #[test]
930 fn history_of_unknown_artifact_is_empty() {
931 let engine = SemanticVersioningEngine::new();
932 assert!(engine.version_history("ghost").is_empty());
933 }
934
935 #[test]
940 fn same_major_fully_compatible() {
941 let mut engine = SemanticVersioningEngine::new();
942 engine.register_artifact(make_artifact("a")).expect("ok");
943 engine.register_artifact(make_artifact("b")).expect("ok");
944 assert_eq!(
945 engine.check_compatibility("a", "b").expect("ok"),
946 CompatibilityLevel::FullyCompatible
947 );
948 }
949
950 #[test]
951 fn adjacent_major_backward_compatible() {
952 let mut engine = SemanticVersioningEngine::new();
953 engine.register_artifact(make_artifact("a")).expect("ok");
955 let mut b = make_artifact("b");
956 b.version = SemVer::new(1, 0, 0);
957 engine.register_artifact(b).expect("ok");
958 assert_eq!(
959 engine.check_compatibility("a", "b").expect("ok"),
960 CompatibilityLevel::BackwardCompatible
961 );
962 }
963
964 #[test]
965 fn check_compatibility_unknown_from() {
966 let mut engine = SemanticVersioningEngine::new();
967 engine.register_artifact(make_artifact("b")).expect("ok");
968 assert!(matches!(
969 engine.check_compatibility("ghost", "b"),
970 Err(SemVerError::ArtifactNotFound(_))
971 ));
972 }
973
974 #[test]
975 fn check_compatibility_unknown_to() {
976 let mut engine = SemanticVersioningEngine::new();
977 engine.register_artifact(make_artifact("a")).expect("ok");
978 assert!(matches!(
979 engine.check_compatibility("a", "ghost"),
980 Err(SemVerError::ArtifactNotFound(_))
981 ));
982 }
983
984 #[test]
989 fn find_breaking_changes_empty_when_no_breaking() {
990 let mut engine = SemanticVersioningEngine::new();
991 engine.register_artifact(make_artifact("a")).expect("ok");
992 engine
993 .publish_change("a", ChangeType::Feature, "f".into(), 100)
994 .expect("ok");
995 let bc = engine.find_breaking_changes("a", &SemVer::new(0, 1, 0));
996 assert!(bc.is_empty());
997 }
998
999 #[test]
1000 fn find_breaking_changes_returns_breaking_after_since() {
1001 let mut engine = SemanticVersioningEngine::new();
1002 engine.register_artifact(make_artifact("a")).expect("ok");
1003 let since = engine.get_version("a").expect("ok").clone();
1004 engine
1005 .publish_change("a", ChangeType::Breaking, "removed X".into(), 100)
1006 .expect("ok");
1007 let bc = engine.find_breaking_changes("a", &since);
1008 assert_eq!(bc.len(), 1);
1009 }
1010
1011 #[test]
1012 fn find_breaking_changes_not_included_at_or_before_since() {
1013 let mut engine = SemanticVersioningEngine::new();
1014 engine.register_artifact(make_artifact("a")).expect("ok");
1015 engine
1016 .publish_change("a", ChangeType::Breaking, "removed X".into(), 100)
1017 .expect("ok");
1018 let since = engine.get_version("a").expect("ok").clone();
1019 let bc = engine.find_breaking_changes("a", &since);
1021 assert!(bc.is_empty());
1022 }
1023
1024 #[test]
1029 fn migration_path_same_version() {
1030 let engine = SemanticVersioningEngine::new();
1031 let v = SemVer::new(1, 2, 3);
1032 let path = engine.migration_path(&v, &v);
1033 assert_eq!(path, vec![v]);
1034 }
1035
1036 #[test]
1037 fn migration_path_same_major_different_minor() {
1038 let engine = SemanticVersioningEngine::new();
1039 let from = SemVer::new(1, 1, 0);
1040 let to = SemVer::new(1, 4, 0);
1041 let path = engine.migration_path(&from, &to);
1042 assert_eq!(path.len(), 3);
1043 assert_eq!(path[1], SemVer::new(1, 4, 0)); }
1045
1046 #[test]
1047 fn migration_path_adjacent_major() {
1048 let engine = SemanticVersioningEngine::new();
1049 let from = SemVer::new(1, 5, 0);
1050 let to = SemVer::new(2, 0, 0);
1051 let path = engine.migration_path(&from, &to);
1052 assert_eq!(path, vec![from, to]);
1053 }
1054
1055 #[test]
1056 fn migration_path_incompatible_gap() {
1057 let engine = SemanticVersioningEngine::new();
1058 let from = SemVer::new(1, 0, 0);
1059 let to = SemVer::new(5, 0, 0);
1060 let path = engine.migration_path(&from, &to);
1061 assert!(path.is_empty());
1062 }
1063
1064 #[test]
1065 fn migration_path_same_minor_different_patch() {
1066 let engine = SemanticVersioningEngine::new();
1067 let from = SemVer::new(2, 3, 0);
1068 let to = SemVer::new(2, 3, 5);
1069 let path = engine.migration_path(&from, &to);
1070 assert_eq!(path, vec![from, to]);
1071 }
1072
1073 #[test]
1078 fn artifacts_at_version_matches() {
1079 let mut engine = SemanticVersioningEngine::new();
1080 engine.register_artifact(make_artifact("a")).expect("ok");
1081 engine.register_artifact(make_artifact("b")).expect("ok");
1082 let mut ids = engine.artifacts_at_version(&SemVer::new(0, 1, 0));
1083 ids.sort_unstable();
1084 assert_eq!(ids, vec!["a", "b"]);
1085 }
1086
1087 #[test]
1088 fn artifacts_at_version_empty_when_none_match() {
1089 let mut engine = SemanticVersioningEngine::new();
1090 engine.register_artifact(make_artifact("a")).expect("ok");
1091 let ids = engine.artifacts_at_version(&SemVer::new(9, 9, 9));
1092 assert!(ids.is_empty());
1093 }
1094
1095 #[test]
1100 fn stats_empty_engine() {
1101 let engine = SemanticVersioningEngine::new();
1102 let s = engine.stats();
1103 assert_eq!(s.total_artifacts, 0);
1104 assert_eq!(s.total_changes, 0);
1105 assert_eq!(s.breaking_changes, 0);
1106 assert_eq!(s.avg_version, "0.0.0");
1107 assert!(s.latest_version.is_none());
1108 }
1109
1110 #[test]
1111 fn stats_single_artifact_no_changes() {
1112 let mut engine = SemanticVersioningEngine::new();
1113 engine.register_artifact(make_artifact("a")).expect("ok");
1114 let s = engine.stats();
1115 assert_eq!(s.total_artifacts, 1);
1116 assert_eq!(s.total_changes, 0);
1117 assert_eq!(s.breaking_changes, 0);
1118 assert_eq!(s.latest_version, Some(SemVer::new(0, 1, 0)));
1119 }
1120
1121 #[test]
1122 fn stats_counts_breaking_changes() {
1123 let mut engine = SemanticVersioningEngine::new();
1124 engine.register_artifact(make_artifact("a")).expect("ok");
1125 engine
1126 .publish_change("a", ChangeType::Breaking, "b1".into(), 100)
1127 .expect("ok");
1128 engine
1129 .publish_change("a", ChangeType::Feature, "f1".into(), 200)
1130 .expect("ok");
1131 let s = engine.stats();
1132 assert_eq!(s.total_changes, 2);
1133 assert_eq!(s.breaking_changes, 1);
1134 }
1135
1136 #[test]
1137 fn stats_latest_version_is_max() {
1138 let mut engine = SemanticVersioningEngine::new();
1139 engine.register_artifact(make_artifact("a")).expect("ok");
1140 let mut b = make_artifact("b");
1141 b.version = SemVer::new(3, 0, 0);
1142 engine.register_artifact(b).expect("ok");
1143 let s = engine.stats();
1144 assert_eq!(s.latest_version, Some(SemVer::new(3, 0, 0)));
1145 }
1146
1147 #[test]
1148 fn stats_avg_version_two_artifacts() {
1149 let mut engine = SemanticVersioningEngine::new();
1150 engine.register_artifact(make_artifact("a")).expect("ok");
1152 let mut b = make_artifact("b");
1153 b.version = SemVer::new(2, 3, 4);
1154 engine.register_artifact(b).expect("ok");
1155 let s = engine.stats();
1156 assert_eq!(s.avg_version, "1.2.2");
1158 }
1159
1160 #[test]
1165 fn change_record_fields_preserved() {
1166 let mut engine = SemanticVersioningEngine::new();
1167 engine.register_artifact(make_artifact("a")).expect("ok");
1168 engine
1169 .publish_change(
1170 "a",
1171 ChangeType::Documentation,
1172 "Update readme".into(),
1173 42_000,
1174 )
1175 .expect("ok");
1176 let history = engine.version_history("a");
1177 let rec: &ChangeRecord = history[0];
1178 assert_eq!(rec.change_type, ChangeType::Documentation);
1179 assert_eq!(rec.description, "Update readme");
1180 assert_eq!(rec.timestamp, 42_000);
1181 }
1182
1183 #[test]
1188 fn default_engine_is_empty() {
1189 let engine = SemanticVersioningEngine::default();
1190 assert!(engine.artifacts.is_empty());
1191 }
1192
1193 #[test]
1198 fn error_display_parse() {
1199 let e = SemVerError::ParseError("bad".into());
1200 assert!(e.to_string().contains("bad"));
1201 }
1202
1203 #[test]
1204 fn error_display_not_found() {
1205 let e = SemVerError::ArtifactNotFound("x".into());
1206 assert!(e.to_string().contains("x"));
1207 }
1208
1209 #[test]
1210 fn error_display_already_exists() {
1211 let e = SemVerError::ArtifactAlreadyExists("x".into());
1212 assert!(e.to_string().contains("x"));
1213 }
1214
1215 #[test]
1216 fn error_display_invalid_version() {
1217 let e = SemVerError::InvalidVersion("0.0.0".into());
1218 assert!(e.to_string().contains("0.0.0"));
1219 }
1220}