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