Skip to main content

changeset_version/
lib.rs

1use changeset_core::{BumpType, PrereleaseSpec, ZeroVersionBehavior};
2use semver::{Prerelease, Version};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum VersionError {
7    #[error("invalid prerelease identifier: {identifier}")]
8    InvalidPrerelease { identifier: String },
9    #[error("cannot graduate from prerelease version '{version}'; release stable 0.x first")]
10    CannotGraduateFromPrerelease { version: String },
11    #[error("can only graduate 0.x versions to 1.0.0; version is {version}")]
12    CanOnlyGraduateZeroVersions { version: String },
13}
14
15#[must_use]
16pub fn max_bump_type(bumps: &[BumpType]) -> Option<BumpType> {
17    bumps.iter().max().copied()
18}
19
20pub fn bump_version(version: &Version, bump_type: BumpType) -> Version {
21    let mut new_version = version.clone();
22
23    match bump_type {
24        BumpType::None => return new_version,
25        BumpType::Major => {
26            new_version.major += 1;
27            new_version.minor = 0;
28            new_version.patch = 0;
29        }
30        BumpType::Minor => {
31            new_version.minor += 1;
32            new_version.patch = 0;
33        }
34        BumpType::Patch => {
35            new_version.patch += 1;
36        }
37    }
38
39    new_version.pre = Prerelease::EMPTY;
40    new_version
41}
42
43fn parse_prerelease(pre: &Prerelease) -> Option<(String, u64)> {
44    let pre_str = pre.as_str();
45    if pre_str.is_empty() {
46        return None;
47    }
48
49    let parts: Vec<&str> = pre_str.split('.').collect();
50    if parts.len() < 2 {
51        return Some((pre_str.to_string(), 1));
52    }
53
54    let last = parts.last()?;
55    if let Ok(num) = last.parse::<u64>() {
56        let tag = parts[..parts.len() - 1].join(".");
57        Some((tag, num))
58    } else {
59        Some((pre_str.to_string(), 1))
60    }
61}
62
63/// Calculates a new version based on bump type and optional prerelease spec.
64///
65/// # Errors
66///
67/// Returns `VersionError::InvalidPrerelease` if the prerelease identifier
68/// produces an invalid semver prerelease string.
69pub fn calculate_new_version(
70    current: &Version,
71    bump_type: Option<BumpType>,
72    prerelease: Option<&PrereleaseSpec>,
73) -> Result<Version, VersionError> {
74    let mut new_version = current.clone();
75
76    match prerelease {
77        Some(spec) => {
78            let tag = spec.identifier();
79
80            if current.pre.is_empty() {
81                let bump = bump_type
82                    .filter(|b| !b.is_noop())
83                    .unwrap_or(BumpType::Patch);
84                new_version = bump_version(current, bump);
85                new_version.pre = make_prerelease(tag, 1)?;
86            } else if let Some((current_tag, current_num)) = parse_prerelease(&current.pre) {
87                if current_tag == tag {
88                    new_version.pre = make_prerelease(tag, current_num + 1)?;
89                } else {
90                    new_version.pre = make_prerelease(tag, 1)?;
91                }
92            } else {
93                new_version.pre = make_prerelease(tag, 1)?;
94            }
95        }
96        None => {
97            if !current.pre.is_empty() {
98                new_version.pre = Prerelease::EMPTY;
99            } else if let Some(bump) = bump_type {
100                new_version = bump_version(current, bump);
101            }
102        }
103    }
104
105    Ok(new_version)
106}
107
108fn make_prerelease(tag: &str, num: u64) -> Result<Prerelease, VersionError> {
109    let identifier = format!("{tag}.{num}");
110    Prerelease::new(&identifier).map_err(|_| VersionError::InvalidPrerelease { identifier })
111}
112
113#[must_use]
114pub fn is_prerelease(version: &Version) -> bool {
115    !version.pre.is_empty()
116}
117
118#[must_use]
119pub fn extract_prerelease_tag(version: &Version) -> Option<String> {
120    parse_prerelease(&version.pre).map(|(tag, _)| tag)
121}
122
123#[must_use]
124pub fn is_zero_version(version: &Version) -> bool {
125    version.major == 0
126}
127
128/// Calculates a new version with special handling for 0.x versions.
129///
130/// When `graduate` is true, the version will be promoted to 1.0.0 (with optional
131/// prerelease tag). Graduation has specific restrictions:
132/// - Cannot graduate from a prerelease version (must release stable 0.x first)
133/// - Cannot graduate a version that is already >= 1.0.0
134///
135/// For 0.x versions without graduation, behavior depends on `zero_behavior`:
136/// - `EffectiveMinor`: major bumps become minor, minor/patch both become patch
137/// - `AutoPromoteOnMajor`: major bumps promote to 1.0.0, minor/patch are standard
138///
139/// # Errors
140///
141/// Returns `VersionError::CannotGraduateFromPrerelease` if graduation is requested
142/// on a prerelease version.
143///
144/// Returns `VersionError::CanOnlyGraduateZeroVersions` if graduation is requested
145/// on a version >= 1.0.0.
146///
147/// Returns `VersionError::InvalidPrerelease` if the prerelease identifier
148/// produces an invalid semver prerelease string.
149pub fn calculate_new_version_with_zero_behavior(
150    current: &Version,
151    bump_type: Option<BumpType>,
152    prerelease: Option<&PrereleaseSpec>,
153    zero_behavior: ZeroVersionBehavior,
154    graduate: bool,
155) -> Result<Version, VersionError> {
156    if graduate {
157        return calculate_graduation(current, prerelease);
158    }
159
160    if current.major >= 1 {
161        return calculate_new_version(current, bump_type, prerelease);
162    }
163
164    let effective_bump = match zero_behavior {
165        ZeroVersionBehavior::AutoPromoteOnMajor => {
166            if bump_type == Some(BumpType::Major) {
167                return apply_prerelease_to_version(Version::new(1, 0, 0), prerelease);
168            }
169            bump_type
170        }
171        _ => bump_type.map(|bt| match bt {
172            BumpType::None => BumpType::None,
173            BumpType::Major => BumpType::Minor,
174            BumpType::Minor | BumpType::Patch => BumpType::Patch,
175        }),
176    };
177
178    calculate_new_version(current, effective_bump, prerelease)
179}
180
181fn calculate_graduation(
182    current: &Version,
183    prerelease: Option<&PrereleaseSpec>,
184) -> Result<Version, VersionError> {
185    if is_prerelease(current) {
186        return Err(VersionError::CannotGraduateFromPrerelease {
187            version: current.to_string(),
188        });
189    }
190
191    if current.major >= 1 {
192        return Err(VersionError::CanOnlyGraduateZeroVersions {
193            version: current.to_string(),
194        });
195    }
196
197    apply_prerelease_to_version(Version::new(1, 0, 0), prerelease)
198}
199
200fn apply_prerelease_to_version(
201    base: Version,
202    prerelease: Option<&PrereleaseSpec>,
203) -> Result<Version, VersionError> {
204    match prerelease {
205        Some(spec) => {
206            let mut version = base;
207            version.pre = make_prerelease(spec.identifier(), 1)?;
208            Ok(version)
209        }
210        None => Ok(base),
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn bump_patch() {
220        let version = Version::parse("1.2.3").unwrap();
221        let bumped = bump_version(&version, BumpType::Patch);
222        assert_eq!(bumped, Version::parse("1.2.4").unwrap());
223    }
224
225    #[test]
226    fn bump_minor() {
227        let version = Version::parse("1.2.3").unwrap();
228        let bumped = bump_version(&version, BumpType::Minor);
229        assert_eq!(bumped, Version::parse("1.3.0").unwrap());
230    }
231
232    #[test]
233    fn bump_major() {
234        let version = Version::parse("1.2.3").unwrap();
235        let bumped = bump_version(&version, BumpType::Major);
236        assert_eq!(bumped, Version::parse("2.0.0").unwrap());
237    }
238
239    #[test]
240    fn bump_version_strips_prerelease() {
241        let version = Version::parse("1.2.3-alpha.1").unwrap();
242        let bumped = bump_version(&version, BumpType::Patch);
243        assert_eq!(bumped, Version::parse("1.2.4").unwrap());
244    }
245
246    #[test]
247    fn bump_none_returns_version_unchanged() {
248        let version = Version::parse("1.2.3").unwrap();
249        let bumped = bump_version(&version, BumpType::None);
250        assert_eq!(bumped, Version::parse("1.2.3").unwrap());
251    }
252
253    #[test]
254    fn bump_none_preserves_prerelease() {
255        let version = Version::parse("1.2.3-alpha.1").unwrap();
256        let bumped = bump_version(&version, BumpType::None);
257        assert_eq!(bumped, Version::parse("1.2.3-alpha.1").unwrap());
258    }
259
260    mod max_bump_type_tests {
261        use super::*;
262
263        #[test]
264        fn returns_none_for_empty_slice() {
265            assert_eq!(max_bump_type(&[]), None);
266        }
267
268        #[test]
269        fn returns_single_element() {
270            assert_eq!(max_bump_type(&[BumpType::Patch]), Some(BumpType::Patch));
271            assert_eq!(max_bump_type(&[BumpType::Minor]), Some(BumpType::Minor));
272            assert_eq!(max_bump_type(&[BumpType::Major]), Some(BumpType::Major));
273        }
274
275        #[test]
276        fn returns_minor_for_patch_and_minor() {
277            assert_eq!(
278                max_bump_type(&[BumpType::Patch, BumpType::Minor]),
279                Some(BumpType::Minor)
280            );
281        }
282
283        #[test]
284        fn returns_major_for_minor_and_major() {
285            assert_eq!(
286                max_bump_type(&[BumpType::Minor, BumpType::Major]),
287                Some(BumpType::Major)
288            );
289        }
290
291        #[test]
292        fn returns_major_for_all_three() {
293            assert_eq!(
294                max_bump_type(&[BumpType::Patch, BumpType::Minor, BumpType::Major]),
295                Some(BumpType::Major)
296            );
297        }
298
299        #[test]
300        fn handles_duplicates() {
301            assert_eq!(
302                max_bump_type(&[BumpType::Patch, BumpType::Patch, BumpType::Minor]),
303                Some(BumpType::Minor)
304            );
305        }
306
307        #[test]
308        fn order_does_not_matter() {
309            assert_eq!(
310                max_bump_type(&[BumpType::Major, BumpType::Patch, BumpType::Minor]),
311                Some(BumpType::Major)
312            );
313        }
314
315        #[test]
316        fn none_with_patch_returns_patch() {
317            assert_eq!(
318                max_bump_type(&[BumpType::None, BumpType::Patch]),
319                Some(BumpType::Patch)
320            );
321        }
322
323        #[test]
324        fn all_none_returns_none() {
325            assert_eq!(
326                max_bump_type(&[BumpType::None, BumpType::None]),
327                Some(BumpType::None)
328            );
329        }
330
331        #[test]
332        fn single_none() {
333            assert_eq!(max_bump_type(&[BumpType::None]), Some(BumpType::None));
334        }
335    }
336
337    mod parse_prerelease_tests {
338        use super::*;
339
340        #[test]
341        fn empty_prerelease_returns_none() {
342            let pre = Prerelease::EMPTY;
343            assert!(parse_prerelease(&pre).is_none());
344        }
345
346        #[test]
347        fn parses_standard_format() {
348            let pre = Prerelease::new("alpha.1").unwrap();
349            let (tag, num) = parse_prerelease(&pre).unwrap();
350            assert_eq!(tag, "alpha");
351            assert_eq!(num, 1);
352        }
353
354        #[test]
355        fn parses_higher_numbers() {
356            let pre = Prerelease::new("rc.42").unwrap();
357            let (tag, num) = parse_prerelease(&pre).unwrap();
358            assert_eq!(tag, "rc");
359            assert_eq!(num, 42);
360        }
361
362        #[test]
363        fn handles_tag_without_number() {
364            let pre = Prerelease::new("beta").unwrap();
365            let (tag, num) = parse_prerelease(&pre).unwrap();
366            assert_eq!(tag, "beta");
367            assert_eq!(num, 1);
368        }
369
370        #[test]
371        fn handles_complex_tag_with_dots() {
372            let pre = Prerelease::new("pre.release.3").unwrap();
373            let (tag, num) = parse_prerelease(&pre).unwrap();
374            assert_eq!(tag, "pre.release");
375            assert_eq!(num, 3);
376        }
377    }
378
379    mod calculate_new_version_tests {
380        use super::*;
381
382        #[test]
383        fn stable_to_alpha_with_patch() {
384            let version = Version::parse("1.0.0").unwrap();
385            let result = calculate_new_version(
386                &version,
387                Some(BumpType::Patch),
388                Some(&PrereleaseSpec::Alpha),
389            )
390            .unwrap();
391            assert_eq!(result, Version::parse("1.0.1-alpha.1").unwrap());
392        }
393
394        #[test]
395        fn stable_to_alpha_with_minor() {
396            let version = Version::parse("1.0.0").unwrap();
397            let result = calculate_new_version(
398                &version,
399                Some(BumpType::Minor),
400                Some(&PrereleaseSpec::Alpha),
401            )
402            .unwrap();
403            assert_eq!(result, Version::parse("1.1.0-alpha.1").unwrap());
404        }
405
406        #[test]
407        fn stable_to_alpha_with_major() {
408            let version = Version::parse("1.0.0").unwrap();
409            let result = calculate_new_version(
410                &version,
411                Some(BumpType::Major),
412                Some(&PrereleaseSpec::Alpha),
413            )
414            .unwrap();
415            assert_eq!(result, Version::parse("2.0.0-alpha.1").unwrap());
416        }
417
418        #[test]
419        fn alpha_increment_same_tag() {
420            let version = Version::parse("1.0.1-alpha.1").unwrap();
421            let result =
422                calculate_new_version(&version, None, Some(&PrereleaseSpec::Alpha)).unwrap();
423            assert_eq!(result, Version::parse("1.0.1-alpha.2").unwrap());
424        }
425
426        #[test]
427        fn alpha_to_beta_transition() {
428            let version = Version::parse("1.0.1-alpha.3").unwrap();
429            let result =
430                calculate_new_version(&version, None, Some(&PrereleaseSpec::Beta)).unwrap();
431            assert_eq!(result, Version::parse("1.0.1-beta.1").unwrap());
432        }
433
434        #[test]
435        fn beta_to_rc_transition() {
436            let version = Version::parse("1.0.1-beta.2").unwrap();
437            let result = calculate_new_version(&version, None, Some(&PrereleaseSpec::Rc)).unwrap();
438            assert_eq!(result, Version::parse("1.0.1-rc.1").unwrap());
439        }
440
441        #[test]
442        fn rc_graduate_to_stable() {
443            let version = Version::parse("1.0.1-rc.1").unwrap();
444            let result = calculate_new_version(&version, None, None).unwrap();
445            assert_eq!(result, Version::parse("1.0.1").unwrap());
446        }
447
448        #[test]
449        fn alpha_graduate_to_stable() {
450            let version = Version::parse("1.0.1-alpha.5").unwrap();
451            let result = calculate_new_version(&version, None, None).unwrap();
452            assert_eq!(result, Version::parse("1.0.1").unwrap());
453        }
454
455        #[test]
456        fn custom_prerelease_tag() {
457            let version = Version::parse("1.0.0").unwrap();
458            let spec = PrereleaseSpec::Custom("dev".to_string());
459            let result =
460                calculate_new_version(&version, Some(BumpType::Patch), Some(&spec)).unwrap();
461            assert_eq!(result, Version::parse("1.0.1-dev.1").unwrap());
462        }
463
464        #[test]
465        fn stable_bump_without_prerelease() {
466            let version = Version::parse("1.0.0").unwrap();
467            let result = calculate_new_version(&version, Some(BumpType::Minor), None).unwrap();
468            assert_eq!(result, Version::parse("1.1.0").unwrap());
469        }
470
471        #[test]
472        fn stable_no_change_without_bump_or_prerelease() {
473            let version = Version::parse("1.0.0").unwrap();
474            let result = calculate_new_version(&version, None, None).unwrap();
475            assert_eq!(result, Version::parse("1.0.0").unwrap());
476        }
477
478        #[test]
479        fn prerelease_defaults_to_patch_when_no_bump_specified() {
480            let version = Version::parse("1.0.0").unwrap();
481            let result =
482                calculate_new_version(&version, None, Some(&PrereleaseSpec::Alpha)).unwrap();
483            assert_eq!(result, Version::parse("1.0.1-alpha.1").unwrap());
484        }
485
486        #[test]
487        fn none_bump_with_prerelease_defaults_to_patch() {
488            let version = Version::parse("1.0.0").unwrap();
489            let result =
490                calculate_new_version(&version, Some(BumpType::None), Some(&PrereleaseSpec::Alpha))
491                    .unwrap();
492            assert_eq!(result, Version::parse("1.0.1-alpha.1").unwrap());
493        }
494
495        #[test]
496        fn none_bump_with_prerelease_on_zero_version_defaults_to_patch() {
497            let version = Version::parse("0.5.0").unwrap();
498            let result =
499                calculate_new_version(&version, Some(BumpType::None), Some(&PrereleaseSpec::Beta))
500                    .unwrap();
501            assert_eq!(result, Version::parse("0.5.1-beta.1").unwrap());
502        }
503    }
504
505    mod is_prerelease_tests {
506        use super::*;
507
508        #[test]
509        fn stable_version_is_not_prerelease() {
510            let version = Version::parse("1.0.0").unwrap();
511            assert!(!is_prerelease(&version));
512        }
513
514        #[test]
515        fn alpha_version_is_prerelease() {
516            let version = Version::parse("1.0.0-alpha.1").unwrap();
517            assert!(is_prerelease(&version));
518        }
519
520        #[test]
521        fn rc_version_is_prerelease() {
522            let version = Version::parse("1.0.0-rc.1").unwrap();
523            assert!(is_prerelease(&version));
524        }
525    }
526
527    mod extract_prerelease_tag_tests {
528        use super::*;
529
530        #[test]
531        fn stable_version_returns_none() {
532            let version = Version::parse("1.0.0").unwrap();
533            assert!(extract_prerelease_tag(&version).is_none());
534        }
535
536        #[test]
537        fn extracts_alpha_tag() {
538            let version = Version::parse("1.0.0-alpha.1").unwrap();
539            assert_eq!(extract_prerelease_tag(&version), Some("alpha".to_string()));
540        }
541
542        #[test]
543        fn extracts_rc_tag() {
544            let version = Version::parse("1.0.0-rc.3").unwrap();
545            assert_eq!(extract_prerelease_tag(&version), Some("rc".to_string()));
546        }
547
548        #[test]
549        fn extracts_custom_tag() {
550            let version = Version::parse("1.0.0-nightly.5").unwrap();
551            assert_eq!(
552                extract_prerelease_tag(&version),
553                Some("nightly".to_string())
554            );
555        }
556    }
557
558    mod is_zero_version_tests {
559        use super::*;
560
561        #[test]
562        fn zero_major_is_zero_version() {
563            let version = Version::parse("0.1.0").unwrap();
564            assert!(is_zero_version(&version));
565        }
566
567        #[test]
568        fn zero_minor_patch_is_zero_version() {
569            let version = Version::parse("0.0.1").unwrap();
570            assert!(is_zero_version(&version));
571        }
572
573        #[test]
574        fn one_major_is_not_zero_version() {
575            let version = Version::parse("1.0.0").unwrap();
576            assert!(!is_zero_version(&version));
577        }
578
579        #[test]
580        fn two_major_is_not_zero_version() {
581            let version = Version::parse("2.3.4").unwrap();
582            assert!(!is_zero_version(&version));
583        }
584
585        #[test]
586        fn zero_prerelease_is_zero_version() {
587            let version = Version::parse("0.1.0-alpha.1").unwrap();
588            assert!(is_zero_version(&version));
589        }
590    }
591
592    mod calculate_new_version_with_zero_behavior_tests {
593        use super::*;
594
595        mod effective_minor_behavior {
596            use super::*;
597
598            #[test]
599            fn major_becomes_minor() {
600                let version = Version::parse("0.1.2").unwrap();
601                let result = calculate_new_version_with_zero_behavior(
602                    &version,
603                    Some(BumpType::Major),
604                    None,
605                    ZeroVersionBehavior::EffectiveMinor,
606                    false,
607                )
608                .unwrap();
609                assert_eq!(result, Version::parse("0.2.0").unwrap());
610            }
611
612            #[test]
613            fn minor_becomes_patch() {
614                let version = Version::parse("0.1.2").unwrap();
615                let result = calculate_new_version_with_zero_behavior(
616                    &version,
617                    Some(BumpType::Minor),
618                    None,
619                    ZeroVersionBehavior::EffectiveMinor,
620                    false,
621                )
622                .unwrap();
623                assert_eq!(result, Version::parse("0.1.3").unwrap());
624            }
625
626            #[test]
627            fn patch_stays_patch() {
628                let version = Version::parse("0.1.2").unwrap();
629                let result = calculate_new_version_with_zero_behavior(
630                    &version,
631                    Some(BumpType::Patch),
632                    None,
633                    ZeroVersionBehavior::EffectiveMinor,
634                    false,
635                )
636                .unwrap();
637                assert_eq!(result, Version::parse("0.1.3").unwrap());
638            }
639
640            #[test]
641            fn major_with_prerelease() {
642                let version = Version::parse("0.1.2").unwrap();
643                let result = calculate_new_version_with_zero_behavior(
644                    &version,
645                    Some(BumpType::Major),
646                    Some(&PrereleaseSpec::Alpha),
647                    ZeroVersionBehavior::EffectiveMinor,
648                    false,
649                )
650                .unwrap();
651                assert_eq!(result, Version::parse("0.2.0-alpha.1").unwrap());
652            }
653
654            #[test]
655            fn double_zero_version() {
656                let version = Version::parse("0.0.5").unwrap();
657                let result = calculate_new_version_with_zero_behavior(
658                    &version,
659                    Some(BumpType::Major),
660                    None,
661                    ZeroVersionBehavior::EffectiveMinor,
662                    false,
663                )
664                .unwrap();
665                assert_eq!(result, Version::parse("0.1.0").unwrap());
666            }
667
668            #[test]
669            fn none_stays_none() {
670                let version = Version::parse("0.1.2").unwrap();
671                let result = calculate_new_version_with_zero_behavior(
672                    &version,
673                    Some(BumpType::None),
674                    None,
675                    ZeroVersionBehavior::EffectiveMinor,
676                    false,
677                )
678                .unwrap();
679                assert_eq!(result, Version::parse("0.1.2").unwrap());
680            }
681        }
682
683        mod auto_promote_behavior {
684            use super::*;
685
686            #[test]
687            fn major_becomes_1_0_0() {
688                let version = Version::parse("0.1.2").unwrap();
689                let result = calculate_new_version_with_zero_behavior(
690                    &version,
691                    Some(BumpType::Major),
692                    None,
693                    ZeroVersionBehavior::AutoPromoteOnMajor,
694                    false,
695                )
696                .unwrap();
697                assert_eq!(result, Version::parse("1.0.0").unwrap());
698            }
699
700            #[test]
701            fn minor_stays_minor() {
702                let version = Version::parse("0.1.2").unwrap();
703                let result = calculate_new_version_with_zero_behavior(
704                    &version,
705                    Some(BumpType::Minor),
706                    None,
707                    ZeroVersionBehavior::AutoPromoteOnMajor,
708                    false,
709                )
710                .unwrap();
711                assert_eq!(result, Version::parse("0.2.0").unwrap());
712            }
713
714            #[test]
715            fn patch_stays_patch() {
716                let version = Version::parse("0.1.2").unwrap();
717                let result = calculate_new_version_with_zero_behavior(
718                    &version,
719                    Some(BumpType::Patch),
720                    None,
721                    ZeroVersionBehavior::AutoPromoteOnMajor,
722                    false,
723                )
724                .unwrap();
725                assert_eq!(result, Version::parse("0.1.3").unwrap());
726            }
727
728            #[test]
729            fn major_with_prerelease() {
730                let version = Version::parse("0.1.2").unwrap();
731                let result = calculate_new_version_with_zero_behavior(
732                    &version,
733                    Some(BumpType::Major),
734                    Some(&PrereleaseSpec::Alpha),
735                    ZeroVersionBehavior::AutoPromoteOnMajor,
736                    false,
737                )
738                .unwrap();
739                assert_eq!(result, Version::parse("1.0.0-alpha.1").unwrap());
740            }
741        }
742
743        mod none_bump_behavior {
744            use super::*;
745
746            #[test]
747            fn none_leaves_version_unchanged() {
748                let version = Version::parse("0.1.2").unwrap();
749                let result = calculate_new_version_with_zero_behavior(
750                    &version,
751                    Some(BumpType::None),
752                    None,
753                    ZeroVersionBehavior::AutoPromoteOnMajor,
754                    false,
755                )
756                .unwrap();
757                assert_eq!(result, Version::parse("0.1.2").unwrap());
758            }
759        }
760
761        mod stable_versions_unaffected {
762            use super::*;
763
764            #[test]
765            fn effective_minor_major_bump() {
766                let version = Version::parse("1.2.3").unwrap();
767                let result = calculate_new_version_with_zero_behavior(
768                    &version,
769                    Some(BumpType::Major),
770                    None,
771                    ZeroVersionBehavior::EffectiveMinor,
772                    false,
773                )
774                .unwrap();
775                assert_eq!(result, Version::parse("2.0.0").unwrap());
776            }
777
778            #[test]
779            fn auto_promote_major_bump() {
780                let version = Version::parse("1.2.3").unwrap();
781                let result = calculate_new_version_with_zero_behavior(
782                    &version,
783                    Some(BumpType::Major),
784                    None,
785                    ZeroVersionBehavior::AutoPromoteOnMajor,
786                    false,
787                )
788                .unwrap();
789                assert_eq!(result, Version::parse("2.0.0").unwrap());
790            }
791        }
792
793        mod graduation {
794            use super::*;
795
796            #[test]
797            fn promotes_zero_to_1_0_0() {
798                let version = Version::parse("0.5.3").unwrap();
799                let result = calculate_new_version_with_zero_behavior(
800                    &version,
801                    None,
802                    None,
803                    ZeroVersionBehavior::EffectiveMinor,
804                    true,
805                )
806                .unwrap();
807                assert_eq!(result, Version::parse("1.0.0").unwrap());
808            }
809
810            #[test]
811            fn with_prerelease() {
812                let version = Version::parse("0.5.3").unwrap();
813                let result = calculate_new_version_with_zero_behavior(
814                    &version,
815                    None,
816                    Some(&PrereleaseSpec::Alpha),
817                    ZeroVersionBehavior::EffectiveMinor,
818                    true,
819                )
820                .unwrap();
821                assert_eq!(result, Version::parse("1.0.0-alpha.1").unwrap());
822            }
823
824            #[test]
825            fn errors_on_prerelease_version() {
826                let version = Version::parse("0.5.3-alpha.1").unwrap();
827                let result = calculate_new_version_with_zero_behavior(
828                    &version,
829                    None,
830                    None,
831                    ZeroVersionBehavior::EffectiveMinor,
832                    true,
833                );
834                assert!(matches!(
835                    result,
836                    Err(VersionError::CannotGraduateFromPrerelease { .. })
837                ));
838            }
839
840            #[test]
841            fn errors_on_stable_version() {
842                let version = Version::parse("1.2.3").unwrap();
843                let result = calculate_new_version_with_zero_behavior(
844                    &version,
845                    None,
846                    None,
847                    ZeroVersionBehavior::EffectiveMinor,
848                    true,
849                );
850                assert!(matches!(
851                    result,
852                    Err(VersionError::CanOnlyGraduateZeroVersions { .. })
853                ));
854            }
855
856            #[test]
857            fn bump_type_ignored_when_graduating() {
858                let version = Version::parse("0.5.3").unwrap();
859                let result = calculate_new_version_with_zero_behavior(
860                    &version,
861                    Some(BumpType::Patch),
862                    None,
863                    ZeroVersionBehavior::EffectiveMinor,
864                    true,
865                )
866                .unwrap();
867                assert_eq!(result, Version::parse("1.0.0").unwrap());
868            }
869
870            #[test]
871            fn behavior_ignored_when_graduating() {
872                let version = Version::parse("0.5.3").unwrap();
873                let result = calculate_new_version_with_zero_behavior(
874                    &version,
875                    None,
876                    None,
877                    ZeroVersionBehavior::AutoPromoteOnMajor,
878                    true,
879                )
880                .unwrap();
881                assert_eq!(result, Version::parse("1.0.0").unwrap());
882            }
883        }
884    }
885}