Rust macro to generate multiple individual tests

Viewed 519

Is it possible to have a macro that generates standalone tests? I have two text files, one with an input and another with an output. Each new line in the text file represents a new test. Currently, this is how I run my tests:

    #[test]
    fn it_works() {
        let input = read_file("input.txt").expect("failed to read input");
        let input = input.split("\n").collect::<Vec<_>>();

        let output = read_file("output.txt").expect("failed to read output");
        let output = output.split("\n").collect::<Vec<_>>();

        input.iter().zip(output).for_each(|(a, b)| {
            println!("a: {}, b: {}", a, b);
            assert_eq!(b, get_result(a));
        })

But, as you can see, if one test fail, all of them fail, since there's a loop inside a single test. And I need each iteration to be a single and isolated test, without having to repeat myself.

So I was wondering if it's possible to achieve that by using macros?

The macro ideally would output something like:

    #[test]
    fn it_works_1() {
        let input = read_file("input.txt").expect("failed to read input");
        let input = input.split("\n").collect::<Vec<_>>();

        let output = read_file("output.txt").expect("failed to read output");
        let output = output.split("\n").collect::<Vec<_>>();

        assert_eq!(output[0], get_result(input[0])); // first test
    }

    #[test]
    fn it_works_2() {
        let input = read_file("input.txt").expect("failed to read input");
        let input = input.split("\n").collect::<Vec<_>>();

        let output = read_file("output.txt").expect("failed to read output");
        let output = output.split("\n").collect::<Vec<_>>();

        assert_eq!(output[1], get_result(input[1])); // second test
    }

    // ... the N remaining tests: it_works_n() 
2 Answers

You can't do this with a declarative macro because a declarative macro cannot generate an identifier to name the test functions. However you can use a crate such as test-case, which can run the same test with different inputs:

use test_case::test_case;

#[test_case(0)]
#[test_case(1)]
#[test_case(2)]
#[test]
fn it_works(index: usize) {
    let input = read_file("input.txt").expect("failed to read input");
    let input = input.split("\n").collect::<Vec<_>>();

    let output = read_file("output.txt").expect("failed to read output");
    let output = output.split("\n").collect::<Vec<_>>();

    assert_eq!(output[index], get_result(input[index])); // first test
}

If you have a lot of different inputs to test, you could use a declarative macro to generate the code above, which would add all of the #[test_case] annotations.

After Peter Hall answer, I was able to achieve what I wanted. I added the seq_macro crate to generate the repeated #[test_case]'s. Maybe there's a way to loop through all test cases instead of manually defining the amount of tests (like I did), but this is good for now:

macro_rules! test {
    ( $from:expr, $to:expr ) => {
        #[cfg(test)]
        mod tests {
            use crate::{get_result, read_file};
            use seq_macro::seq;
            use test_case::test_case;

            seq!(N in $from..$to {
                #(#[test_case(N)])*
                fn it_works(index: usize) {
                    let input = read_file("input.txt").expect("failed to read input");
                    let input = input.split("\n").collect::<Vec<_>>();

                    let output = read_file("output.txt").expect("failed to read output");
                    let output = output.split("\n").collect::<Vec<_>>();

                    let res = get_result(input[index]);
                    assert_eq!(
                        output[index], res,
                        "Test '{}': Want '{}' got '{}'",
                        input[index], output[index], res
                    );
                }
            });
        }
    };
}

test!(0, 82);
Related