How can I test a collection of !Eq types?

I have some integration tests where I end up with a bunch of values in a vec. Order doesn't matter, but they're in a vec because they don't implement Eq so I can't put them in a set. (I can't just implement Eq because it's generated code.)

I also have some assertions that I want to apply to each of these things. As far as I can tell, the only way to do this is to iterate over the checks (which return bool), and assert on an any() iterator over the values. Like this, except pretend &str doesn't implement Eq :

fn main() {
    let list_of_values = vec!["magic", "founftain"];

    let check_magic = |c: &str| -> bool { matches!(c, "magic") };

    let check_fountain = |c: &str| -> bool { matches!(c, "fountain") };

    let checks: Vec<Box<dyn Fn(&str) -> bool>> =
        vec![Box::new(check_magic), Box::new(check_fountain)];

    checks
        .iter()
        .for_each(|chk| assert!(list_of_values.iter().copied().any(chk)));
}

I don't really care about the O(mn) behaviour, because there's a small set of possible values. But I can't really get any information out of the assert. Specifically:

checks
    .iter()
    .for_each(|chk| {
        assert!(list_of_values.iter().copied().any(chk))
    });

gives

thread 'main' panicked at 'assertion failed: list_of_values.iter().copied().any(chk)', src/main.rs:13:25

There is also the flaw that this does not test for uniqueness of each assertion, but that's not hugely important.

I can't just put the assertions in the check function, because that won't work with any() - my tests will fail on the first assertion failure, rather than try the rest of the items in the vec.

Is there a way to (a) simplify this a bit (eg. asserting deeply nested structures leads to a lot of indentation) and (b) get more information from assertion failures?

For part A, it might be possible to use a procedural macro to implement Eq. Could you tell us more about this generated code?

For part B, if you could define your checks in a macro in order to capture their names as strings also, you could at least print the name of the failing check:

fn main() {
    let list_of_values = vec!["magic", "founftain"];

    let check_magic = Box::new(|c: &str| -> bool { matches!(c, "magic") });

    let check_fountain = Box::new(|c: &str| -> bool { matches!(c, "fountain") });

    let checks: Vec<(Box<dyn Fn(&str) -> bool>, &'static str)> =
        vec![(check_magic, "check_magic"), (check_fountain, "check_fountain")];

    checks
        .iter()
        .for_each(|(chk, name)| assert!(list_of_values.iter().copied().any(chk), "failed: {}", name));
}

Playground

It's Cap'n Proto code. A "message" value is really just a collection of bytes that the generated code owns and knows how to read fields out of when you call accessor methods. Even the parts of the messages that can be reduced to enums don't implement Eq and must be compared with eg. matches!().

For example, a simple Cap'n Proto union-in-a-struct that can be Title or Extra looks like:

struct Config {
    union {
        title        @1 :Text;
        extra :group {
            index    @4 :UInt8;
            settings @5 :Settings;
        }
    }
}

A simple check for the title in a test looks like:

let title_check = |c: ipc::conf_capnp::config::Reader| -> bool {
    // c is the capnp struct
    matches!(
        c.which(), // which() tells you what union type it is.
        Ok(ipc::conf_capnp::config::Which::Title(Ok(
            "The Title"
        )))
    )
};

Here's a more complicated example for Extra:

let input_check = |c: ipc::conf_capnp::config::Reader| -> bool {
    match c.which() {
        Ok(ipc::conf_capnp::config::Which::Extra(inner)) => {
            (inner.get_index() == 0)
                && match inner.get_settings() {
                    Ok(inner_inner) => {
                        inner_inner.get_flag()
                            && inner_inner.get_available()
                            && match inner_inner.which() {
                                Ok(ipc::conf_capnp::input_config::Which::Food(
                                    inner_inner_inner,
                                )) => {
                                    matches!(
                                        inner_inner_inner.get_subtype(),
                                        Ok(ipc::common_capnp::FoodType::Salad)
                                    ) && match inner_inner_inner.get_details() {
                                        Ok(inner_inner_inner_inner) => {
                                            matches!(
                                                inner_inner_inner_inner.get_name(),
                                                Ok("Green salad")
                                            ) && inner_inner_inner_inner.get_vegetarian()
                                        }
                                        _ => false,
                                    }
                                }
                                _ => false,
                            }
                    }
                    _ => false,
                }
        }
        _ => false,
    }
};

Fun times :upside_down_face: (I just wrote this this minute, there's probably a way to simplify it down. But having assertions in there instead would at least reduce the line count, if not the nesting.)

I cannot simply replace the matches!() with an ==, and I cannot take the list of ipc::conf_capnp::config::Readers and put them in a HashSet and compare the two sets for equality. I can't even use assert_matches!() because I have to apply the check to every response in a list of responses to see if any of them pass.

So I'm a bit stuck.

I reduced that abomination somewhat by remembering that { } scope blocks are expressions, so match inner.get_settings() { Ok(...) } can be replaced by { let inner_inner = ... ; condition }. Plus if guards on some of the matches.

Ah, Cap'n Proto. I tried that once.

In that case, I'm afraid my part B answer is all I can offer. However, I can at least give you that macro:

use std::collections::HashMap;

macro_rules! add_check {
    ($checks: expr, $name:ident, $func:expr) => {{
        let name: &'static str = stringify!($name);
        let boxed_fn: Box<dyn Fn(&str) -> bool> = Box::new($func);
        $checks.insert(name, boxed_fn);
    }}
}

fn main() {
    let list_of_values = vec!["magic", "founftain"];
    
    let mut checks: HashMap<&'static str, _> = HashMap::new();
    add_check!(checks, magic, |c: &str| -> bool { matches!(c, "magic") });
    add_check!(checks, fountain, |c: &str| -> bool { matches!(c, "fountain") });

    checks
        .iter()
        .for_each(|(name, chk)| assert!(list_of_values.iter().copied().any(chk), "failed: {}", name));
}

Hopefully that is a little less ugly?

Instead of deeply nesting I would use early returns throughout to flatten the checks. And I also would consider adding a helper macro.

// morf => match_or_return_false
macro_rules! morf {
    ($expression:expr, $return_ident:ident, $( $pattern:pat )|+ $( if $guard: expr )? $(,)?) => {
        match $expression {
            $( $pattern )|+ $( if $guard )? => $return_ident,
            _ => return false
        }
    }
}
let inner = morf!(c.which(), v, Ok(ipc::conf_capnp::config::Which::Extra(v)));
if inner.get_index() != 0 { return false };
let inner_inner = morf!(inner, v, Ok(v));
if !inner_inner.get_available() { return false };
let inner_inner_inner = morf!(inner_inner.which(), v, Ok(ipc::conf_capnp::config::Which::Food(v)));
....

The macro could be more ergonomic if it didn't require duplicating the return variable name, but its not that bad.

If it's Cap'n Proto then can't you just put the byte representation in a set? Or maybe create a HashMap<&[u8], Message> so you can do assertions using the serialized message as a key, then look up the message when an assertion fails so you can print out something with a nice Debug implementation.

In theory, but this won't be very future proof. If I change the schema without affecting the actual message structure for a subset of messages, the on-the-wire representation might change, but 90% of my tests shouldn't suddenly start failing because I've added a field that affects the other 10%. (Yes, Cap'n P has the ability to do backwards compatibility changes, but I don't want to have to care about that at this stage.)

Huge thanks to @jhwgh1968 and @drewkett for their advice and macro code, in the last few weeks I've gotten over my fear of macro_rules! and discovered how useful they can be (especially in extremely repetitive test code). I've already incorporated some of it into my tests to great benefit in readability.

2 Likes