【问题标题】:How do I perform brute force function verification inside a function when running Rust tests?运行 Rust 测试时,如何在函数内部执行暴力函数验证?
【发布时间】:2019-11-11 09:28:23
【问题描述】:

当我测试具有明显、较慢、蛮力替代方案的函数时,我经常发现编写这两个函数并在调试标志打开时验证输出是否匹配很有帮助。在 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,我真的很喜欢在测试和生产代码之间建立的原生利益分离。我正在尝试做与上述类似的事情,但无法弄清楚最好的方法是什么。根据我在this thread 中收集的信息,我可能可以在源代码中使用macro_rules!#[cfg!( ... )] 的某种组合来实现,但感觉就像我会打破测试/生产障碍。理想情况下,我希望能够在已经定义的函数周围放置一个验证包装器,但仅用于测试。宏和cfg 是我最好的选择吗?我可以在测试时重新定义导入包的默认命名空间,还是用宏做一些更聪明的事情?我知道通常文件不应该能够修改导入的链接方式,但是测试有例外吗?如果正在测试导入它的模块,我还希望它被包装怎么办?

我也愿意接受这样的回应,即这是一种不好的测试/验证方式,但请说明我上面提到的优点。 (或者作为奖励,有没有办法改进 C 代码?)

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

【问题讨论】:

    标签: testing rust


    【解决方案1】:

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

    是的,但我不明白你为什么担心这个;您现有的代码已经打破了这个界限。您可以使用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 来...

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

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

    【讨论】:

    • 我意识到我的 C 代码中的边界已经被打破,我只是希望有更好的方法来做到这一点。我喜欢验证策略而不是随机输入,因为它感觉像是对输出总是正确的更有力的保证,而不是可能遗漏某些东西的随机输入。特别是对于计算几何的东西,例如很容易错过共线性边缘情况。 debug_assertions 可能是我的最佳选择,谢谢。
    • @user3476782 坦率地说,我不明白您的使用与任意/随机测试有何本质不同。您依赖从更高级别的代码中流入的任何值来驱动您的断言。与基于属性的测试相比,我尤其不明白您的风格如何保证 输出 是正确的。这两种方法都依赖于函数的输入和测试预言的实现。请注意,基于属性的测试不一定是随机的——您可以将其配置为探索任何有用的搜索空间。
    • 它并不能帮助您保证它适用于所有输入,但至少如果您的代码在某处失败,调试器会因为假设被破坏而停在正确的位置。尤其是它可以更轻松地追踪逻辑错误的来源。
    猜你喜欢
    • 2021-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-04
    • 1970-01-01
    • 2019-10-04
    • 2020-11-30
    • 1970-01-01
    相关资源
    最近更新 更多