1pub mod code_similarity;
13pub mod commit_message;
14pub mod dependency_freshness;
15pub mod report;
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
23#[serde(rename_all = "lowercase")]
24pub enum Status {
25 Pass,
26 Warn,
27 Fail,
28}
29
30impl std::fmt::Display for Status {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::Pass => write!(f, "pass"),
34 Self::Warn => write!(f, "warn"),
35 Self::Fail => write!(f, "fail"),
36 }
37 }
38}
39
40#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
56pub struct Thresholds {
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub warn: Option<u64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub fail: Option<u64>,
61}
62
63#[derive(Debug, PartialEq)]
75pub struct Evaluation {
76 pub target: String,
77 pub outcome: Outcome,
78}
79
80impl Evaluation {
81 pub fn completed(
82 target: impl Into<String>,
83 observed: u64,
84 thresholds: Thresholds,
85 evidence: Vec<Evidence>,
86 ) -> Self {
87 Self {
88 target: target.into(),
89 outcome: Outcome::completed(observed, thresholds, evidence),
90 }
91 }
92
93 pub fn errored(target: impl Into<String>, error: ExecutionError) -> Self {
94 Self {
95 target: target.into(),
96 outcome: Outcome::Errored(error),
97 }
98 }
99
100 #[must_use]
101 pub fn is_pass(&self) -> bool {
102 self.has_status(Status::Pass)
103 }
104
105 #[must_use]
106 pub fn is_warn(&self) -> bool {
107 self.has_status(Status::Warn)
108 }
109
110 #[must_use]
111 pub fn is_fail(&self) -> bool {
112 self.has_status(Status::Fail)
113 }
114
115 fn has_status(&self, expected: Status) -> bool {
116 matches!(&self.outcome, Outcome::Completed { status, .. } if *status == expected)
117 }
118
119 #[must_use]
120 pub fn is_error(&self) -> bool {
121 matches!(&self.outcome, Outcome::Errored(_))
122 }
123}
124
125#[derive(Debug, PartialEq)]
127pub enum Outcome {
128 Completed {
129 status: Status,
130 observed: u64,
131 thresholds: Thresholds,
132 evidence: Vec<Evidence>,
133 },
134 Errored(ExecutionError),
135}
136
137impl Outcome {
138 #[must_use]
140 pub fn completed(observed: u64, thresholds: Thresholds, evidence: Vec<Evidence>) -> Self {
141 Self::Completed {
142 status: derive_status(observed, &thresholds),
143 observed,
144 thresholds,
145 evidence,
146 }
147 }
148}
149
150#[derive(Debug, PartialEq, Serialize)]
152pub struct ExecutionError {
153 pub code: String,
154 pub message: String,
155 pub recovery: String,
156}
157
158#[derive(Clone, Debug, PartialEq, Serialize)]
170#[serde(untagged)]
171pub enum Expected {
172 Text(String),
173 List(Vec<String>),
174}
175
176#[derive(Debug, PartialEq, Serialize)]
195pub struct Evidence {
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub rule: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub location: Option<String>,
200 pub found: String,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub expected: Option<Expected>,
203}
204
205impl Evidence {
206 #[must_use]
207 pub fn new(rule: &str, found: &str) -> Self {
208 Self {
209 rule: Some(rule.into()),
210 location: None,
211 found: found.into(),
212 expected: None,
213 }
214 }
215
216 #[must_use]
217 pub fn with_expected(rule: &str, found: &str, expected: Expected) -> Self {
218 Self {
219 rule: Some(rule.into()),
220 location: None,
221 found: found.into(),
222 expected: Some(expected),
223 }
224 }
225}
226
227pub(crate) fn derive_status(observed: u64, thresholds: &Thresholds) -> Status {
228 let higher_is_worse = match (thresholds.warn, thresholds.fail) {
229 (Some(w), Some(f)) => w < f,
230 _ => true,
231 };
232
233 let exceeds = if higher_is_worse {
234 |observed: u64, threshold: u64| observed > threshold
235 } else {
236 |observed: u64, threshold: u64| observed < threshold
237 };
238
239 if let Some(fail) = thresholds.fail
240 && exceeds(observed, fail)
241 {
242 return Status::Fail;
243 }
244
245 if let Some(warn) = thresholds.warn
246 && exceeds(observed, warn)
247 {
248 return Status::Warn;
249 }
250
251 Status::Pass
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use test_case::test_case;
258
259 #[test_case(3, None, Some(5), Status::Pass ; "below fail without warn passes")]
261 #[test_case(5, None, Some(5), Status::Pass ; "at fail without warn passes")]
262 #[test_case(10, None, Some(5), Status::Fail ; "above fail without warn fails")]
263 #[test_case(5, Some(5), Some(10), Status::Pass ; "at warn threshold passes")]
264 #[test_case(10, Some(3), Some(10), Status::Warn ; "at fail with warn returns warn")]
265 fn higher_is_worse(observed: u64, warn: Option<u64>, fail: Option<u64>, expected: Status) {
266 assert_eq!(
267 derive_status(observed, &Thresholds { warn, fail }),
268 expected
269 );
270 }
271
272 #[test_case(40, Status::Fail ; "below fail threshold fails")]
274 #[test_case(50, Status::Warn ; "at fail threshold returns warn")]
275 #[test_case(70, Status::Pass ; "at warn threshold passes")]
276 #[test_case(80, Status::Pass ; "above warn threshold passes")]
277 fn lower_is_worse(observed: u64, expected: Status) {
278 let thresholds = Thresholds {
279 warn: Some(70),
280 fail: Some(50),
281 };
282 assert_eq!(derive_status(observed, &thresholds), expected);
283 }
284
285 #[test]
286 fn evaluation_is_pass_for_completed_pass() {
287 let eval = Evaluation::completed(
288 "test",
289 0,
290 Thresholds {
291 warn: None,
292 fail: Some(0),
293 },
294 vec![],
295 );
296
297 assert!(eval.is_pass());
298 assert!(!eval.is_fail());
299 assert!(!eval.is_warn());
300 assert!(!eval.is_error());
301 }
302
303 #[test]
304 fn evaluation_is_fail_for_completed_fail() {
305 let eval = Evaluation::completed(
306 "test",
307 1,
308 Thresholds {
309 warn: None,
310 fail: Some(0),
311 },
312 vec![],
313 );
314
315 assert!(eval.is_fail());
316 assert!(!eval.is_pass());
317 }
318
319 #[test]
320 fn evaluation_is_error_for_errored_outcome() {
321 let eval = Evaluation::errored(
322 "test",
323 ExecutionError {
324 code: "boom".into(),
325 message: "broken".into(),
326 recovery: "fix".into(),
327 },
328 );
329
330 assert!(eval.is_error());
331 assert!(!eval.is_pass());
332 assert!(!eval.is_fail());
333 }
334}