Is there a way to construct tests in Rust to throw a warning when not exhaustive?

Viewed 240

Is there a way to construct tests in Rust to throw a warning when not exhaustive? Certainly, I don't expect a solution for this in general, but I'm looking for a solution that would work when the arguments to a function are enumerated types. I'd like to check that all combinations are used in a way a match statement checks that all combinations are covered. For example, consider the code:

// Terrible numerical type
#[derive(Debug,PartialEq)]
pub enum Num {
    Int(i32),
    Float(f32),
}

// Mathematical operation on this terrible type
pub fn myadd(x : crate::Num, y :Num) -> Num {
    match (x,y) {
        (Num::Int(x),Num::Int(y)) => Num::Int(x+y),
        (Num::Int(x),Num::Float(y)) => Num::Float((x as f32) + y),
        (Num::Float(x),Num::Int(y)) => Num::Float(x+(y as f32)),
        (Num::Float(x),Num::Float(y)) => Num::Float(x+y),
    }
}

// Add testing
#[cfg(test)]
mod test{
    use super::*;

    #[test]
    fn int_int() {
        assert_eq!(myadd(Num::Int(1),Num::Int(2)),Num::Int(3));
    }

    #[test]
    fn float_int() {
        assert_eq!(myadd(Num::Float(1.),Num::Int(2)),Num::Float(3.));
    }

    #[test]
    fn int_float() {
        assert_eq!(myadd(Num::Int(1),Num::Float(2.)),Num::Float(3.));
    }
}

Here, we're missing the test float_float. I'd like a way to throw a warning to denote this test is missing. If we forgot the Float,Float case in pattern matching, we'd get the error:

error[E0004]: non-exhaustive patterns: `(Float(_), Float(_))` not covered
  --> src/lib.rs:10:11
   |
10 |     match (x,y) {
   |           ^^^^^ pattern `(Float(_), Float(_))` not covered
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms

I'm trying to get something similar for the testing combinations. In case it matters, I don't care if all of the tests are combined to a single function rather than split into four different tests. I didn't know if there was a trick using pattern matching to achieve this or some other mechanism.

1 Answers

This is by no means an ideal solution, but you can do this by using a pretty short macro:

macro_rules! exhaustive_tests {
    (
        $group_name:ident = match $type:ty {
            $( $test_name:ident = $pattern:pat => $body:block )*
        }
    ) => {
        paste::item!{
            #[allow(warnings)]
            fn [< exhaustive_check_ $group_name >]() {
                let expr: $type = unreachable!();
                match expr {
                    $( $pattern => unreachable!(), )*
                }
            }
        }

        $(
            #[test] fn $test_name() { $body }
        )*
    };
}

The macro will let you declare a group of tests, with a pattern for each test that must be exhaustive to compile.

exhaustive_tests!{
    my_tests = match (Num, Num) {
        int_int = (Num::Int(_), Num::Int(_)) => {
            assert_eq!(myadd(Num::Int(1),Num::Int(2)),Num::Int(3));
        }
        float_int = (Num::Float(_), Num::Int(_)) => {
            assert_eq!(myadd(Num::Int(1),Num::Int(2)),Num::Int(3));
        }
        int_float = (Num::Int(_), Num::Float(_)) => {
            assert_eq!(myadd(Num::Int(1),Num::Float(2.)),Num::Float(3.));
        }
    }
}

In addition to generating each test (#[test] fn $test_name() { $body }), it will create a single function exhaustive_check_... with a match statement with the pattern for each test. The match statement must cover all patterns (as usual), otherwise compilation will fail:

// generated by macro
fn exhaustive_check_my_tests() {
    let expr: (Num, Num) = unreachable!();
    match expr {
        (Num::Int(_), Num::Int(_)) => unreachable!(),
        (Num::Float(_), Num::Int(_)) => unreachable!(),
        (Num::Int(_), Num::Float(_)) => unreachable!(),
        // error[E0004]: non-exhaustive patterns: `(Float(_), Float(_))` not covered
    }
}

Try it in Playground

Related