Skip to main content

dev_coverage/
producer.rs

1//! [`Producer`] integration: wrap a [`CoverageRun`] + [`CoverageThreshold`]
2//! and emit a [`Report`] every time the producer runs.
3//!
4//! [`Producer`]: dev_report::Producer
5//! [`Report`]: dev_report::Report
6
7use dev_report::{CheckResult, Producer, Report, Severity};
8
9use crate::{Baseline, CoverageRun, CoverageThreshold};
10
11/// `Producer` adapter that runs a [`CoverageRun`], compares the result
12/// against the configured [`CoverageThreshold`], and (optionally) flags
13/// regressions against a stored [`Baseline`].
14///
15/// Subprocess failures map to a failing `CheckResult` named
16/// `coverage::<subject>` with `Severity::Critical`. No panics.
17///
18/// # Example
19///
20/// ```no_run
21/// use dev_coverage::{CoverageProducer, CoverageRun, CoverageThreshold};
22/// use dev_report::Producer;
23///
24/// let producer = CoverageProducer::new(
25///     CoverageRun::new("my-crate", "0.1.0"),
26///     CoverageThreshold::min_line_pct(80.0),
27/// );
28/// let report = producer.produce();
29/// println!("{}", report.to_json().unwrap());
30/// ```
31pub struct CoverageProducer {
32    run: CoverageRun,
33    threshold: CoverageThreshold,
34    baseline: Option<Baseline>,
35    regression_tolerance_pct: f64,
36}
37
38impl CoverageProducer {
39    /// Build a producer with a threshold only.
40    pub fn new(run: CoverageRun, threshold: CoverageThreshold) -> Self {
41        Self {
42            run,
43            threshold,
44            baseline: None,
45            regression_tolerance_pct: 0.0,
46        }
47    }
48
49    /// Compare each run against the given baseline. When the current
50    /// result regresses by more than `tolerance_pct`, a separate
51    /// `coverage::regression` check is pushed alongside the threshold check.
52    pub fn with_baseline(mut self, baseline: Baseline, tolerance_pct: f64) -> Self {
53        self.baseline = Some(baseline);
54        self.regression_tolerance_pct = tolerance_pct;
55        self
56    }
57}
58
59impl Producer for CoverageProducer {
60    fn produce(&self) -> Report {
61        let subject = self.run.subject().to_string();
62        let version = self.run.subject_version().to_string();
63        let mut report = Report::new(&subject, &version).with_producer("dev-coverage");
64        match self.run.execute() {
65            Ok(result) => {
66                if let Some(baseline) = &self.baseline {
67                    let diff = result.diff(baseline, self.regression_tolerance_pct);
68                    let detail = format!(
69                        "line {:+.2}pp, function {:+.2}pp, region {:+.2}pp (tolerance {:.2}pp)",
70                        diff.line_pct_delta,
71                        diff.function_pct_delta,
72                        diff.region_pct_delta,
73                        self.regression_tolerance_pct
74                    );
75                    let regression_check = if diff.regressed {
76                        CheckResult::fail(
77                            format!("coverage::regression::{}", subject),
78                            Severity::Error,
79                        )
80                        .with_detail(detail)
81                        .with_tag("coverage")
82                        .with_tag("regression")
83                    } else {
84                        CheckResult::pass(format!("coverage::regression::{}", subject))
85                            .with_detail(detail)
86                            .with_tag("coverage")
87                    };
88                    report.push(regression_check);
89                }
90                report.push(result.into_check_result(self.threshold));
91            }
92            Err(e) => {
93                let detail = e.to_string();
94                let check = CheckResult::fail(format!("coverage::{}", subject), Severity::Critical)
95                    .with_detail(detail)
96                    .with_tag("coverage")
97                    .with_tag("subprocess");
98                report.push(check);
99            }
100        }
101        report.finish();
102        report
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn produce_emits_subprocess_fail_when_tool_missing() {
112        // We can't easily mock `cargo llvm-cov` here, but if the tool
113        // happens to be installed the call succeeds and we just check
114        // that a non-empty report comes back. If absent, we get the
115        // failing critical check. Either path is acceptable for the
116        // contract: no panic, returns a Report.
117        let producer = CoverageProducer::new(
118            CoverageRun::new("self", "0.0.0"),
119            CoverageThreshold::min_line_pct(80.0),
120        );
121        let report = producer.produce();
122        assert_eq!(report.subject, "self");
123        assert!(!report.checks.is_empty());
124    }
125}