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