kpreid / exhaust

Exhaustive iteration trait in Rust
https://docs.rs/exhaust/
Apache License 2.0
14 stars 2 forks source link

Using Exhaust without iterating numbers #17

Closed TheNeikos closed 10 months ago

TheNeikos commented 10 months ago

I would like to use exhaust to verify my schema implementation is correct for a given schema that exists externally. I at first wanted to use exhaust, but sadly it creates too big of an iteration space as to being useful for me. The biggest culprit so far is numbers. I have several places where I use u32s (in 'datapoints' for example). While this is of course useful in some cases, it is not useful in mine as the 'shape' is more important than the content.

The question would be, how could this be added? I am thinking that the exhaust macro could gain a configuration option with which one can reduce the space of a given member. Either user-supplied or pre-generated. For example:

#[derive(Exhaust)]
struct Data {
  #[exhaust(only_number_extremes)]
  id: u32,
  #[exhaust(only_number_extremes)]
  value: i32,
}

This way for each u32/u32 only the following would be generated: 0, MAX, MIN and 42. So that we have both ends the zero case as well as some 'random' value in the middle. This could be then extended with something like #[exhaust(only_values = [u32::MAX, u32::MIN])] in the future if that is wanted.

NB: I did not look into how the macro generates the impls yet. I might be able to implement this next year if such a feature is wanted.

kpreid commented 10 months ago

An implementation of Exhaust is incorrect if it does not exhaust the entire space of possible values of the type. So, it is only correct to constrain a field (e.g. id) if the containing type (Data) only allows the field to take on those values as an invariant enforced by its normal constructors. If that were the case, you could improve your code and make use of exhaust by using an enum with four variants instead of a u32. For your testing, you could define just such a narrower data type and then convert it to your real structure:

#[derive(Exhaust)]
enum Num {
    Zero,
    Min,
    Max,
    Mid,
}

#[derive(Exhaust)]
struct TestData {
    id: Num,
    value: Num,
}

impl From<TestData> for Data {
    ...
}

#[test]
fn t() {
    for data in TestData::exhaust().map(Data::from) {
        ...
    }
}

But outside of going to those lengths, “test only the edge cases” is not what exhaust is meant to do. You might be interested in libraries and tools for fuzz testing or property-based testing, instead. In particular, coverage-guided fuzzing can automatically discover edge cases worth testing. (I wrote exhaust not primarily as a testing tool, but as a generalization of the idea behind strum::EnumIter to nested fields.)

All that said, there is a general use case for a derive attribute like you proposed: when a field in a structure has an invariant that depends on another field (e.g. an index which should be always less than a size). That's something I do want to support, though it's likely that it won't be in the form of macro attributes (because they'd greatly complicate the macro implementation to support dependencies different from source code field order) but rather provide tools to more easily write a suitable impl Exhaust.