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}