sample_test/
tester.rs

1//! Test utilities for [`sample_std::Sample`].
2//!
3//! It is a direct port of [`quickcheck::QuickCheck`], with some key differences:
4//!
5//! - We use the [`Debug`] impl of tuples whose parts impl [`Debug`]. This means we
6//!   can create a single general [`Testable::shrink`] definition.
7//! - We use an iterative shrinking process instead of a recursive one (see
8//!   [`Testable::shrink`]). This allows us to halt after a fixed number of
9//!   shrinking steps, which sidesteps accidental infinite shrinking
10//!   implementations and avoids the potential for stack overflows.
11use std::cmp;
12use std::env;
13use std::fmt::Debug;
14use std::panic;
15
16use sample_std::{Random, Sample};
17
18use crate::tester::Status::{Discard, Fail, Pass};
19use crate::{error, info, trace};
20
21/// The main [SampleTest] type for setting configuration and running sample-based testing.
22pub struct SampleTest {
23    tests: u64,
24    max_tests: u64,
25    min_tests_passed: u64,
26    gen: Random,
27}
28
29fn st_tests() -> u64 {
30    let default = 100;
31    match env::var("SAMPLE_TEST_TESTS") {
32        Ok(val) => val.parse().unwrap_or(default),
33        Err(_) => default,
34    }
35}
36
37fn st_max_tests() -> u64 {
38    let default = 10_000;
39    match env::var("SAMPLE_TEST_MAX_TESTS") {
40        Ok(val) => val.parse().unwrap_or(default),
41        Err(_) => default,
42    }
43}
44
45fn st_min_tests_passed() -> u64 {
46    let default = 0;
47    match env::var("SAMPLE_TEST_MIN_TESTS_PASSED") {
48        Ok(val) => val.parse().unwrap_or(default),
49        Err(_) => default,
50    }
51}
52
53impl SampleTest {
54    /// Creates a new [SampleTest] value.
55    ///
56    /// This can be used to run [SampleTest] on things that implement [Testable].
57    /// You may also adjust the configuration, such as the number of tests to
58    /// run.
59    ///
60    /// By default, the maximum number of passed tests is set to `100`, the max
61    /// number of overall tests is set to `10000` and the generator is created
62    /// with a size of `100`.
63    pub fn new() -> SampleTest {
64        let gen = Random::new();
65        let tests = st_tests();
66        let max_tests = cmp::max(tests, st_max_tests());
67        let min_tests_passed = st_min_tests_passed();
68
69        SampleTest {
70            tests,
71            max_tests,
72            min_tests_passed,
73            gen,
74        }
75    }
76
77    /// Set the number of tests to run.
78    ///
79    /// This actually refers to the maximum number of *passed* tests that
80    /// can occur. Namely, if a test causes a failure, future testing on that
81    /// property stops. Additionally, if tests are discarded, there may be
82    /// fewer than `tests` passed.
83    pub fn tests(mut self, tests: u64) -> SampleTest {
84        self.tests = tests;
85        self
86    }
87
88    /// Set the maximum number of tests to run.
89    ///
90    /// The number of invocations of a property will never exceed this number.
91    /// This is necessary to cap the number of tests because [SampleTest]
92    /// properties can discard tests.
93    pub fn max_tests(mut self, max_tests: u64) -> SampleTest {
94        self.max_tests = max_tests;
95        self
96    }
97
98    /// Set the minimum number of tests that needs to pass.
99    ///
100    /// This actually refers to the minimum number of *valid* *passed* tests
101    /// that needs to pass for the property to be considered successful.
102    pub fn min_tests_passed(mut self, min_tests_passed: u64) -> SampleTest {
103        self.min_tests_passed = min_tests_passed;
104        self
105    }
106
107    /// Tests a property and returns the result.
108    ///
109    /// The result returned is either the number of tests passed or a witness
110    /// of failure.
111    ///
112    /// (If you're using Rust's unit testing infrastructure, then you'll
113    /// want to use the `sample_test` method, which will `panic!` on failure.)
114    pub fn sample_test_count<S, A>(&mut self, mut s: S, f: A) -> Result<u64, TestResult>
115    where
116        A: Testable<S>,
117        S: Sample,
118        S::Output: Clone + Debug,
119    {
120        let mut n_tests_passed = 0;
121        for _ in 0..self.max_tests {
122            if n_tests_passed >= self.tests {
123                break;
124            }
125            match f.test_once(&mut s, &mut self.gen) {
126                TestResult { status: Pass, .. } => n_tests_passed += 1,
127                TestResult {
128                    status: Discard, ..
129                } => continue,
130                r @ TestResult { status: Fail, .. } => return Err(r),
131            }
132        }
133        Ok(n_tests_passed)
134    }
135
136    /// Tests a property and calls `panic!` on failure.
137    ///
138    /// The `panic!` message will include a (hopefully) minimal witness of
139    /// failure.
140    ///
141    /// It is appropriate to use this method with Rust's unit testing
142    /// infrastructure.
143    ///
144    /// Note that if the environment variable `RUST_LOG` is set to enable
145    /// `info` level log messages for the `sample_test` crate, then this will
146    /// include output on how many [SampleTest] tests were passed.
147    ///
148    /// # Example
149    ///
150    /// ```rust
151    /// use sample_test::{SampleTest};
152    /// use sample_std::VecSampler;
153    ///
154    /// fn prop_reverse_reverse() {
155    ///     fn revrev(xs: Vec<usize>) -> bool {
156    ///         let rev: Vec<_> = xs.clone().into_iter().rev().collect();
157    ///         let revrev: Vec<_> = rev.into_iter().rev().collect();
158    ///         xs == revrev
159    ///     }
160    ///     let sampler = (VecSampler { length: (0..20), el: (0..100usize) },);
161    ///     SampleTest::new().sample_test(sampler, revrev as fn(Vec<usize>) -> bool);
162    /// }
163    /// ```
164    pub fn sample_test<S, A>(&mut self, s: S, f: A)
165    where
166        A: Testable<S>,
167        S: Sample,
168        S::Output: Clone + Debug,
169    {
170        // Ignore log init failures, implying it has already been done.
171        let _ = crate::env_logger_init();
172
173        let n_tests_passed = match self.sample_test_count(s, f) {
174            Ok(n_tests_passed) => n_tests_passed,
175            Err(result) => panic!("{}", result.failed_msg()),
176        };
177
178        if n_tests_passed >= self.min_tests_passed {
179            info!("(Passed {} SampleTest tests.)", n_tests_passed)
180        } else {
181            panic!(
182                "(Unable to generate enough tests, {} not discarded.)",
183                n_tests_passed
184            )
185        }
186    }
187}
188
189/// Convenience function for running [SampleTest].
190///
191/// This is an alias for `SampleTest::new().sample_test(f)`.
192pub fn sample_test<S, A>(s: S, f: A)
193where
194    A: Testable<S>,
195    S: Sample,
196    S::Output: Clone + Debug,
197{
198    SampleTest::new().sample_test(s, f)
199}
200
201/// Describes the status of a single instance of a test.
202///
203/// All testable things must be capable of producing a `TestResult`.
204#[derive(Clone, Debug)]
205pub struct TestResult {
206    status: Status,
207    arguments: String,
208    err: Option<String>,
209}
210
211/// Whether a test has passed, failed or been discarded.
212#[derive(Clone, Debug)]
213enum Status {
214    Pass,
215    Fail,
216    Discard,
217}
218
219impl TestResult {
220    /// Produces a test result that indicates the current test has passed.
221    pub fn passed() -> TestResult {
222        TestResult::from_bool(true)
223    }
224
225    /// Produces a test result that indicates the current test has failed.
226    pub fn failed() -> TestResult {
227        TestResult::from_bool(false)
228    }
229
230    /// Produces a test result that indicates failure from a runtime error.
231    pub fn error<S: Into<String>>(msg: S) -> TestResult {
232        let mut r = TestResult::from_bool(false);
233        r.err = Some(msg.into());
234        r
235    }
236
237    /// Produces a test result that instructs `sample_test` to ignore it.
238    /// This is useful for restricting the domain of your properties.
239    /// When a test is discarded, `sample_test` will replace it with a
240    /// fresh one (up to a certain limit).
241    pub fn discard() -> TestResult {
242        TestResult {
243            status: Discard,
244            arguments: String::from(""),
245            err: None,
246        }
247    }
248
249    /// Converts a `bool` to a `TestResult`. A `true` value indicates that
250    /// the test has passed and a `false` value indicates that the test
251    /// has failed.
252    pub fn from_bool(b: bool) -> TestResult {
253        TestResult {
254            status: if b { Pass } else { Fail },
255            arguments: String::from(""),
256            err: None,
257        }
258    }
259
260    /// Tests if a "procedure" fails when executed. The test passes only if
261    /// `f` generates a task failure during its execution.
262    pub fn must_fail<T, F>(f: F) -> TestResult
263    where
264        F: FnOnce() -> T,
265        F: 'static,
266        T: 'static,
267    {
268        let f = panic::AssertUnwindSafe(f);
269        TestResult::from_bool(panic::catch_unwind(f).is_err())
270    }
271
272    /// Returns `true` if and only if this test result describes a successful
273    /// test.
274    pub fn is_success(&self) -> bool {
275        match self.status {
276            Pass => true,
277            Fail | Discard => false,
278        }
279    }
280
281    /// Returns `true` if and only if this test result describes a failing
282    /// test.
283    pub fn is_failure(&self) -> bool {
284        match self.status {
285            Fail => true,
286            Pass | Discard => false,
287        }
288    }
289
290    /// Returns `true` if and only if this test result describes a failing
291    /// test as a result of a run time error.
292    pub fn is_error(&self) -> bool {
293        self.is_failure() && self.err.is_some()
294    }
295
296    pub fn arguments(&self) -> &str {
297        &self.arguments
298    }
299
300    fn failed_msg(&self) -> String {
301        match self.err {
302            None => format!("[sample_test] TEST FAILED. Arguments: ({})", self.arguments),
303            Some(ref err) => format!(
304                "[sample_test] TEST FAILED (runtime error). \
305                 Arguments: ({})\nError: {}",
306                self.arguments, err
307            ),
308        }
309    }
310}
311
312/// `Testable` describes types (e.g., a function) whose values can be
313/// tested.
314///
315/// Anything that can be tested must be capable of producing a [TestResult]
316/// from the output of an instance of [Sample].
317///
318/// It's unlikely that you'll have to implement this trait yourself.
319pub trait Testable<S>: 'static
320where
321    S: Sample,
322{
323    /// Report a [`TestResult`] from a given value.
324    fn result(&self, v: S::Output) -> TestResult;
325
326    /// Convenience function for running this [`Testable`] once on a random
327    /// value, and shrinking any failures.
328    fn test_once(&self, s: &mut S, rng: &mut Random) -> TestResult
329    where
330        S::Output: Clone + Debug,
331    {
332        let v = Sample::generate(s, rng);
333        let r = self.result(v.clone());
334        match r.status {
335            Pass | Discard => r,
336            Fail => {
337                error!("{:?}", r);
338                self.shrink(s, r, v)
339            }
340        }
341    }
342
343    /// Iteratively shrink the given test result until the iteration limit is
344    /// reached or no further shrinkage is possible.
345    fn shrink(&self, s: &S, r: TestResult, v: S::Output) -> TestResult
346    where
347        S::Output: Clone + Debug,
348    {
349        trace!("shrinking {:?}", v);
350        let mut result = r;
351        let mut it = s.shrink(v);
352        let iterations = 10_000_000;
353
354        for _ in 0..iterations {
355            let sv = it.next();
356            if let Some(sv) = sv {
357                let r_new = self.result(sv.clone());
358                if r_new.is_failure() {
359                    trace!("shrinking {:?}", sv);
360                    result = r_new;
361                    it = s.shrink(sv);
362                }
363            } else {
364                return result;
365            }
366        }
367
368        trace!(
369            "halting shrinkage after {} iterations with: {:?}",
370            iterations,
371            result
372        );
373
374        result
375    }
376}
377
378impl From<bool> for TestResult {
379    fn from(value: bool) -> TestResult {
380        TestResult::from_bool(value)
381    }
382}
383
384impl From<()> for TestResult {
385    fn from(_: ()) -> TestResult {
386        TestResult::passed()
387    }
388}
389
390impl<A, E> From<Result<A, E>> for TestResult
391where
392    TestResult: From<A>,
393    E: Debug + 'static,
394{
395    fn from(value: Result<A, E>) -> TestResult {
396        match value {
397            Ok(r) => r.into(),
398            Err(err) => TestResult::error(format!("{:?}", err)),
399        }
400    }
401}
402
403macro_rules! testable_fn {
404    ($($name: ident),*) => {
405
406impl<T: 'static, S, $($name),*> Testable<S> for fn($($name),*) -> T
407where
408    TestResult: From<T>,
409    S: Sample<Output=($($name),*,)>,
410    ($($name),*,): Clone,
411    $($name: Debug + 'static),*
412{
413    #[allow(non_snake_case)]
414    fn result(&self, v: S::Output) -> TestResult {
415        let ( $($name,)* ) = v.clone();
416        let f: fn($($name),*) -> T = *self;
417        let mut r = <TestResult as From<Result<T, String>>>::from(safe(move || {f($($name),*)}));
418
419        {
420            let ( $(ref $name,)* ) = v;
421            r.arguments = format!("{:?}", &($($name),*));
422        }
423        r
424    }
425}}}
426
427testable_fn!(A);
428testable_fn!(A, B);
429testable_fn!(A, B, C);
430testable_fn!(A, B, C, D);
431testable_fn!(A, B, C, D, E);
432testable_fn!(A, B, C, D, E, F);
433testable_fn!(A, B, C, D, E, F, G);
434testable_fn!(A, B, C, D, E, F, G, H);
435
436fn safe<T, F>(fun: F) -> Result<T, String>
437where
438    F: FnOnce() -> T,
439    F: 'static,
440    T: 'static,
441{
442    panic::catch_unwind(panic::AssertUnwindSafe(fun)).map_err(|any_err| {
443        // Extract common types of panic payload:
444        // panic and assert produce &str or String
445        if let Some(&s) = any_err.downcast_ref::<&str>() {
446            s.to_owned()
447        } else if let Some(s) = any_err.downcast_ref::<String>() {
448            s.to_owned()
449        } else {
450            "UNABLE TO SHOW RESULT OF PANIC.".to_owned()
451        }
452    })
453}