Skip to main content

dev_fuzz/
producer.rs

1//! [`Producer`] integration: wrap a [`FuzzRun`] and emit a [`Report`]
2//! 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::FuzzRun;
10
11/// `Producer` adapter that drives a [`FuzzRun`] and converts the result
12/// into a `Report`.
13///
14/// Subprocess failures (missing tool, missing nightly, target not
15/// found, libFuzzer harness error) map to a single failing
16/// `CheckResult` named `fuzz::<target>` with `Severity::Critical`. No
17/// panics.
18///
19/// # Example
20///
21/// ```no_run
22/// use dev_fuzz::{FuzzBudget, FuzzProducer, FuzzRun};
23/// use dev_report::Producer;
24/// use std::time::Duration;
25///
26/// let producer = FuzzProducer::new(
27///     FuzzRun::new("parse_input", "0.1.0")
28///         .budget(FuzzBudget::time(Duration::from_secs(60))),
29/// );
30/// let report = producer.produce();
31/// println!("{}", report.to_json().unwrap());
32/// ```
33pub struct FuzzProducer {
34    run: FuzzRun,
35}
36
37impl FuzzProducer {
38    /// Build a producer that drives the given [`FuzzRun`].
39    pub fn new(run: FuzzRun) -> Self {
40        Self { run }
41    }
42}
43
44impl Producer for FuzzProducer {
45    fn produce(&self) -> Report {
46        let target = self.run.target_name().to_string();
47        let version = self.run.subject_version().to_string();
48        let mut report = Report::new(&target, &version).with_producer("dev-fuzz");
49        match self.run.execute() {
50            Ok(result) => {
51                let r = result.into_report();
52                // `into_report` already populated `producer` and
53                // started/finished times; replace `report` with the
54                // result-derived one so we don't double-finish.
55                return r;
56            }
57            Err(e) => {
58                let detail = e.to_string();
59                let check = CheckResult::fail(format!("fuzz::{target}"), Severity::Critical)
60                    .with_detail(detail)
61                    .with_tag("fuzz")
62                    .with_tag("subprocess");
63                report.push(check);
64            }
65        }
66        report.finish();
67        report
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use dev_report::Verdict;
75
76    #[test]
77    fn produce_maps_run_failure_to_critical_fail() {
78        // An empty directory has no fuzz/ project, so the run fails fast
79        // whichever prerequisite is missing first (cargo-fuzz, nightly,
80        // or the target itself).
81        let dir = tempfile::tempdir().unwrap();
82        let producer =
83            FuzzProducer::new(FuzzRun::new("nonexistent_target", "0.0.0").in_dir(dir.path()));
84        let report = producer.produce();
85        assert_eq!(report.subject, "nonexistent_target");
86        assert_eq!(report.checks.len(), 1);
87        let check = &report.checks[0];
88        assert_eq!(check.name, "fuzz::nonexistent_target");
89        assert_eq!(check.verdict, Verdict::Fail);
90        assert_eq!(check.severity, Some(Severity::Critical));
91        assert!(check.has_tag("subprocess"));
92    }
93
94    #[test]
95    fn produce_maps_missing_workdir_to_critical_fail() {
96        let producer =
97            FuzzProducer::new(FuzzRun::new("parse", "0.0.0").in_dir("definitely/not/a/dir/7f3a"));
98        let report = producer.produce();
99        assert_eq!(report.checks.len(), 1);
100        assert_eq!(report.checks[0].verdict, Verdict::Fail);
101        assert!(report.checks[0]
102            .detail
103            .as_deref()
104            .unwrap_or("")
105            .contains("does not exist"));
106    }
107}