#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use dev_report::{CheckResult, Evidence, Report, Severity};
#[derive(Debug, Clone)]
pub struct FlakyRun {
name: String,
version: String,
iterations: u32,
}
impl FlakyRun {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
iterations: 10,
}
}
pub fn iterations(mut self, n: u32) -> Self {
self.iterations = n.max(2);
self
}
pub fn iteration_count(&self) -> u32 {
self.iterations
}
pub fn execute(&self) -> Result<FlakyResult, FlakyError> {
Ok(FlakyResult {
name: self.name.clone(),
version: self.version.clone(),
iterations: self.iterations,
tests: Vec::new(),
})
}
}
#[derive(Debug, Clone)]
pub struct TestReliability {
pub name: String,
pub passes: u32,
pub failures: u32,
}
impl TestReliability {
pub fn reliability(&self) -> f64 {
let total = self.passes + self.failures;
if total == 0 {
return 0.0;
}
self.passes as f64 / total as f64
}
pub fn is_stable(&self) -> bool {
self.failures == 0
}
pub fn is_broken(&self) -> bool {
self.passes == 0
}
pub fn is_flaky(&self) -> bool {
self.passes > 0 && self.failures > 0
}
}
#[derive(Debug, Clone)]
pub struct FlakyResult {
pub name: String,
pub version: String,
pub iterations: u32,
pub tests: Vec<TestReliability>,
}
impl FlakyResult {
pub fn flaky_count(&self) -> usize {
self.tests.iter().filter(|t| t.is_flaky()).count()
}
pub fn into_report(self) -> Report {
let mut report = Report::new(&self.name, &self.version).with_producer("dev-flaky");
if self.tests.is_empty() {
report.push(CheckResult::pass("flaky::scan"));
} else {
for t in &self.tests {
let name = format!("flaky::{}", t.name);
let reliability_pct = t.reliability() * 100.0;
let detail = format!(
"{}/{} passed ({:.1}%)",
t.passes,
t.passes + t.failures,
reliability_pct
);
let check = if t.is_broken() {
CheckResult::fail(name, Severity::Error).with_detail(detail)
} else if t.is_flaky() {
CheckResult::warn(name, Severity::Warning).with_detail(detail)
} else {
CheckResult::pass(name).with_detail(detail)
};
report.push(
check.with_evidence(Evidence::numeric("reliability_pct", reliability_pct)),
);
}
}
report.finish();
report
}
}
#[derive(Debug)]
pub enum FlakyError {
SubprocessFailed(String),
ParseError(String),
}
impl std::fmt::Display for FlakyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SubprocessFailed(s) => write!(f, "subprocess failed: {s}"),
Self::ParseError(s) => write!(f, "parse error: {s}"),
}
}
}
impl std::error::Error for FlakyError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iterations_min_two() {
let r = FlakyRun::new("x", "0.1.0").iterations(1);
assert_eq!(r.iteration_count(), 2);
}
#[test]
fn stable_test_classified_correctly() {
let t = TestReliability {
name: "a".into(),
passes: 10,
failures: 0,
};
assert!(t.is_stable());
assert!(!t.is_flaky());
assert!(!t.is_broken());
assert_eq!(t.reliability(), 1.0);
}
#[test]
fn broken_test_classified_correctly() {
let t = TestReliability {
name: "b".into(),
passes: 0,
failures: 10,
};
assert!(t.is_broken());
assert!(!t.is_flaky());
assert_eq!(t.reliability(), 0.0);
}
#[test]
fn flaky_test_classified_correctly() {
let t = TestReliability {
name: "c".into(),
passes: 7,
failures: 3,
};
assert!(t.is_flaky());
assert!(!t.is_stable());
assert!(!t.is_broken());
assert!((t.reliability() - 0.7).abs() < 0.0001);
}
#[test]
fn empty_result_passes() {
let r = FlakyResult {
name: "x".into(),
version: "0.1.0".into(),
iterations: 10,
tests: Vec::new(),
};
let report = r.into_report();
assert!(report.passed());
}
#[test]
fn broken_test_produces_failing_report() {
let r = FlakyResult {
name: "x".into(),
version: "0.1.0".into(),
iterations: 10,
tests: vec![TestReliability {
name: "broken".into(),
passes: 0,
failures: 10,
}],
};
assert!(r.into_report().failed());
}
#[test]
fn flaky_count_correct() {
let r = FlakyResult {
name: "x".into(),
version: "0.1.0".into(),
iterations: 10,
tests: vec![
TestReliability {
name: "stable".into(),
passes: 10,
failures: 0,
},
TestReliability {
name: "flaky_a".into(),
passes: 7,
failures: 3,
},
TestReliability {
name: "flaky_b".into(),
passes: 5,
failures: 5,
},
],
};
assert_eq!(r.flaky_count(), 2);
}
}