1#![cfg_attr(docsrs, feature(doc_cfg))]
31#![warn(missing_docs)]
32#![warn(rust_2018_idioms)]
33
34use dev_report::{CheckResult, Evidence, Report, Severity};
35
36#[derive(Debug, Clone)]
38pub struct FlakyRun {
39 name: String,
40 version: String,
41 iterations: u32,
42}
43
44impl FlakyRun {
45 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
47 Self {
48 name: name.into(),
49 version: version.into(),
50 iterations: 10,
51 }
52 }
53
54 pub fn iterations(mut self, n: u32) -> Self {
56 self.iterations = n.max(2);
57 self
58 }
59
60 pub fn iteration_count(&self) -> u32 {
62 self.iterations
63 }
64
65 pub fn execute(&self) -> Result<FlakyResult, FlakyError> {
69 Ok(FlakyResult {
70 name: self.name.clone(),
71 version: self.version.clone(),
72 iterations: self.iterations,
73 tests: Vec::new(),
74 })
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct TestReliability {
81 pub name: String,
83 pub passes: u32,
85 pub failures: u32,
87}
88
89impl TestReliability {
90 pub fn reliability(&self) -> f64 {
92 let total = self.passes + self.failures;
93 if total == 0 {
94 return 0.0;
95 }
96 self.passes as f64 / total as f64
97 }
98
99 pub fn is_stable(&self) -> bool {
101 self.failures == 0
102 }
103
104 pub fn is_broken(&self) -> bool {
106 self.passes == 0
107 }
108
109 pub fn is_flaky(&self) -> bool {
111 self.passes > 0 && self.failures > 0
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct FlakyResult {
118 pub name: String,
120 pub version: String,
122 pub iterations: u32,
124 pub tests: Vec<TestReliability>,
126}
127
128impl FlakyResult {
129 pub fn flaky_count(&self) -> usize {
131 self.tests.iter().filter(|t| t.is_flaky()).count()
132 }
133
134 pub fn into_report(self) -> Report {
139 let mut report = Report::new(&self.name, &self.version).with_producer("dev-flaky");
140 if self.tests.is_empty() {
141 report.push(CheckResult::pass("flaky::scan"));
142 } else {
143 for t in &self.tests {
144 let name = format!("flaky::{}", t.name);
145 let reliability_pct = t.reliability() * 100.0;
146 let detail = format!(
147 "{}/{} passed ({:.1}%)",
148 t.passes,
149 t.passes + t.failures,
150 reliability_pct
151 );
152 let check = if t.is_broken() {
153 CheckResult::fail(name, Severity::Error).with_detail(detail)
154 } else if t.is_flaky() {
155 CheckResult::warn(name, Severity::Warning).with_detail(detail)
156 } else {
157 CheckResult::pass(name).with_detail(detail)
158 };
159 report.push(
160 check.with_evidence(Evidence::numeric("reliability_pct", reliability_pct)),
161 );
162 }
163 }
164 report.finish();
165 report
166 }
167}
168
169#[derive(Debug)]
171pub enum FlakyError {
172 SubprocessFailed(String),
174 ParseError(String),
176}
177
178impl std::fmt::Display for FlakyError {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
182 Self::ParseError(s) => write!(f, "parse error: {s}"),
183 }
184 }
185}
186
187impl std::error::Error for FlakyError {}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn iterations_min_two() {
195 let r = FlakyRun::new("x", "0.1.0").iterations(1);
196 assert_eq!(r.iteration_count(), 2);
197 }
198
199 #[test]
200 fn stable_test_classified_correctly() {
201 let t = TestReliability {
202 name: "a".into(),
203 passes: 10,
204 failures: 0,
205 };
206 assert!(t.is_stable());
207 assert!(!t.is_flaky());
208 assert!(!t.is_broken());
209 assert_eq!(t.reliability(), 1.0);
210 }
211
212 #[test]
213 fn broken_test_classified_correctly() {
214 let t = TestReliability {
215 name: "b".into(),
216 passes: 0,
217 failures: 10,
218 };
219 assert!(t.is_broken());
220 assert!(!t.is_flaky());
221 assert_eq!(t.reliability(), 0.0);
222 }
223
224 #[test]
225 fn flaky_test_classified_correctly() {
226 let t = TestReliability {
227 name: "c".into(),
228 passes: 7,
229 failures: 3,
230 };
231 assert!(t.is_flaky());
232 assert!(!t.is_stable());
233 assert!(!t.is_broken());
234 assert!((t.reliability() - 0.7).abs() < 0.0001);
235 }
236
237 #[test]
238 fn empty_result_passes() {
239 let r = FlakyResult {
240 name: "x".into(),
241 version: "0.1.0".into(),
242 iterations: 10,
243 tests: Vec::new(),
244 };
245 let report = r.into_report();
246 assert!(report.passed());
247 }
248
249 #[test]
250 fn broken_test_produces_failing_report() {
251 let r = FlakyResult {
252 name: "x".into(),
253 version: "0.1.0".into(),
254 iterations: 10,
255 tests: vec![TestReliability {
256 name: "broken".into(),
257 passes: 0,
258 failures: 10,
259 }],
260 };
261 assert!(r.into_report().failed());
262 }
263
264 #[test]
265 fn flaky_count_correct() {
266 let r = FlakyResult {
267 name: "x".into(),
268 version: "0.1.0".into(),
269 iterations: 10,
270 tests: vec![
271 TestReliability {
272 name: "stable".into(),
273 passes: 10,
274 failures: 0,
275 },
276 TestReliability {
277 name: "flaky_a".into(),
278 passes: 7,
279 failures: 3,
280 },
281 TestReliability {
282 name: "flaky_b".into(),
283 passes: 5,
284 failures: 5,
285 },
286 ],
287 };
288 assert_eq!(r.flaky_count(), 2);
289 }
290}