Skip to main content

assay_core/
attempts.rs

1use crate::model::{AttemptRow, TestStatus};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum FailureClass {
5    DeterministicPass,
6    DeterministicFail,
7    Flaky,    // fail -> pass
8    Unstable, // pass <-> fail patterns (non-monotonic)
9    Error,    // any error (or deterministic error)
10    Skipped,
11}
12
13pub fn classify_attempts(attempts: &[AttemptRow]) -> FailureClass {
14    use TestStatus::*;
15
16    if attempts.is_empty() {
17        return FailureClass::Error;
18    }
19
20    if attempts.len() == 1 {
21        if let Skipped = attempts[0].status {
22            return FailureClass::Skipped;
23        }
24    }
25
26    let has_error = attempts.iter().any(|a| matches!(a.status, Error));
27    if has_error {
28        return FailureClass::Error;
29    }
30
31    let statuses: Vec<TestStatus> = attempts.iter().map(|a| a.status.clone()).collect();
32
33    let any_fail = statuses.iter().any(|s| matches!(s, Fail));
34    let any_pass = statuses.iter().any(|s| matches!(s, Pass | Warn | Flaky));
35    // Note: Treats Warn/Flaky as form of pass for "did it work eventually?" perspective
36    // or strictly Pass? Plan says "Pass: all pass".
37    // But for Flaky detection: Fail -> Pass.
38    // Let's stick to explicit Match types.
39
40    if any_fail && any_pass {
41        // Detect classic flaky: Fail... then Pass (and stays Pass?)
42        // User definition: "fail->pass".
43        // Unstable: "mixed".
44
45        let first_fail_idx = statuses.iter().position(|s| matches!(s, Fail));
46        let first_pass_idx = statuses.iter().position(|s| matches!(s, Pass)); // Strictly Pass?
47
48        if let (Some(fail_i), Some(pass_i)) = (first_fail_idx, first_pass_idx) {
49            if fail_i < pass_i {
50                // Check if it stays passing?
51                // User snippet: "Fail then later Pass, and last is Pass"
52                let last = statuses.last().unwrap();
53                if matches!(last, Pass) {
54                    return FailureClass::Flaky;
55                }
56            }
57        }
58        return FailureClass::Unstable;
59    }
60
61    if any_fail {
62        return FailureClass::DeterministicFail;
63    }
64
65    // If we are here, no errors, no fails.
66    FailureClass::DeterministicPass
67}