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