lundi 11 novembre 2019

How do I perform brute force function verification inside a function when running Rust tests?

When I'm testing functions that have an obvious, slower, brute-force alternative, I've often found it helpful to write both functions, and verify that the outputs match when debugging flags are on. In C, it might look something like this:

#include <inttypes.h>
#include <stdio.h>

#ifdef NDEBUG
#define _rsqrt rsqrt
#else
#include <assert.h>
#include <math.h>
#endif

// https://en.wikipedia.org/wiki/Fast_inverse_square_root
float _rsqrt(float number) {
    const float x2 = number * 0.5F;
    const float threehalfs = 1.5F;

    union {
        float f;
        uint32_t i;
    } conv = {number}; // member 'f' set to value of 'number'.
    // approximation via Newton's method
    conv.i = 0x5f3759df - (conv.i >> 1);
    conv.f *= (threehalfs - (x2 * conv.f * conv.f));
    return conv.f;
}


#ifndef NDEBUG
float rsqrt(float number) {
    float res = _rsqrt(number);
    // brute force solution to verify
    float correct = 1 / sqrt(number);
    // make sure the approximation is within 1% of correct
    float err = fabs(res - correct) / correct;
    assert(err < 0.01);
    // for exposition sake: large scale systems would verify quietly
    printf("DEBUG: rsqrt(%f) -> %f error\n", number, err);
    return res;
}
#endif

float graphics_code() {
    // graphics code that invokes rsqrt a bunch of different ways
    float total = 0;
    for (float i = 1; i < 10; i++)
        total += rsqrt(i);
    return total;
}

int main(int argc, char *argv[]) {
    printf("%f\n", graphics_code());
    return 0;
}

and execution might look like this (if the above code is in tmp.c):

$ clang tmp.c -o tmp -lm && ./tmp # debug mode
DEBUG: rsqrt(1.000000) -> 0.001693 error
DEBUG: rsqrt(2.000000) -> 0.000250 error
DEBUG: rsqrt(3.000000) -> 0.000872 error
DEBUG: rsqrt(4.000000) -> 0.001693 error
DEBUG: rsqrt(5.000000) -> 0.000162 error
DEBUG: rsqrt(6.000000) -> 0.001389 error
DEBUG: rsqrt(7.000000) -> 0.001377 error
DEBUG: rsqrt(8.000000) -> 0.000250 error
DEBUG: rsqrt(9.000000) -> 0.001140 error
4.699923
$ clang tmp.c -o tmp -lm -O3 -DNDEBUG && ./tmp # production mode
4.699923

I like to do this in addition to unit and integration tests because it makes the source of a lot of errors more obvious. It will catch boundary cases that I may have forgotten to unit test, and will naturally expand to the scope to whatever more complex cases I may need in the future (e.g. if the light settings change and I need accuracy for much higher values).

I'm learning Rust, and I really like the natively established separation of interests between testing and production code. I'm trying to do something similar to the above, but can't figure out what the best way to do it is. From what I gather in this thread, I could probably do it with some combination of macro_rules! and #[cfg!( ... )] in the source code, but it feels like I would be breaking the test/production barrier. Ideally I would like to be able to just drop a verification wrapper in around the already defined function, but only for testing. Are macros and cfg my best option here? Can I redefine the default namespace for the imported package just when testing, or do something more clever with macros? I understand that normally files shouldn't be able to modify how imports are linked, but is there an exception for testing? What if I also want it to be wrapped if the module importing it is being tested?

I'm also open to the response that this is a bad way to do testing/verification, but please address the advantages I mentioned above. (Or as a bonus, is there a way the C code can be improved?)

If this isn't currently possible, is it a reasonable thing to go into a feature request?

Aucun commentaire:

Enregistrer un commentaire