Skip to main content

fallow_output/
pr_status.rs

1use 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}