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