1use std::collections::{HashMap, HashSet};
2
3use changeset_core::{BumpType, Changeset, PackageInfo, PrereleaseSpec, ZeroVersionBehavior};
4use changeset_version::{
5 VersionError, calculate_new_version, calculate_new_version_with_zero_behavior, is_zero_version,
6 max_bump_type,
7};
8use gset::Getset;
9use indexmap::IndexMap;
10
11use crate::types::{PackageReleaseConfig, PackageVersion};
12
13#[derive(Debug, Clone, Getset)]
14pub struct ReleasePlan {
15 #[getset(get, vis = "pub")]
16 releases: Vec<PackageVersion>,
17 #[getset(get, vis = "pub")]
18 unknown_packages: Vec<String>,
19}
20
21impl ReleasePlan {
22 pub(crate) fn new(releases: Vec<PackageVersion>, unknown_packages: Vec<String>) -> Self {
23 Self {
24 releases,
25 unknown_packages,
26 }
27 }
28}
29
30pub struct VersionPlanner;
31
32impl VersionPlanner {
33 pub fn plan_releases(
37 changesets: &[Changeset],
38 packages: &[PackageInfo],
39 ) -> Result<ReleasePlan, VersionError> {
40 Self::plan_releases_with_prerelease(changesets, packages, None)
41 }
42
43 pub fn plan_releases_with_prerelease(
47 changesets: &[Changeset],
48 packages: &[PackageInfo],
49 prerelease: Option<&PrereleaseSpec>,
50 ) -> Result<ReleasePlan, VersionError> {
51 let package_lookup: IndexMap<_, _> =
52 packages.iter().map(|p| (p.name().clone(), p)).collect();
53 let bumps_by_package = Self::aggregate_bumps(changesets);
54 let graduates = Self::collect_graduates(changesets);
55
56 let mut releases = Vec::new();
57 let mut unknown_packages = Vec::new();
58
59 for (name, bumps) in &bumps_by_package {
60 let bump_type = max_bump_type(bumps);
61 let should_graduate = graduates.contains(name);
62
63 if Self::should_skip_package(bump_type, prerelease, should_graduate) {
64 continue;
65 }
66
67 if let Some(pkg) = package_lookup.get(name) {
68 let (new_version, effective_bump) = Self::compute_version_and_bump(
69 pkg.version(),
70 bump_type,
71 prerelease,
72 should_graduate,
73 )?;
74 releases.push(PackageVersion::new(
75 name.clone(),
76 pkg.version().clone(),
77 new_version,
78 effective_bump,
79 false,
80 ));
81 } else {
82 unknown_packages.push(name.clone());
83 }
84 }
85
86 Ok(ReleasePlan::new(releases, unknown_packages))
87 }
88
89 pub fn plan_graduation(packages: &[PackageInfo]) -> Result<ReleasePlan, VersionError> {
93 let mut releases = Vec::new();
94
95 for pkg in packages {
96 if changeset_version::is_prerelease(pkg.version()) {
97 let new_version = calculate_new_version(pkg.version(), None, None)?;
98 releases.push(PackageVersion::new(
99 pkg.name().clone(),
100 pkg.version().clone(),
101 new_version,
102 BumpType::Patch,
103 false,
104 ));
105 }
106 }
107
108 Ok(ReleasePlan::new(releases, Vec::new()))
109 }
110
111 pub fn plan_releases_with_behavior(
115 changesets: &[Changeset],
116 packages: &[PackageInfo],
117 prerelease: Option<&PrereleaseSpec>,
118 zero_behavior: ZeroVersionBehavior,
119 ) -> Result<ReleasePlan, VersionError> {
120 let package_lookup: IndexMap<_, _> =
121 packages.iter().map(|p| (p.name().clone(), p)).collect();
122 let bumps_by_package = Self::aggregate_bumps(changesets);
123 let graduates = Self::collect_graduates(changesets);
124
125 let mut releases = Vec::new();
126 let mut unknown_packages = Vec::new();
127
128 for (name, bumps) in &bumps_by_package {
129 let bump_type = max_bump_type(bumps);
130 let should_graduate = graduates.contains(name);
131
132 if Self::should_skip_package(bump_type, prerelease, should_graduate) {
133 continue;
134 }
135
136 if let Some(pkg) = package_lookup.get(name) {
137 let (new_version, effective_bump) =
138 Self::compute_version_and_bump_with_zero_behavior(
139 pkg.version(),
140 bump_type,
141 prerelease,
142 zero_behavior,
143 should_graduate,
144 )?;
145 releases.push(PackageVersion::new(
146 name.clone(),
147 pkg.version().clone(),
148 new_version,
149 effective_bump,
150 false,
151 ));
152 } else {
153 unknown_packages.push(name.clone());
154 }
155 }
156
157 Ok(ReleasePlan::new(releases, unknown_packages))
158 }
159
160 pub fn plan_zero_graduation(
164 packages: &[PackageInfo],
165 prerelease: Option<&PrereleaseSpec>,
166 ) -> Result<ReleasePlan, VersionError> {
167 let mut releases = Vec::new();
168
169 for pkg in packages {
170 if is_zero_version(pkg.version()) {
171 let new_version = calculate_new_version_with_zero_behavior(
172 pkg.version(),
173 None,
174 prerelease,
175 ZeroVersionBehavior::EffectiveMinor,
176 true,
177 )?;
178 releases.push(PackageVersion::new(
179 pkg.name().clone(),
180 pkg.version().clone(),
181 new_version,
182 BumpType::Major,
183 false,
184 ));
185 }
186 }
187
188 Ok(ReleasePlan::new(releases, Vec::new()))
189 }
190
191 pub fn plan_releases_per_package(
195 changesets: &[Changeset],
196 packages: &[PackageInfo],
197 per_package_config: &HashMap<String, PackageReleaseConfig>,
198 zero_behavior: ZeroVersionBehavior,
199 ) -> Result<ReleasePlan, VersionError> {
200 let package_lookup: IndexMap<_, _> =
201 packages.iter().map(|p| (p.name().clone(), p)).collect();
202 let bumps_by_package = Self::aggregate_bumps(changesets);
203 let changeset_graduates = Self::collect_graduates(changesets);
204
205 let mut releases = Vec::new();
206 let mut unknown_packages = Vec::new();
207
208 for (name, bumps) in &bumps_by_package {
209 let bump_type = max_bump_type(bumps);
210 let config = per_package_config.get(name);
211
212 let prerelease = config.and_then(|c| c.prerelease());
213 let should_graduate = config.is_some_and(PackageReleaseConfig::graduate_zero)
214 || changeset_graduates.contains(name);
215
216 if Self::should_skip_package(bump_type, prerelease, should_graduate) {
217 continue;
218 }
219
220 if let Some(pkg) = package_lookup.get(name) {
221 let (new_version, effective_bump) =
222 Self::compute_version_and_bump_with_zero_behavior(
223 pkg.version(),
224 bump_type,
225 prerelease,
226 zero_behavior,
227 should_graduate,
228 )?;
229 releases.push(PackageVersion::new(
230 name.clone(),
231 pkg.version().clone(),
232 new_version,
233 effective_bump,
234 false,
235 ));
236 } else {
237 unknown_packages.push(name.clone());
238 }
239 }
240
241 for (name, config) in per_package_config {
242 if bumps_by_package.contains_key(name) {
243 continue;
244 }
245
246 if config.prerelease().is_none() && !config.graduate_zero() {
247 continue;
248 }
249
250 if let Some(pkg) = package_lookup.get(name) {
251 let (new_version, effective_bump) =
252 Self::compute_version_and_bump_with_zero_behavior(
253 pkg.version(),
254 None,
255 config.prerelease(),
256 zero_behavior,
257 config.graduate_zero(),
258 )?;
259 releases.push(PackageVersion::new(
260 name.clone(),
261 pkg.version().clone(),
262 new_version,
263 effective_bump,
264 false,
265 ));
266 }
267 }
268
269 Ok(ReleasePlan::new(releases, unknown_packages))
270 }
271
272 fn effective_bump_type(aggregated_bump: Option<BumpType>, should_graduate: bool) -> BumpType {
273 if should_graduate {
274 BumpType::Major
275 } else {
276 aggregated_bump.unwrap_or(BumpType::Patch)
277 }
278 }
279
280 fn compute_version_and_bump(
281 current: &semver::Version,
282 bump_type: Option<BumpType>,
283 prerelease: Option<&PrereleaseSpec>,
284 should_graduate: bool,
285 ) -> Result<(semver::Version, BumpType), VersionError> {
286 let new_version = if should_graduate {
287 calculate_new_version_with_zero_behavior(
288 current,
289 bump_type,
290 prerelease,
291 ZeroVersionBehavior::default(),
292 true,
293 )?
294 } else {
295 calculate_new_version(current, bump_type, prerelease)?
296 };
297 Ok((
298 new_version,
299 Self::effective_bump_type(bump_type, should_graduate),
300 ))
301 }
302
303 fn compute_version_and_bump_with_zero_behavior(
304 current: &semver::Version,
305 bump_type: Option<BumpType>,
306 prerelease: Option<&PrereleaseSpec>,
307 zero_behavior: ZeroVersionBehavior,
308 should_graduate: bool,
309 ) -> Result<(semver::Version, BumpType), VersionError> {
310 let new_version = calculate_new_version_with_zero_behavior(
311 current,
312 bump_type,
313 prerelease,
314 zero_behavior,
315 should_graduate,
316 )?;
317 Ok((
318 new_version,
319 Self::effective_bump_type(bump_type, should_graduate),
320 ))
321 }
322
323 fn should_skip_package(
324 bump_type: Option<BumpType>,
325 prerelease: Option<&PrereleaseSpec>,
326 should_graduate: bool,
327 ) -> bool {
328 let is_noop_or_absent = bump_type.is_none() || bump_type.is_some_and(|b| b.is_noop());
329 is_noop_or_absent && prerelease.is_none() && !should_graduate
330 }
331
332 fn collect_graduates(changesets: &[Changeset]) -> HashSet<String> {
333 changesets
334 .iter()
335 .filter(|c| c.graduate())
336 .flat_map(|c| c.releases().iter().map(|r| r.name().clone()))
337 .collect()
338 }
339
340 #[must_use]
341 pub fn aggregate_bumps(changesets: &[Changeset]) -> IndexMap<String, Vec<BumpType>> {
342 let mut bumps_by_package: IndexMap<String, Vec<BumpType>> = IndexMap::new();
343
344 for changeset in changesets {
345 for release in changeset.releases() {
346 bumps_by_package
347 .entry(release.name().clone())
348 .or_default()
349 .push(release.bump_type());
350 }
351 }
352
353 bumps_by_package
354 }
355
356 #[must_use]
357 pub fn partition_packages(
358 changesets: &[Changeset],
359 packages: &[PackageInfo],
360 ) -> (HashSet<String>, Vec<PackageInfo>) {
361 let packages_with_changesets: HashSet<String> = changesets
362 .iter()
363 .flat_map(|c| c.releases().iter().map(|r| r.name().clone()))
364 .collect();
365
366 let unchanged_packages: Vec<PackageInfo> = packages
367 .iter()
368 .filter(|p| !packages_with_changesets.contains(p.name()))
369 .cloned()
370 .collect();
371
372 (packages_with_changesets, unchanged_packages)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use changeset_core::{ChangeCategory, PackageRelease};
380 use semver::Version;
381 use std::path::PathBuf;
382
383 use crate::types::PackageReleaseConfigBuilder;
384
385 fn make_package(name: &str, version: &str) -> PackageInfo {
386 PackageInfo::new(
387 name.to_string(),
388 version.parse().expect("valid version"),
389 PathBuf::from(format!("/mock/crates/{name}")),
390 )
391 }
392
393 fn make_changeset(package_name: &str, bump: BumpType, summary: &str) -> Changeset {
394 Changeset::new(
395 summary.to_string(),
396 vec![PackageRelease::new(package_name.to_string(), bump)],
397 ChangeCategory::Changed,
398 )
399 }
400
401 fn make_multi_changeset(releases: Vec<(&str, BumpType)>, summary: &str) -> Changeset {
402 Changeset::new(
403 summary.to_string(),
404 releases
405 .into_iter()
406 .map(|(name, bump)| PackageRelease::new(name.to_string(), bump))
407 .collect(),
408 ChangeCategory::Changed,
409 )
410 }
411
412 #[test]
413 fn plan_releases_empty_changesets_returns_empty_plan() {
414 let packages = vec![make_package("my-crate", "1.0.0")];
415
416 let plan = VersionPlanner::plan_releases(&[], &packages).expect("plan_releases");
417
418 assert!(plan.releases().is_empty());
419 assert!(plan.unknown_packages().is_empty());
420 }
421
422 #[test]
423 fn plan_releases_single_package_single_bump() {
424 let packages = vec![make_package("my-crate", "1.0.0")];
425 let changesets = vec![make_changeset("my-crate", BumpType::Patch, "Fix bug")];
426
427 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
428
429 assert_eq!(plan.releases().len(), 1);
430 assert!(plan.unknown_packages().is_empty());
431
432 let release = &plan.releases()[0];
433 assert_eq!(release.name(), "my-crate");
434 assert_eq!(*release.current_version(), Version::new(1, 0, 0));
435 assert_eq!(*release.new_version(), Version::new(1, 0, 1));
436 assert_eq!(release.bump_type(), BumpType::Patch);
437 }
438
439 #[test]
440 fn plan_releases_single_package_takes_max_bump() {
441 let packages = vec![make_package("my-crate", "1.0.0")];
442 let changesets = vec![
443 make_changeset("my-crate", BumpType::Patch, "Fix bug"),
444 make_changeset("my-crate", BumpType::Minor, "Add feature"),
445 make_changeset("my-crate", BumpType::Patch, "Another fix"),
446 ];
447
448 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
449
450 assert_eq!(plan.releases().len(), 1);
451 let release = &plan.releases()[0];
452 assert_eq!(*release.new_version(), Version::new(1, 1, 0));
453 assert_eq!(release.bump_type(), BumpType::Minor);
454 }
455
456 #[test]
457 fn plan_releases_multiple_packages_independent_bumps() {
458 let packages = vec![
459 make_package("crate-a", "1.0.0"),
460 make_package("crate-b", "2.5.3"),
461 ];
462 let changesets = vec![
463 make_changeset("crate-a", BumpType::Minor, "Add feature to A"),
464 make_changeset("crate-b", BumpType::Major, "Breaking change in B"),
465 ];
466
467 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
468
469 assert_eq!(plan.releases().len(), 2);
470 assert!(plan.unknown_packages().is_empty());
471
472 let release_a = plan
473 .releases()
474 .iter()
475 .find(|r| r.name() == "crate-a")
476 .expect("crate-a should be in releases");
477 assert_eq!(*release_a.new_version(), Version::new(1, 1, 0));
478
479 let release_b = plan
480 .releases()
481 .iter()
482 .find(|r| r.name() == "crate-b")
483 .expect("crate-b should be in releases");
484 assert_eq!(*release_b.new_version(), Version::new(3, 0, 0));
485 }
486
487 #[test]
488 fn plan_releases_unknown_package_collected_not_errored() {
489 let packages = vec![make_package("known-crate", "1.0.0")];
490 let changesets = vec![make_changeset("unknown-crate", BumpType::Patch, "Fix")];
491
492 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
493
494 assert!(plan.releases().is_empty());
495 assert_eq!(plan.unknown_packages(), &["unknown-crate"]);
496 }
497
498 #[test]
499 fn plan_releases_mixed_known_and_unknown_packages() {
500 let packages = vec![make_package("known-crate", "1.0.0")];
501 let changesets = vec![make_multi_changeset(
502 vec![
503 ("known-crate", BumpType::Minor),
504 ("unknown-crate", BumpType::Patch),
505 ],
506 "Mixed changes",
507 )];
508
509 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
510
511 assert_eq!(plan.releases().len(), 1);
512 assert_eq!(plan.releases()[0].name(), "known-crate");
513 assert_eq!(plan.unknown_packages(), &["unknown-crate"]);
514 }
515
516 #[test]
517 fn aggregate_bumps_collects_all_bump_types() {
518 let changesets = vec![
519 make_changeset("crate-a", BumpType::Patch, "Fix"),
520 make_changeset("crate-a", BumpType::Minor, "Feature"),
521 make_changeset("crate-b", BumpType::Major, "Breaking"),
522 ];
523
524 let bumps = VersionPlanner::aggregate_bumps(&changesets);
525
526 assert_eq!(bumps["crate-a"], vec![BumpType::Patch, BumpType::Minor]);
527 assert_eq!(bumps["crate-b"], vec![BumpType::Major]);
528 }
529
530 #[test]
531 fn partition_packages_identifies_changed_and_unchanged() {
532 let packages = vec![
533 make_package("changed", "1.0.0"),
534 make_package("unchanged", "2.0.0"),
535 ];
536 let changesets = vec![make_changeset("changed", BumpType::Patch, "Fix")];
537
538 let (with_changesets, without) = VersionPlanner::partition_packages(&changesets, &packages);
539
540 assert!(with_changesets.contains("changed"));
541 assert!(!with_changesets.contains("unchanged"));
542 assert_eq!(without.len(), 1);
543 assert_eq!(without[0].name(), "unchanged");
544 }
545
546 #[test]
547 fn plan_releases_handles_prerelease_versions() {
548 let packages = vec![make_package("my-crate", "1.0.0-alpha.1")];
549 let changesets = vec![make_changeset("my-crate", BumpType::Patch, "Fix")];
550
551 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
552
553 assert_eq!(plan.releases().len(), 1);
554 let release = &plan.releases()[0];
555 assert_eq!(
556 *release.current_version(),
557 "1.0.0-alpha.1".parse::<Version>().expect("valid")
558 );
559 assert!(release.new_version() > release.current_version());
560 }
561
562 #[test]
563 fn plan_releases_handles_build_metadata() {
564 let packages = vec![make_package("my-crate", "1.0.0+build.123")];
565 let changesets = vec![make_changeset("my-crate", BumpType::Minor, "Feature")];
566
567 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
568
569 assert_eq!(plan.releases().len(), 1);
570 let release = &plan.releases()[0];
571 assert_eq!(
572 *release.current_version(),
573 "1.0.0+build.123".parse::<Version>().expect("valid")
574 );
575 }
576
577 #[test]
578 fn plan_releases_with_zero_major_version() {
579 let packages = vec![make_package("my-crate", "0.1.0")];
580 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
581
582 let plan = VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
583
584 assert_eq!(plan.releases().len(), 1);
585 let release = &plan.releases()[0];
586 assert_eq!(*release.current_version(), Version::new(0, 1, 0));
587 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
588 }
589
590 #[test]
591 fn plan_releases_with_prerelease_creates_alpha_version() {
592 let packages = vec![make_package("my-crate", "1.0.0")];
593 let changesets = vec![make_changeset("my-crate", BumpType::Patch, "Fix")];
594
595 let plan = VersionPlanner::plan_releases_with_prerelease(
596 &changesets,
597 &packages,
598 Some(&PrereleaseSpec::Alpha),
599 )
600 .expect("plan_releases_with_prerelease");
601
602 assert_eq!(plan.releases().len(), 1);
603 let release = &plan.releases()[0];
604 assert_eq!(
605 *release.new_version(),
606 "1.0.1-alpha.1".parse::<Version>().expect("valid")
607 );
608 }
609
610 #[test]
611 fn plan_releases_with_prerelease_increments_existing() {
612 let packages = vec![make_package("my-crate", "1.0.1-alpha.2")];
613 let changesets = vec![make_changeset("my-crate", BumpType::Patch, "Fix")];
614
615 let plan = VersionPlanner::plan_releases_with_prerelease(
616 &changesets,
617 &packages,
618 Some(&PrereleaseSpec::Alpha),
619 )
620 .expect("plan_releases_with_prerelease");
621
622 assert_eq!(plan.releases().len(), 1);
623 let release = &plan.releases()[0];
624 assert_eq!(
625 *release.new_version(),
626 "1.0.1-alpha.3".parse::<Version>().expect("valid")
627 );
628 }
629
630 #[test]
631 fn plan_graduation_creates_stable_from_prerelease() {
632 let packages = vec![
633 make_package("crate-a", "1.0.1-rc.1"),
634 make_package("crate-b", "2.0.0"),
635 ];
636
637 let plan = VersionPlanner::plan_graduation(&packages).expect("plan_graduation");
638
639 assert_eq!(plan.releases().len(), 1);
640 let release = &plan.releases()[0];
641 assert_eq!(release.name(), "crate-a");
642 assert_eq!(*release.new_version(), Version::new(1, 0, 1));
643 }
644
645 #[test]
646 fn plan_graduation_empty_for_all_stable() {
647 let packages = vec![
648 make_package("crate-a", "1.0.0"),
649 make_package("crate-b", "2.0.0"),
650 ];
651
652 let plan = VersionPlanner::plan_graduation(&packages).expect("plan_graduation");
653
654 assert!(plan.releases().is_empty());
655 }
656
657 mod zero_version_behavior_tests {
658 use super::*;
659
660 #[test]
661 fn effective_minor_converts_major_to_minor() {
662 let packages = vec![make_package("my-crate", "0.1.2")];
663 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
664
665 let plan = VersionPlanner::plan_releases_with_behavior(
666 &changesets,
667 &packages,
668 None,
669 ZeroVersionBehavior::EffectiveMinor,
670 )
671 .expect("plan_releases_with_behavior");
672
673 assert_eq!(plan.releases().len(), 1);
674 let release = &plan.releases()[0];
675 assert_eq!(*release.new_version(), Version::new(0, 2, 0));
676 }
677
678 #[test]
679 fn effective_minor_converts_minor_to_patch() {
680 let packages = vec![make_package("my-crate", "0.1.2")];
681 let changesets = vec![make_changeset("my-crate", BumpType::Minor, "Feature")];
682
683 let plan = VersionPlanner::plan_releases_with_behavior(
684 &changesets,
685 &packages,
686 None,
687 ZeroVersionBehavior::EffectiveMinor,
688 )
689 .expect("plan_releases_with_behavior");
690
691 assert_eq!(plan.releases().len(), 1);
692 let release = &plan.releases()[0];
693 assert_eq!(*release.new_version(), Version::new(0, 1, 3));
694 }
695
696 #[test]
697 fn auto_promote_major_becomes_1_0_0() {
698 let packages = vec![make_package("my-crate", "0.1.2")];
699 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
700
701 let plan = VersionPlanner::plan_releases_with_behavior(
702 &changesets,
703 &packages,
704 None,
705 ZeroVersionBehavior::AutoPromoteOnMajor,
706 )
707 .expect("plan_releases_with_behavior");
708
709 assert_eq!(plan.releases().len(), 1);
710 let release = &plan.releases()[0];
711 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
712 }
713
714 #[test]
715 fn auto_promote_minor_stays_minor() {
716 let packages = vec![make_package("my-crate", "0.1.2")];
717 let changesets = vec![make_changeset("my-crate", BumpType::Minor, "Feature")];
718
719 let plan = VersionPlanner::plan_releases_with_behavior(
720 &changesets,
721 &packages,
722 None,
723 ZeroVersionBehavior::AutoPromoteOnMajor,
724 )
725 .expect("plan_releases_with_behavior");
726
727 assert_eq!(plan.releases().len(), 1);
728 let release = &plan.releases()[0];
729 assert_eq!(*release.new_version(), Version::new(0, 2, 0));
730 }
731
732 #[test]
733 fn stable_version_unaffected_by_behavior() {
734 let packages = vec![make_package("my-crate", "1.2.3")];
735 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
736
737 let plan = VersionPlanner::plan_releases_with_behavior(
738 &changesets,
739 &packages,
740 None,
741 ZeroVersionBehavior::EffectiveMinor,
742 )
743 .expect("plan_releases_with_behavior");
744
745 assert_eq!(plan.releases().len(), 1);
746 let release = &plan.releases()[0];
747 assert_eq!(*release.new_version(), Version::new(2, 0, 0));
748 }
749
750 #[test]
751 fn with_prerelease_tag() {
752 let packages = vec![make_package("my-crate", "0.1.2")];
753 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
754
755 let plan = VersionPlanner::plan_releases_with_behavior(
756 &changesets,
757 &packages,
758 Some(&PrereleaseSpec::Alpha),
759 ZeroVersionBehavior::EffectiveMinor,
760 )
761 .expect("plan_releases_with_behavior");
762
763 assert_eq!(plan.releases().len(), 1);
764 let release = &plan.releases()[0];
765 assert_eq!(
766 *release.new_version(),
767 "0.2.0-alpha.1".parse::<Version>().expect("valid")
768 );
769 }
770
771 #[test]
772 fn auto_promote_none_bump_excludes_package() {
773 let packages = vec![make_package("my-crate", "0.1.2")];
774 let changesets = vec![make_changeset("my-crate", BumpType::None, "internal")];
775
776 let plan = VersionPlanner::plan_releases_with_behavior(
777 &changesets,
778 &packages,
779 None,
780 ZeroVersionBehavior::AutoPromoteOnMajor,
781 )
782 .expect("plan_releases_with_behavior");
783
784 assert!(
785 plan.releases().is_empty(),
786 "package with only None bumps should not appear in releases"
787 );
788 }
789 }
790
791 mod zero_graduation_tests {
792 use super::*;
793
794 #[test]
795 fn graduates_zero_version_to_1_0_0() {
796 let packages = vec![
797 make_package("crate-a", "0.5.3"),
798 make_package("crate-b", "1.0.0"),
799 ];
800
801 let plan = VersionPlanner::plan_zero_graduation(&packages, None)
802 .expect("plan_zero_graduation");
803
804 assert_eq!(plan.releases().len(), 1);
805 let release = &plan.releases()[0];
806 assert_eq!(release.name(), "crate-a");
807 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
808 }
809
810 #[test]
811 fn graduates_with_prerelease() {
812 let packages = vec![make_package("crate-a", "0.5.3")];
813
814 let plan =
815 VersionPlanner::plan_zero_graduation(&packages, Some(&PrereleaseSpec::Alpha))
816 .expect("plan_zero_graduation");
817
818 assert_eq!(plan.releases().len(), 1);
819 let release = &plan.releases()[0];
820 assert_eq!(
821 *release.new_version(),
822 "1.0.0-alpha.1".parse::<Version>().expect("valid")
823 );
824 }
825
826 #[test]
827 fn empty_for_all_stable() {
828 let packages = vec![
829 make_package("crate-a", "1.0.0"),
830 make_package("crate-b", "2.5.0"),
831 ];
832
833 let plan = VersionPlanner::plan_zero_graduation(&packages, None)
834 .expect("plan_zero_graduation");
835
836 assert!(plan.releases().is_empty());
837 }
838
839 #[test]
840 fn multiple_zero_versions() {
841 let packages = vec![
842 make_package("crate-a", "0.1.0"),
843 make_package("crate-b", "0.5.3"),
844 make_package("crate-c", "1.0.0"),
845 ];
846
847 let plan = VersionPlanner::plan_zero_graduation(&packages, None)
848 .expect("plan_zero_graduation");
849
850 assert_eq!(plan.releases().len(), 2);
851 for release in plan.releases() {
852 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
853 }
854 }
855 }
856
857 mod changeset_graduate_field_tests {
858 use super::*;
859
860 fn make_graduating_changeset(package_name: &str, bump: BumpType) -> Changeset {
861 Changeset::new(
862 "Graduate to 1.0".to_string(),
863 vec![PackageRelease::new(package_name.to_string(), bump)],
864 ChangeCategory::Changed,
865 )
866 .with_graduate(true)
867 }
868
869 #[test]
870 fn graduate_field_triggers_graduation() {
871 let packages = vec![make_package("my-crate", "0.5.3")];
872 let changesets = vec![make_graduating_changeset("my-crate", BumpType::Major)];
873
874 let plan = VersionPlanner::plan_releases_with_behavior(
875 &changesets,
876 &packages,
877 None,
878 ZeroVersionBehavior::EffectiveMinor,
879 )
880 .expect("plan_releases_with_behavior");
881
882 assert_eq!(plan.releases().len(), 1);
883 let release = &plan.releases()[0];
884 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
885 }
886
887 #[test]
888 fn graduate_field_ignored_for_stable_returns_error() {
889 let packages = vec![make_package("my-crate", "1.2.3")];
890 let changesets = vec![make_graduating_changeset("my-crate", BumpType::Major)];
891
892 let result = VersionPlanner::plan_releases_with_behavior(
893 &changesets,
894 &packages,
895 None,
896 ZeroVersionBehavior::EffectiveMinor,
897 );
898
899 assert!(result.is_err());
900 }
901
902 #[test]
903 fn mixed_graduate_and_regular_changesets() {
904 let packages = vec![
905 make_package("graduating", "0.5.0"),
906 make_package("regular", "0.3.0"),
907 ];
908 let changesets = vec![
909 make_graduating_changeset("graduating", BumpType::Major),
910 make_changeset("regular", BumpType::Major, "Breaking"),
911 ];
912
913 let plan = VersionPlanner::plan_releases_with_behavior(
914 &changesets,
915 &packages,
916 None,
917 ZeroVersionBehavior::EffectiveMinor,
918 )
919 .expect("plan_releases_with_behavior");
920
921 assert_eq!(plan.releases().len(), 2);
922
923 let graduating = plan
924 .releases()
925 .iter()
926 .find(|r| r.name() == "graduating")
927 .expect("graduating should be in releases");
928 assert_eq!(graduating.new_version(), &Version::new(1, 0, 0));
929
930 let regular = plan
931 .releases()
932 .iter()
933 .find(|r| r.name() == "regular")
934 .expect("regular should be in releases");
935 assert_eq!(regular.new_version(), &Version::new(0, 4, 0));
936 }
937 }
938
939 mod graduation_with_none_bump {
940 use super::*;
941
942 fn make_graduating_changeset(package_name: &str, bump: BumpType) -> Changeset {
943 Changeset::new(
944 "Graduate to 1.0".to_string(),
945 vec![PackageRelease::new(package_name.to_string(), bump)],
946 ChangeCategory::Changed,
947 )
948 .with_graduate(true)
949 }
950
951 #[test]
952 fn graduate_field_with_none_bump_still_graduates() {
953 let packages = vec![make_package("my-crate", "0.5.3")];
954 let changesets = vec![make_graduating_changeset("my-crate", BumpType::None)];
955
956 let plan = VersionPlanner::plan_releases_with_behavior(
957 &changesets,
958 &packages,
959 None,
960 ZeroVersionBehavior::EffectiveMinor,
961 )
962 .expect("plan_releases_with_behavior");
963
964 assert_eq!(plan.releases().len(), 1);
965 let release = &plan.releases()[0];
966 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
967 }
968
969 #[test]
970 fn per_package_graduate_with_none_bump_still_graduates() {
971 let packages = vec![make_package("my-crate", "0.5.3")];
972 let changesets = vec![make_graduating_changeset("my-crate", BumpType::None)];
973
974 let mut config = HashMap::new();
975 config.insert(
976 "my-crate".to_string(),
977 PackageReleaseConfigBuilder::default()
978 .graduate_zero(true)
979 .build()
980 .expect("all fields have defaults"),
981 );
982
983 let plan = VersionPlanner::plan_releases_per_package(
984 &changesets,
985 &packages,
986 &config,
987 ZeroVersionBehavior::EffectiveMinor,
988 )
989 .expect("plan_releases_per_package");
990
991 assert_eq!(plan.releases().len(), 1);
992 let release = &plan.releases()[0];
993 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
994 }
995
996 #[test]
997 fn prerelease_plan_graduate_with_none_bump_still_graduates() {
998 let packages = vec![make_package("my-crate", "0.5.3")];
999 let changesets = vec![make_graduating_changeset("my-crate", BumpType::None)];
1000
1001 let plan = VersionPlanner::plan_releases_with_prerelease(&changesets, &packages, None)
1002 .expect("plan_releases_with_prerelease");
1003
1004 assert_eq!(plan.releases().len(), 1);
1005 let release = &plan.releases()[0];
1006 assert_eq!(*release.new_version(), Version::new(1, 0, 0));
1007 assert_eq!(release.bump_type(), BumpType::Major);
1008 }
1009
1010 #[test]
1011 fn prerelease_plan_graduate_with_none_bump_and_prerelease_spec() {
1012 let packages = vec![make_package("my-crate", "0.5.3")];
1013 let changesets = vec![make_graduating_changeset("my-crate", BumpType::None)];
1014
1015 let plan = VersionPlanner::plan_releases_with_prerelease(
1016 &changesets,
1017 &packages,
1018 Some(&PrereleaseSpec::Alpha),
1019 )
1020 .expect("plan_releases_with_prerelease");
1021
1022 assert_eq!(plan.releases().len(), 1);
1023 let release = &plan.releases()[0];
1024 assert_eq!(
1025 *release.new_version(),
1026 "1.0.0-alpha.1".parse::<Version>().expect("valid")
1027 );
1028 assert_eq!(release.bump_type(), BumpType::Major);
1029 }
1030 }
1031
1032 mod per_package_config_tests {
1033 use super::*;
1034
1035 #[test]
1036 fn per_package_prerelease_applies_to_specific_crate() {
1037 let packages = vec![
1038 make_package("crate-a", "1.0.0"),
1039 make_package("crate-b", "1.0.0"),
1040 ];
1041 let changesets = vec![
1042 make_changeset("crate-a", BumpType::Patch, "Fix A"),
1043 make_changeset("crate-b", BumpType::Patch, "Fix B"),
1044 ];
1045
1046 let mut config = HashMap::new();
1047 config.insert(
1048 "crate-a".to_string(),
1049 PackageReleaseConfigBuilder::default()
1050 .prerelease(Some(PrereleaseSpec::Alpha))
1051 .build()
1052 .expect("all fields have defaults"),
1053 );
1054
1055 let plan = VersionPlanner::plan_releases_per_package(
1056 &changesets,
1057 &packages,
1058 &config,
1059 ZeroVersionBehavior::EffectiveMinor,
1060 )
1061 .expect("plan_releases_per_package");
1062
1063 let release_a = plan
1064 .releases()
1065 .iter()
1066 .find(|r| r.name() == "crate-a")
1067 .expect("crate-a should be in releases");
1068 let release_b = plan
1069 .releases()
1070 .iter()
1071 .find(|r| r.name() == "crate-b")
1072 .expect("crate-b should be in releases");
1073
1074 assert_eq!(
1075 release_a.new_version(),
1076 &"1.0.1-alpha.1".parse::<Version>().expect("valid")
1077 );
1078 assert_eq!(release_b.new_version(), &Version::new(1, 0, 1));
1079 }
1080
1081 #[test]
1082 fn per_package_graduation_applies_to_specific_crate() {
1083 let packages = vec![
1084 make_package("crate-a", "0.5.0"),
1085 make_package("crate-b", "0.3.0"),
1086 ];
1087 let changesets = vec![
1088 make_changeset("crate-a", BumpType::Minor, "Feature A"),
1089 make_changeset("crate-b", BumpType::Minor, "Feature B"),
1090 ];
1091
1092 let mut config = HashMap::new();
1093 config.insert(
1094 "crate-a".to_string(),
1095 PackageReleaseConfigBuilder::default()
1096 .graduate_zero(true)
1097 .build()
1098 .expect("all fields have defaults"),
1099 );
1100
1101 let plan = VersionPlanner::plan_releases_per_package(
1102 &changesets,
1103 &packages,
1104 &config,
1105 ZeroVersionBehavior::EffectiveMinor,
1106 )
1107 .expect("plan_releases_per_package");
1108
1109 let release_a = plan
1110 .releases()
1111 .iter()
1112 .find(|r| r.name() == "crate-a")
1113 .expect("crate-a should be in releases");
1114 let release_b = plan
1115 .releases()
1116 .iter()
1117 .find(|r| r.name() == "crate-b")
1118 .expect("crate-b should be in releases");
1119
1120 assert_eq!(release_a.new_version(), &Version::new(1, 0, 0));
1121 assert_eq!(release_b.new_version(), &Version::new(0, 3, 1));
1122 }
1123
1124 #[test]
1125 fn graduation_without_changesets() {
1126 let packages = vec![make_package("crate-a", "0.5.0")];
1127 let changesets: Vec<Changeset> = vec![];
1128
1129 let mut config = HashMap::new();
1130 config.insert(
1131 "crate-a".to_string(),
1132 PackageReleaseConfigBuilder::default()
1133 .graduate_zero(true)
1134 .build()
1135 .expect("all fields have defaults"),
1136 );
1137
1138 let plan = VersionPlanner::plan_releases_per_package(
1139 &changesets,
1140 &packages,
1141 &config,
1142 ZeroVersionBehavior::EffectiveMinor,
1143 )
1144 .expect("plan_releases_per_package");
1145
1146 assert_eq!(plan.releases().len(), 1);
1147 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 0));
1148 }
1149
1150 #[test]
1151 fn graduation_with_prerelease_creates_prerelease_1_0_0() {
1152 let packages = vec![make_package("crate-a", "0.5.0")];
1153 let changesets = vec![make_changeset("crate-a", BumpType::Minor, "Feature")];
1154
1155 let mut config = HashMap::new();
1156 config.insert(
1157 "crate-a".to_string(),
1158 PackageReleaseConfigBuilder::default()
1159 .prerelease(Some(PrereleaseSpec::Rc))
1160 .graduate_zero(true)
1161 .build()
1162 .expect("all fields have defaults"),
1163 );
1164
1165 let plan = VersionPlanner::plan_releases_per_package(
1166 &changesets,
1167 &packages,
1168 &config,
1169 ZeroVersionBehavior::EffectiveMinor,
1170 )
1171 .expect("plan_releases_per_package");
1172
1173 assert_eq!(plan.releases().len(), 1);
1174 assert_eq!(
1175 plan.releases()[0].new_version(),
1176 &"1.0.0-rc.1".parse::<Version>().expect("valid")
1177 );
1178 }
1179
1180 #[test]
1181 fn empty_config_uses_defaults() {
1182 let packages = vec![make_package("crate-a", "1.0.0")];
1183 let changesets = vec![make_changeset("crate-a", BumpType::Patch, "Fix")];
1184 let config = HashMap::new();
1185
1186 let plan = VersionPlanner::plan_releases_per_package(
1187 &changesets,
1188 &packages,
1189 &config,
1190 ZeroVersionBehavior::EffectiveMinor,
1191 )
1192 .expect("plan_releases_per_package");
1193
1194 assert_eq!(plan.releases().len(), 1);
1195 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 1));
1196 }
1197
1198 #[test]
1199 fn mixed_prerelease_and_stable_releases() {
1200 let packages = vec![
1201 make_package("alpha-crate", "1.0.0"),
1202 make_package("beta-crate", "2.0.0"),
1203 make_package("stable-crate", "3.0.0"),
1204 ];
1205 let changesets = vec![
1206 make_changeset("alpha-crate", BumpType::Minor, "Feature"),
1207 make_changeset("beta-crate", BumpType::Patch, "Fix"),
1208 make_changeset("stable-crate", BumpType::Major, "Breaking"),
1209 ];
1210
1211 let mut config = HashMap::new();
1212 config.insert(
1213 "alpha-crate".to_string(),
1214 PackageReleaseConfigBuilder::default()
1215 .prerelease(Some(PrereleaseSpec::Alpha))
1216 .build()
1217 .expect("all fields have defaults"),
1218 );
1219 config.insert(
1220 "beta-crate".to_string(),
1221 PackageReleaseConfigBuilder::default()
1222 .prerelease(Some(PrereleaseSpec::Beta))
1223 .build()
1224 .expect("all fields have defaults"),
1225 );
1226
1227 let plan = VersionPlanner::plan_releases_per_package(
1228 &changesets,
1229 &packages,
1230 &config,
1231 ZeroVersionBehavior::EffectiveMinor,
1232 )
1233 .expect("plan_releases_per_package");
1234
1235 assert_eq!(plan.releases().len(), 3);
1236
1237 let alpha = plan
1238 .releases()
1239 .iter()
1240 .find(|r| r.name() == "alpha-crate")
1241 .expect("alpha-crate should be in releases");
1242 let beta = plan
1243 .releases()
1244 .iter()
1245 .find(|r| r.name() == "beta-crate")
1246 .expect("beta-crate should be in releases");
1247 let stable = plan
1248 .releases()
1249 .iter()
1250 .find(|r| r.name() == "stable-crate")
1251 .expect("stable-crate should be in releases");
1252
1253 assert_eq!(
1254 alpha.new_version(),
1255 &"1.1.0-alpha.1".parse::<Version>().expect("valid")
1256 );
1257 assert_eq!(
1258 beta.new_version(),
1259 &"2.0.1-beta.1".parse::<Version>().expect("valid")
1260 );
1261 assert_eq!(stable.new_version(), &Version::new(4, 0, 0));
1262 }
1263
1264 #[test]
1265 fn changeset_graduate_field_combined_with_config() {
1266 let packages = vec![make_package("crate-a", "0.5.0")];
1267
1268 let changesets = vec![
1269 Changeset::new(
1270 "Graduate".to_string(),
1271 vec![PackageRelease::new("crate-a".to_string(), BumpType::Major)],
1272 ChangeCategory::Changed,
1273 )
1274 .with_graduate(true),
1275 ];
1276
1277 let mut config = HashMap::new();
1278 config.insert(
1279 "crate-a".to_string(),
1280 PackageReleaseConfigBuilder::default()
1281 .prerelease(Some(PrereleaseSpec::Rc))
1282 .build()
1283 .expect("all fields have defaults"),
1284 );
1285
1286 let plan = VersionPlanner::plan_releases_per_package(
1287 &changesets,
1288 &packages,
1289 &config,
1290 ZeroVersionBehavior::EffectiveMinor,
1291 )
1292 .expect("plan_releases_per_package");
1293
1294 assert_eq!(plan.releases().len(), 1);
1295 assert_eq!(
1296 plan.releases()[0].new_version(),
1297 &"1.0.0-rc.1".parse::<Version>().expect("valid")
1298 );
1299 }
1300
1301 #[test]
1302 fn config_graduation_without_changeset_graduation() {
1303 let packages = vec![make_package("crate-a", "0.5.0")];
1304 let changesets = vec![make_changeset("crate-a", BumpType::Minor, "Feature")];
1305
1306 let mut config = HashMap::new();
1307 config.insert(
1308 "crate-a".to_string(),
1309 PackageReleaseConfigBuilder::default()
1310 .graduate_zero(true)
1311 .build()
1312 .expect("all fields have defaults"),
1313 );
1314
1315 let plan = VersionPlanner::plan_releases_per_package(
1316 &changesets,
1317 &packages,
1318 &config,
1319 ZeroVersionBehavior::EffectiveMinor,
1320 )
1321 .expect("plan_releases_per_package");
1322
1323 assert_eq!(plan.releases().len(), 1);
1324 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 0));
1325 }
1326
1327 #[test]
1328 fn unknown_package_in_changeset_collected() {
1329 let packages = vec![make_package("known", "1.0.0")];
1330 let changesets = vec![make_changeset("unknown", BumpType::Patch, "Fix")];
1331 let config = HashMap::new();
1332
1333 let plan = VersionPlanner::plan_releases_per_package(
1334 &changesets,
1335 &packages,
1336 &config,
1337 ZeroVersionBehavior::EffectiveMinor,
1338 )
1339 .expect("plan_releases_per_package");
1340
1341 assert!(plan.releases().is_empty());
1342 assert_eq!(plan.unknown_packages(), &["unknown"]);
1343 }
1344
1345 #[test]
1346 fn config_without_changesets_ignored_for_unknown_package() {
1347 let packages = vec![make_package("known", "1.0.0")];
1348 let changesets: Vec<Changeset> = vec![];
1349
1350 let mut config = HashMap::new();
1351 config.insert(
1352 "unknown".to_string(),
1353 PackageReleaseConfigBuilder::default()
1354 .prerelease(Some(PrereleaseSpec::Alpha))
1355 .build()
1356 .expect("all fields have defaults"),
1357 );
1358
1359 let plan = VersionPlanner::plan_releases_per_package(
1360 &changesets,
1361 &packages,
1362 &config,
1363 ZeroVersionBehavior::EffectiveMinor,
1364 )
1365 .expect("plan_releases_per_package");
1366
1367 assert!(plan.releases().is_empty());
1368 }
1369
1370 #[test]
1371 fn prerelease_only_config_without_changesets() {
1372 let packages = vec![make_package("crate-a", "1.0.0")];
1373 let changesets: Vec<Changeset> = vec![];
1374
1375 let mut config = HashMap::new();
1376 config.insert(
1377 "crate-a".to_string(),
1378 PackageReleaseConfigBuilder::default()
1379 .prerelease(Some(PrereleaseSpec::Alpha))
1380 .build()
1381 .expect("all fields have defaults"),
1382 );
1383
1384 let plan = VersionPlanner::plan_releases_per_package(
1385 &changesets,
1386 &packages,
1387 &config,
1388 ZeroVersionBehavior::EffectiveMinor,
1389 )
1390 .expect("plan_releases_per_package");
1391
1392 assert_eq!(plan.releases().len(), 1);
1393 assert_eq!(
1394 plan.releases()[0].new_version(),
1395 &"1.0.1-alpha.1".parse::<Version>().expect("valid")
1396 );
1397 }
1398
1399 #[test]
1400 fn zero_behavior_applied_with_config() {
1401 let packages = vec![make_package("crate-a", "0.5.0")];
1402 let changesets = vec![make_changeset("crate-a", BumpType::Major, "Breaking")];
1403 let config = HashMap::new();
1404
1405 let plan = VersionPlanner::plan_releases_per_package(
1406 &changesets,
1407 &packages,
1408 &config,
1409 ZeroVersionBehavior::EffectiveMinor,
1410 )
1411 .expect("plan_releases_per_package");
1412
1413 assert_eq!(plan.releases().len(), 1);
1414 assert_eq!(plan.releases()[0].new_version(), &Version::new(0, 6, 0));
1415 }
1416
1417 #[test]
1418 fn auto_promote_behavior_with_config() {
1419 let packages = vec![make_package("crate-a", "0.5.0")];
1420 let changesets = vec![make_changeset("crate-a", BumpType::Major, "Breaking")];
1421 let config = HashMap::new();
1422
1423 let plan = VersionPlanner::plan_releases_per_package(
1424 &changesets,
1425 &packages,
1426 &config,
1427 ZeroVersionBehavior::AutoPromoteOnMajor,
1428 )
1429 .expect("plan_releases_per_package");
1430
1431 assert_eq!(plan.releases().len(), 1);
1432 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 0));
1433 }
1434
1435 #[test]
1436 fn package_with_no_changesets_and_no_config_not_included() {
1437 let packages = vec![
1438 make_package("with-changeset", "1.0.0"),
1439 make_package("no-changeset", "2.0.0"),
1440 ];
1441 let changesets = vec![make_changeset("with-changeset", BumpType::Patch, "Fix")];
1442 let config = HashMap::new();
1443
1444 let plan = VersionPlanner::plan_releases_per_package(
1445 &changesets,
1446 &packages,
1447 &config,
1448 ZeroVersionBehavior::EffectiveMinor,
1449 )
1450 .expect("plan_releases_per_package");
1451
1452 assert_eq!(plan.releases().len(), 1);
1453 assert_eq!(plan.releases()[0].name(), "with-changeset");
1454 assert!(plan.unknown_packages().is_empty());
1455 }
1456
1457 #[test]
1458 fn graduation_only_config_without_changeset() {
1459 let packages = vec![make_package("crate-a", "0.5.0")];
1460 let changesets: Vec<Changeset> = vec![];
1461
1462 let mut config = HashMap::new();
1463 config.insert(
1464 "crate-a".to_string(),
1465 PackageReleaseConfigBuilder::default()
1466 .graduate_zero(true)
1467 .build()
1468 .expect("all fields have defaults"),
1469 );
1470
1471 let plan = VersionPlanner::plan_releases_per_package(
1472 &changesets,
1473 &packages,
1474 &config,
1475 ZeroVersionBehavior::EffectiveMinor,
1476 )
1477 .expect("plan_releases_per_package");
1478
1479 assert_eq!(plan.releases().len(), 1);
1480 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 0));
1481 }
1482
1483 #[test]
1484 fn config_with_neither_prerelease_nor_graduation_not_included() {
1485 let packages = vec![make_package("crate-a", "1.0.0")];
1486 let changesets: Vec<Changeset> = vec![];
1487
1488 let mut config = HashMap::new();
1489 config.insert(
1490 "crate-a".to_string(),
1491 PackageReleaseConfigBuilder::default()
1492 .build()
1493 .expect("all fields have defaults"),
1494 );
1495
1496 let plan = VersionPlanner::plan_releases_per_package(
1497 &changesets,
1498 &packages,
1499 &config,
1500 ZeroVersionBehavior::EffectiveMinor,
1501 )
1502 .expect("plan_releases_per_package");
1503
1504 assert!(plan.releases().is_empty());
1505 }
1506
1507 #[test]
1508 fn all_packages_graduating_simultaneously() {
1509 let packages = vec![
1510 make_package("crate-a", "0.1.0"),
1511 make_package("crate-b", "0.5.0"),
1512 make_package("crate-c", "0.9.0"),
1513 ];
1514 let changesets: Vec<Changeset> = vec![];
1515
1516 let mut config = HashMap::new();
1517 for pkg in &packages {
1518 config.insert(
1519 pkg.name().clone(),
1520 PackageReleaseConfigBuilder::default()
1521 .graduate_zero(true)
1522 .build()
1523 .expect("all fields have defaults"),
1524 );
1525 }
1526
1527 let plan = VersionPlanner::plan_releases_per_package(
1528 &changesets,
1529 &packages,
1530 &config,
1531 ZeroVersionBehavior::EffectiveMinor,
1532 )
1533 .expect("plan_releases_per_package");
1534
1535 assert_eq!(plan.releases().len(), 3);
1536 for release in plan.releases() {
1537 assert_eq!(
1538 *release.new_version(),
1539 Version::new(1, 0, 0),
1540 "{} should graduate to 1.0.0",
1541 release.name()
1542 );
1543 }
1544 }
1545
1546 #[test]
1547 fn prerelease_graduation_with_prerelease_tag() {
1548 let packages = vec![make_package("crate-a", "0.5.0")];
1549 let changesets: Vec<Changeset> = vec![];
1550
1551 let mut config = HashMap::new();
1552 config.insert(
1553 "crate-a".to_string(),
1554 PackageReleaseConfigBuilder::default()
1555 .prerelease(Some(PrereleaseSpec::Beta))
1556 .graduate_zero(true)
1557 .build()
1558 .expect("all fields have defaults"),
1559 );
1560
1561 let plan = VersionPlanner::plan_releases_per_package(
1562 &changesets,
1563 &packages,
1564 &config,
1565 ZeroVersionBehavior::EffectiveMinor,
1566 )
1567 .expect("plan_releases_per_package");
1568
1569 assert_eq!(plan.releases().len(), 1);
1570 assert_eq!(
1571 plan.releases()[0].new_version(),
1572 &"1.0.0-beta.1".parse::<Version>().expect("valid")
1573 );
1574 }
1575 }
1576
1577 mod auto_promote_zero_behavior {
1578 use super::*;
1579
1580 #[test]
1581 fn auto_promote_with_major_bump_graduates() {
1582 let packages = vec![make_package("my-crate", "0.1.2")];
1583 let changesets = vec![make_changeset("my-crate", BumpType::Major, "Breaking")];
1584
1585 let plan = VersionPlanner::plan_releases_with_behavior(
1586 &changesets,
1587 &packages,
1588 None,
1589 ZeroVersionBehavior::AutoPromoteOnMajor,
1590 )
1591 .expect("plan_releases_with_behavior");
1592
1593 assert_eq!(plan.releases().len(), 1);
1594 assert_eq!(plan.releases()[0].new_version(), &Version::new(1, 0, 0));
1595 }
1596
1597 #[test]
1598 fn auto_promote_with_minor_bump_stays_zero() {
1599 let packages = vec![make_package("my-crate", "0.1.2")];
1600 let changesets = vec![make_changeset("my-crate", BumpType::Minor, "Feature")];
1601
1602 let plan = VersionPlanner::plan_releases_with_behavior(
1603 &changesets,
1604 &packages,
1605 None,
1606 ZeroVersionBehavior::AutoPromoteOnMajor,
1607 )
1608 .expect("plan_releases_with_behavior");
1609
1610 assert_eq!(plan.releases().len(), 1);
1611 assert_eq!(plan.releases()[0].new_version(), &Version::new(0, 2, 0));
1612 }
1613
1614 #[test]
1615 fn auto_promote_with_patch_bump_stays_zero() {
1616 let packages = vec![make_package("my-crate", "0.1.2")];
1617 let changesets = vec![make_changeset("my-crate", BumpType::Patch, "Fix")];
1618
1619 let plan = VersionPlanner::plan_releases_with_behavior(
1620 &changesets,
1621 &packages,
1622 None,
1623 ZeroVersionBehavior::AutoPromoteOnMajor,
1624 )
1625 .expect("plan_releases_with_behavior");
1626
1627 assert_eq!(plan.releases().len(), 1);
1628 assert_eq!(plan.releases()[0].new_version(), &Version::new(0, 1, 3));
1629 }
1630 }
1631
1632 mod unknown_packages {
1633 use super::*;
1634
1635 #[test]
1636 fn multiple_unknown_packages_collected() {
1637 let packages = vec![make_package("known", "1.0.0")];
1638 let changesets = vec![make_multi_changeset(
1639 vec![
1640 ("unknown1", BumpType::Patch),
1641 ("unknown2", BumpType::Minor),
1642 ("unknown3", BumpType::Major),
1643 ],
1644 "Changes to unknown packages",
1645 )];
1646
1647 let plan =
1648 VersionPlanner::plan_releases(&changesets, &packages).expect("plan_releases");
1649
1650 assert!(plan.releases().is_empty());
1651 assert_eq!(plan.unknown_packages().len(), 3);
1652 assert!(plan.unknown_packages().contains(&"unknown1".to_string()));
1653 assert!(plan.unknown_packages().contains(&"unknown2".to_string()));
1654 assert!(plan.unknown_packages().contains(&"unknown3".to_string()));
1655 }
1656
1657 #[test]
1658 fn per_package_config_for_nonexistent_is_silently_ignored() {
1659 let packages = vec![make_package("known", "1.0.0")];
1660 let changesets: Vec<Changeset> = vec![];
1661
1662 let mut config = HashMap::new();
1663 config.insert(
1664 "nonexistent".to_string(),
1665 PackageReleaseConfigBuilder::default()
1666 .prerelease(Some(PrereleaseSpec::Alpha))
1667 .build()
1668 .expect("all fields have defaults"),
1669 );
1670
1671 let plan = VersionPlanner::plan_releases_per_package(
1672 &changesets,
1673 &packages,
1674 &config,
1675 ZeroVersionBehavior::EffectiveMinor,
1676 )
1677 .expect("plan_releases_per_package");
1678
1679 assert!(plan.releases().is_empty());
1680 assert!(
1681 plan.unknown_packages().is_empty(),
1682 "config for nonexistent packages does not add to unknown_packages"
1683 );
1684 }
1685 }
1686
1687 mod none_bump_type {
1688 use super::*;
1689
1690 #[test]
1691 fn all_none_bumps_exclude_package_from_releases() {
1692 let packages = vec![make_package("my-crate", "1.0.0")];
1693 let changesets = vec![make_changeset("my-crate", BumpType::None, "internal")];
1694
1695 let plan =
1696 VersionPlanner::plan_releases(&changesets, &packages).expect("should succeed");
1697
1698 assert!(
1699 plan.releases().is_empty(),
1700 "package with only None bumps should not appear in releases"
1701 );
1702 }
1703
1704 #[test]
1705 fn none_bumps_still_tracked_in_aggregate() {
1706 let changesets = vec![make_changeset("my-crate", BumpType::None, "internal")];
1707
1708 let bumps = VersionPlanner::aggregate_bumps(&changesets);
1709
1710 assert!(bumps.contains_key("my-crate"));
1711 assert_eq!(bumps["my-crate"], vec![BumpType::None]);
1712 }
1713
1714 #[test]
1715 fn mixed_none_and_patch_uses_patch() {
1716 let packages = vec![make_package("my-crate", "1.0.0")];
1717 let changesets = vec![
1718 make_changeset("my-crate", BumpType::None, "internal"),
1719 make_changeset("my-crate", BumpType::Patch, "fix bug"),
1720 ];
1721
1722 let plan =
1723 VersionPlanner::plan_releases(&changesets, &packages).expect("should succeed");
1724
1725 assert_eq!(plan.releases().len(), 1);
1726 assert_eq!(plan.releases()[0].bump_type(), BumpType::Patch);
1727 assert_eq!(
1728 plan.releases()[0].new_version(),
1729 &Version::parse("1.0.1").expect("valid version")
1730 );
1731 }
1732
1733 #[test]
1734 fn all_none_excluded_from_behavior_plan() {
1735 let packages = vec![make_package("my-crate", "0.1.0")];
1736 let changesets = vec![make_changeset("my-crate", BumpType::None, "internal")];
1737
1738 let plan = VersionPlanner::plan_releases_with_behavior(
1739 &changesets,
1740 &packages,
1741 None,
1742 ZeroVersionBehavior::EffectiveMinor,
1743 )
1744 .expect("should succeed");
1745
1746 assert!(plan.releases().is_empty());
1747 }
1748
1749 #[test]
1750 fn all_none_excluded_from_per_package_plan() {
1751 let packages = vec![make_package("my-crate", "1.0.0")];
1752 let changesets = vec![make_changeset("my-crate", BumpType::None, "internal")];
1753
1754 let plan = VersionPlanner::plan_releases_per_package(
1755 &changesets,
1756 &packages,
1757 &HashMap::new(),
1758 ZeroVersionBehavior::EffectiveMinor,
1759 )
1760 .expect("should succeed");
1761
1762 assert!(plan.releases().is_empty());
1763 }
1764 }
1765}