bolero_hydro/test/
mod.rs

1#![cfg_attr(fuzzing_random, allow(dead_code))]
2
3use bolero_engine::{
4    driver::{self, exhaustive, object::Object},
5    rng, Engine, Failure, Seed, TargetLocation, Test,
6};
7use core::{fmt, mem::size_of, time::Duration};
8use std::path::PathBuf;
9
10type ExhastiveDriver = Box<Object<exhaustive::Driver>>;
11
12mod input;
13mod outcome;
14mod report;
15
16/// Engine implementation which mimics Rust's default test
17/// harness. By default, the test inputs will include any present
18/// `corpus` and `crashes` files, as well as generating
19#[derive(Debug)]
20pub struct TestEngine {
21    location: TargetLocation,
22    rng_cfg: rng::Options,
23}
24
25struct NamedTest {
26    name: String,
27    data: input::Test,
28}
29
30impl fmt::Display for NamedTest {
31    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32        if let input::Test::Rng(test) = &self.data {
33            write!(f, "[BOLERO_RANDOM_SEED={}]", test.seed)
34        } else {
35            write!(f, "{}", self.name)
36        }
37    }
38}
39
40impl From<input::RngTest> for NamedTest {
41    #[inline]
42    fn from(value: input::RngTest) -> Self {
43        Self {
44            name: String::new(),
45            data: input::Test::Rng(value),
46        }
47    }
48}
49
50impl TestEngine {
51    #[allow(dead_code)]
52    pub fn new(location: TargetLocation) -> Self {
53        Self {
54            location,
55            rng_cfg: Default::default(),
56        }
57    }
58
59    pub fn with_test_time(&mut self, test_time: Duration) -> &mut Self {
60        self.rng_cfg.test_time = self.rng_cfg.test_time.or(Some(test_time));
61        self
62    }
63
64    pub fn with_iterations(&mut self, iterations: usize) -> &mut Self {
65        self.rng_cfg.iterations = self.rng_cfg.iterations.or(Some(iterations));
66        self
67    }
68
69    pub fn with_max_len(&mut self, max_len: usize) -> &mut Self {
70        self.rng_cfg.max_len = self.rng_cfg.max_len.or(Some(max_len));
71        self
72    }
73
74    fn sub_dir<'a, D: Iterator<Item = &'a str>>(&self, dirs: D) -> PathBuf {
75        let mut fuzz_target_path = self
76            .location
77            .work_dir()
78            .expect("could not resolve target work dir");
79
80        fuzz_target_path.extend(dirs);
81
82        fuzz_target_path
83    }
84
85    fn file_tests<'a, D: Iterator<Item = &'a str> + std::panic::UnwindSafe>(
86        &self,
87        sub_dirs: D,
88    ) -> impl Iterator<Item = NamedTest> {
89        std::fs::read_dir(self.sub_dir(sub_dirs))
90            .ok()
91            .into_iter()
92            .flat_map(move |dir| {
93                dir.filter_map(Result::ok)
94                    .map(|item| item.path())
95                    .filter(|path| path.is_file())
96                    .filter(|path| !path.file_name().unwrap().to_str().unwrap().starts_with('.'))
97                    .map(move |path| NamedTest {
98                        name: format!("{}", path.display()),
99                        data: input::Test::File(input::FileTest { path }),
100                    })
101            })
102    }
103
104    fn seed_tests(&self) -> impl Iterator<Item = input::RngTest> {
105        self.rng_cfg
106            .seed
107            .into_iter()
108            .map(move |seed| input::RngTest { seed })
109    }
110
111    fn rng_tests(&self) -> impl Iterator<Item = input::RngTest> {
112        use rand::{rngs::StdRng, RngCore, SeedableRng};
113
114        let iterations = self.rng_cfg.iterations_or_default();
115        // use StdRng for high entropy seeds
116        let mut seed_rng = StdRng::from_os_rng();
117
118        (0..iterations).map(move |_| {
119            let mut seed = [0; size_of::<Seed>()];
120            seed_rng.fill_bytes(&mut seed);
121            let seed = Seed::from_le_bytes(seed);
122            input::RngTest { seed }
123        })
124    }
125
126    #[cfg(fuzzing_random)]
127    fn tests(&self) -> impl Iterator<Item = NamedTest> {
128        self.seed_tests()
129            .map(|t| t.into())
130            .chain(self.rng_tests().map(|t| t.into()))
131    }
132
133    #[cfg(not(fuzzing_random))]
134    fn tests(&self) -> impl Iterator<Item = NamedTest> {
135        self.seed_tests()
136            .map(|t| t.into())
137            .chain(self.file_tests(["crashes"].iter().cloned()))
138            .chain(self.file_tests(["afl_state", "crashes"].iter().cloned()))
139            .chain(self.file_tests(["afl_state", "hangs"].iter().cloned()))
140            .chain(self.file_tests(["corpus"].iter().cloned()))
141            .chain(self.file_tests(["afl_state", "queue"].iter().cloned()))
142            .chain(self.rng_tests().map(|t| t.into()))
143    }
144
145    fn run_with_value<T>(self, test: T, options: driver::Options) -> bolero_engine::Never
146    where
147        T: Test,
148        T::Value: core::fmt::Debug,
149    {
150        if options.exhaustive() {
151            let mut buffer = vec![];
152
153            assert!(!options.replay_on_fail(), "replay_on_fail is not supported with run_with_value");
154            let testfn = |mut driver: Box<Object<exhaustive::Driver>>, _is_replay: bool, test: &mut T| {
155                let mut input = input::ExhastiveInput {
156                    driver: &mut driver,
157                    buffer: &mut buffer,
158                };
159
160                let result = match test.test(&mut input) {
161                    Ok(is_valid) => Ok(is_valid),
162                    Err(error) => {
163                        // restart the driver to replay what was selected
164                        input.driver.replay();
165                        let input = test.generate_value(&mut input);
166                        let error = Failure {
167                            seed: None,
168                            error,
169                            hide_error: false,
170                            input,
171                        };
172                        Err(error.to_string())
173                    }
174                };
175
176                (driver, result)
177            };
178
179            return self.run_exhaustive(test, testfn, options);
180        }
181
182        let file_options = options.clone();
183        let rng_options = options;
184
185        let file_options = &file_options;
186        let rng_options = &rng_options;
187
188        let mut buffer = vec![];
189        let mut cache = driver::cache::Cache::default();
190
191        assert!(!rng_options.replay_on_fail(), "replay_on_fail is not supported with run_with_value");
192        let testfn = |test: &mut T, _is_replay: bool, data: &input::Test| {
193            buffer.clear();
194            match data {
195                input::Test::File(file) => {
196                    file.read_into(&mut buffer);
197
198                    let mut input = input::Bytes::new(&buffer, file_options);
199                    test.test(&mut input).map_err(|error| {
200                        let shrunken = test.shrink(buffer.clone(), data.seed(), file_options);
201
202                        if let Some((_, shrunken)) = shrunken {
203                            format!("{shrunken:#}")
204                        } else {
205                            format!(
206                                "{:#}",
207                                Failure {
208                                    seed: data.seed(),
209                                    error,
210                                    hide_error: false,
211                                    input: buffer.clone()
212                                }
213                            )
214                        }
215                    })
216                }
217                input::Test::Rng(conf) => {
218                    let mut input = conf.input(&mut buffer, &mut cache, rng_options);
219                    test.test(&mut input).map_err(|error| {
220                        let shrunken = if rng_options.shrink_time_or_default().is_zero() {
221                            None
222                        } else {
223                            // reseed the input and buffer the rng for shrinking
224                            let mut input = conf.buffered_input(&mut buffer, rng_options);
225                            let _ = test.generate_value(&mut input);
226
227                            test.shrink(buffer.clone(), data.seed(), rng_options)
228                        };
229
230                        if let Some((_, shrunken)) = shrunken {
231                            format!("{shrunken:#}")
232                        } else {
233                            buffer.clear();
234                            let mut input = conf.input(&mut buffer, &mut cache, rng_options);
235                            let input = test.generate_value(&mut input);
236                            format!(
237                                "{:#}",
238                                Failure {
239                                    seed: data.seed(),
240                                    error,
241                                    hide_error: false,
242                                    input
243                                }
244                            )
245                        }
246                    })
247                }
248            }
249        };
250
251        self.run_tests(test, testfn, rng_options.replay_on_fail())
252    }
253
254    #[cfg(feature = "std")]
255    fn run_with_scope<T, R>(self, test: T, options: driver::Options)
256    where
257        T: FnMut(bool) -> R + core::panic::RefUnwindSafe,
258        R: bolero_engine::IntoResult,
259    {
260        if options.exhaustive() {
261            let replay_on_fail = options.replay_on_fail();
262            let testfn = |driver: ExhastiveDriver, is_replay: bool, test: &mut T| {
263                if is_replay {
264                    bolero_engine::any::scope::with(driver, || {
265                        test(true);
266                    });
267
268                    unreachable!("Did not crash when replaying, is it deterministic?");
269                } else {
270                    let (driver, result) = bolero_engine::any::run(driver, || test(is_replay));
271                    let result = result.map_err(|error| {
272                        Failure {
273                            seed: None,
274                            error,
275                            hide_error: replay_on_fail,
276                            input: driver.serialize(),
277                        }
278                        .to_string()
279                    });
280                    (driver, result)
281                }
282            };
283
284            return self.run_exhaustive(test, testfn, options);
285        }
286
287        let file_options = options.clone();
288        let rng_options = options;
289
290        let file_options = &file_options;
291        let rng_options = &rng_options;
292
293        let mut buffer = vec![];
294        // TODO
295        // let mut cache = driver::cache::Cache::default();
296        let file_driver = bolero_engine::driver::bytes::Driver::new(vec![], file_options);
297        let file_driver = bolero_engine::driver::object::Object(file_driver);
298        let file_driver = Box::new(file_driver);
299        let mut file_driver = Some(file_driver);
300
301        let testfn = |test: &mut T, is_replay: bool, data: &input::Test| {
302            buffer.clear();
303            match data {
304                input::Test::File(file) => {
305                    let mut driver = file_driver.take().unwrap();
306
307                    let mut buf = core::mem::take(&mut buffer);
308                    file.read_into(&mut buf);
309                    driver.reset(buf, file_options);
310
311                    if is_replay {
312                        bolero_engine::any::scope::with(driver, || {
313                            test(true);
314                        });
315
316                        unreachable!("Did not crash when replaying, is it deterministic?");
317                    } else {
318                        let (mut driver, result) = bolero_engine::any::run(driver, || test(false));
319                        buffer = driver.reset(vec![], file_options);
320                        file_driver = Some(driver);
321
322                        // TODO shrinking
323
324                        result.map_err(|error| {
325                            Failure {
326                                seed: None,
327                                error,
328                                hide_error: false,
329                                input: (), // TODO figure out a better input to show
330                            }
331                            .to_string()
332                        })
333                    }
334                }
335                input::Test::Rng(conf) => {
336                    let seed = conf.seed;
337                    let driver = conf.driver(rng_options);
338                    let driver = Box::new(Object(driver));
339
340                    if is_replay {
341                        bolero_engine::any::scope::with(driver, || {
342                            test(true);
343                        });
344
345                        unreachable!("Did not crash when replaying, is it deterministic?");
346                    } else {
347                        let (_driver, result) = bolero_engine::any::run(driver, || test(is_replay));
348
349                        // TODO shrinking
350
351                        result.map_err(|error| {
352                            Failure {
353                                seed: Some(seed),
354                                error,
355                                hide_error: false,
356                                input: (), // TODO figure out a better input to show
357                            }
358                            .to_string()
359                        })
360                    }
361                }
362            }
363        };
364
365        self.run_tests(test, testfn, rng_options.replay_on_fail())
366    }
367
368    fn run_tests<S, T>(mut self, mut state: S, mut testfn: T, replay_on_fail: bool)
369    where
370        T: FnMut(&mut S, bool, &input::Test) -> Result<bool, String>,
371    {
372        // if we're fuzzing with cargo-bolero and the iteration count isn't specified
373        // then go forever
374        if cfg!(fuzzing_random) && self.rng_cfg.iterations.is_none() {
375            self.rng_cfg.iterations = Some(usize::MAX);
376        }
377
378        let tests = self.tests();
379
380        let start_time = std::time::Instant::now();
381        let test_time = if cfg!(fuzzing_random) {
382            self.rng_cfg.test_time
383        } else {
384            Some(self.rng_cfg.test_time_or_default()).filter(|v| *v < Duration::MAX)
385        };
386
387        let mut report = report::Report::default();
388        if cfg!(fuzzing_random) {
389            report.spawn_timer();
390        }
391
392        let mut outcome = outcome::Outcome::new(&self.location, start_time);
393
394        bolero_engine::panic::set_hook();
395        bolero_engine::panic::forward_panic(false);
396
397        for input in tests {
398            if let Some(test_time) = test_time {
399                if start_time.elapsed() > test_time {
400                    outcome.on_exit(outcome::ExitReason::MaxDurationExceeded {
401                        limit: test_time,
402                        default: self.rng_cfg.test_time.is_none(),
403                    });
404                    break;
405                }
406            }
407
408            outcome.on_named_test(&input.data);
409
410            match testfn(&mut state, false, &input.data) {
411                Ok(is_valid) => {
412                    report.on_result(is_valid);
413                }
414                Err(err) => {
415                    bolero_engine::panic::forward_panic(true);
416                    outcome.on_exit(outcome::ExitReason::TestFailure);
417                    drop(outcome);
418                    eprintln!("{err}");
419
420                    if replay_on_fail {
421                        let _ = testfn(&mut state, true, &input.data).expect_err("Did not crash when replaying, is it deterministic?");
422                    }
423                    panic!("test failed");
424                }
425            }
426        }
427    }
428
429    fn run_exhaustive<S, F>(self, mut state: S, mut testfn: F, options: driver::Options)
430    where
431        F: FnMut(ExhastiveDriver, bool, &mut S) -> (ExhastiveDriver, Result<bool, String>),
432    {
433        bolero_engine::panic::set_hook();
434        bolero_engine::panic::forward_panic(false);
435
436        let driver = exhaustive::Driver::new(&options);
437        let mut driver = Box::new(Object(driver));
438        let test_time = self.rng_cfg.test_time;
439        let start_time = std::time::Instant::now();
440
441        let mut report = report::Report::default();
442        // when running exhaustive tests, it's nice to have the progress displayed
443        report.spawn_timer();
444
445        let mut outcome = outcome::Outcome::new(&self.location, start_time);
446
447        while driver.step().is_continue() {
448            if let Some(test_time) = test_time {
449                if start_time.elapsed() > test_time {
450                    outcome.on_exit(outcome::ExitReason::MaxDurationExceeded {
451                        limit: test_time,
452                        default: false,
453                    });
454                    break;
455                }
456            }
457
458            outcome.on_exhaustive_input();
459
460            let (drvr, result) = testfn(driver, false, &mut state);
461            driver = drvr;
462
463            match result {
464                Ok(is_valid) => {
465                    report.on_estimate(driver.estimate());
466                    report.on_result(is_valid);
467                }
468                Err(error) => {
469                    bolero_engine::panic::forward_panic(true);
470                    outcome.on_exit(outcome::ExitReason::TestFailure);
471                    drop(outcome);
472                    eprintln!("{error}");
473
474                    if options.replay_on_fail() {
475                        driver.replay();
476                        let _ = testfn(driver, true, &mut state).1.expect_err("Did not crash when replaying, is it deterministic?");
477                    }
478
479                    panic!("test failed");
480                }
481            }
482        }
483    }
484}
485
486impl<T> Engine<T> for TestEngine
487where
488    T: Test,
489    T::Value: core::fmt::Debug,
490{
491    type Output = ();
492
493    fn run(self, test: T, options: driver::Options) -> Self::Output {
494        self.run_with_value(test, options);
495        bolero_engine::panic::forward_panic(true);
496    }
497}
498
499#[cfg(feature = "std")]
500impl bolero_engine::ScopedEngine for TestEngine {
501    type Output = ();
502
503    fn run<F, R>(self, test: F, options: driver::Options) -> Self::Output
504    where
505        F: FnMut(bool) -> R + core::panic::RefUnwindSafe,
506        R: bolero_engine::IntoResult,
507    {
508        self.run_with_scope(test, options);
509        bolero_engine::panic::forward_panic(true);
510    }
511}