Skip to main content

rez_next_version/
version.rs

1//! Version implementation
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rez_next_common::RezCoreError;
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::hash::{Hash, Hasher};
9
10/// Pre-compiled regex for token extraction (compiled once, reused across all parse calls)
11static TOKEN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[a-zA-Z0-9_]+").unwrap());
12
13/// High-performance version representation compatible with rez
14#[derive(Debug)]
15pub struct Version {
16    /// Version tokens
17    tokens: Vec<String>,
18    /// Separators between tokens
19    separators: Vec<String>,
20    /// Cached string representation
21    pub string_repr: String,
22    /// Cached hash value
23    cached_hash: Option<u64>,
24}
25
26impl Serialize for Version {
27    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28    where
29        S: serde::Serializer,
30    {
31        // Serialize as string representation for simplicity
32        self.string_repr.serialize(serializer)
33    }
34}
35
36impl std::fmt::Display for Version {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(f, "{}", self.as_str())
39    }
40}
41
42impl<'de> Deserialize<'de> for Version {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        let s = String::deserialize(deserializer)?;
48        Self::parse(&s).map_err(serde::de::Error::custom)
49    }
50}
51
52impl Version {
53    pub fn new(version_str: Option<&str>) -> Result<Self, RezCoreError> {
54        let version_str = version_str.unwrap_or("");
55        Self::parse(version_str)
56    }
57
58    pub fn as_str(&self) -> &str {
59        &self.string_repr
60    }
61
62    /// Return whether this version begins with every token in `prefix`.
63    pub fn has_prefix(&self, prefix: &Version) -> bool {
64        self.tokens.len() >= prefix.tokens.len()
65            && self
66                .tokens
67                .iter()
68                .zip(&prefix.tokens)
69                .all(|(token, prefix_token)| {
70                    Self::compare_single_token(token, prefix_token) == Ordering::Equal
71                })
72    }
73}
74
75impl Version {
76    /// Internal parsing function that runs without GIL
77    /// Returns (tokens, separators) as pure Rust data
78    fn parse_internal_gil_free(s: &str) -> Result<(Vec<String>, Vec<String>), RezCoreError> {
79        // Validate version format - reject obvious invalid patterns
80        if s.starts_with('v') || s.starts_with('V') {
81            return Err(RezCoreError::VersionParse(format!(
82                "Version prefixes not supported: '{s}'"
83            )));
84        }
85
86        // Check for invalid characters or patterns
87        if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
88            return Err(RezCoreError::VersionParse(format!(
89                "Invalid version syntax: '{s}'"
90            )));
91        }
92
93        // Use pre-compiled regex to find tokens (alphanumeric + underscore)
94        let tokens: Vec<&str> = TOKEN_REGEX.find_iter(s).map(|m| m.as_str()).collect();
95
96        if tokens.is_empty() {
97            return Err(RezCoreError::VersionParse(format!(
98                "Invalid version syntax: '{s}'"
99            )));
100        }
101
102        // Check for too many numeric-only tokens (reject versions like 1.2.3.4.5.6)
103        let numeric_tokens: Vec<_> = tokens
104            .iter()
105            .filter(|t| t.chars().all(|c| c.is_ascii_digit()))
106            .collect();
107        if numeric_tokens.len() > 5 {
108            return Err(RezCoreError::VersionParse(format!(
109                "Version too complex: '{s}'"
110            )));
111        }
112
113        // Check for too many tokens overall
114        if tokens.len() > 10 {
115            return Err(RezCoreError::VersionParse(format!(
116                "Version too complex: '{s}'"
117            )));
118        }
119
120        // Extract separators
121        let separators: Vec<&str> = TOKEN_REGEX.split(s).collect();
122
123        // Validate separators (should be empty at start/end, single char in middle)
124        if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
125            return Err(RezCoreError::VersionParse(format!(
126                "Invalid version syntax: '{s}'"
127            )));
128        }
129
130        for sep in &separators[1..separators.len() - 1] {
131            if sep.len() > 1 {
132                return Err(RezCoreError::VersionParse(format!(
133                    "Invalid version syntax: '{s}'"
134                )));
135            }
136            // Only allow specific separators
137            if !matches!(*sep, "." | "-" | "_" | "+") {
138                return Err(RezCoreError::VersionParse(format!(
139                    "Invalid separator '{sep}' in version: '{s}'"
140                )));
141            }
142        }
143
144        // Validate tokens before creating them
145        for token_str in &tokens {
146            // Check if token contains only valid characters
147            if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
148                return Err(RezCoreError::VersionParse(format!(
149                    "Invalid characters in token: '{token_str}'"
150                )));
151            }
152
153            // Check for invalid patterns
154            if token_str.starts_with('_') || token_str.ends_with('_') {
155                return Err(RezCoreError::VersionParse(format!(
156                    "Invalid token format: '{token_str}'"
157                )));
158            }
159
160            // Reject tokens that are purely alphabetic and don't look like version components
161            if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
162                return Err(RezCoreError::VersionParse(format!(
163                    "Invalid version token: '{token_str}'"
164                )));
165            }
166
167            // Reject common invalid patterns
168            if *token_str == "not" || *token_str == "version" {
169                return Err(RezCoreError::VersionParse(format!(
170                    "Invalid version token: '{token_str}'"
171                )));
172            }
173        }
174
175        // Convert to owned strings
176        let token_strings: Vec<String> = tokens.into_iter().map(ToString::to_string).collect();
177        let sep_strings: Vec<String> = separators[1..separators.len() - 1]
178            .iter()
179            .map(ToString::to_string)
180            .collect();
181
182        Ok((token_strings, sep_strings))
183    }
184
185    /// Create the infinite version (largest possible version)
186    #[must_use]
187    pub fn inf() -> Self {
188        Self {
189            tokens: vec![],
190            separators: vec![],
191            string_repr: "inf".to_string(),
192            cached_hash: None,
193        }
194    }
195
196    /// Check if this is the infinite version
197    #[must_use]
198    pub fn is_inf(&self) -> bool {
199        self.string_repr == "inf"
200    }
201
202    /// Create an empty version (smallest possible version)
203    #[must_use]
204    pub fn empty() -> Self {
205        Self {
206            tokens: vec![],
207            separators: vec![],
208            string_repr: String::new(),
209            cached_hash: None,
210        }
211    }
212
213    /// Create the epsilon version (alias for empty, smallest possible version)
214    #[must_use]
215    pub fn epsilon() -> Self {
216        Self::empty()
217    }
218
219    /// Check if this is an empty version
220    pub fn is_empty(&self) -> bool {
221        self.tokens.is_empty() && self.string_repr.is_empty()
222    }
223
224    /// Number of version tokens (components)
225    pub fn len(&self) -> usize {
226        self.tokens.len()
227    }
228
229    /// Get the major version component (first token as u64), if available
230    pub fn major(&self) -> Option<u64> {
231        self.tokens.first().and_then(|t| t.parse::<u64>().ok())
232    }
233
234    /// Get the minor version component (second token as u64), if available
235    pub fn minor(&self) -> Option<u64> {
236        self.tokens.get(1).and_then(|t| t.parse::<u64>().ok())
237    }
238
239    /// Get the patch version component (third token as u64), if available
240    pub fn patch(&self) -> Option<u64> {
241        self.tokens.get(2).and_then(|t| t.parse::<u64>().ok())
242    }
243
244    /// Check if this is the epsilon version (alias for is_empty)
245    pub fn is_epsilon(&self) -> bool {
246        self.is_empty()
247    }
248
249    /// Check if this version is a prerelease version
250    pub fn is_prerelease(&self) -> bool {
251        if self.is_empty() || self.is_inf() {
252            return false;
253        }
254
255        // Check if any token contains alphabetic characters that indicate prerelease
256        for token in &self.tokens {
257            let s_lower = token.to_lowercase();
258            // Common prerelease indicators
259            if s_lower.contains("alpha")
260                || s_lower.contains("beta")
261                || s_lower.contains("rc")
262                || s_lower.contains("dev")
263                || s_lower.contains("pre")
264                || s_lower.contains("snapshot")
265            {
266                return true;
267            }
268        }
269        false
270    }
271
272    /// Parse a version string into a Version object
273    pub fn parse(s: &str) -> Result<Self, RezCoreError> {
274        let s = s.trim();
275
276        // Handle empty version (epsilon version)
277        if s.is_empty() {
278            return Ok(Self::empty());
279        }
280
281        // Handle infinite version
282        if s == "inf" {
283            return Ok(Self::inf());
284        }
285
286        // Handle epsilon version explicitly
287        if s == "epsilon" {
288            return Ok(Self::epsilon());
289        }
290
291        // Parse using the GIL-free method
292        let (tokens, separators) = Self::parse_internal_gil_free(s)?;
293
294        Ok(Self {
295            tokens,
296            separators,
297            string_repr: s.to_string(),
298            cached_hash: None,
299        })
300    }
301
302    /// Compare two versions using rez-compatible rules
303    fn compare_rez(&self, other: &Self) -> Ordering {
304        // Handle infinite versions (inf is largest)
305        match (self.is_inf(), other.is_inf()) {
306            (true, true) => return Ordering::Equal,
307            (true, false) => return Ordering::Greater,
308            (false, true) => return Ordering::Less,
309            (false, false) => {} // Continue with normal comparison
310        }
311
312        // Handle empty/epsilon versions (epsilon version is smallest)
313        match (self.is_empty(), other.is_empty()) {
314            (true, true) => return Ordering::Equal,
315            (true, false) => return Ordering::Less,
316            (false, true) => return Ordering::Greater,
317            (false, false) => {} // Continue with normal comparison
318        }
319
320        // Compare tokens using string comparison for now
321        Self::compare_token_strings(&self.tokens, &other.tokens)
322    }
323
324    /// Compare a single token, handling mixed alphanumeric strings.
325    ///
326    /// Rez token comparison rules:
327    /// 1. Both numeric: numeric integer comparison.
328    /// 2. Both alpha: lexicographic comparison.
329    /// 3. Mixed (alpha vs numeric segment): **alpha < numeric** — alphabetic segments
330    ///    sort *before* numeric ones, matching rez semantics where `1.0.alpha < 1.0.0`.
331    /// 4. Mixed alphanumeric tokens (e.g. "alpha10"): split into alternating segments and
332    ///    apply rules 1–3 per segment pair.
333    fn compare_single_token(t1: &str, t2: &str) -> Ordering {
334        // Fast path: both purely numeric
335        if let (Ok(n1), Ok(n2)) = (t1.parse::<i64>(), t2.parse::<i64>()) {
336            return n1.cmp(&n2);
337        }
338        // Fast path: equal strings
339        if t1 == t2 {
340            return Ordering::Equal;
341        }
342
343        // Fast path: purely alpha vs purely numeric → alpha is Less (rez spec)
344        let t1_all_alpha = t1.chars().all(|c| c.is_alphabetic() || c == '_');
345        let t2_all_alpha = t2.chars().all(|c| c.is_alphabetic() || c == '_');
346        let t1_all_num = t1.chars().all(|c| c.is_ascii_digit());
347        let t2_all_num = t2.chars().all(|c| c.is_ascii_digit());
348        if t1_all_alpha && t2_all_num {
349            return Ordering::Less;
350        }
351        if t1_all_num && t2_all_alpha {
352            return Ordering::Greater;
353        }
354        // Both purely alpha: lexicographic
355        if t1_all_alpha && t2_all_alpha {
356            return t1.cmp(t2);
357        }
358
359        // Mixed alphanumeric tokens: split into segments and compare segment-by-segment.
360        // Within a segment pair: alpha segment < numeric segment (rez spec).
361        let seg1 = Self::split_token_segments(t1);
362        let seg2 = Self::split_token_segments(t2);
363
364        for (s1, s2) in seg1.iter().zip(seg2.iter()) {
365            let s1_is_num = s1.parse::<u64>().is_ok();
366            let s2_is_num = s2.parse::<u64>().is_ok();
367            let cmp = match (s1_is_num, s2_is_num) {
368                (true, true) => {
369                    let n1: u64 = s1.parse().unwrap();
370                    let n2: u64 = s2.parse().unwrap();
371                    n1.cmp(&n2)
372                }
373                (false, false) => s1.as_str().cmp(s2.as_str()),
374                (false, true) => Ordering::Less, // alpha segment < numeric segment
375                (true, false) => Ordering::Greater, // numeric segment > alpha segment
376            };
377            if cmp != Ordering::Equal {
378                return cmp;
379            }
380        }
381        seg1.len().cmp(&seg2.len())
382    }
383
384    /// Split a token into alternating alpha/numeric segments.
385    /// E.g. "alpha10" → ["alpha", "10"], "rc2" → ["rc", "2"]
386    fn split_token_segments(s: &str) -> Vec<String> {
387        let mut segments = Vec::new();
388        let mut current = String::new();
389        let mut in_digits = false;
390
391        for ch in s.chars() {
392            let is_digit = ch.is_ascii_digit();
393            if current.is_empty() {
394                in_digits = is_digit;
395                current.push(ch);
396            } else if is_digit == in_digits {
397                current.push(ch);
398            } else {
399                segments.push(current.clone());
400                current.clear();
401                in_digits = is_digit;
402                current.push(ch);
403            }
404        }
405        if !current.is_empty() {
406            segments.push(current);
407        }
408        segments
409    }
410
411    /// Compare token arrays using rez-compatible rules.
412    /// In rez, trailing zeros are implicit: 1.0 == 1.0.0 == 1.0.0.0
413    /// Rez epoch semantics: shorter version = higher epoch
414    /// - If extra tokens are all zeros → equal (trailing zeros implicit)
415    /// - If extra tokens start with non-zero numeric → shorter is greater (higher epoch)
416    /// - If extra tokens start with alpha → shorter is LESS (pre-release, so longer is greater)
417    fn compare_token_strings(tokens1: &[String], tokens2: &[String]) -> Ordering {
418        for (t1, t2) in tokens1.iter().zip(tokens2.iter()) {
419            let cmp = Self::compare_single_token(t1, t2);
420            if cmp != Ordering::Equal {
421                return cmp;
422            }
423        }
424
425        // If all compared tokens are equal, check extra tokens
426        // Rez semantics: trailing zeros are implicit
427        // - 2 == 2.0.0 (extra tokens are all "0")
428        // - 1.0.1 > 1.0 (shorter has implicit Z+ which is > any digit)
429        // - 1.0 > 1.0.alpha (release > pre-release)
430        if tokens1.len() < tokens2.len() {
431            // tokens1 is shorter, tokens2 has extra tokens
432            let extra_tokens = &tokens2[tokens1.len()..];
433            if extra_tokens.iter().all(|t| t == "0") {
434                // Trailing zeros implicit → equal
435                Ordering::Equal
436            } else {
437                // Check first extra token
438                let first_extra = &tokens2[tokens1.len()];
439                if first_extra.chars().all(|c| c.is_ascii_digit()) {
440                    // Extra token is numeric and non-zero
441                    // Shorter has implicit Z+ which is > any digit
442                    Ordering::Greater
443                } else {
444                    // Extra token is alpha → tokens2 is pre-release
445                    // Release (tokens1) > pre-release (tokens2)
446                    Ordering::Greater
447                }
448            }
449        } else if tokens2.len() < tokens1.len() {
450            // tokens2 is shorter, tokens1 has extra tokens
451            let extra_tokens = &tokens1[tokens2.len()..];
452            if extra_tokens.iter().all(|t| t == "0") {
453                // Trailing zeros implicit → equal
454                Ordering::Equal
455            } else {
456                // Check first extra token
457                let first_extra = &tokens1[tokens2.len()];
458                if first_extra.chars().all(|c| c.is_ascii_digit()) {
459                    // Extra token is numeric and non-zero
460                    // tokens2 has implicit Z+ which is > any digit
461                    // So tokens2 > tokens1, meaning tokens1 < tokens2
462                    Ordering::Less
463                } else {
464                    // Extra token is alpha → tokens1 is pre-release
465                    // tokens2 is release, Release > pre-release
466                    // So tokens2 > tokens1, meaning tokens1 < tokens2
467                    Ordering::Less
468                }
469            }
470        } else {
471            Ordering::Equal
472        }
473    }
474}
475
476impl PartialEq for Version {
477    fn eq(&self, other: &Self) -> bool {
478        self.compare_rez(other) == Ordering::Equal
479    }
480}
481
482impl Eq for Version {}
483
484impl Ord for Version {
485    fn cmp(&self, other: &Self) -> Ordering {
486        self.compare_rez(other)
487    }
488}
489
490impl PartialOrd for Version {
491    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
492        Some(self.cmp(other))
493    }
494}
495
496impl Hash for Version {
497    fn hash<H: Hasher>(&self, state: &mut H) {
498        self.string_repr.hash(state);
499    }
500}
501
502impl Clone for Version {
503    fn clone(&self) -> Self {
504        Self {
505            tokens: self.tokens.clone(),
506            separators: self.separators.clone(),
507            string_repr: self.string_repr.clone(),
508            cached_hash: self.cached_hash,
509        }
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    fn ver(s: &str) -> Version {
518        Version::parse(s).unwrap()
519    }
520
521    #[test]
522    fn test_version_creation() {
523        let version = Version::parse("1.2.3").unwrap();
524        assert_eq!(version.as_str(), "1.2.3");
525        assert_eq!(version.tokens.len(), 3);
526        assert!(!version.is_empty());
527    }
528
529    #[test]
530    fn test_empty_version() {
531        let version = Version::parse("").unwrap();
532        assert_eq!(version.as_str(), "");
533        assert_eq!(version.tokens.len(), 0);
534        assert!(version.is_empty());
535    }
536
537    #[test]
538    fn test_version_inf() {
539        let version = Version::inf();
540        assert_eq!(version.as_str(), "inf");
541        assert!(version.is_inf());
542    }
543
544    #[test]
545    fn test_version_epsilon() {
546        let version = Version::epsilon();
547        assert_eq!(version.as_str(), "");
548        assert!(version.is_epsilon());
549        assert!(version.is_empty());
550    }
551
552    #[test]
553    fn test_version_empty() {
554        let version = Version::empty();
555        assert_eq!(version.as_str(), "");
556        assert!(version.is_empty());
557        assert!(version.is_epsilon());
558    }
559
560    #[test]
561    fn test_version_parsing_special() {
562        // Test parsing empty version
563        let empty = Version::parse("").unwrap();
564        assert!(empty.is_empty());
565
566        // Test parsing inf version
567        let inf = Version::parse("inf").unwrap();
568        assert!(inf.is_inf());
569
570        // Test parsing epsilon version
571        let epsilon = Version::parse("epsilon").unwrap();
572        assert!(epsilon.is_epsilon());
573    }
574
575    #[test]
576    fn test_version_comparison_boundaries() {
577        let empty = Version::empty();
578        let epsilon = Version::epsilon();
579        let normal = Version::parse("1.0.0").unwrap();
580        let inf = Version::inf();
581
582        // Test epsilon/empty equivalence
583        assert_eq!(empty.cmp(&epsilon), Ordering::Equal);
584
585        // Test ordering: epsilon < normal < inf
586        assert_eq!(epsilon.cmp(&normal), Ordering::Less);
587        assert_eq!(normal.cmp(&inf), Ordering::Less);
588        assert_eq!(epsilon.cmp(&inf), Ordering::Less);
589
590        // Test reverse ordering
591        assert_eq!(inf.cmp(&normal), Ordering::Greater);
592        assert_eq!(normal.cmp(&epsilon), Ordering::Greater);
593        assert_eq!(inf.cmp(&epsilon), Ordering::Greater);
594    }
595
596    #[test]
597    fn test_version_prerelease_comparison() {
598        // Test that release versions are greater than pre-release versions
599        let release = Version::parse("2").unwrap();
600        let prerelease = Version::parse("2.alpha1").unwrap();
601
602        // "2" should be greater than "2.alpha1"
603        assert_eq!(release.cmp(&prerelease), Ordering::Greater);
604        assert_eq!(prerelease.cmp(&release), Ordering::Less);
605
606        // Test with comparison operators
607        assert!(release >= prerelease); // "2" < "2.alpha1" should be false
608        assert!(prerelease < release); // "2.alpha1" < "2" should be true
609    }
610
611    #[test]
612    fn test_version_copy() {
613        let version = Version::parse("1.2.3").unwrap();
614        let copied = version.clone();
615        assert_eq!(version.as_str(), copied.as_str());
616        assert_eq!(version.tokens.len(), copied.tokens.len());
617    }
618
619    #[test]
620    fn test_version_ord_basic() {
621        let v1 = ver("1.0.0");
622        let v2 = ver("1.0.0");
623        assert!(v1 >= v2, "1.0.0 >= 1.0.0 should be true");
624        assert!(v1 <= v2, "1.0.0 <= 1.0.0 should be true");
625    }
626
627    #[test]
628    fn test_version_ord_greater() {
629        let v1 = ver("2.0.0");
630        let v2 = ver("1.0.0");
631        assert!(v1 > v2, "2.0.0 > 1.0.0 should be true");
632        assert!(v2 < v1, "1.0.0 < 2.0.0 should be true");
633    }
634
635    #[test]
636    fn test_range_contains_ge() {
637        // Test if VersionRange::contains() bug is fixed
638        let range = crate::VersionRange::parse(">=1.0.0").unwrap();
639        assert!(
640            range.contains(&ver("1.0.0")),
641            ">=1.0.0 should contain 1.0.0"
642        );
643    }
644
645    #[test]
646    fn test_version_trim() {
647        let version = Version::parse("1.2.3.4").unwrap();
648        // Create a trimmed version by taking only first 2 tokens
649        let mut trimmed_tokens = version.tokens.clone();
650        trimmed_tokens.truncate(2);
651        assert_eq!(trimmed_tokens.len(), 2);
652    }
653
654    // ─── Pre-release token ordering chain tests (Cycle 29) ──────────────
655
656    #[test]
657    fn test_prerelease_alpha_beta_rc_ordering() {
658        // Standard prerelease ordering: alpha < beta < rc < release
659        let alpha = Version::parse("1.0.alpha").unwrap();
660        let beta = Version::parse("1.0.beta").unwrap();
661        let rc = Version::parse("1.0.rc").unwrap();
662        let release = Version::parse("1.0").unwrap();
663
664        assert!(alpha < beta, "alpha should be less than beta");
665        assert!(beta < rc, "beta should be less than rc");
666        assert!(rc < release, "rc should be less than release");
667        assert!(alpha < release, "alpha should be less than release");
668    }
669
670    #[test]
671    fn test_prerelease_alpha_numbered_variants() {
672        // Numbered alpha variants: alpha1 < alpha2 < alpha10
673        let a1 = Version::parse("1.0.alpha1").unwrap();
674        let a2 = Version::parse("1.0.alpha2").unwrap();
675        let a10 = Version::parse("1.0.alpha10").unwrap();
676
677        assert!(a1 < a2, "alpha1 < alpha2");
678        assert!(a2 < a10, "alpha2 < alpha10 (numeric comparison)");
679    }
680
681    #[test]
682    fn test_prerelease_dev_pre_snapshot_ordering() {
683        // In rez, token comparison is lexicographic for alphabetic tokens.
684        // "dev" (d...) > "alpha" (a...) by dictionary order.
685        // The key property is that all these sort BELOW the base release (shorter token list).
686        let dev = Version::parse("1.0.dev").unwrap();
687        let alpha = Version::parse("1.0.alpha").unwrap();
688        let pre = Version::parse("1.0.pre").unwrap();
689        let snapshot = Version::parse("1.0.snapshot").unwrap();
690        let release = Version::parse("1.0").unwrap();
691
692        // All prerelease variants are less than the base release (shorter = greater in rez)
693        assert!(dev < release, "1.0.dev < 1.0");
694        assert!(alpha < release, "1.0.alpha < 1.0");
695        assert!(pre < release, "1.0.pre < 1.0");
696        assert!(snapshot < release, "1.0.snapshot < 1.0");
697
698        // Lexicographic order among prerelease labels
699        assert!(alpha < dev, "alpha < dev (a < d)");
700        assert!(dev < pre, "dev < pre (d < p)");
701        assert!(pre < snapshot, "pre < snapshot (p < s)");
702
703        // is_prerelease detection
704        assert!(dev.is_prerelease(), "dev is detected as prerelease");
705        assert!(pre.is_prerelease(), "pre is detected as prerelease");
706        assert!(
707            snapshot.is_prerelease(),
708            "snapshot is detected as prerelease"
709        );
710    }
711
712    #[test]
713    fn test_prerelease_mixed_with_numeric_tokens() {
714        // Versions like 2.0.0-alpha vs 2.0.0-beta
715        let v_alpha = Version::parse("2.0.0-alpha").unwrap();
716        let v_beta = Version::parse("2.0.0-beta").unwrap();
717        let v_stable = Version::parse("2.0.0").unwrap();
718
719        assert!(v_alpha < v_beta, "2.0.0-alpha < 2.0.0-beta");
720        assert!(v_beta < v_stable, "2.0.0-beta < 2.0.0");
721        assert!(v_alpha.is_prerelease());
722        assert!(v_beta.is_prerelease());
723        assert!(!v_stable.is_prerelease());
724    }
725
726    #[test]
727    fn test_prerelease_rc_vs_stable_same_prefix() {
728        // RC versions sort below their corresponding release
729        let rc1 = Version::parse("3.0.rc1").unwrap();
730        let stable = Version::parse("3.0").unwrap();
731        let rc2 = Version::parse("3.0.rc2").unwrap();
732
733        assert!(rc1 < stable, "rc1 < stable 3.0");
734        assert!(rc2 < stable, "rc2 < stable 3.0");
735        assert!(rc1 < rc2, "rc1 < rc2");
736    }
737
738    #[test]
739    fn test_prerelease_is_prerelease_detection() {
740        // Verify all known prerelease markers are detected
741        assert!(Version::parse("1.alpha").unwrap().is_prerelease());
742        assert!(Version::parse("1.beta").unwrap().is_prerelease());
743        assert!(Version::parse("1.rc").unwrap().is_prerelease());
744        assert!(Version::parse("1.dev").unwrap().is_prerelease());
745        assert!(Version::parse("1.pre").unwrap().is_prerelease());
746        assert!(Version::parse("1.snapshot").unwrap().is_prerelease());
747
748        // Non-prerelease versions
749        assert!(!Version::parse("1.0").unwrap().is_prerelease());
750        assert!(!Version::parse("1.0.0").unwrap().is_prerelease());
751        assert!(!Version::parse("2024.5").unwrap().is_prerelease());
752
753        // Edge cases: empty/inf are not prerelease
754        assert!(!Version::empty().is_prerelease());
755        assert!(!Version::inf().is_prerelease());
756    }
757
758    // ─── Edge Case Tests (Cycle 190) ─────────────────────────────
759
760    #[test]
761    fn test_version_very_large_numbers() {
762        // Test parsing version with large numbers
763        let v1 = Version::parse("999999999.1").unwrap();
764        assert_eq!(v1.as_str(), "999999999.1");
765
766        let v2 = Version::parse("1.999999999").unwrap();
767        assert_eq!(v2.as_str(), "1.999999999");
768
769        // Compare large numbers
770        let v_small = Version::parse("1.0").unwrap();
771        assert!(v_small < v1);
772    }
773
774    #[test]
775    fn test_version_borderline_token_count() {
776        // 10 tokens should pass (boundary) - use non-numeric to avoid numeric token limit
777        let v10 = Version::parse("a.b.c.d.e.f.g.h.i.j").unwrap();
778        assert_eq!(v10.tokens.len(), 10);
779
780        // 11 tokens should fail
781        let v11_result = Version::parse("a.b.c.d.e.f.g.h.i.j.k");
782        assert!(v11_result.is_err(), "Version with 11 tokens should fail");
783    }
784
785    #[test]
786    fn test_version_borderline_numeric_token_count() {
787        // 5 numeric tokens should pass (boundary)
788        let v5 = Version::parse("1.2.3.4.5").unwrap();
789        assert_eq!(v5.as_str(), "1.2.3.4.5");
790
791        // 6 numeric tokens should fail
792        let v6_result = Version::parse("1.2.3.4.5.6");
793        assert!(
794            v6_result.is_err(),
795            "Version with 6 numeric tokens should fail"
796        );
797    }
798
799    #[test]
800    fn test_version_underscore_in_tokens() {
801        // Test underscores in tokens (valid per regex [a-zA-Z0-9_]+)
802        let v = Version::parse("1_0.2_0").unwrap();
803        assert_eq!(v.as_str(), "1_0.2_0");
804        assert_eq!(v.tokens.len(), 2);
805    }
806
807    #[test]
808    fn test_version_single_token() {
809        // Single token version
810        let v = Version::parse("123").unwrap();
811        assert_eq!(v.as_str(), "123");
812        assert_eq!(v.tokens.len(), 1);
813    }
814
815    #[test]
816    fn test_version_hash_consistency() {
817        // Same version string should produce same hash
818        let v1 = Version::parse("1.2.3").unwrap();
819        let v2 = Version::parse("1.2.3").unwrap();
820
821        let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
822        let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
823        v1.hash(&mut hasher1);
824        v2.hash(&mut hasher2);
825
826        assert_eq!(hasher1.finish(), hasher2.finish());
827    }
828
829    #[test]
830    fn test_version_equality_different_instances() {
831        // Different instances with same version string should be equal
832        let v1 = Version::parse("1.0").unwrap();
833        let v2 = Version::parse("1.0").unwrap();
834        assert_eq!(v1, v2);
835    }
836
837    #[test]
838    fn test_version_ordering_transitivity() {
839        // If a < b and b < c, then a < c
840        let a = Version::parse("1.0").unwrap();
841        let b = Version::parse("2.0").unwrap();
842        let c = Version::parse("3.0").unwrap();
843
844        assert!(a < b);
845        assert!(b < c);
846        assert!(a < c);
847    }
848
849    #[test]
850    fn test_version_invalid_prefix() {
851        // Versions starting with 'v' or 'V' should fail
852        assert!(Version::parse("v1.0").is_err());
853        assert!(Version::parse("V1.0").is_err());
854    }
855
856    #[test]
857    fn test_version_invalid_syntax() {
858        // Versions with ".." should fail
859        assert!(Version::parse("1..2").is_err());
860
861        // Versions starting/ending with '.' should fail
862        assert!(Version::parse(".1.2").is_err());
863        assert!(Version::parse("1.2.").is_err());
864    }
865
866    #[test]
867    fn test_version_no_tokens() {
868        // String with no valid tokens should fail
869        assert!(Version::parse("...").is_err());
870        assert!(Version::parse("---").is_err());
871    }
872
873    #[test]
874    fn test_version_alphanumeric_mixed() {
875        // Mixed alphanumeric tokens
876        let v = Version::parse("1a.2b.3c").unwrap();
877        assert_eq!(v.as_str(), "1a.2b.3c");
878        assert_eq!(v.tokens.len(), 3);
879    }
880}