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