Skip to main content

dix_diff/
version.rs

1use std::{
2  cmp,
3  fmt,
4};
5
6use derive_more::{
7  Deref,
8  Display,
9};
10
11/// Separators used to split version strings.
12const SEPARATORS: &[char] = &['.', '-', '_', '+', '*', '=', '×', ' '];
13
14/// A version string with semantic comparison support.
15///
16/// Ordering compares separator-delimited components. Empty versions sort before
17/// non-empty versions; after a shared prefix, text suffixes sort below the base
18/// version and numeric suffixes sort above it. `Ord` uses the raw version
19/// string as a final tie-breaker so distinct strings remain distinct
20/// ordered-map keys.
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct Version {
23  pub name: String,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VersionChangeOrdering {
28  Ordered(cmp::Ordering),
29  Unordered,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum ComparedVersionComponents {
34  Ordered(cmp::Ordering),
35  Equal,
36  Unordered,
37}
38
39impl Version {
40  #[must_use]
41  pub fn new(version: impl Into<String>) -> Self {
42    Self {
43      name: version.into(),
44    }
45  }
46
47  /// Iterate over components only.
48  pub fn components(&self) -> impl Iterator<Item = VersionComponent<'_>> {
49    Pieces::new(&self.name).filter_map(VersionPiece::component)
50  }
51
52  /// Iterate over all pieces (components and separators).
53  #[must_use]
54  pub fn iter(&self) -> Pieces<'_> {
55    Pieces::new(&self.name)
56  }
57
58  pub(crate) fn change_ordering(&self, other: &Self) -> VersionChangeOrdering {
59    match self.compare_components_with(other, |self_comp, other_comp| {
60      if self_comp.is_git_hash_pair(other_comp) {
61        ComparedVersionComponents::Unordered
62      } else {
63        ComparedVersionComponents::Ordered(self_comp.cmp(&other_comp))
64      }
65    }) {
66      ComparedVersionComponents::Ordered(ordering) => {
67        VersionChangeOrdering::Ordered(ordering)
68      },
69      ComparedVersionComponents::Equal => {
70        VersionChangeOrdering::Ordered(cmp::Ordering::Equal)
71      },
72      ComparedVersionComponents::Unordered => VersionChangeOrdering::Unordered,
73    }
74  }
75
76  fn compare_components_with(
77    &self,
78    other: &Self,
79    mut compare_component: impl FnMut(
80      VersionComponent<'_>,
81      VersionComponent<'_>,
82    ) -> ComparedVersionComponents,
83  ) -> ComparedVersionComponents {
84    let self_comps: Vec<_> = self.components().collect();
85    let other_comps: Vec<_> = other.components().collect();
86    let mut saw_unordered = false;
87
88    for index in 0..self_comps.len().max(other_comps.len()) {
89      let self_comp = self_comps.get(index).copied();
90      let other_comp = other_comps.get(index).copied();
91
92      if self_comp == other_comp {
93        continue;
94      }
95
96      match compare_component_at(
97        index,
98        self_comp,
99        other_comp,
100        &mut compare_component,
101      ) {
102        ComparedVersionComponents::Equal
103        | ComparedVersionComponents::Ordered(cmp::Ordering::Equal) => {},
104        ComparedVersionComponents::Ordered(ordering) => {
105          return ComparedVersionComponents::Ordered(ordering);
106        },
107        ComparedVersionComponents::Unordered => {
108          saw_unordered = true;
109        },
110      }
111    }
112
113    if saw_unordered {
114      ComparedVersionComponents::Unordered
115    } else {
116      ComparedVersionComponents::Equal
117    }
118  }
119}
120
121fn compare_component_at(
122  index: usize,
123  self_comp: Option<VersionComponent<'_>>,
124  other_comp: Option<VersionComponent<'_>>,
125  compare_component: &mut impl FnMut(
126    VersionComponent<'_>,
127    VersionComponent<'_>,
128  ) -> ComparedVersionComponents,
129) -> ComparedVersionComponents {
130  let rank_ordering =
131    component_rank(index, self_comp).cmp(&component_rank(index, other_comp));
132
133  let (Some(self_comp), Some(other_comp)) = (self_comp, other_comp) else {
134    return ComparedVersionComponents::Ordered(rank_ordering);
135  };
136
137  match compare_component(self_comp, other_comp) {
138    ComparedVersionComponents::Unordered => {
139      ComparedVersionComponents::Unordered
140    },
141    ComparedVersionComponents::Equal
142    | ComparedVersionComponents::Ordered(cmp::Ordering::Equal) => {
143      ComparedVersionComponents::Equal
144    },
145    ComparedVersionComponents::Ordered(ordering) => {
146      ComparedVersionComponents::Ordered(rank_ordering.then(ordering))
147    },
148  }
149}
150
151fn component_rank(
152  index: usize,
153  component: Option<VersionComponent<'_>>,
154) -> ComponentRank {
155  // The first slot sorts empty versions below real versions. After a shared
156  // prefix, text suffixes sort below the base and numeric suffixes above it.
157  match (index, component.map(|component| component.is_numeric())) {
158    (_, None) => ComponentRank::Missing,
159    (0, Some(true)) => ComponentRank::FirstNumeric,
160    (0, Some(false)) => ComponentRank::FirstText,
161    (_, Some(false)) => ComponentRank::SuffixText,
162    (_, Some(true)) => ComponentRank::SuffixNumeric,
163  }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
167enum ComponentRank {
168  SuffixText,
169  Missing,
170  FirstNumeric,
171  FirstText,
172  SuffixNumeric,
173}
174
175impl<T: Into<String>> From<T> for Version {
176  fn from(s: T) -> Self {
177    Self::new(s)
178  }
179}
180
181impl PartialOrd for Version {
182  fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
183    Some(self.cmp(other))
184  }
185}
186
187impl Ord for Version {
188  fn cmp(&self, other: &Self) -> cmp::Ordering {
189    let component_ordering =
190      match self.compare_components_with(other, |self_comp, other_comp| {
191        ComparedVersionComponents::Ordered(self_comp.cmp(&other_comp))
192      }) {
193        ComparedVersionComponents::Ordered(ordering) => ordering,
194        ComparedVersionComponents::Equal
195        | ComparedVersionComponents::Unordered => cmp::Ordering::Equal,
196      };
197
198    component_ordering.then_with(|| self.name.cmp(&other.name))
199  }
200}
201
202impl<'a> IntoIterator for &'a Version {
203  type Item = VersionPiece<'a>;
204  type IntoIter = Pieces<'a>;
205
206  fn into_iter(self) -> Self::IntoIter {
207    Pieces::new(&self.name)
208  }
209}
210
211/// Iterator over version pieces (components and separators).
212#[derive(Clone, Copy)]
213pub struct Pieces<'a> {
214  remaining: &'a str,
215}
216
217impl<'a> Pieces<'a> {
218  const fn new(s: &'a str) -> Self {
219    Self { remaining: s }
220  }
221}
222
223#[expect(clippy::copy_iterator)]
224impl<'a> Iterator for Pieces<'a> {
225  type Item = VersionPiece<'a>;
226
227  fn next(&mut self) -> Option<Self::Item> {
228    if self.remaining.is_empty() {
229      return None;
230    }
231
232    let first = self.remaining.chars().next()?;
233
234    if SEPARATORS.contains(&first) {
235      let len = first.len_utf8();
236      let sep = &self.remaining[..len];
237      self.remaining = &self.remaining[len..];
238      return Some(VersionPiece::Separator(sep));
239    }
240
241    let len = self
242      .remaining
243      .chars()
244      .take_while(|c| !SEPARATORS.contains(c))
245      .map(char::len_utf8)
246      .sum();
247
248    let comp = &self.remaining[..len];
249    self.remaining = &self.remaining[len..];
250    Some(VersionPiece::Component(VersionComponent(comp)))
251  }
252}
253
254/// Either a component or separator from a version string.
255#[derive(Clone, Copy, Debug, PartialEq, Eq)]
256pub enum VersionPiece<'a> {
257  Component(VersionComponent<'a>),
258  Separator(&'a str),
259}
260
261impl<'a> VersionPiece<'a> {
262  #[must_use]
263  pub const fn component(self) -> Option<VersionComponent<'a>> {
264    match self {
265      VersionPiece::Component(c) => Some(c),
266      VersionPiece::Separator(_) => None,
267    }
268  }
269
270  #[must_use]
271  pub const fn separator(self) -> Option<&'a str> {
272    match self {
273      VersionPiece::Component(_) => None,
274      VersionPiece::Separator(s) => Some(s),
275    }
276  }
277}
278
279/// A single version component (numeric or text).
280#[derive(Display, Debug, Clone, Copy, Deref, PartialEq, Eq)]
281pub struct VersionComponent<'a>(&'a str);
282
283impl VersionComponent<'_> {
284  #[must_use]
285  pub fn is_numeric(&self) -> bool {
286    !self.0.is_empty() && self.0.bytes().all(|b| b.is_ascii_digit())
287  }
288
289  #[must_use]
290  pub fn as_u64(&self) -> Option<u64> {
291    self.is_numeric().then(|| self.0.parse().ok()).flatten()
292  }
293
294  fn is_git_hash_pair(&self, other: Self) -> bool {
295    is_hex_hash_component(self.0)
296      && is_hex_hash_component(other.0)
297      && (has_ascii_hex_letter(self.0) || has_ascii_hex_letter(other.0))
298  }
299}
300
301fn is_hex_hash_component(component: &str) -> bool {
302  (7..=40).contains(&component.len())
303    && component.bytes().all(|b| b.is_ascii_hexdigit())
304}
305
306fn has_ascii_hex_letter(component: &str) -> bool {
307  component
308    .bytes()
309    .any(|b| matches!(b, b'a'..=b'f' | b'A'..=b'F'))
310}
311
312impl PartialOrd for VersionComponent<'_> {
313  fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
314    Some(self.cmp(other))
315  }
316}
317
318impl Ord for VersionComponent<'_> {
319  fn cmp(&self, other: &Self) -> cmp::Ordering {
320    match (self.is_numeric(), other.is_numeric()) {
321      (true, true) => {
322        match (self.as_u64(), other.as_u64()) {
323          (Some(a), Some(b)) => a.cmp(&b),
324          _ => self.0.cmp(other.0),
325        }
326      },
327      (false, false) => {
328        match (self.0, other.0) {
329          ("pre", _) => cmp::Ordering::Less,
330          (_, "pre") => cmp::Ordering::Greater,
331          _ => self.0.cmp(other.0),
332        }
333      },
334      (true, false) => cmp::Ordering::Less,
335      (false, true) => cmp::Ordering::Greater,
336    }
337  }
338}
339
340impl fmt::Display for Version {
341  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342    f.write_str(&self.name)
343  }
344}
345
346impl fmt::Write for Version {
347  fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
348    fmt::write(&mut self.name, args)
349  }
350
351  fn write_str(&mut self, s: &str) -> fmt::Result {
352    self.name.push_str(s);
353    Ok(())
354  }
355}
356
357#[cfg(test)]
358mod tests {
359  use proptest::proptest;
360
361  use super::{
362    Version,
363    VersionChangeOrdering,
364    VersionComponent,
365    VersionPiece,
366  };
367
368  // tests to ensure that [`Version::cmp`] is a total order
369  proptest! {
370    #[test]
371    fn test_version_transitivity(
372      a in ".*",
373      b in ".*",
374      c in ".*",
375    ) {
376      let a = Version::new(a);
377      let b = Version::new(b);
378      let c = Version::new(c);
379
380      // a < b < c -> a < c
381      assert!(!(a < b && b < c) || a < c);
382      // a < c < b -> a < b
383      assert!(!(a < c && c < b) || a < b);
384      // b < a < c -> b < c
385      assert!(!(b < a && a < c) || b < c);
386      // b < c < a -> b < a
387      assert!(!(b < c && c < a) || b < a);
388      // c < a < b -> c < b
389      assert!(!(c < a && a < b) || c < b);
390      // c < b < a -> c < a
391      assert!(!(c < b && b < a) || c < a);
392    }
393
394    #[test]
395    fn test_version_reflexivity(
396      a in ".*",
397    ) {
398      let a = Version::new(a);
399      let b = a.clone();
400
401      assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal);
402      assert_eq!(a, b);
403    }
404
405    #[test]
406    fn test_version_antisymmetry(
407      a in ".*",
408      b in ".*",
409    ) {
410      let a = Version::new(a);
411      let b = Version::new(b);
412
413      assert_eq!(a.cmp(&b), b.cmp(&a).reverse());
414      if a.cmp(&b) == std::cmp::Ordering::Equal {
415        assert_eq!(a, b);
416      }
417    }
418
419  }
420
421  #[test]
422  fn version_transitivity_regression_for_empty_text_and_numeric_components() {
423    let empty = Version::new("");
424    let numeric = Version::new("0");
425    let text = Version::new(":");
426
427    assert!(empty < text);
428    assert!(numeric < text);
429    assert!(empty < numeric);
430  }
431
432  #[test]
433  fn version_suffix_order_is_transitive() {
434    let base = Version::new("1");
435    let text = Version::new("1-alpha");
436    let hyphen_numeric = Version::new("1-0");
437    let dot_numeric = Version::new("1.0");
438
439    assert!(text < base);
440    assert!(base < hyphen_numeric);
441    assert!(base < dot_numeric);
442    assert!(text < hyphen_numeric);
443    assert!(text < dot_numeric);
444  }
445
446  #[test]
447  fn version_order_keeps_component_equal_strings_distinct() {
448    use std::collections::BTreeSet;
449
450    let dotted = Version::new("1.0");
451    let hyphenated = Version::new("1-0");
452    let mut versions = BTreeSet::new();
453
454    versions.insert(dotted.clone());
455    versions.insert(hyphenated.clone());
456
457    assert_ne!(dotted, hyphenated);
458    assert_ne!(dotted.cmp(&hyphenated), std::cmp::Ordering::Equal);
459    assert_eq!(versions.len(), 2);
460  }
461
462  #[test]
463  fn change_ordering_treats_component_equal_strings_as_changed() {
464    let dotted = Version::new("1.0");
465    let hyphenated = Version::new("1-0");
466
467    assert_eq!(
468      dotted.change_ordering(&hyphenated),
469      VersionChangeOrdering::Ordered(std::cmp::Ordering::Equal),
470    );
471    assert_ne!(dotted.cmp(&hyphenated), std::cmp::Ordering::Equal);
472  }
473
474  #[test]
475  fn version_component_iter() {
476    let version = "132.1.2test234-1-man----.--.......---------..---";
477
478    assert_eq!(
479      Version::new(version)
480        .into_iter()
481        .filter_map(VersionPiece::component)
482        .collect::<Vec<_>>(),
483      [
484        VersionComponent("132"),
485        VersionComponent("1"),
486        VersionComponent("2test234"),
487        VersionComponent("1"),
488        VersionComponent("man")
489      ]
490    );
491  }
492
493  #[test]
494  fn version_comparison() {
495    assert!(Version::new("2.0.0") > Version::new("1.9.9"));
496    assert!(Version::new("2.1.0") > Version::new("2.0.9"));
497    assert!(Version::new("2.0.1") > Version::new("2.0.0"));
498    assert!(Version::new("1.0.0") > Version::new("1.0.0-pre"));
499    assert!(Version::new("1.0.0") > Version::new("1.0.0-alpha"));
500    assert!(Version::new("1.0.0-beta") > Version::new("1.0.0-alpha"));
501    assert!(Version::new("1.0.0-beta.11") > Version::new("1.0.0-beta.2"));
502    assert_eq!(Version::new("1.0.0"), Version::new("1.0.0"));
503  }
504
505  #[test]
506  fn change_ordering_treats_git_short_hash_pair_as_unordered() {
507    let old = Version::new("0.11.1-946aa34");
508    let new = Version::new("0.11.1-3564204");
509
510    assert_eq!(old.change_ordering(&new), VersionChangeOrdering::Unordered);
511    assert_eq!(new.change_ordering(&old), VersionChangeOrdering::Unordered);
512  }
513
514  #[test]
515  fn change_ordering_treats_git_long_hash_pair_as_unordered() {
516    let old = Version::new("0bf8387987c21bf2f8ed41d2575a8f22b139687f");
517    let new = Version::new("cd1931314beafeebc957964c65802961e283411e");
518
519    assert_eq!(old.change_ordering(&new), VersionChangeOrdering::Unordered);
520    assert_eq!(new.change_ordering(&old), VersionChangeOrdering::Unordered);
521  }
522
523  #[test]
524  fn change_ordering_uses_non_hash_components_before_hashes() {
525    let old = Version::new("25.05.31pre20250531_946aa34");
526    let new = Version::new("25.05.31pre20250601_3564204");
527
528    assert_eq!(
529      old.change_ordering(&new),
530      VersionChangeOrdering::Ordered(std::cmp::Ordering::Less),
531    );
532    assert_eq!(
533      new.change_ordering(&old),
534      VersionChangeOrdering::Ordered(std::cmp::Ordering::Greater),
535    );
536  }
537
538  #[test]
539  fn change_ordering_keeps_numeric_components_ordered() {
540    let old = Version::new("1.0-1234567");
541    let new = Version::new("1.0-2345678");
542
543    assert_eq!(
544      old.change_ordering(&new),
545      VersionChangeOrdering::Ordered(std::cmp::Ordering::Less),
546    );
547  }
548
549  #[test]
550  fn version_piece_iterator_includes_separators() {
551    let version = Version::new("1.2.3-alpha");
552    let pieces: Vec<_> = version.into_iter().collect();
553    assert_eq!(pieces.len(), 7);
554    assert!(matches!(pieces[0], VersionPiece::Component(_)));
555    assert!(matches!(pieces[1], VersionPiece::Separator(".")));
556    assert!(matches!(pieces[2], VersionPiece::Component(_)));
557    assert!(matches!(pieces[3], VersionPiece::Separator(".")));
558    assert!(matches!(pieces[4], VersionPiece::Component(_)));
559    assert!(matches!(pieces[5], VersionPiece::Separator("-")));
560    assert!(matches!(pieces[6], VersionPiece::Component(_)));
561  }
562
563  #[test]
564  fn version_piece_methods() {
565    let comp = VersionPiece::Component(VersionComponent("123"));
566    let sep = VersionPiece::Separator("-");
567
568    assert_eq!(comp.component(), Some(VersionComponent("123")));
569    assert_eq!(comp.separator(), None);
570    assert_eq!(sep.component(), None);
571    assert_eq!(sep.separator(), Some("-"));
572  }
573
574  #[test]
575  fn version_component_is_numeric() {
576    assert!(VersionComponent("123").is_numeric());
577    assert!(VersionComponent("0").is_numeric());
578    assert!(!VersionComponent("abc").is_numeric());
579    assert!(!VersionComponent("123abc").is_numeric());
580    assert!(!VersionComponent("").is_numeric());
581    assert!(!VersionComponent("12.3").is_numeric());
582  }
583
584  #[test]
585  fn version_component_as_u64() {
586    assert_eq!(VersionComponent("123").as_u64(), Some(123));
587    assert_eq!(VersionComponent("0").as_u64(), Some(0));
588    assert_eq!(
589      VersionComponent("18446744073709551615").as_u64(),
590      Some(u64::MAX)
591    );
592    assert_eq!(VersionComponent("abc").as_u64(), None);
593    assert_eq!(VersionComponent("123abc").as_u64(), None);
594    assert_eq!(VersionComponent("").as_u64(), None);
595  }
596
597  #[test]
598  fn component_comparison_numeric() {
599    assert!(VersionComponent("10") > VersionComponent("2"));
600    assert!(VersionComponent("2") < VersionComponent("10"));
601    assert_eq!(VersionComponent("5"), VersionComponent("5"));
602  }
603
604  #[test]
605  fn component_comparison_text() {
606    assert!(VersionComponent("beta") > VersionComponent("alpha"));
607    assert!(VersionComponent("rc") > VersionComponent("beta"));
608    assert_eq!(VersionComponent("stable"), VersionComponent("stable"));
609  }
610
611  #[test]
612  fn component_comparison_pre_special_case() {
613    assert!(VersionComponent("pre") < VersionComponent("alpha"));
614    assert!(VersionComponent("pre") > VersionComponent("1")); // text > numeric
615    assert!(VersionComponent("alpha") > VersionComponent("pre"));
616  }
617
618  #[test]
619  fn component_comparison_mixed_types() {
620    assert!(VersionComponent("2") < VersionComponent("alpha"));
621    assert!(VersionComponent("alpha") > VersionComponent("2"));
622  }
623
624  #[test]
625  fn component_comparison_mixed_alphanumeric() {
626    assert_eq!(VersionComponent("2test234"), VersionComponent("2test234"));
627    assert!(VersionComponent("abc123") > VersionComponent("123abc"));
628  }
629
630  #[test]
631  fn version_comparison_equal_lengths() {
632    assert!(Version::new("1.2.3") > Version::new("1.2.2"));
633    assert!(Version::new("1.2.3") < Version::new("1.2.4"));
634    assert_eq!(Version::new("1.2.3"), Version::new("1.2.3"));
635  }
636
637  #[test]
638  fn version_comparison_suffix_edge_cases() {
639    assert!(Version::new("1.0.0") > Version::new("1.0.0-alpha"));
640    assert!(Version::new("1.0.0-alpha") < Version::new("1.0.0"));
641    assert!(Version::new("1.0.0-1") > Version::new("1.0.0"));
642    assert!(Version::new("1.0.0-alpha") < Version::new("1.0.0-1"));
643    assert!(Version::new("1.0.0-alpha") < Version::new("1.0.0-beta"));
644    assert!(Version::new("1.0.0-1") < Version::new("1.0.0-2"));
645    assert!(Version::new("1.0.0-9") < Version::new("1.0.0-10"));
646  }
647
648  #[test]
649  fn version_comparison_numeric_extensions() {
650    assert!(Version::new("1.0.0.1") > Version::new("1.0.0"));
651    assert!(Version::new("1.0.0") < Version::new("1.0.0.1"));
652  }
653
654  #[test]
655  fn version_display() {
656    let v1 = Version::new("1.2.3");
657    assert_eq!(format!("{v1}"), "1.2.3");
658  }
659
660  #[test]
661  fn version_write() {
662    use std::fmt::Write;
663
664    let mut v = Version::new("1.0");
665    write!(v, ".{}-beta", 2).unwrap();
666    assert_eq!(v.name, "1.0.2-beta");
667  }
668
669  #[test]
670  fn empty_version() {
671    let v = Version::new("");
672    assert_eq!(v.components().count(), 0);
673  }
674
675  #[test]
676  fn version_with_only_separators() {
677    let v = Version::new("...---___");
678    assert_eq!(v.components().count(), 0);
679  }
680
681  #[test]
682  fn version_from_string() {
683    let v1: Version = "1.2.3".into();
684    assert_eq!(v1.name, "1.2.3");
685
686    let v2: Version = String::from("4.5.6").into();
687    assert_eq!(v2.name, "4.5.6");
688  }
689
690  #[test]
691  fn various_separators() {
692    let v = Version::new("1_2+3=4*5×6 7");
693    let comps: Vec<_> = v.components().collect();
694    assert_eq!(comps.len(), 7); // 1, 2, 3, 4, 5, 6, 7
695    assert_eq!(comps[0].0, "1");
696    assert_eq!(comps[6].0, "7");
697  }
698
699  #[test]
700  fn complex_version_parsing() {
701    let v = Version::new("firefox-123.0.1_beta-1-x86_64");
702    let comps: Vec<_> = v.components().collect();
703    assert_eq!(comps[0].0, "firefox");
704    assert_eq!(comps[1].0, "123");
705    assert_eq!(comps[2].0, "0");
706    assert_eq!(comps[3].0, "1"); // _ is a separator, so "1_beta" splits
707    assert_eq!(comps[4].0, "beta");
708    assert_eq!(comps[5].0, "1");
709    assert_eq!(comps[6].0, "x86"); // _ is a separator, so "x86_64" splits
710    assert_eq!(comps[7].0, "64");
711  }
712
713  #[test]
714  fn version_clone_and_eq() {
715    let v1 = Version::new("1.0.0");
716    let v2 = v1.clone();
717    assert_eq!(v1, v2);
718    assert_eq!(v1.name, v2.name);
719  }
720
721  #[test]
722  fn version_hash_consistency() {
723    use std::collections::HashSet;
724
725    let v1 = Version::new("1.0.0");
726    let v2 = Version::new("1.0.0");
727    let v3 = Version::new("2.0.0");
728
729    let mut set = HashSet::new();
730    set.insert(v1);
731    assert!(set.contains(&v2));
732    assert!(!set.contains(&v3));
733  }
734}