Как выполнить проверку грубой силы внутри функции при выполнении тестов Rust? - PullRequest
1 голос
/ 11 ноября 2019

Когда я тестирую функции, которые имеют очевидную, более медленную, грубую альтернативу, я часто считаю полезным написать обе функции и убедиться, что выходные данные совпадают, когда включены флаги отладки. В C это может выглядеть примерно так:

#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;
}

, и выполнение может выглядеть так (если приведенный выше код находится в 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

Мне нравится делатьэто в дополнение к модульным и интеграционным тестам, потому что это делает источник многих ошибок более очевидным. Он будет охватывать граничные случаи, которые я, возможно, забыл в модульном тестировании, и, естественно, расширится до объема, который может понадобиться в будущем для более сложных случаев (например, если настройки освещения меняются, и мне нужна точность для гораздо более высоких значений).

Я изучаю Rust, и мне действительно нравится естественное разделение интересов между тестированием и производственным кодом. Я пытаюсь сделать что-то похожее на вышеперечисленное, но не могу понять, как лучше это сделать. Из того, что я собрал в этой теме , я, вероятно, мог бы сделать это с некоторой комбинацией macro_rules! и #[cfg!( ... )] в исходном коде, но мне кажется, что я бы преодолел барьер тестирования / производства. В идеале я хотел бы иметь возможность просто поместить проверочную оболочку вокруг уже определенной функции, но только для тестирования. Макросы и cfg мой лучший вариант здесь? Могу ли я переопределить пространство имен по умолчанию для импортированного пакета только во время тестирования или сделать что-то более умное с макросами? Я понимаю, что, как правило, файлы не должны иметь возможность изменять привязку импорта, но есть ли исключение для тестирования? Что, если я также хочу, чтобы он был упакован, если модуль, импортирующий его, тестируется?

Я также открыт для ответа, что это плохой способ провести тестирование / проверку, но, пожалуйста, рассмотрите преимущества, которые яупомянутое выше. (Или в качестве бонуса, есть ли способ улучшить код C?)

Если в настоящее время это невозможно, целесообразно ли переходить к запросу на функцию?

1 Ответ

2 голосов
/ 11 ноября 2019

такое ощущение, что я бы преодолел барьер тестирования / производства.

Да, но я не понимаю, почему вас это беспокоит;ваш существующий код уже нарушает эту границу. Вы можете использовать debug_assert и друзей, чтобы гарантировать, что функция вызывается и проверяется только при включенных утверждениях отладки. Если вы хотите быть уверенным вдвойне, вы также можете использовать cfg(debug_assertions) только для определения вашей медленной функции:

pub fn add(a: i32, b: i32) -> i32 {
    let fast = fast_but_tricky(a, b);
    debug_assert_eq!(fast, slow_but_right(a, b));
    fast
}

fn fast_but_tricky(a: i32, b: i32) -> i32 {
    a + a + b - a
}

#[cfg(debug_assertions)]
fn slow_but_right(a: i32, b: i32) -> i32 {
    a + b
}

Мне не нравится это решение. Я предпочитаю держать код тестирования более отличным от производственного кода. Вместо этого я использую тестирование на основе свойств , чтобы убедиться, что мои тесты охватывают то, что важно. Я использовал proptest для ...

Я обычно беру любые найденные случаи и создаю для них выделенные юнит-тесты.

В этом случае протестант может выглядеть так:

pub fn add(a: i32, b: i32) -> i32 {
    // fast but tricky
    a + a + b - a
}

#[cfg(test)]
mod test {
    use super::*;
    use proptest::{proptest, prop_assert_eq};

    fn slow_but_right(a: i32, b: i32) -> i32 {
        a + b
    }

    proptest! {
        #[test]
        fn same_as_slow_version(a: i32, b: i32) {
            prop_assert_eq!(add(a, b), slow_but_right(a, b));
        }
    }
}

Который находит ошибку с моей "умной" реализацией менее чем за одну десятую секунды:

thread 'test::same_as_slow_version' panicked at 'Test failed: attempt to add with overflow; minimal failing
input: a = 375403587, b = 1396676474
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...