fallow_output/
pr_status.rs1use serde::{Deserialize, Serialize};
2
3use crate::{PrDecisionConclusion, PrDecisionSurface};
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct PrStatusContext {
8 pub name: String,
11 pub conclusion: PrDecisionConclusion,
13 pub summary: String,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum PrStatusMode {
20 Aggregate,
22 Split,
24 AggregateAndSplit,
26}
27
28pub fn pr_status_contexts_with_mode(
30 surface: &PrDecisionSurface,
31 mode: PrStatusMode,
32) -> Vec<PrStatusContext> {
33 match mode {
34 PrStatusMode::Aggregate => vec![aggregate_status_context(surface)],
35 PrStatusMode::Split => split_status_contexts(surface),
36 PrStatusMode::AggregateAndSplit => {
37 let mut contexts = vec![aggregate_status_context(surface)];
38 contexts.extend(split_status_contexts(surface));
39 contexts
40 }
41 }
42}
43
44fn split_status_contexts(surface: &PrDecisionSurface) -> Vec<PrStatusContext> {
45 surface
46 .gates
47 .iter()
48 .map(|gate| PrStatusContext {
49 name: format!("Fallow / {}", gate.id),
50 conclusion: gate.status,
51 summary: match &gate.threshold {
52 Some(threshold) => {
53 format!("{}: {} (threshold {threshold})", gate.label, gate.observed)
54 }
55 None => format!("{}: {}", gate.label, gate.observed),
56 },
57 })
58 .collect()
59}
60
61fn aggregate_status_context(surface: &PrDecisionSurface) -> PrStatusContext {
62 let total = surface.gates.len();
63 let failing = surface
64 .gates
65 .iter()
66 .filter(|gate| gate.status == PrDecisionConclusion::Failure)
67 .count();
68 let neutral = surface
69 .gates
70 .iter()
71 .filter(|gate| gate.status == PrDecisionConclusion::Neutral)
72 .count();
73 let summary = if total == 0 {
74 "No PR gates reported.".to_owned()
75 } else if failing > 0 {
76 format!("{failing} of {total} PR gates failed.")
77 } else if neutral > 0 {
78 format!("{neutral} of {total} PR gates need review.")
79 } else {
80 format!("All {total} PR gates passed.")
81 };
82 PrStatusContext {
83 name: "Fallow".to_owned(),
84 conclusion: surface.conclusion,
85 summary,
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::{PR_DECISION_SCHEMA, PrDecisionDetails, PrDecisionGate, PrDecisionSurface};
93
94 #[test]
95 fn builds_status_contexts_from_decision_gates() {
96 let surface = PrDecisionSurface {
97 schema: PR_DECISION_SCHEMA.to_owned(),
98 title: "Fallow".to_owned(),
99 conclusion: PrDecisionConclusion::Neutral,
100 gates: vec![PrDecisionGate {
101 id: "duplication".to_owned(),
102 label: "Duplication".to_owned(),
103 status: PrDecisionConclusion::Neutral,
104 observed: "2 clone groups".to_owned(),
105 threshold: Some("configured rules".to_owned()),
106 scope: "new code".to_owned(),
107 }],
108 annotations: vec![],
109 details: PrDecisionDetails {
110 summary_markdown: "Review recommended.".to_owned(),
111 full_report_path: None,
112 details_url: None,
113 },
114 };
115
116 let contexts = pr_status_contexts_with_mode(&surface, PrStatusMode::Split);
117
118 assert_eq!(contexts.len(), 1);
119 assert_eq!(contexts[0].name, "Fallow / duplication");
120 assert_eq!(contexts[0].conclusion, PrDecisionConclusion::Neutral);
121 assert_eq!(
122 contexts[0].summary,
123 "Duplication: 2 clone groups (threshold configured rules)"
124 );
125 }
126
127 #[test]
128 fn aggregate_and_split_status_contexts_keep_one_consolidated_context() {
129 let surface = PrDecisionSurface {
130 schema: PR_DECISION_SCHEMA.to_owned(),
131 title: "Fallow".to_owned(),
132 conclusion: PrDecisionConclusion::Neutral,
133 gates: vec![PrDecisionGate {
134 id: "health".to_owned(),
135 label: "Health".to_owned(),
136 status: PrDecisionConclusion::Neutral,
137 observed: "1 finding".to_owned(),
138 threshold: None,
139 scope: "new code".to_owned(),
140 }],
141 annotations: vec![],
142 details: PrDecisionDetails {
143 summary_markdown: "Review recommended.".to_owned(),
144 full_report_path: None,
145 details_url: None,
146 },
147 };
148
149 let contexts = pr_status_contexts_with_mode(&surface, PrStatusMode::AggregateAndSplit);
150
151 assert_eq!(contexts.len(), 2);
152 assert_eq!(contexts[0].name, "Fallow");
153 assert_eq!(contexts[0].summary, "1 of 1 PR gates need review.");
154 assert_eq!(contexts[1].name, "Fallow / health");
155 }
156}