首页 > 解决方案 > 运行 Rust 测试时,如何在函数内部执行蛮力函数验证?

问题描述

当我测试具有明显的、较慢的、蛮力替代方案的函数时,我经常发现编写这两个函数并在调试标志打开时验证输出是否匹配很有帮助。在 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 代码?)

如果目前无法做到这一点,那么进入功能请求是否合理?

标签: testingrust

解决方案


感觉就像我会打破测试/生产障碍。

是的,但我不明白你为什么担心这个;您现有的代码已经打破了这个界限。可以使用debug_assertandfriends来确保该函数只在启用调试断言时才被调用和验证。如果你想加倍确定,你也可以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来...

我通常会接受任何找到的案例并为它们创建专门的单元测试。

在这种情况下,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

推荐阅读