stem_rs/
version.rs

1//! Tor version parsing and comparison.
2//!
3//! This module provides functionality for parsing and comparing Tor version strings.
4//! Tor versions follow the format specified in the
5//! [Tor version-spec](https://gitweb.torproject.org/torspec.git/tree/version-spec.txt):
6//! `major.minor.micro[.patch][-status][ (extra)]`
7//!
8//! # Conceptual Role
9//!
10//! The [`Version`] type enables:
11//! - Parsing version strings from Tor's GETINFO responses
12//! - Comparing versions to check feature availability
13//! - Determining if a Tor instance meets minimum requirements
14//!
15//! # Version Format
16//!
17//! A Tor version string consists of:
18//! - **major**: Major version number (required)
19//! - **minor**: Minor version number (required)
20//! - **micro**: Micro version number (required)
21//! - **patch**: Patch level (optional, defaults to 0 for comparison)
22//! - **status**: Release status tag like "alpha", "beta", "rc", "dev" (optional)
23//! - **extra**: Additional info like git commit (parsed but not stored)
24//!
25//! # Comparison Semantics
26//!
27//! Versions are compared component by component:
28//! 1. Major, minor, micro, patch are compared numerically
29//! 2. Missing patch is treated as 0
30//! 3. Status tags are compared by release priority:
31//!    - `dev` < `alpha` < `beta` < `rc` < (no status/release)
32//!    - Unknown status tags are treated as release versions
33//!
34//! # Example
35//!
36//! ```rust
37//! use stem_rs::Version;
38//!
39//! // Parse version strings
40//! let v1 = Version::parse("0.4.7.1-alpha").unwrap();
41//! let v2 = Version::parse("0.4.7.1").unwrap();
42//!
43//! // Alpha versions are less than release versions
44//! assert!(v1 < v2);
45//!
46//! // Compare against minimum requirements
47//! let minimum = Version::new(0, 4, 5);
48//! assert!(v1 > minimum);
49//!
50//! // Build versions programmatically
51//! let v3 = Version::new(0, 4, 8)
52//!     .with_patch(1)
53//!     .with_status("beta");
54//! assert_eq!(v3.to_string(), "0.4.8.1-beta");
55//! ```
56//!
57//! # See Also
58//!
59//! - [`Controller::get_version`](crate::Controller::get_version) - Query Tor's version
60//! - Python Stem equivalent: `stem.version.Version`
61
62use std::cmp::Ordering;
63use std::fmt;
64use std::str::FromStr;
65
66use crate::Error;
67
68/// A parsed Tor version with comparison support.
69///
70/// Represents a Tor version in the format `major.minor.micro[.patch][-status]`.
71/// Versions can be parsed from strings, compared, and converted back to strings.
72///
73/// # Invariants
74///
75/// - `major`, `minor`, and `micro` are always present
76/// - `patch` is `None` if not specified in the version string
77/// - `status` is `None` if no status tag was present
78/// - When comparing, missing `patch` is treated as 0
79///
80/// # Comparison
81///
82/// Versions implement [`Ord`] with the following semantics:
83/// - Numeric components are compared in order: major → minor → micro → patch
84/// - Status tags affect ordering: `dev` < `alpha` < `beta` < `rc` < release
85/// - Two versions differing only by unknown status tags are considered equal
86///
87/// # Example
88///
89/// ```rust
90/// use stem_rs::Version;
91///
92/// let alpha = Version::parse("0.4.7.1-alpha").unwrap();
93/// let beta = Version::parse("0.4.7.1-beta").unwrap();
94/// let release = Version::parse("0.4.7.1").unwrap();
95///
96/// assert!(alpha < beta);
97/// assert!(beta < release);
98///
99/// // Missing patch is treated as 0 for ordering (but not equality)
100/// let v1 = Version::parse("0.4.7").unwrap();
101/// let v2 = Version::parse("0.4.7.0").unwrap();
102/// use std::cmp::Ordering;
103/// assert_eq!(v1.cmp(&v2), Ordering::Equal);
104/// ```
105#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub struct Version {
107    /// Major version number.
108    ///
109    /// Historically, Tor major versions have been 0.x.y.z.
110    pub major: u32,
111
112    /// Minor version number.
113    ///
114    /// Incremented for significant feature releases.
115    pub minor: u32,
116
117    /// Micro version number.
118    ///
119    /// Incremented for smaller feature releases within a minor version.
120    pub micro: u32,
121
122    /// Patch level, if specified.
123    ///
124    /// Used for bug-fix releases. When comparing versions, `None` is treated as 0.
125    pub patch: Option<u32>,
126
127    /// Release status tag, if present.
128    ///
129    /// Common values include:
130    /// - `"dev"` - Development build
131    /// - `"alpha"` - Alpha release
132    /// - `"beta"` - Beta release
133    /// - `"rc"` or `"rc1"`, `"rc2"`, etc. - Release candidate
134    /// - `None` - Stable release
135    ///
136    /// Status affects version comparison: dev < alpha < beta < rc < release.
137    pub status: Option<String>,
138}
139
140impl Version {
141    /// Creates a new version with the specified major, minor, and micro components.
142    ///
143    /// The patch level defaults to `None` and status defaults to `None`,
144    /// representing a stable release.
145    ///
146    /// # Example
147    ///
148    /// ```rust
149    /// use stem_rs::Version;
150    ///
151    /// let v = Version::new(0, 4, 7);
152    /// assert_eq!(v.to_string(), "0.4.7");
153    /// assert_eq!(v.patch, None);
154    /// assert_eq!(v.status, None);
155    /// ```
156    pub fn new(major: u32, minor: u32, micro: u32) -> Self {
157        Self {
158            major,
159            minor,
160            micro,
161            patch: None,
162            status: None,
163        }
164    }
165
166    /// Sets the patch level for this version.
167    ///
168    /// This is a builder method that consumes and returns `self`,
169    /// allowing method chaining.
170    ///
171    /// # Example
172    ///
173    /// ```rust
174    /// use stem_rs::Version;
175    ///
176    /// let v = Version::new(0, 4, 7).with_patch(1);
177    /// assert_eq!(v.patch, Some(1));
178    /// assert_eq!(v.to_string(), "0.4.7.1");
179    /// ```
180    pub fn with_patch(mut self, patch: u32) -> Self {
181        self.patch = Some(patch);
182        self
183    }
184
185    /// Sets the status tag for this version.
186    ///
187    /// This is a builder method that consumes and returns `self`,
188    /// allowing method chaining.
189    ///
190    /// # Status Tags and Comparison
191    ///
192    /// The status tag affects version comparison:
193    /// - `"dev"` versions are less than `"alpha"`
194    /// - `"alpha"` versions are less than `"beta"`
195    /// - `"beta"` versions are less than `"rc"` (release candidate)
196    /// - `"rc"` versions are less than stable releases (no status)
197    ///
198    /// # Example
199    ///
200    /// ```rust
201    /// use stem_rs::Version;
202    ///
203    /// let alpha = Version::new(0, 4, 7).with_status("alpha");
204    /// let beta = Version::new(0, 4, 7).with_status("beta");
205    /// let release = Version::new(0, 4, 7);
206    ///
207    /// assert!(alpha < beta);
208    /// assert!(beta < release);
209    /// assert_eq!(alpha.to_string(), "0.4.7-alpha");
210    /// ```
211    pub fn with_status(mut self, status: impl Into<String>) -> Self {
212        self.status = Some(status.into());
213        self
214    }
215
216    /// Parses a version string into a [`Version`].
217    ///
218    /// This is a convenience wrapper around [`FromStr::from_str`].
219    ///
220    /// # Format
221    ///
222    /// The version string format is: `major.minor.micro[.patch][-status][ (extra)]`
223    ///
224    /// - `major`, `minor`, `micro`: Required numeric components
225    /// - `patch`: Optional fourth numeric component
226    /// - `status`: Optional status tag after `-` (e.g., "alpha", "beta", "rc1")
227    /// - `extra`: Optional parenthesized info (e.g., git commit) - parsed but not stored
228    ///
229    /// # Errors
230    ///
231    /// Returns [`Error::Parse`] if:
232    /// - The string is empty
233    /// - Numeric components cannot be parsed as `u32`
234    /// - The format is otherwise invalid
235    ///
236    /// # Example
237    ///
238    /// ```rust
239    /// use stem_rs::Version;
240    ///
241    /// // Simple version
242    /// let v = Version::parse("0.4.7").unwrap();
243    /// assert_eq!((v.major, v.minor, v.micro), (0, 4, 7));
244    ///
245    /// // Version with patch and status
246    /// let v = Version::parse("0.4.7.1-alpha").unwrap();
247    /// assert_eq!(v.patch, Some(1));
248    /// assert_eq!(v.status, Some("alpha".to_string()));
249    ///
250    /// // Version with git commit info (extra info is parsed but not stored)
251    /// let v = Version::parse("0.4.7.1 (git-abc123)").unwrap();
252    /// assert_eq!(v.patch, Some(1));
253    ///
254    /// // Invalid versions
255    /// assert!(Version::parse("").is_err());
256    /// assert!(Version::parse("not.a.version").is_err());
257    /// ```
258    pub fn parse(s: &str) -> Result<Self, Error> {
259        s.parse()
260    }
261}
262
263/// Parses a [`Version`] from a string.
264///
265/// See [`Version::parse`] for format details and examples.
266impl FromStr for Version {
267    type Err = Error;
268
269    fn from_str(s: &str) -> Result<Self, Self::Err> {
270        let s = s.trim();
271        if s.is_empty() {
272            return Err(Error::Parse {
273                location: "version".to_string(),
274                reason: "empty version string".to_string(),
275            });
276        }
277
278        let (version_part, status) =
279            if let Some(idx) = s.find(|c: char| !c.is_ascii_digit() && c != '.') {
280                let (v, rest) = s.split_at(idx);
281                let status = rest.trim_start_matches(['-', ' ']);
282                (
283                    v,
284                    if status.is_empty() {
285                        None
286                    } else {
287                        Some(status.to_string())
288                    },
289                )
290            } else {
291                (s, None)
292            };
293
294        let parts: Vec<&str> = version_part.split('.').collect();
295        if parts.is_empty() || parts.len() > 4 {
296            return Err(Error::Parse {
297                location: "version".to_string(),
298                reason: format!("invalid version format: {}", s),
299            });
300        }
301
302        let parse_component = |part: &str, name: &str| -> Result<u32, Error> {
303            part.parse().map_err(|_| Error::Parse {
304                location: "version".to_string(),
305                reason: format!("invalid {} component: {}", name, part),
306            })
307        };
308
309        let major = parse_component(parts.first().unwrap_or(&"0"), "major")?;
310        let minor = if parts.len() > 1 {
311            parse_component(parts[1], "minor")?
312        } else {
313            0
314        };
315        let micro = if parts.len() > 2 {
316            parse_component(parts[2], "micro")?
317        } else {
318            0
319        };
320        let patch = if parts.len() > 3 {
321            Some(parse_component(parts[3], "patch")?)
322        } else {
323            None
324        };
325
326        Ok(Version {
327            major,
328            minor,
329            micro,
330            patch,
331            status,
332        })
333    }
334}
335
336/// Formats the version as a string.
337///
338/// The output format is `major.minor.micro[.patch][-status]`.
339///
340/// # Example
341///
342/// ```rust
343/// use stem_rs::Version;
344///
345/// let v = Version::new(0, 4, 7).with_patch(1).with_status("alpha");
346/// assert_eq!(format!("{}", v), "0.4.7.1-alpha");
347///
348/// let v = Version::new(0, 4, 7);
349/// assert_eq!(format!("{}", v), "0.4.7");
350/// ```
351impl fmt::Display for Version {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
354        if let Some(patch) = self.patch {
355            write!(f, ".{}", patch)?;
356        }
357        if let Some(ref status) = self.status {
358            write!(f, "-{}", status)?;
359        }
360        Ok(())
361    }
362}
363
364/// Determines the comparison priority of a status tag.
365///
366/// Status tags are ordered as: dev (0) < alpha (1) < beta (2) < rc (3) < release/unknown (4).
367/// This ordering ensures that pre-release versions sort before stable releases.
368fn status_priority(status: &Option<String>) -> u8 {
369    match status.as_deref() {
370        None => 4,
371        Some(s) => {
372            let s_lower = s.to_lowercase();
373            if s_lower.starts_with("dev") {
374                0
375            } else if s_lower.starts_with("alpha") {
376                1
377            } else if s_lower.starts_with("beta") {
378                2
379            } else if s_lower.starts_with("rc") {
380                3
381            } else {
382                4
383            }
384        }
385    }
386}
387
388/// Provides total ordering for versions.
389///
390/// Versions are compared component by component in this order:
391/// 1. `major` - compared numerically
392/// 2. `minor` - compared numerically
393/// 3. `micro` - compared numerically
394/// 4. `patch` - compared numerically (`None` treated as 0)
395/// 5. `status` - compared by release priority
396///
397/// # Status Priority
398///
399/// Status tags are ordered by release maturity:
400/// - `dev` < `alpha` < `beta` < `rc` < (no status)
401/// - Unknown status tags are treated as release versions
402///
403/// # Example
404///
405/// ```rust
406/// use stem_rs::Version;
407///
408/// // Numeric comparison
409/// assert!(Version::parse("0.4.7").unwrap() < Version::parse("0.4.8").unwrap());
410/// assert!(Version::parse("0.4.7").unwrap() < Version::parse("0.5.0").unwrap());
411///
412/// // Status comparison
413/// assert!(Version::parse("0.4.7-dev").unwrap() < Version::parse("0.4.7-alpha").unwrap());
414/// assert!(Version::parse("0.4.7-alpha").unwrap() < Version::parse("0.4.7-beta").unwrap());
415/// assert!(Version::parse("0.4.7-beta").unwrap() < Version::parse("0.4.7-rc1").unwrap());
416/// assert!(Version::parse("0.4.7-rc1").unwrap() < Version::parse("0.4.7").unwrap());
417///
418/// // Missing patch is treated as 0 for ordering
419/// use std::cmp::Ordering;
420/// assert_eq!(Version::parse("0.4.7").unwrap().cmp(&Version::parse("0.4.7.0").unwrap()), Ordering::Equal);
421/// ```
422impl Ord for Version {
423    fn cmp(&self, other: &Self) -> Ordering {
424        match self.major.cmp(&other.major) {
425            Ordering::Equal => {}
426            ord => return ord,
427        }
428        match self.minor.cmp(&other.minor) {
429            Ordering::Equal => {}
430            ord => return ord,
431        }
432        match self.micro.cmp(&other.micro) {
433            Ordering::Equal => {}
434            ord => return ord,
435        }
436        match self.patch.unwrap_or(0).cmp(&other.patch.unwrap_or(0)) {
437            Ordering::Equal => {}
438            ord => return ord,
439        }
440        status_priority(&self.status).cmp(&status_priority(&other.status))
441    }
442}
443
444/// Provides partial ordering for versions.
445///
446/// This implementation delegates to [`Ord::cmp`], so all versions are comparable.
447/// See [`Ord`] implementation for comparison semantics.
448impl PartialOrd for Version {
449    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
450        Some(self.cmp(other))
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_parse_simple_version() {
460        let v = Version::parse("0.4.7").unwrap();
461        assert_eq!(v.major, 0);
462        assert_eq!(v.minor, 4);
463        assert_eq!(v.micro, 7);
464        assert_eq!(v.patch, None);
465        assert_eq!(v.status, None);
466    }
467
468    #[test]
469    fn test_parse_version_with_patch() {
470        let v = Version::parse("0.4.7.1").unwrap();
471        assert_eq!(v.major, 0);
472        assert_eq!(v.minor, 4);
473        assert_eq!(v.micro, 7);
474        assert_eq!(v.patch, Some(1));
475        assert_eq!(v.status, None);
476    }
477
478    #[test]
479    fn test_parse_version_with_status() {
480        let v = Version::parse("0.4.7.1-alpha").unwrap();
481        assert_eq!(v.major, 0);
482        assert_eq!(v.minor, 4);
483        assert_eq!(v.micro, 7);
484        assert_eq!(v.patch, Some(1));
485        assert_eq!(v.status, Some("alpha".to_string()));
486    }
487
488    #[test]
489    fn test_parse_version_with_complex_status() {
490        let v = Version::parse("0.4.8.0-alpha-dev").unwrap();
491        assert_eq!(v.status, Some("alpha-dev".to_string()));
492    }
493
494    #[test]
495    fn test_display() {
496        let v = Version::new(0, 4, 7).with_patch(1).with_status("alpha");
497        assert_eq!(v.to_string(), "0.4.7.1-alpha");
498    }
499
500    #[test]
501    fn test_display_no_patch() {
502        let v = Version::new(0, 4, 7);
503        assert_eq!(v.to_string(), "0.4.7");
504    }
505
506    #[test]
507    fn test_comparison_major() {
508        let v1 = Version::new(0, 4, 7);
509        let v2 = Version::new(1, 0, 0);
510        assert!(v1 < v2);
511    }
512
513    #[test]
514    fn test_comparison_minor() {
515        let v1 = Version::new(0, 4, 7);
516        let v2 = Version::new(0, 5, 0);
517        assert!(v1 < v2);
518    }
519
520    #[test]
521    fn test_comparison_micro() {
522        let v1 = Version::new(0, 4, 7);
523        let v2 = Version::new(0, 4, 8);
524        assert!(v1 < v2);
525    }
526
527    #[test]
528    fn test_comparison_patch() {
529        let v1 = Version::new(0, 4, 7).with_patch(1);
530        let v2 = Version::new(0, 4, 7).with_patch(2);
531        assert!(v1 < v2);
532    }
533
534    #[test]
535    fn test_comparison_status_priority() {
536        let dev = Version::new(0, 4, 7).with_status("dev");
537        let alpha = Version::new(0, 4, 7).with_status("alpha");
538        let beta = Version::new(0, 4, 7).with_status("beta");
539        let rc = Version::new(0, 4, 7).with_status("rc1");
540        let release = Version::new(0, 4, 7);
541
542        assert!(dev < alpha);
543        assert!(alpha < beta);
544        assert!(beta < rc);
545        assert!(rc < release);
546    }
547
548    #[test]
549    fn test_equality() {
550        let v1 = Version::new(0, 4, 7).with_patch(1).with_status("alpha");
551        let v2 = Version::new(0, 4, 7).with_patch(1).with_status("alpha");
552        assert_eq!(v1, v2);
553    }
554
555    #[test]
556    fn test_parse_invalid_empty() {
557        assert!(Version::parse("").is_err());
558    }
559
560    #[test]
561    fn test_parse_invalid_non_numeric() {
562        assert!(Version::parse("abc.def.ghi").is_err());
563    }
564
565    #[test]
566    fn test_version_missing_patch_equals_zero() {
567        let v1 = Version::parse("0.4.7").unwrap();
568        let v2 = Version::parse("0.4.7.0").unwrap();
569        assert!(v1.cmp(&v2) == std::cmp::Ordering::Equal);
570    }
571
572    #[test]
573    fn test_version_with_git_extra() {
574        let v = Version::parse("0.4.7.1 (git-73ff13ab3cc9570d)").unwrap();
575        assert_eq!(v.major, 0);
576        assert_eq!(v.minor, 4);
577        assert_eq!(v.micro, 7);
578        assert_eq!(v.patch, Some(1));
579    }
580
581    #[test]
582    fn test_version_comparison_with_status() {
583        let v1 = Version::parse("0.4.7.3-tag").unwrap();
584        let v2 = Version::parse("0.4.7.3").unwrap();
585        assert_eq!(v1.cmp(&v2), std::cmp::Ordering::Equal);
586
587        let v_dev = Version::parse("0.4.7.3-dev").unwrap();
588        let v_release = Version::parse("0.4.7.3").unwrap();
589        assert!(v_dev < v_release);
590    }
591
592    #[test]
593    fn test_parsing_various_components() {
594        let v = Version::parse("0.1.2.3-tag").unwrap();
595        assert_eq!(v.major, 0);
596        assert_eq!(v.minor, 1);
597        assert_eq!(v.micro, 2);
598        assert_eq!(v.patch, Some(3));
599        assert_eq!(v.status, Some("tag".to_string()));
600
601        let v = Version::parse("0.1.2.3").unwrap();
602        assert_eq!(v.patch, Some(3));
603        assert_eq!(v.status, None);
604
605        let v = Version::parse("0.1.2-tag").unwrap();
606        assert_eq!(v.patch, None);
607        assert_eq!(v.status, Some("tag".to_string()));
608
609        let v = Version::parse("0.1.2").unwrap();
610        assert_eq!(v.patch, None);
611        assert_eq!(v.status, None);
612    }
613
614    #[test]
615    fn test_parsing_empty_tag() {
616        let v = Version::parse("0.1.2.3-").unwrap();
617        assert_eq!(v.patch, Some(3));
618
619        let v = Version::parse("0.1.2-").unwrap();
620        assert_eq!(v.patch, None);
621    }
622
623    #[test]
624    fn test_parsing_with_extra_info() {
625        let v = Version::parse("0.1.2.3-tag (git-73ff13ab3cc9570d)").unwrap();
626        assert_eq!(v.major, 0);
627        assert_eq!(v.minor, 1);
628        assert_eq!(v.micro, 2);
629        assert_eq!(v.patch, Some(3));
630        assert!(v.status.is_some());
631
632        let v = Version::parse("0.1.2 (git-73ff13ab3cc9570d)").unwrap();
633        assert_eq!(v.major, 0);
634        assert_eq!(v.minor, 1);
635        assert_eq!(v.micro, 2);
636    }
637
638    #[test]
639    fn test_invalid_version_strings() {
640        assert!(Version::parse("").is_err());
641        assert!(Version::parse("1.2.a.4").is_err());
642        assert!(Version::parse("1.2.3.a").is_err());
643    }
644
645    #[test]
646    fn test_comparison_basic_incrementing() {
647        assert!(Version::parse("1.1.2.3-tag").unwrap() > Version::parse("0.1.2.3-tag").unwrap());
648        assert!(Version::parse("0.2.2.3-tag").unwrap() > Version::parse("0.1.2.3-tag").unwrap());
649        assert!(Version::parse("0.1.3.3-tag").unwrap() > Version::parse("0.1.2.3-tag").unwrap());
650        assert!(Version::parse("0.1.2.4-tag").unwrap() > Version::parse("0.1.2.3-tag").unwrap());
651        assert_eq!(
652            Version::parse("0.1.2.3-tag").unwrap(),
653            Version::parse("0.1.2.3-tag").unwrap()
654        );
655    }
656
657    #[test]
658    fn test_comparison_common_tags() {
659        assert!(Version::parse("0.1.2.3-beta").unwrap() > Version::parse("0.1.2.3-alpha").unwrap());
660        assert!(Version::parse("0.1.2.3-rc").unwrap() > Version::parse("0.1.2.3-beta").unwrap());
661    }
662
663    #[test]
664    fn test_missing_patch_equals_zero() {
665        let v1 = Version::parse("0.1.2").unwrap();
666        let v2 = Version::parse("0.1.2.0").unwrap();
667        assert_eq!(v1.cmp(&v2), std::cmp::Ordering::Equal);
668
669        let v1 = Version::parse("0.1.2-tag").unwrap();
670        let v2 = Version::parse("0.1.2.0-tag").unwrap();
671        assert_eq!(v1.cmp(&v2), std::cmp::Ordering::Equal);
672    }
673
674    #[test]
675    fn test_comparison_missing_patch_or_status() {
676        let v_with_tag = Version::parse("0.1.2.3-tag").unwrap();
677        let v_without_tag = Version::parse("0.1.2.3").unwrap();
678        assert_eq!(v_with_tag.cmp(&v_without_tag), std::cmp::Ordering::Equal);
679
680        assert!(Version::parse("0.1.2.3-tag").unwrap() > Version::parse("0.1.2-tag").unwrap());
681    }
682
683    #[test]
684    fn test_string_conversion_roundtrip() {
685        let versions = ["0.1.2.3-tag", "0.1.2.3", "0.1.2"];
686        for v_str in versions {
687            let v = Version::parse(v_str).unwrap();
688            assert_eq!(v.to_string(), v_str);
689        }
690    }
691
692    #[test]
693    fn test_nonversion_comparison() {
694        let v = Version::parse("0.1.2.3").unwrap();
695        assert_ne!(v, Version::parse("0.1.2.4").unwrap());
696    }
697}
698
699#[cfg(test)]
700mod proptests {
701    use super::*;
702    use proptest::prelude::*;
703
704    fn version_strategy() -> impl Strategy<Value = Version> {
705        (
706            0u32..100,
707            0u32..100,
708            0u32..100,
709            proptest::option::of(0u32..100),
710        )
711            .prop_map(|(major, minor, micro, patch)| Version {
712                major,
713                minor,
714                micro,
715                patch,
716                status: None,
717            })
718    }
719
720    fn version_with_status_strategy() -> impl Strategy<Value = Version> {
721        let status_strategy = proptest::option::of(prop_oneof![
722            Just("dev".to_string()),
723            Just("alpha".to_string()),
724            Just("beta".to_string()),
725            Just("rc1".to_string()),
726            Just("rc2".to_string()),
727        ]);
728        (
729            0u32..100,
730            0u32..100,
731            0u32..100,
732            proptest::option::of(0u32..100),
733            status_strategy,
734        )
735            .prop_map(|(major, minor, micro, patch, status)| Version {
736                major,
737                minor,
738                micro,
739                patch,
740                status,
741            })
742    }
743
744    proptest! {
745        #![proptest_config(ProptestConfig::with_cases(100))]
746
747        #[test]
748        fn prop_version_roundtrip(version in version_with_status_strategy()) {
749            let s = version.to_string();
750            let parsed = Version::parse(&s).expect("should parse successfully");
751            prop_assert_eq!(version.major, parsed.major);
752            prop_assert_eq!(version.minor, parsed.minor);
753            prop_assert_eq!(version.micro, parsed.micro);
754            prop_assert_eq!(version.patch, parsed.patch);
755            prop_assert_eq!(version.status, parsed.status);
756        }
757
758        #[test]
759        fn prop_version_comparison_transitivity(
760            a in version_strategy(),
761            b in version_strategy(),
762            c in version_strategy()
763        ) {
764            if a < b && b < c {
765                prop_assert!(a < c, "transitivity violated: {:?} < {:?} < {:?}", a, b, c);
766            }
767            if a == b && b == c {
768                prop_assert_eq!(a, c, "equality transitivity violated");
769            }
770        }
771
772        #[test]
773        fn prop_version_comparison_consistency(a in version_strategy(), b in version_strategy()) {
774            let lt = a < b;
775            let eq = a == b;
776            let gt = a > b;
777            let count = [lt, eq, gt].iter().filter(|&&x| x).count();
778            prop_assert_eq!(count, 1, "exactly one comparison should be true for {:?} vs {:?}", a, b);
779        }
780    }
781}