Когда я тестирую функции, которые имеют очевидную, более медленную, грубую альтернативу, я часто считаю полезным написать обе функции и убедиться, что выходные данные совпадают, когда включены флаги отладки. В 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?)
Если в настоящее время это невозможно, целесообразно ли переходить к запросу на функцию?