1use crate::error::{Error, Result};
6use crate::platform::PlatformService;
7use crate::submit::SubmissionAnalysis;
8use crate::submit::analysis::{generate_pr_title, get_base_branch};
9use crate::types::{Bookmark, NarrowedBookmarkSegment, PullRequest};
10use std::cmp::Reverse;
11use std::collections::{BinaryHeap, HashMap, HashSet};
12
13#[derive(Debug, Clone)]
15pub struct PrToCreate {
16 pub bookmark: Bookmark,
18 pub base_branch: String,
20 pub title: String,
22 pub draft: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct PrBaseUpdate {
29 pub bookmark: Bookmark,
31 pub current_base: String,
33 pub expected_base: String,
35 pub pr: PullRequest,
37}
38
39#[derive(Debug, Clone)]
41pub enum ExecutionStep {
42 Push(Bookmark),
44 UpdateBase(PrBaseUpdate),
46 CreatePr(PrToCreate),
48 PublishPr(PullRequest),
50}
51
52impl ExecutionStep {
53 pub fn bookmark_name(&self) -> &str {
55 match self {
56 Self::Push(bm) => &bm.name,
57 Self::UpdateBase(update) => &update.bookmark.name,
58 Self::CreatePr(create) => &create.bookmark.name,
59 Self::PublishPr(pr) => &pr.head_ref,
60 }
61 }
62}
63
64impl std::fmt::Display for ExecutionStep {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::Push(bm) => write!(f, "push {}", bm.name),
68 Self::UpdateBase(update) => write!(
69 f,
70 "update {} (PR #{}) {} → {}",
71 update.bookmark.name, update.pr.number, update.current_base, update.expected_base
72 ),
73 Self::CreatePr(create) => {
74 write!(
75 f,
76 "create PR {} → {} ({})",
77 create.bookmark.name, create.base_branch, create.title
78 )?;
79 if create.draft {
80 write!(f, " [draft]")?;
81 }
82 Ok(())
83 }
84 Self::PublishPr(pr) => write!(f, "publish PR #{} ({})", pr.number, pr.head_ref),
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96pub struct PushRef(pub String);
97
98#[derive(Debug, Clone, PartialEq, Eq, Hash)]
100pub struct UpdateRef(pub String);
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash)]
104pub struct CreateRef(pub String);
105
106#[derive(Debug, Clone)]
115pub enum ExecutionConstraint {
116 PushOrder {
119 parent: PushRef,
121 child: PushRef,
123 },
124
125 PushBeforeRetarget {
128 base: PushRef,
130 pr: UpdateRef,
132 },
133
134 RetargetBeforePush {
138 pr: UpdateRef,
140 old_base: PushRef,
142 },
143
144 PushBeforeCreate {
147 push: PushRef,
149 create: CreateRef,
151 },
152
153 CreateOrder {
156 parent: CreateRef,
158 child: CreateRef,
160 },
161}
162
163impl std::fmt::Display for ExecutionConstraint {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::PushOrder { parent, child } => {
167 write!(f, "Push({}) → Push({})", parent.0, child.0)
168 }
169 Self::PushBeforeRetarget { base, pr } => {
170 write!(f, "Push({}) → UpdateBase({})", base.0, pr.0)
171 }
172 Self::RetargetBeforePush { pr, old_base } => {
173 write!(f, "UpdateBase({}) → Push({})", pr.0, old_base.0)
174 }
175 Self::PushBeforeCreate { push, create } => {
176 write!(f, "Push({}) → CreatePr({})", push.0, create.0)
177 }
178 Self::CreateOrder { parent, child } => {
179 write!(f, "CreatePr({}) → CreatePr({})", parent.0, child.0)
180 }
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
187struct NodeIdx(usize);
188
189#[derive(Debug, Default)]
192struct NodeRegistry {
193 push: HashMap<String, NodeIdx>,
194 update: HashMap<String, NodeIdx>,
195 create: HashMap<String, NodeIdx>,
196 publish: HashMap<String, NodeIdx>,
197}
198
199impl NodeRegistry {
200 fn register_push(&mut self, name: &str, idx: usize) {
201 self.push.insert(name.to_string(), NodeIdx(idx));
202 }
203
204 fn register_update(&mut self, name: &str, idx: usize) {
205 self.update.insert(name.to_string(), NodeIdx(idx));
206 }
207
208 fn register_create(&mut self, name: &str, idx: usize) {
209 self.create.insert(name.to_string(), NodeIdx(idx));
210 }
211
212 fn register_publish(&mut self, name: &str, idx: usize) {
213 self.publish.insert(name.to_string(), NodeIdx(idx));
214 }
215
216 fn len(&self) -> usize {
217 self.push.len() + self.update.len() + self.create.len() + self.publish.len()
218 }
219}
220
221impl ExecutionConstraint {
222 fn resolve(&self, registry: &NodeRegistry) -> Option<(usize, usize)> {
227 match self {
228 Self::PushOrder { parent, child } => {
229 let from = registry.push.get(&parent.0)?;
230 let to = registry.push.get(&child.0)?;
231 Some((from.0, to.0))
232 }
233 Self::PushBeforeRetarget { base, pr } => {
234 let from = registry.push.get(&base.0)?;
235 let to = registry.update.get(&pr.0)?;
236 Some((from.0, to.0))
237 }
238 Self::RetargetBeforePush { pr, old_base } => {
239 let from = registry.update.get(&pr.0)?;
240 let to = registry.push.get(&old_base.0)?;
241 Some((from.0, to.0))
242 }
243 Self::PushBeforeCreate { push, create } => {
244 let from = registry.push.get(&push.0)?;
245 let to = registry.create.get(&create.0)?;
246 Some((from.0, to.0))
247 }
248 Self::CreateOrder { parent, child } => {
249 let from = registry.create.get(&parent.0)?;
250 let to = registry.create.get(&child.0)?;
251 Some((from.0, to.0))
252 }
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
259struct ExecutionNode {
260 step: ExecutionStep,
261 order: usize,
262}
263
264#[derive(Debug, Clone)]
266pub struct SubmissionPlan {
267 pub segments: Vec<NarrowedBookmarkSegment>,
269 pub constraints: Vec<ExecutionConstraint>,
271 pub execution_steps: Vec<ExecutionStep>,
273 pub existing_prs: HashMap<String, PullRequest>,
275 pub remote: String,
277 pub default_branch: String,
279}
280
281impl SubmissionPlan {
282 pub const fn is_empty(&self) -> bool {
284 self.execution_steps.is_empty()
285 }
286
287 pub fn count_pushes(&self) -> usize {
289 self.execution_steps
290 .iter()
291 .filter(|s| matches!(s, ExecutionStep::Push(_)))
292 .count()
293 }
294
295 pub fn count_creates(&self) -> usize {
297 self.execution_steps
298 .iter()
299 .filter(|s| matches!(s, ExecutionStep::CreatePr(_)))
300 .count()
301 }
302
303 pub fn count_updates(&self) -> usize {
305 self.execution_steps
306 .iter()
307 .filter(|s| matches!(s, ExecutionStep::UpdateBase(_)))
308 .count()
309 }
310
311 pub fn count_publishes(&self) -> usize {
313 self.execution_steps
314 .iter()
315 .filter(|s| matches!(s, ExecutionStep::PublishPr(_)))
316 .count()
317 }
318}
319
320pub async fn create_submission_plan(
327 analysis: &SubmissionAnalysis,
328 platform: &dyn PlatformService,
329 remote: &str,
330 default_branch: &str,
331) -> Result<SubmissionPlan> {
332 let segments = &analysis.segments;
333 let bookmarks: Vec<&Bookmark> = segments.iter().map(|s| &s.bookmark).collect();
334
335 let mut existing_prs = HashMap::new();
337 for bookmark in &bookmarks {
338 if let Some(pr) = platform.find_existing_pr(&bookmark.name).await? {
339 existing_prs.insert(bookmark.name.clone(), pr);
340 }
341 }
342
343 let mut bookmarks_needing_push = Vec::new();
345 let mut prs_to_create = Vec::new();
346 let mut prs_to_update_base = Vec::new();
347
348 for bookmark in &bookmarks {
349 if !bookmark.has_remote || !bookmark.is_synced {
351 bookmarks_needing_push.push((*bookmark).clone());
352 }
353
354 if let Some(pr) = existing_prs.get(&bookmark.name) {
356 let expected_base = get_base_branch(&bookmark.name, segments, default_branch)?;
358
359 if pr.base_ref != expected_base {
360 prs_to_update_base.push(PrBaseUpdate {
361 bookmark: (*bookmark).clone(),
362 current_base: pr.base_ref.clone(),
363 expected_base,
364 pr: pr.clone(),
365 });
366 }
367 } else {
368 let base_branch = get_base_branch(&bookmark.name, segments, default_branch)?;
370 let title = generate_pr_title(&bookmark.name, segments)?;
371
372 prs_to_create.push(PrToCreate {
373 bookmark: (*bookmark).clone(),
374 base_branch,
375 title,
376 draft: false,
377 });
378 }
379 }
380
381 let (constraints, execution_steps) = build_execution_steps(
383 segments,
384 &bookmarks_needing_push,
385 &prs_to_update_base,
386 &prs_to_create,
387 &[], )?;
389
390 Ok(SubmissionPlan {
391 segments: segments.clone(),
392 constraints,
393 execution_steps,
394 existing_prs,
395 remote: remote.to_string(),
396 default_branch: default_branch.to_string(),
397 })
398}
399
400fn build_execution_steps(
404 segments: &[NarrowedBookmarkSegment],
405 bookmarks_needing_push: &[Bookmark],
406 prs_to_update_base: &[PrBaseUpdate],
407 prs_to_create: &[PrToCreate],
408 prs_to_publish: &[PullRequest],
409) -> Result<(Vec<ExecutionConstraint>, Vec<ExecutionStep>)> {
410 let stack_index = build_stack_index(segments);
411
412 let constraints =
414 collect_constraints(segments, prs_to_update_base, prs_to_create, &stack_index);
415
416 tracing::debug!(
417 constraint_count = constraints.len(),
418 "Collected execution constraints"
419 );
420
421 let (nodes, registry) = build_execution_nodes(
423 segments,
424 bookmarks_needing_push,
425 prs_to_update_base,
426 prs_to_create,
427 prs_to_publish,
428 );
429
430 let edges = resolve_constraints(&constraints, ®istry);
432
433 let steps = topo_sort_steps(&nodes, &edges)?;
435
436 Ok((constraints, steps))
437}
438
439fn build_stack_index(segments: &[NarrowedBookmarkSegment]) -> HashMap<String, usize> {
441 segments
442 .iter()
443 .enumerate()
444 .map(|(idx, seg)| (seg.bookmark.name.clone(), idx))
445 .collect()
446}
447
448fn collect_constraints(
454 segments: &[NarrowedBookmarkSegment],
455 prs_to_update_base: &[PrBaseUpdate],
456 prs_to_create: &[PrToCreate],
457 stack_index: &HashMap<String, usize>,
458) -> Vec<ExecutionConstraint> {
459 let mut constraints = Vec::new();
460
461 for window in segments.windows(2) {
463 constraints.push(ExecutionConstraint::PushOrder {
464 parent: PushRef(window[0].bookmark.name.clone()),
465 child: PushRef(window[1].bookmark.name.clone()),
466 });
467 }
468
469 for update in prs_to_update_base {
471 constraints.push(ExecutionConstraint::PushBeforeRetarget {
472 base: PushRef(update.expected_base.clone()),
473 pr: UpdateRef(update.bookmark.name.clone()),
474 });
475 }
476
477 for update in prs_to_update_base {
479 if update.expected_base != update.current_base {
480 let current_pos = stack_index.get(&update.current_base);
481 let bookmark_pos = stack_index.get(&update.bookmark.name);
482 if let (Some(¤t_pos), Some(&bookmark_pos)) = (current_pos, bookmark_pos)
483 && current_pos > bookmark_pos
484 {
485 constraints.push(ExecutionConstraint::RetargetBeforePush {
487 pr: UpdateRef(update.bookmark.name.clone()),
488 old_base: PushRef(update.current_base.clone()),
489 });
490 }
491 }
492 }
493
494 for create in prs_to_create {
496 constraints.push(ExecutionConstraint::PushBeforeCreate {
497 push: PushRef(create.bookmark.name.clone()),
498 create: CreateRef(create.bookmark.name.clone()),
499 });
500 }
501
502 for window in segments.windows(2) {
504 constraints.push(ExecutionConstraint::CreateOrder {
505 parent: CreateRef(window[0].bookmark.name.clone()),
506 child: CreateRef(window[1].bookmark.name.clone()),
507 });
508 }
509
510 constraints
511}
512
513fn build_execution_nodes(
515 segments: &[NarrowedBookmarkSegment],
516 bookmarks_needing_push: &[Bookmark],
517 prs_to_update_base: &[PrBaseUpdate],
518 prs_to_create: &[PrToCreate],
519 prs_to_publish: &[PullRequest],
520) -> (Vec<ExecutionNode>, NodeRegistry) {
521 let mut nodes = Vec::new();
522 let mut order = 0usize;
523 let mut registry = NodeRegistry::default();
524
525 let push_set: HashSet<_> = bookmarks_needing_push.iter().map(|b| &b.name).collect();
527
528 for seg in segments {
530 if push_set.contains(&seg.bookmark.name) {
531 let bookmark = bookmarks_needing_push
532 .iter()
533 .find(|b| b.name == seg.bookmark.name)
534 .expect("bookmark in push_set verified above")
535 .clone();
536 registry.register_push(&seg.bookmark.name, nodes.len());
537 nodes.push(ExecutionNode {
538 step: ExecutionStep::Push(bookmark),
539 order,
540 });
541 order += 1;
542 }
543 }
544
545 for bookmark in bookmarks_needing_push {
547 if !registry.push.contains_key(&bookmark.name) {
548 registry.register_push(&bookmark.name, nodes.len());
549 nodes.push(ExecutionNode {
550 step: ExecutionStep::Push(bookmark.clone()),
551 order,
552 });
553 order += 1;
554 }
555 }
556
557 for update in prs_to_update_base {
559 registry.register_update(&update.bookmark.name, nodes.len());
560 nodes.push(ExecutionNode {
561 step: ExecutionStep::UpdateBase(update.clone()),
562 order,
563 });
564 order += 1;
565 }
566
567 let create_set: HashSet<_> = prs_to_create.iter().map(|c| &c.bookmark.name).collect();
569 for seg in segments {
570 if create_set.contains(&seg.bookmark.name) {
571 let create = prs_to_create
572 .iter()
573 .find(|c| c.bookmark.name == seg.bookmark.name)
574 .expect("bookmark in create_set verified above")
575 .clone();
576 registry.register_create(&seg.bookmark.name, nodes.len());
577 nodes.push(ExecutionNode {
578 step: ExecutionStep::CreatePr(create),
579 order,
580 });
581 order += 1;
582 }
583 }
584
585 for pr in prs_to_publish {
587 registry.register_publish(&pr.head_ref, nodes.len());
588 nodes.push(ExecutionNode {
589 step: ExecutionStep::PublishPr(pr.clone()),
590 order,
591 });
592 order += 1;
593 }
594
595 (nodes, registry)
596}
597
598fn resolve_constraints(
603 constraints: &[ExecutionConstraint],
604 registry: &NodeRegistry,
605) -> Vec<Vec<usize>> {
606 let mut edges = vec![Vec::new(); registry.len()];
607
608 for constraint in constraints {
609 if let Some((from, to)) = constraint.resolve(registry) {
610 if !edges[from].contains(&to) {
611 edges[from].push(to);
612 tracing::trace!(%constraint, from, to, "Resolved constraint to edge");
613 }
614 } else {
615 tracing::trace!(%constraint, "Constraint skipped (endpoint not in plan)");
616 }
617 }
618
619 edges
620}
621
622fn topo_sort_steps(nodes: &[ExecutionNode], edges: &[Vec<usize>]) -> Result<Vec<ExecutionStep>> {
624 let mut indegree = vec![0usize; nodes.len()];
626 for edge_list in edges {
627 for &to in edge_list {
628 indegree[to] += 1;
629 }
630 }
631
632 let mut ready = BinaryHeap::new();
634 for (idx, node) in nodes.iter().enumerate() {
635 if indegree[idx] == 0 {
636 ready.push(Reverse((node.order, idx)));
637 }
638 }
639
640 let mut sorted = Vec::with_capacity(nodes.len());
641 while let Some(Reverse((_order, idx))) = ready.pop() {
642 sorted.push(idx);
643 for &to in &edges[idx] {
644 indegree[to] -= 1;
645 if indegree[to] == 0 {
646 ready.push(Reverse((nodes[to].order, to)));
647 }
648 }
649 }
650
651 if sorted.len() != nodes.len() {
652 let cycle_nodes: Vec<String> = nodes
654 .iter()
655 .enumerate()
656 .filter(|(idx, _)| indegree[*idx] > 0)
657 .map(|(_, node)| format!("{}", node.step))
658 .collect();
659
660 tracing::error!(
661 cycle_nodes = ?cycle_nodes,
662 "Scheduler cycle detected - this is a bug in jj-ryu"
663 );
664
665 return Err(Error::SchedulerCycle {
666 message:
667 "Dependency cycle in execution plan - this is a bug in jj-ryu, please report it"
668 .to_string(),
669 cycle_nodes,
670 });
671 }
672
673 Ok(sorted
674 .into_iter()
675 .map(|idx| nodes[idx].step.clone())
676 .collect())
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682
683 fn make_bookmark(name: &str, has_remote: bool, is_synced: bool) -> Bookmark {
684 Bookmark {
685 name: name.to_string(),
686 commit_id: format!("{name}_commit"),
687 change_id: format!("{name}_change"),
688 has_remote,
689 is_synced,
690 }
691 }
692
693 fn make_segment(name: &str) -> NarrowedBookmarkSegment {
694 NarrowedBookmarkSegment {
695 bookmark: make_bookmark(name, false, false),
696 changes: vec![],
697 }
698 }
699
700 fn make_pr(number: u64, bookmark: &str, base: &str) -> PullRequest {
701 PullRequest {
702 number,
703 html_url: format!("https://github.com/test/test/pull/{number}"),
704 base_ref: base.to_string(),
705 head_ref: bookmark.to_string(),
706 title: format!("PR for {bookmark}"),
707 node_id: Some(format!("PR_node_{number}")),
708 is_draft: false,
709 }
710 }
711
712 fn make_update(
713 bookmark: &Bookmark,
714 current_base: &str,
715 expected_base: &str,
716 pr_number: u64,
717 ) -> PrBaseUpdate {
718 PrBaseUpdate {
719 bookmark: bookmark.clone(),
720 current_base: current_base.to_string(),
721 expected_base: expected_base.to_string(),
722 pr: make_pr(pr_number, &bookmark.name, current_base),
723 }
724 }
725
726 fn make_create(bookmark: &Bookmark, base_branch: &str) -> PrToCreate {
727 PrToCreate {
728 bookmark: bookmark.clone(),
729 base_branch: base_branch.to_string(),
730 title: format!("Add {}", bookmark.name),
731 draft: false,
732 }
733 }
734
735 fn find_step_index(
736 steps: &[ExecutionStep],
737 predicate: impl Fn(&ExecutionStep) -> bool,
738 ) -> Option<usize> {
739 steps.iter().position(predicate)
740 }
741
742 #[test]
743 fn test_bookmark_needs_push() {
744 let bm1 = make_bookmark("feat-a", false, false);
745 assert!(!bm1.has_remote || !bm1.is_synced);
746
747 let bm2 = make_bookmark("feat-b", true, false);
748 assert!(!bm2.has_remote || !bm2.is_synced);
749
750 let bm3 = make_bookmark("feat-c", true, true);
751 assert!(bm3.has_remote && bm3.is_synced);
752 }
753
754 #[test]
755 fn test_pr_to_create_structure() {
756 let pr_create = PrToCreate {
757 bookmark: make_bookmark("feat-a", false, false),
758 base_branch: "main".to_string(),
759 title: "Add feature A".to_string(),
760 draft: false,
761 };
762
763 assert_eq!(pr_create.bookmark.name, "feat-a");
764 assert_eq!(pr_create.base_branch, "main");
765 assert_eq!(pr_create.title, "Add feature A");
766 assert!(!pr_create.draft);
767 }
768
769 #[test]
770 fn test_execution_steps_simple_push_order() {
771 let segments = vec![make_segment("a"), make_segment("b")];
772 let pushes = vec![
773 make_bookmark("a", false, false),
774 make_bookmark("b", false, false),
775 ];
776
777 let (_constraints, steps) =
778 build_execution_steps(&segments, &pushes, &[], &[], &[]).unwrap();
779
780 let push_a = find_step_index(
781 &steps,
782 |s| matches!(s, ExecutionStep::Push(b) if b.name == "a"),
783 );
784 let push_b = find_step_index(
785 &steps,
786 |s| matches!(s, ExecutionStep::Push(b) if b.name == "b"),
787 );
788
789 assert!(
790 push_a.unwrap() < push_b.unwrap(),
791 "pushes should follow stack order"
792 );
793 }
794
795 #[test]
796 fn test_execution_steps_push_before_create() {
797 let bm_a = make_bookmark("a", false, false);
798 let segments = vec![make_segment("a")];
799 let pushes = vec![bm_a.clone()];
800 let creates = vec![make_create(&bm_a, "main")];
801
802 let (_constraints, steps) =
803 build_execution_steps(&segments, &pushes, &[], &creates, &[]).unwrap();
804
805 let push_a = find_step_index(
806 &steps,
807 |s| matches!(s, ExecutionStep::Push(b) if b.name == "a"),
808 )
809 .unwrap();
810 let create_a = find_step_index(
811 &steps,
812 |s| matches!(s, ExecutionStep::CreatePr(c) if c.bookmark.name == "a"),
813 )
814 .unwrap();
815
816 assert!(push_a < create_a, "push must happen before create");
817 }
818
819 #[test]
820 fn test_execution_steps_create_order_follows_stack() {
821 let bm_a = make_bookmark("a", false, false);
822 let bm_b = make_bookmark("b", false, false);
823 let segments = vec![make_segment("a"), make_segment("b")];
824 let pushes = vec![bm_a.clone(), bm_b.clone()];
825 let creates = vec![make_create(&bm_a, "main"), make_create(&bm_b, "a")];
826
827 let (_constraints, steps) =
828 build_execution_steps(&segments, &pushes, &[], &creates, &[]).unwrap();
829
830 let create_a = find_step_index(
831 &steps,
832 |s| matches!(s, ExecutionStep::CreatePr(c) if c.bookmark.name == "a"),
833 )
834 .unwrap();
835 let create_b = find_step_index(
836 &steps,
837 |s| matches!(s, ExecutionStep::CreatePr(c) if c.bookmark.name == "b"),
838 )
839 .unwrap();
840
841 assert!(create_a < create_b, "creates should follow stack order");
842 }
843
844 #[test]
845 fn test_execution_steps_swap_order() {
846 let bm_a = make_bookmark("a", false, false);
848 let bm_b = make_bookmark("b", false, false);
849
850 let segments = vec![make_segment("b"), make_segment("a")];
852 let pushes = vec![bm_a.clone(), bm_b.clone()];
853 let updates = vec![
854 make_update(&bm_b, "a", "main", 2), make_update(&bm_a, "main", "b", 1), ];
857
858 let (_constraints, steps) =
859 build_execution_steps(&segments, &pushes, &updates, &[], &[]).unwrap();
860
861 let retarget_b = find_step_index(
862 &steps,
863 |s| matches!(s, ExecutionStep::UpdateBase(u) if u.bookmark.name == "b"),
864 )
865 .unwrap();
866 let push_a = find_step_index(
867 &steps,
868 |s| matches!(s, ExecutionStep::Push(b) if b.name == "a"),
869 )
870 .unwrap();
871 let push_b = find_step_index(
872 &steps,
873 |s| matches!(s, ExecutionStep::Push(b) if b.name == "b"),
874 )
875 .unwrap();
876
877 assert!(retarget_b < push_a, "b must move off a before pushing a");
878 assert!(
879 push_b < push_a,
880 "push order should follow new stack (b before a)"
881 );
882 }
883
884 #[test]
885 fn test_plan_is_empty() {
886 let plan = SubmissionPlan {
887 segments: vec![],
888 constraints: vec![],
889 execution_steps: vec![],
890 existing_prs: HashMap::new(),
891 remote: "origin".to_string(),
892 default_branch: "main".to_string(),
893 };
894
895 assert!(plan.is_empty());
896 assert_eq!(plan.count_pushes(), 0);
897 assert_eq!(plan.count_creates(), 0);
898 }
899
900 #[test]
901 fn test_plan_counts() {
902 let bm = make_bookmark("a", false, false);
903 let plan = SubmissionPlan {
904 segments: vec![make_segment("a")],
905 constraints: vec![],
906 execution_steps: vec![
907 ExecutionStep::Push(bm.clone()),
908 ExecutionStep::CreatePr(make_create(&bm, "main")),
909 ],
910 existing_prs: HashMap::new(),
911 remote: "origin".to_string(),
912 default_branch: "main".to_string(),
913 };
914
915 assert!(!plan.is_empty());
916 assert_eq!(plan.count_pushes(), 1);
917 assert_eq!(plan.count_creates(), 1);
918 assert_eq!(plan.count_updates(), 0);
919 assert_eq!(plan.count_publishes(), 0);
920 }
921}