1use std::path::Path;
2
3use changeset_project::{CargoProject, GraduationState, PrereleaseState};
4use changeset_version::{is_prerelease, is_zero_version};
5
6use crate::Result;
7use crate::error::OperationError;
8use crate::traits::{
9 GraduationAction, GraduationInteractionProvider, MenuSelection, PrereleaseAction,
10 PrereleaseInteractionProvider, ProjectProvider, ReleaseStateIO,
11};
12
13pub struct PrereleaseManageOperation<P, S, I> {
14 project_provider: P,
15 release_state_io: S,
16 interaction_provider: I,
17}
18
19impl<P, S, I> PrereleaseManageOperation<P, S, I>
20where
21 P: ProjectProvider,
22 S: ReleaseStateIO,
23 I: PrereleaseInteractionProvider + GraduationInteractionProvider,
24{
25 pub fn new(project_provider: P, release_state_io: S, interaction_provider: I) -> Self {
26 Self {
27 project_provider,
28 release_state_io,
29 interaction_provider,
30 }
31 }
32
33 pub fn execute(&self, start_path: &Path) -> Result<Vec<PrereleaseEvent>> {
37 let project = self.project_provider.discover_project(start_path)?;
38 let (root_config, _) = self.project_provider.load_configs(&project)?;
39 let changeset_dir = project.root().join(root_config.changeset_dir());
40
41 let mut prerelease_state = self
42 .release_state_io
43 .load_prerelease_state(&changeset_dir)?
44 .unwrap_or_default();
45
46 let mut graduation_state = self
47 .release_state_io
48 .load_graduation_state(&changeset_dir)?
49 .unwrap_or_default();
50
51 let mut events = Vec::new();
52
53 loop {
54 events.push(PrereleaseEvent::DisplayState(
55 prerelease_state
56 .iter()
57 .map(|(k, v)| (k.to_string(), v.to_string()))
58 .collect(),
59 ));
60
61 let action = self.interaction_provider.select_prerelease_action()?;
62
63 match action {
64 MenuSelection::Selected(PrereleaseAction::Add) => {
65 self.handle_add(&project, &changeset_dir, &mut prerelease_state, &mut events)?;
66 }
67 MenuSelection::Selected(PrereleaseAction::Remove) => {
68 self.handle_remove(&changeset_dir, &mut prerelease_state, &mut events)?;
69 }
70 MenuSelection::Selected(PrereleaseAction::Graduate) => {
71 self.handle_graduate(
72 &project,
73 &changeset_dir,
74 &mut prerelease_state,
75 &mut graduation_state,
76 &mut events,
77 )?;
78 }
79 MenuSelection::Selected(PrereleaseAction::Done) | MenuSelection::Cancelled => {
80 break;
81 }
82 }
83 }
84
85 Ok(events)
86 }
87
88 fn handle_add(
89 &self,
90 project: &CargoProject,
91 changeset_dir: &Path,
92 prerelease_state: &mut PrereleaseState,
93 events: &mut Vec<PrereleaseEvent>,
94 ) -> Result<()> {
95 let available: Vec<_> = project
96 .packages()
97 .iter()
98 .filter(|p| !prerelease_state.contains(&p.name))
99 .collect();
100
101 if available.is_empty() {
102 events.push(PrereleaseEvent::AllPackagesInPrerelease);
103 return Ok(());
104 }
105
106 let selection = self
107 .interaction_provider
108 .select_package_for_prerelease(&available)?;
109
110 let MenuSelection::Selected(index) = selection else {
111 return Ok(());
112 };
113
114 let crate_name = &available[index].name;
115 let tag = self.interaction_provider.get_prerelease_tag()?;
116
117 validate_prerelease_tag(&tag)?;
118
119 prerelease_state.insert(crate_name.clone(), tag.clone());
120 self.release_state_io
121 .save_prerelease_state(changeset_dir, prerelease_state)?;
122 events.push(PrereleaseEvent::Added {
123 crate_name: crate_name.clone(),
124 tag,
125 });
126
127 Ok(())
128 }
129
130 fn handle_remove(
131 &self,
132 changeset_dir: &Path,
133 prerelease_state: &mut PrereleaseState,
134 events: &mut Vec<PrereleaseEvent>,
135 ) -> Result<()> {
136 if prerelease_state.is_empty() {
137 events.push(PrereleaseEvent::NoPrereleasePackages);
138 return Ok(());
139 }
140
141 let mut items: Vec<_> = prerelease_state
142 .iter()
143 .map(|(name, tag)| (name.to_string(), tag.to_string()))
144 .collect();
145 items.sort_by(|a, b| a.0.cmp(&b.0));
146
147 let refs: Vec<(&str, &str)> = items
148 .iter()
149 .map(|(n, t)| (n.as_str(), t.as_str()))
150 .collect();
151 let selection = self
152 .interaction_provider
153 .select_package_to_remove_prerelease(&refs)?;
154
155 let MenuSelection::Selected(index) = selection else {
156 return Ok(());
157 };
158
159 let crate_name = items[index].0.clone();
160 let _ = prerelease_state.remove(&crate_name);
161 self.release_state_io
162 .save_prerelease_state(changeset_dir, prerelease_state)?;
163 events.push(PrereleaseEvent::Removed { crate_name });
164
165 Ok(())
166 }
167
168 fn handle_graduate(
169 &self,
170 project: &CargoProject,
171 changeset_dir: &Path,
172 prerelease_state: &mut PrereleaseState,
173 graduation_state: &mut GraduationState,
174 events: &mut Vec<PrereleaseEvent>,
175 ) -> Result<()> {
176 let eligible: Vec<_> = project
177 .packages()
178 .iter()
179 .filter(|p| is_zero_version(&p.version) && !is_prerelease(&p.version))
180 .collect();
181
182 if eligible.is_empty() {
183 events.push(PrereleaseEvent::NoEligibleForGraduation);
184 return Ok(());
185 }
186
187 let selection = self
188 .interaction_provider
189 .select_package_for_graduation(&eligible)?;
190
191 let MenuSelection::Selected(index) = selection else {
192 return Ok(());
193 };
194
195 let crate_name = &eligible[index].name;
196
197 if prerelease_state.remove(crate_name).is_some() {
198 self.release_state_io
199 .save_prerelease_state(changeset_dir, prerelease_state)?;
200 }
201
202 graduation_state.add(crate_name.clone());
203 self.release_state_io
204 .save_graduation_state(changeset_dir, graduation_state)?;
205 events.push(PrereleaseEvent::MovedToGraduation {
206 crate_name: crate_name.clone(),
207 });
208
209 Ok(())
210 }
211}
212
213pub struct GraduationManageOperation<P, S, I> {
214 project_provider: P,
215 release_state_io: S,
216 interaction_provider: I,
217}
218
219impl<P, S, I> GraduationManageOperation<P, S, I>
220where
221 P: ProjectProvider,
222 S: ReleaseStateIO,
223 I: GraduationInteractionProvider,
224{
225 pub fn new(project_provider: P, release_state_io: S, interaction_provider: I) -> Self {
226 Self {
227 project_provider,
228 release_state_io,
229 interaction_provider,
230 }
231 }
232
233 pub fn execute(&self, start_path: &Path) -> Result<Vec<GraduationEvent>> {
237 let project = self.project_provider.discover_project(start_path)?;
238 let (root_config, _) = self.project_provider.load_configs(&project)?;
239 let changeset_dir = project.root().join(root_config.changeset_dir());
240
241 let mut state = self
242 .release_state_io
243 .load_graduation_state(&changeset_dir)?
244 .unwrap_or_default();
245
246 let mut events = Vec::new();
247
248 loop {
249 events.push(GraduationEvent::DisplayState(
250 state.iter().map(str::to_string).collect(),
251 ));
252
253 let action = self.interaction_provider.select_graduation_action()?;
254
255 match action {
256 MenuSelection::Selected(GraduationAction::Add) => {
257 let eligible: Vec<_> = project
258 .packages()
259 .iter()
260 .filter(|p| {
261 is_zero_version(&p.version)
262 && !is_prerelease(&p.version)
263 && !state.contains(&p.name)
264 })
265 .collect();
266
267 if eligible.is_empty() {
268 events.push(GraduationEvent::NoEligibleForGraduation);
269 continue;
270 }
271
272 let selection = self
273 .interaction_provider
274 .select_package_for_graduation(&eligible)?;
275
276 let MenuSelection::Selected(index) = selection else {
277 continue;
278 };
279
280 let crate_name = &eligible[index].name;
281 state.add(crate_name.clone());
282 self.release_state_io
283 .save_graduation_state(&changeset_dir, &state)?;
284 events.push(GraduationEvent::Added {
285 crate_name: crate_name.clone(),
286 });
287 }
288 MenuSelection::Selected(GraduationAction::Remove) => {
289 if state.is_empty() {
290 events.push(GraduationEvent::NoGraduationPackages);
291 continue;
292 }
293
294 let mut items: Vec<String> = state.iter().map(str::to_string).collect();
295 items.sort();
296
297 let selection = self
298 .interaction_provider
299 .select_package_to_remove_graduation(&items)?;
300
301 let MenuSelection::Selected(index) = selection else {
302 continue;
303 };
304
305 let crate_name = &items[index];
306 let _ = state.remove(crate_name);
307 self.release_state_io
308 .save_graduation_state(&changeset_dir, &state)?;
309 events.push(GraduationEvent::Removed {
310 crate_name: crate_name.clone(),
311 });
312 }
313 MenuSelection::Selected(GraduationAction::Done) | MenuSelection::Cancelled => {
314 break;
315 }
316 }
317 }
318
319 Ok(events)
320 }
321}
322
323pub struct PrereleaseDirectOperation<P, S> {
324 project_provider: P,
325 release_state_io: S,
326}
327
328impl<P, S> PrereleaseDirectOperation<P, S>
329where
330 P: ProjectProvider,
331 S: ReleaseStateIO,
332{
333 pub fn new(project_provider: P, release_state_io: S) -> Self {
334 Self {
335 project_provider,
336 release_state_io,
337 }
338 }
339
340 pub fn execute(
344 &self,
345 start_path: &Path,
346 input: &PrereleaseDirectInput,
347 ) -> Result<Vec<PrereleaseEvent>> {
348 let project = self.project_provider.discover_project(start_path)?;
349 let (root_config, _) = self.project_provider.load_configs(&project)?;
350 let changeset_dir = project.root().join(root_config.changeset_dir());
351
352 let mut prerelease_state = self
353 .release_state_io
354 .load_prerelease_state(&changeset_dir)?
355 .unwrap_or_default();
356
357 let mut graduation_state = self
358 .release_state_io
359 .load_graduation_state(&changeset_dir)?
360 .unwrap_or_default();
361
362 let mut events = Vec::new();
363 let mut modified_prerelease = false;
364 let mut modified_graduation = false;
365
366 for entry in &input.add {
367 let (crate_name, tag) = parse_prerelease_entry(entry)?;
368 validate_package_exists(&project, &crate_name)?;
369 validate_prerelease_tag(&tag)?;
370
371 prerelease_state.insert(crate_name.clone(), tag.clone());
372 modified_prerelease = true;
373 events.push(PrereleaseEvent::Added { crate_name, tag });
374 }
375
376 for crate_name in &input.remove {
377 if prerelease_state.remove(crate_name).is_some() {
378 modified_prerelease = true;
379 events.push(PrereleaseEvent::Removed {
380 crate_name: crate_name.clone(),
381 });
382 }
383 }
384
385 for crate_name in &input.graduate {
386 validate_package_exists(&project, crate_name)?;
387 validate_can_graduate(&project, crate_name)?;
388
389 if prerelease_state.remove(crate_name).is_some() {
390 modified_prerelease = true;
391 }
392
393 graduation_state.add(crate_name.clone());
394 modified_graduation = true;
395 events.push(PrereleaseEvent::MovedToGraduation {
396 crate_name: crate_name.clone(),
397 });
398 }
399
400 if modified_prerelease {
401 self.release_state_io
402 .save_prerelease_state(&changeset_dir, &prerelease_state)?;
403 }
404 if modified_graduation {
405 self.release_state_io
406 .save_graduation_state(&changeset_dir, &graduation_state)?;
407 }
408
409 if input.list {
410 events.push(PrereleaseEvent::DisplayState(
411 prerelease_state
412 .iter()
413 .map(|(k, v)| (k.to_string(), v.to_string()))
414 .collect(),
415 ));
416 }
417
418 Ok(events)
419 }
420}
421
422pub struct PrereleaseDirectInput {
423 add: Vec<String>,
424 remove: Vec<String>,
425 graduate: Vec<String>,
426 list: bool,
427}
428
429impl PrereleaseDirectInput {
430 #[must_use]
431 pub fn new(add: Vec<String>, remove: Vec<String>, graduate: Vec<String>, list: bool) -> Self {
432 Self {
433 add,
434 remove,
435 graduate,
436 list,
437 }
438 }
439}
440
441pub struct GraduationDirectOperation<P, S> {
442 project_provider: P,
443 release_state_io: S,
444}
445
446impl<P, S> GraduationDirectOperation<P, S>
447where
448 P: ProjectProvider,
449 S: ReleaseStateIO,
450{
451 pub fn new(project_provider: P, release_state_io: S) -> Self {
452 Self {
453 project_provider,
454 release_state_io,
455 }
456 }
457
458 pub fn execute(
462 &self,
463 start_path: &Path,
464 input: &GraduationDirectInput,
465 ) -> Result<Vec<GraduationEvent>> {
466 let project = self.project_provider.discover_project(start_path)?;
467 let (root_config, _) = self.project_provider.load_configs(&project)?;
468 let changeset_dir = project.root().join(root_config.changeset_dir());
469
470 let mut state = self
471 .release_state_io
472 .load_graduation_state(&changeset_dir)?
473 .unwrap_or_default();
474
475 let mut events = Vec::new();
476 let mut modified = false;
477
478 for crate_name in &input.add {
479 validate_package_exists(&project, crate_name)?;
480 validate_can_graduate(&project, crate_name)?;
481
482 state.add(crate_name.clone());
483 modified = true;
484 events.push(GraduationEvent::Added {
485 crate_name: crate_name.clone(),
486 });
487 }
488
489 for crate_name in &input.remove {
490 if state.remove(crate_name) {
491 modified = true;
492 events.push(GraduationEvent::Removed {
493 crate_name: crate_name.clone(),
494 });
495 }
496 }
497
498 if modified {
499 self.release_state_io
500 .save_graduation_state(&changeset_dir, &state)?;
501 }
502
503 if input.list {
504 events.push(GraduationEvent::DisplayState(
505 state.iter().map(str::to_string).collect(),
506 ));
507 }
508
509 Ok(events)
510 }
511}
512
513pub struct GraduationDirectInput {
514 add: Vec<String>,
515 remove: Vec<String>,
516 list: bool,
517}
518
519impl GraduationDirectInput {
520 #[must_use]
521 pub fn new(add: Vec<String>, remove: Vec<String>, list: bool) -> Self {
522 Self { add, remove, list }
523 }
524}
525
526#[derive(Debug, Clone, PartialEq, Eq)]
527pub enum PrereleaseEvent {
528 DisplayState(Vec<(String, String)>),
529 Added { crate_name: String, tag: String },
530 Removed { crate_name: String },
531 MovedToGraduation { crate_name: String },
532 AllPackagesInPrerelease,
533 NoPrereleasePackages,
534 NoEligibleForGraduation,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
538pub enum GraduationEvent {
539 DisplayState(Vec<String>),
540 Added { crate_name: String },
541 Removed { crate_name: String },
542 NoEligibleForGraduation,
543 NoGraduationPackages,
544}
545
546fn parse_prerelease_entry(input: &str) -> Result<(String, String)> {
547 let Some((crate_name, tag)) = input.split_once(':') else {
548 return Err(OperationError::InvalidPrereleaseFormat {
549 input: input.to_string(),
550 });
551 };
552
553 if crate_name.is_empty() || tag.is_empty() {
554 return Err(OperationError::InvalidPrereleaseFormat {
555 input: input.to_string(),
556 });
557 }
558
559 Ok((crate_name.to_string(), tag.to_string()))
560}
561
562fn validate_prerelease_tag(tag: &str) -> Result<()> {
563 crate::error::parse_prerelease_tag(tag)?;
564 Ok(())
565}
566
567fn validate_package_exists(project: &CargoProject, name: &str) -> Result<()> {
568 if !project.packages().iter().any(|p| p.name == name) {
569 return Err(OperationError::PackageNotFound {
570 name: name.to_string(),
571 });
572 }
573 Ok(())
574}
575
576fn validate_can_graduate(project: &CargoProject, name: &str) -> Result<()> {
577 let package = project
578 .packages()
579 .iter()
580 .find(|p| p.name == name)
581 .ok_or_else(|| OperationError::PackageNotFound {
582 name: name.to_string(),
583 })?;
584
585 if is_prerelease(&package.version) {
586 return Err(OperationError::CannotGraduatePrerelease {
587 package: name.to_string(),
588 version: package.version.clone(),
589 });
590 }
591
592 if !is_zero_version(&package.version) {
593 return Err(OperationError::CannotGraduateStable {
594 package: name.to_string(),
595 version: package.version.clone(),
596 });
597 }
598
599 Ok(())
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use changeset_core::PackageInfo;
606 use changeset_project::{CargoProject, ProjectKind};
607 use std::path::PathBuf;
608
609 fn make_project(packages: Vec<(&str, &str)>) -> CargoProject {
610 CargoProject::new(
611 PathBuf::from("/mock/project"),
612 ProjectKind::VirtualWorkspace,
613 packages
614 .into_iter()
615 .map(|(name, version)| PackageInfo {
616 name: name.to_string(),
617 version: version.parse().expect("valid version"),
618 path: PathBuf::from(format!("/mock/project/crates/{name}")),
619 })
620 .collect(),
621 )
622 }
623
624 mod parse_prerelease_entry_tests {
625 use super::*;
626
627 #[test]
628 fn parses_valid_format() {
629 let result = parse_prerelease_entry("my-crate:alpha");
630
631 assert!(result.is_ok());
632 let (name, tag) = result.expect("should parse");
633 assert_eq!(name, "my-crate");
634 assert_eq!(tag, "alpha");
635 }
636
637 #[test]
638 fn parses_custom_tag() {
639 let result = parse_prerelease_entry("crate-name:nightly");
640
641 assert!(result.is_ok());
642 let (name, tag) = result.expect("should parse");
643 assert_eq!(name, "crate-name");
644 assert_eq!(tag, "nightly");
645 }
646
647 #[test]
648 fn rejects_missing_colon() {
649 let result = parse_prerelease_entry("no-colon-here");
650
651 assert!(result.is_err());
652 assert!(matches!(
653 result.expect_err("should fail"),
654 OperationError::InvalidPrereleaseFormat { .. }
655 ));
656 }
657
658 #[test]
659 fn rejects_empty_crate_name() {
660 let result = parse_prerelease_entry(":alpha");
661
662 assert!(result.is_err());
663 assert!(matches!(
664 result.expect_err("should fail"),
665 OperationError::InvalidPrereleaseFormat { .. }
666 ));
667 }
668
669 #[test]
670 fn rejects_empty_tag() {
671 let result = parse_prerelease_entry("crate-name:");
672
673 assert!(result.is_err());
674 assert!(matches!(
675 result.expect_err("should fail"),
676 OperationError::InvalidPrereleaseFormat { .. }
677 ));
678 }
679
680 #[test]
681 fn handles_multiple_colons() {
682 let result = parse_prerelease_entry("crate:tag:extra");
683
684 assert!(result.is_ok());
685 let (name, tag) = result.expect("should parse");
686 assert_eq!(name, "crate");
687 assert_eq!(tag, "tag:extra");
688 }
689 }
690
691 mod validate_prerelease_tag_tests {
692 use super::*;
693
694 #[test]
695 fn accepts_alpha() {
696 assert!(validate_prerelease_tag("alpha").is_ok());
697 }
698
699 #[test]
700 fn accepts_beta() {
701 assert!(validate_prerelease_tag("beta").is_ok());
702 }
703
704 #[test]
705 fn accepts_rc() {
706 assert!(validate_prerelease_tag("rc").is_ok());
707 }
708
709 #[test]
710 fn accepts_custom_alphanumeric() {
711 assert!(validate_prerelease_tag("nightly").is_ok());
712 assert!(validate_prerelease_tag("dev123").is_ok());
713 }
714
715 #[test]
716 fn accepts_hyphenated_tags() {
717 assert!(validate_prerelease_tag("pre-release").is_ok());
718 }
719
720 #[test]
721 fn rejects_empty() {
722 let result = validate_prerelease_tag("");
723
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn rejects_invalid_characters() {
729 let result = validate_prerelease_tag("alpha.1");
730
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn rejects_spaces() {
736 let result = validate_prerelease_tag("alpha 1");
737
738 assert!(result.is_err());
739 assert!(matches!(
740 result.expect_err("should fail"),
741 OperationError::InvalidPrereleaseTag { .. }
742 ));
743 }
744
745 #[test]
746 fn rejects_underscores() {
747 let result = validate_prerelease_tag("alpha_1");
748
749 assert!(result.is_err());
750 }
751 }
752
753 mod validate_package_exists_tests {
754 use super::*;
755
756 #[test]
757 fn succeeds_for_existing_package() {
758 let project = make_project(vec![("crate-a", "1.0.0"), ("crate-b", "2.0.0")]);
759
760 let result = validate_package_exists(&project, "crate-a");
761
762 assert!(result.is_ok());
763 }
764
765 #[test]
766 fn fails_for_unknown_package() {
767 let project = make_project(vec![("crate-a", "1.0.0")]);
768
769 let result = validate_package_exists(&project, "nonexistent");
770
771 assert!(result.is_err());
772 let err = result.expect_err("should fail");
773 assert!(matches!(err, OperationError::PackageNotFound { .. }));
774 assert!(err.to_string().contains("nonexistent"));
775 }
776
777 #[test]
778 fn fails_for_empty_project() {
779 let project = make_project(vec![]);
780
781 let result = validate_package_exists(&project, "any-crate");
782
783 assert!(result.is_err());
784 assert!(matches!(
785 result.expect_err("should fail"),
786 OperationError::PackageNotFound { .. }
787 ));
788 }
789 }
790
791 mod validate_can_graduate_tests {
792 use super::*;
793
794 #[test]
795 fn succeeds_for_zero_stable_version() {
796 let project = make_project(vec![("crate-a", "0.5.0")]);
797
798 let result = validate_can_graduate(&project, "crate-a");
799
800 assert!(result.is_ok());
801 }
802
803 #[test]
804 fn fails_for_prerelease_version() {
805 let project = make_project(vec![("crate-a", "0.5.0-alpha.1")]);
806
807 let result = validate_can_graduate(&project, "crate-a");
808
809 assert!(result.is_err());
810 let err = result.expect_err("should fail");
811 assert!(matches!(
812 err,
813 OperationError::CannotGraduatePrerelease { .. }
814 ));
815 assert!(err.to_string().contains("crate-a"));
816 assert!(err.to_string().contains("prerelease"));
817 }
818
819 #[test]
820 fn fails_for_stable_version_1_0_0() {
821 let project = make_project(vec![("crate-a", "1.0.0")]);
822
823 let result = validate_can_graduate(&project, "crate-a");
824
825 assert!(result.is_err());
826 let err = result.expect_err("should fail");
827 assert!(matches!(err, OperationError::CannotGraduateStable { .. }));
828 assert!(err.to_string().contains("stable"));
829 }
830
831 #[test]
832 fn fails_for_stable_version_above_1() {
833 let project = make_project(vec![("crate-a", "2.5.3")]);
834
835 let result = validate_can_graduate(&project, "crate-a");
836
837 assert!(result.is_err());
838 assert!(matches!(
839 result.expect_err("should fail"),
840 OperationError::CannotGraduateStable { .. }
841 ));
842 }
843
844 #[test]
845 fn fails_for_unknown_package() {
846 let project = make_project(vec![("crate-a", "0.5.0")]);
847
848 let result = validate_can_graduate(&project, "nonexistent");
849
850 assert!(result.is_err());
851 assert!(matches!(
852 result.expect_err("should fail"),
853 OperationError::PackageNotFound { .. }
854 ));
855 }
856
857 #[test]
858 fn fails_for_zero_prerelease_version() {
859 let project = make_project(vec![("crate-a", "0.1.0-beta.1")]);
860
861 let result = validate_can_graduate(&project, "crate-a");
862
863 assert!(result.is_err());
864 assert!(matches!(
865 result.expect_err("should fail"),
866 OperationError::CannotGraduatePrerelease { .. }
867 ));
868 }
869 }
870
871 mod prerelease_operation {
872 use super::*;
873 use crate::mocks::{
874 MockManageInteractionProvider, MockProjectProvider, MockReleaseStateIO,
875 };
876 use std::sync::Arc;
877
878 #[test]
879 fn exits_on_done_action() {
880 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
881 let release_state_io = Arc::new(MockReleaseStateIO::new());
882 let interaction = Arc::new(
883 MockManageInteractionProvider::new()
884 .with_prerelease_actions(vec![MenuSelection::Selected(PrereleaseAction::Done)]),
885 );
886
887 let operation = PrereleaseManageOperation::new(
888 project_provider,
889 Arc::clone(&release_state_io),
890 Arc::clone(&interaction),
891 );
892
893 let events = operation
894 .execute(std::path::Path::new("/any"))
895 .expect("prerelease manage operation should execute without error");
896
897 assert_eq!(events.len(), 1);
898 assert!(
899 events
900 .iter()
901 .any(|e| matches!(e, PrereleaseEvent::DisplayState(_)))
902 );
903 }
904
905 #[test]
906 fn exits_on_cancelled() {
907 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
908 let release_state_io = Arc::new(MockReleaseStateIO::new());
909 let interaction = Arc::new(
910 MockManageInteractionProvider::new()
911 .with_prerelease_actions(vec![MenuSelection::Cancelled]),
912 );
913
914 let operation = PrereleaseManageOperation::new(
915 project_provider,
916 Arc::clone(&release_state_io),
917 Arc::clone(&interaction),
918 );
919
920 let events = operation
921 .execute(std::path::Path::new("/any"))
922 .expect("prerelease manage operation should execute without error");
923
924 assert_eq!(events.len(), 1);
925 }
926
927 #[test]
928 fn adds_package_to_prerelease() {
929 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
930 let release_state_io = Arc::new(MockReleaseStateIO::new());
931 let interaction = Arc::new(
932 MockManageInteractionProvider::new()
933 .with_prerelease_actions(vec![
934 MenuSelection::Selected(PrereleaseAction::Add),
935 MenuSelection::Selected(PrereleaseAction::Done),
936 ])
937 .with_package_selections(vec![MenuSelection::Selected(0)])
938 .with_prerelease_tags(vec!["alpha".to_string()]),
939 );
940
941 let operation = PrereleaseManageOperation::new(
942 project_provider,
943 Arc::clone(&release_state_io),
944 Arc::clone(&interaction),
945 );
946
947 let events = operation
948 .execute(std::path::Path::new("/any"))
949 .expect("prerelease manage operation should execute without error");
950
951 assert!(events.contains(&PrereleaseEvent::Added {
952 crate_name: "crate-a".to_string(),
953 tag: "alpha".to_string(),
954 }));
955
956 let prerelease_state = release_state_io
957 .get_prerelease_state()
958 .expect("prerelease state should be saved");
959 assert!(prerelease_state.contains("crate-a"));
960 assert_eq!(prerelease_state.get("crate-a"), Some("alpha"));
961 }
962
963 #[test]
964 fn removes_package_from_prerelease() {
965 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "1.0.0")]);
966 let release_state_io = Arc::new(MockReleaseStateIO::new().with_prerelease_state({
967 let mut state = PrereleaseState::default();
968 state.insert("crate-a".to_string(), "alpha".to_string());
969 state
970 }));
971 let interaction = Arc::new(
972 MockManageInteractionProvider::new()
973 .with_prerelease_actions(vec![
974 MenuSelection::Selected(PrereleaseAction::Remove),
975 MenuSelection::Selected(PrereleaseAction::Done),
976 ])
977 .with_remove_prerelease_selections(vec![MenuSelection::Selected(0)]),
978 );
979
980 let operation = PrereleaseManageOperation::new(
981 project_provider,
982 Arc::clone(&release_state_io),
983 Arc::clone(&interaction),
984 );
985
986 let events = operation
987 .execute(std::path::Path::new("/any"))
988 .expect("prerelease manage operation should execute without error");
989
990 assert!(events.contains(&PrereleaseEvent::Removed {
991 crate_name: "crate-a".to_string(),
992 }));
993 match release_state_io.get_prerelease_state() {
994 None => {}
995 Some(state) => assert!(!state.contains("crate-a")),
996 }
997 }
998
999 #[test]
1000 fn reports_no_prerelease_packages_on_remove_when_empty() {
1001 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "1.0.0")]);
1002 let release_state_io = Arc::new(MockReleaseStateIO::new());
1003 let interaction = Arc::new(
1004 MockManageInteractionProvider::new().with_prerelease_actions(vec![
1005 MenuSelection::Selected(PrereleaseAction::Remove),
1006 MenuSelection::Selected(PrereleaseAction::Done),
1007 ]),
1008 );
1009
1010 let operation = PrereleaseManageOperation::new(
1011 project_provider,
1012 Arc::clone(&release_state_io),
1013 Arc::clone(&interaction),
1014 );
1015
1016 let events = operation
1017 .execute(std::path::Path::new("/any"))
1018 .expect("prerelease manage operation should execute without error");
1019
1020 assert!(events.contains(&PrereleaseEvent::NoPrereleasePackages));
1021 }
1022
1023 #[test]
1024 fn moves_package_to_graduation() {
1025 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1026 let release_state_io = Arc::new(MockReleaseStateIO::new().with_prerelease_state({
1027 let mut state = PrereleaseState::default();
1028 state.insert("crate-a".to_string(), "alpha".to_string());
1029 state
1030 }));
1031 let interaction = Arc::new(
1032 MockManageInteractionProvider::new()
1033 .with_prerelease_actions(vec![
1034 MenuSelection::Selected(PrereleaseAction::Graduate),
1035 MenuSelection::Selected(PrereleaseAction::Done),
1036 ])
1037 .with_graduation_selections(vec![MenuSelection::Selected(0)]),
1038 );
1039
1040 let operation = PrereleaseManageOperation::new(
1041 project_provider,
1042 Arc::clone(&release_state_io),
1043 Arc::clone(&interaction),
1044 );
1045
1046 let events = operation
1047 .execute(std::path::Path::new("/any"))
1048 .expect("prerelease manage operation should execute without error");
1049
1050 assert!(events.contains(&PrereleaseEvent::MovedToGraduation {
1051 crate_name: "crate-a".to_string(),
1052 }));
1053 let graduation_state = release_state_io
1054 .get_graduation_state()
1055 .expect("graduation state should be saved");
1056 assert!(graduation_state.contains("crate-a"));
1057 match release_state_io.get_prerelease_state() {
1058 None => {}
1059 Some(state) => assert!(!state.contains("crate-a")),
1060 }
1061 }
1062
1063 #[test]
1064 fn reports_no_eligible_for_graduation_when_all_stable() {
1065 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "1.0.0")]);
1066 let release_state_io = Arc::new(MockReleaseStateIO::new());
1067 let interaction = Arc::new(
1068 MockManageInteractionProvider::new().with_prerelease_actions(vec![
1069 MenuSelection::Selected(PrereleaseAction::Graduate),
1070 MenuSelection::Selected(PrereleaseAction::Done),
1071 ]),
1072 );
1073
1074 let operation = PrereleaseManageOperation::new(
1075 project_provider,
1076 Arc::clone(&release_state_io),
1077 Arc::clone(&interaction),
1078 );
1079
1080 let events = operation
1081 .execute(std::path::Path::new("/any"))
1082 .expect("prerelease manage operation should execute without error");
1083
1084 assert!(events.contains(&PrereleaseEvent::NoEligibleForGraduation));
1085 }
1086
1087 #[test]
1088 fn cancels_add_when_package_selection_cancelled() {
1089 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1090 let release_state_io = Arc::new(MockReleaseStateIO::new());
1091 let interaction = Arc::new(
1092 MockManageInteractionProvider::new()
1093 .with_prerelease_actions(vec![
1094 MenuSelection::Selected(PrereleaseAction::Add),
1095 MenuSelection::Selected(PrereleaseAction::Done),
1096 ])
1097 .with_package_selections(vec![MenuSelection::Cancelled]),
1098 );
1099
1100 let operation = PrereleaseManageOperation::new(
1101 project_provider,
1102 Arc::clone(&release_state_io),
1103 Arc::clone(&interaction),
1104 );
1105
1106 let events = operation
1107 .execute(std::path::Path::new("/any"))
1108 .expect("prerelease manage operation should execute without error");
1109
1110 assert!(
1111 !events
1112 .iter()
1113 .any(|e| matches!(e, PrereleaseEvent::Added { .. }))
1114 );
1115 assert!(release_state_io.get_prerelease_state().is_none());
1116 }
1117
1118 #[test]
1119 fn reports_all_packages_in_prerelease() {
1120 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1121 let release_state_io = Arc::new(MockReleaseStateIO::new().with_prerelease_state({
1122 let mut state = PrereleaseState::default();
1123 state.insert("crate-a".to_string(), "alpha".to_string());
1124 state
1125 }));
1126 let interaction = Arc::new(
1127 MockManageInteractionProvider::new().with_prerelease_actions(vec![
1128 MenuSelection::Selected(PrereleaseAction::Add),
1129 MenuSelection::Selected(PrereleaseAction::Done),
1130 ]),
1131 );
1132
1133 let operation = PrereleaseManageOperation::new(
1134 project_provider,
1135 Arc::clone(&release_state_io),
1136 Arc::clone(&interaction),
1137 );
1138
1139 let events = operation
1140 .execute(std::path::Path::new("/any"))
1141 .expect("prerelease manage operation should execute without error");
1142
1143 assert!(events.contains(&PrereleaseEvent::AllPackagesInPrerelease));
1144 }
1145 }
1146
1147 mod graduation_operation {
1148 use super::*;
1149 use crate::mocks::{
1150 MockManageInteractionProvider, MockProjectProvider, MockReleaseStateIO,
1151 };
1152 use std::sync::Arc;
1153
1154 #[test]
1155 fn exits_on_done_action() {
1156 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1157 let release_state_io = Arc::new(MockReleaseStateIO::new());
1158 let interaction = Arc::new(
1159 MockManageInteractionProvider::new()
1160 .with_graduation_actions(vec![MenuSelection::Selected(GraduationAction::Done)]),
1161 );
1162
1163 let operation = GraduationManageOperation::new(
1164 project_provider,
1165 Arc::clone(&release_state_io),
1166 Arc::clone(&interaction),
1167 );
1168
1169 let events = operation
1170 .execute(std::path::Path::new("/any"))
1171 .expect("graduation manage operation should execute without error");
1172
1173 assert_eq!(events.len(), 1);
1174 assert!(
1175 events
1176 .iter()
1177 .any(|e| matches!(e, GraduationEvent::DisplayState(_)))
1178 );
1179 }
1180
1181 #[test]
1182 fn exits_on_cancelled() {
1183 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1184 let release_state_io = Arc::new(MockReleaseStateIO::new());
1185 let interaction = Arc::new(
1186 MockManageInteractionProvider::new()
1187 .with_graduation_actions(vec![MenuSelection::Cancelled]),
1188 );
1189
1190 let operation = GraduationManageOperation::new(
1191 project_provider,
1192 Arc::clone(&release_state_io),
1193 Arc::clone(&interaction),
1194 );
1195
1196 let events = operation
1197 .execute(std::path::Path::new("/any"))
1198 .expect("graduation manage operation should execute without error");
1199
1200 assert_eq!(events.len(), 1);
1201 assert!(
1202 events
1203 .iter()
1204 .any(|e| matches!(e, GraduationEvent::DisplayState(_)))
1205 );
1206 }
1207
1208 #[test]
1209 fn cancels_add_when_package_selection_cancelled() {
1210 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1211 let release_state_io = Arc::new(MockReleaseStateIO::new());
1212 let interaction = Arc::new(
1213 MockManageInteractionProvider::new()
1214 .with_graduation_actions(vec![
1215 MenuSelection::Selected(GraduationAction::Add),
1216 MenuSelection::Selected(GraduationAction::Done),
1217 ])
1218 .with_graduation_selections(vec![MenuSelection::Cancelled]),
1219 );
1220
1221 let operation = GraduationManageOperation::new(
1222 project_provider,
1223 Arc::clone(&release_state_io),
1224 Arc::clone(&interaction),
1225 );
1226
1227 let events = operation
1228 .execute(std::path::Path::new("/any"))
1229 .expect("graduation manage operation should execute without error");
1230
1231 assert!(
1232 !events
1233 .iter()
1234 .any(|e| matches!(e, GraduationEvent::Added { .. }))
1235 );
1236 assert!(release_state_io.get_graduation_state().is_none());
1237 }
1238
1239 #[test]
1240 fn reports_no_eligible_for_graduation_when_all_stable() {
1241 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "1.0.0")]);
1242 let release_state_io = Arc::new(MockReleaseStateIO::new());
1243 let interaction = Arc::new(
1244 MockManageInteractionProvider::new().with_graduation_actions(vec![
1245 MenuSelection::Selected(GraduationAction::Add),
1246 MenuSelection::Selected(GraduationAction::Done),
1247 ]),
1248 );
1249
1250 let operation = GraduationManageOperation::new(
1251 project_provider,
1252 Arc::clone(&release_state_io),
1253 Arc::clone(&interaction),
1254 );
1255
1256 let events = operation
1257 .execute(std::path::Path::new("/any"))
1258 .expect("graduation manage operation should execute without error");
1259
1260 assert!(events.contains(&GraduationEvent::NoEligibleForGraduation));
1261 }
1262
1263 #[test]
1264 fn cancels_remove_when_selection_cancelled() {
1265 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1266 let release_state_io = Arc::new(MockReleaseStateIO::new().with_graduation_state({
1267 let mut state = GraduationState::default();
1268 state.add("crate-a".to_string());
1269 state
1270 }));
1271 let interaction = Arc::new(
1272 MockManageInteractionProvider::new()
1273 .with_graduation_actions(vec![
1274 MenuSelection::Selected(GraduationAction::Remove),
1275 MenuSelection::Selected(GraduationAction::Done),
1276 ])
1277 .with_remove_graduation_selections(vec![MenuSelection::Cancelled]),
1278 );
1279
1280 let operation = GraduationManageOperation::new(
1281 project_provider,
1282 Arc::clone(&release_state_io),
1283 Arc::clone(&interaction),
1284 );
1285
1286 let events = operation
1287 .execute(std::path::Path::new("/any"))
1288 .expect("graduation manage operation should execute without error");
1289
1290 assert!(
1291 !events
1292 .iter()
1293 .any(|e| matches!(e, GraduationEvent::Removed { .. }))
1294 );
1295 let graduation_state = release_state_io
1296 .get_graduation_state()
1297 .expect("graduation state should still exist");
1298 assert!(graduation_state.contains("crate-a"));
1299 }
1300
1301 #[test]
1302 fn adds_package_to_graduation() {
1303 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1304 let release_state_io = Arc::new(MockReleaseStateIO::new());
1305 let interaction = Arc::new(
1306 MockManageInteractionProvider::new()
1307 .with_graduation_actions(vec![
1308 MenuSelection::Selected(GraduationAction::Add),
1309 MenuSelection::Selected(GraduationAction::Done),
1310 ])
1311 .with_graduation_selections(vec![MenuSelection::Selected(0)]),
1312 );
1313
1314 let operation = GraduationManageOperation::new(
1315 project_provider,
1316 Arc::clone(&release_state_io),
1317 Arc::clone(&interaction),
1318 );
1319
1320 let events = operation
1321 .execute(std::path::Path::new("/any"))
1322 .expect("graduation manage operation should execute without error");
1323
1324 assert!(events.contains(&GraduationEvent::Added {
1325 crate_name: "crate-a".to_string(),
1326 }));
1327
1328 let graduation_state = release_state_io
1329 .get_graduation_state()
1330 .expect("graduation state should be saved");
1331 assert!(graduation_state.contains("crate-a"));
1332 }
1333
1334 #[test]
1335 fn removes_package_from_graduation() {
1336 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1337 let release_state_io = Arc::new(MockReleaseStateIO::new().with_graduation_state({
1338 let mut state = GraduationState::default();
1339 state.add("crate-a".to_string());
1340 state
1341 }));
1342 let interaction = Arc::new(
1343 MockManageInteractionProvider::new()
1344 .with_graduation_actions(vec![
1345 MenuSelection::Selected(GraduationAction::Remove),
1346 MenuSelection::Selected(GraduationAction::Done),
1347 ])
1348 .with_remove_graduation_selections(vec![MenuSelection::Selected(0)]),
1349 );
1350
1351 let operation = GraduationManageOperation::new(
1352 project_provider,
1353 Arc::clone(&release_state_io),
1354 Arc::clone(&interaction),
1355 );
1356
1357 let events = operation
1358 .execute(std::path::Path::new("/any"))
1359 .expect("graduation manage operation should execute without error");
1360
1361 assert!(events.contains(&GraduationEvent::Removed {
1362 crate_name: "crate-a".to_string(),
1363 }));
1364 match release_state_io.get_graduation_state() {
1365 None => {}
1366 Some(state) => assert!(!state.contains("crate-a")),
1367 }
1368 }
1369
1370 #[test]
1371 fn reports_no_graduation_packages_on_remove() {
1372 let project_provider = MockProjectProvider::workspace(vec![("crate-a", "0.1.0")]);
1373 let release_state_io = Arc::new(MockReleaseStateIO::new());
1374 let interaction = Arc::new(
1375 MockManageInteractionProvider::new().with_graduation_actions(vec![
1376 MenuSelection::Selected(GraduationAction::Remove),
1377 MenuSelection::Selected(GraduationAction::Done),
1378 ]),
1379 );
1380
1381 let operation = GraduationManageOperation::new(
1382 project_provider,
1383 Arc::clone(&release_state_io),
1384 Arc::clone(&interaction),
1385 );
1386
1387 let events = operation
1388 .execute(std::path::Path::new("/any"))
1389 .expect("graduation manage operation should execute without error");
1390
1391 assert!(events.contains(&GraduationEvent::NoGraduationPackages));
1392 }
1393 }
1394}