使用 boost::multiprecision 进行单元测试
Unit tests with boost::multiprecision
自从调整一些代码以启用多精度以来,我的一些单元测试开始失败。头文件:
#ifndef SCRATCH_UNITTESTBOOST_INCLUDED
#define SCRATCH_UNITTESTBOOST_INCLUDED
#include <boost/multiprecision/cpp_dec_float.hpp>
// typedef double FLOAT;
typedef boost::multiprecision::cpp_dec_float_50 FLOAT;
const FLOAT ONE(FLOAT(1));
struct Rect
{
Rect(const FLOAT &width, const FLOAT &height) : Width(width), Height(height){};
FLOAT getArea() const { return Width * Height; }
FLOAT Width, Height;
};
#endif
主要测试文件:
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/test/unit_test.hpp>
#include "SCRATCH_UnitTestBoost.h"
namespace utf = boost::unit_test;
// Failing
BOOST_AUTO_TEST_CASE(AreaTest1)
{
Rect R(ONE / 2, ONE / 3);
FLOAT expected_area = (ONE / 2) * (ONE / 3);
std::cout << std::setprecision(std::numeric_limits<FLOAT>::digits10) << std::showpoint;
std::cout << "Expected: " << expected_area << std::endl;
std::cout << "Actual : " << R.getArea() << std::endl;
// BOOST_CHECK_EQUAL(expected_area, R.getArea());
BOOST_TEST(expected_area == R.getArea());
}
// Tolerance has no effect?
BOOST_AUTO_TEST_CASE(AreaTestTol, *utf::tolerance(1e-40))
{
Rect R(ONE / 2, ONE / 3);
FLOAT expected_area = (ONE / 2) * (ONE / 3);
BOOST_TEST(expected_area == R.getArea());
}
// Passing
BOOST_AUTO_TEST_CASE(AreaTest2)
{
Rect R(ONE / 7, ONE / 2);
FLOAT expected_area = (ONE / 7) * (ONE / 2);
BOOST_CHECK_EQUAL(expected_area, R.getArea());
}
注意,当定义FLOAT
为double
类型时,所有的测试都通过了。令我困惑的是,当打印准确的预期值和实际值时(请参阅 AreaTest1),我们会看到相同的结果。但是BOOST_TEST
报错是:
error: in "AreaTest1": check expected_area == R.getArea() has failed
[0.16666666666666666666666666666666666666666666666666666666666666666666666666666666 !=
0.16666666666666666666666666666666666666666666666666666666666666666666666672236366]
使用 g++ SCRATCH_UnitTestBoost.cpp -o utb.o -lboost_unit_test_framework
编译。
问题:
- 为什么测试失败?
- 为什么在
AreaTestTol
中使用 tolerance
没有给出记录的输出 here?
相关信息:
两期:
- 差异从何而来
- 如何应用epsilon?
差异从何而来
Boost Multiprecision 使用模板表达式来延迟计算。
此外,您选择了一些不能精确表示为 10 进制的有理分数(cpp_dec_float 使用小数,因此以 10 为底)。
这意味着当你这样做时
T x = 1/3;
T y = 1/7;
这实际上会不准确地近似两个分数。
这样做:
T z = 1/3 * 1/7;
实际上会计算右侧的 表达式模板 ,因此不像之前计算 x
ans y
那样的临时变量,右侧有一种类型:
expression<detail::multiplies, detail::expression<?>, detail::expression<?>, [2 * ...]>
这是实际类型的缩写:
boost::multiprecision::detail::expression<
boost::multiprecision::detail::multiplies,
boost::multiprecision::detail::expression<
boost::multiprecision::detail::divide_immediates,
boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
int, void>, (boost::multiprecision::expression_template_option)1>, int,
void, void>,
boost::multiprecision::detail::expression<
boost::multiprecision::detail::divide_immediates,
boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
int, void>, (boost::multiprecision::expression_template_option)1>, int,
void, void>,
void, void>
长话短说,这就是您想要的,因为它可以节省您的工作并保持更好的准确性,因为表达式首先被规范化为 1/(3*7)
所以 1/21
.
这就是你与众不同的地方。通过以下任一方法修复它:
关闭表达式模板
using T = boost::multiprecision::number<
boost::multiprecision::cpp_dec_float<50>,
boost::multiprecision::et_off > >;
重写表达式以等效于您的实现:
T expected_area = T(ONE / 7) * T(ONE / 2);
T expected_area = (ONE / 7).eval() * (ONE / 2).eval();
应用 Tole运行ce
我发现很难解析关于此的 Boost 单元测试文档,但这里有经验数据:
BOOST_CHECK_EQUAL(expected_area, R.getArea());
T const eps = std::numeric_limits<T>::epsilon();
BOOST_CHECK_CLOSE(expected_area, R.getArea(), eps);
BOOST_TEST(expected_area == R.getArea(), tt::tolerance(eps));
第一次失败,最后两次通过。确实,除此之外,下面两个也失败了:
BOOST_CHECK_EQUAL(expected_area, R.getArea());
BOOST_TEST(expected_area == R.getArea());
所以看来在 utf::tolerance
装饰器生效之前必须做一些事情。使用本地双打测试告诉我只有 BOOST_TEST
隐式应用 tole运行ce。所以深入研究预处理扩展:
::boost::unit_test::unit_test_log.set_checkpoint(
::boost::unit_test::const_string(
"/home/sehe/Projects/Whosebug/test.cpp",
sizeof("/home/sehe/Projects/Whosebug/test.cpp") - 1),
static_cast<std::size_t>(42));
::boost::test_tools::tt_detail::report_assertion(
(::boost::test_tools::assertion::seed()->*a == b).evaluate(),
(::boost::unit_test::lazy_ostream::instance()
<< ::boost::unit_test::const_string("a == b", sizeof("a == b") - 1)),
::boost::unit_test::const_string(
"/home/sehe/Projects/Whosebug/test.cpp",
sizeof("/home/sehe/Projects/Whosebug/test.cpp") - 1),
static_cast<std::size_t>(42), ::boost::test_tools::tt_detail::CHECK,
::boost::test_tools::tt_detail::CHECK_BUILT_ASSERTION, 0);
} while (::boost::test_tools::tt_detail::dummy_cond());
深入挖掘,我 运行 进入:
/*!@brief Indicates if a type can be compared using a tolerance scheme
*
* This is a metafunction that should evaluate to @c mpl::true_ if the type
* @c T can be compared using a tolerance based method, typically for floating point
* types.
*
* This metafunction can be specialized further to declare user types that are
* floating point (eg. boost.multiprecision).
*/
template <typename T>
struct tolerance_based : tolerance_based_delegate<T, !is_array<T>::value && !is_abstract_class_or_function<T>::value>::type {};
好了!但是没有,
static_assert(boost::math::fpc::tolerance_based<double>::value);
static_assert(boost::math::fpc::tolerance_based<cpp_dec_float_50>::value);
两个都已经通过了。嗯。
查看装饰器,我注意到注入到夹具上下文中的 tole运行ce 是 typed.
根据实验,我得出的结论是 tole运行ce 装饰器 需要 具有相同的 static 类型参数 比较中的ope运行ds才能生效。
这实际上可能非常有用(对于不同的浮点类型你可以有不同的隐式 tole运行ces),但它也很令人惊讶。
TL;DR
这是完整的测试集,供您欣赏:
- 考虑评估顺序和对准确性的影响
- 使用
utf::tolerance(v)
中的静态类型来匹配你的操作运行ds
- 不要使用 BOOST_CHECK_EQUAL 进行基于 tole运行ce 的比较
- 我建议使用显式
test_tools::tolerance
而不是依赖“环境”来 运行ce。毕竟,我们要测试的是我们的代码,而不是测试框架
靠 Coliru 生活
template <typename T> struct Rect {
Rect(const T &width, const T &height) : width(width), height(height){};
T getArea() const { return width * height; }
private:
T width, height;
};
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/multiprecision/cpp_dec_float.hpp>
using DecFloat = boost::multiprecision::cpp_dec_float_50;
#include <boost/test/unit_test.hpp>
namespace utf = boost::unit_test;
namespace tt = boost::test_tools;
namespace {
template <typename T>
static inline const T Eps = std::numeric_limits<T>::epsilon();
template <typename T> struct Fixture {
T const epsilon = Eps<T>;
T const ONE = 1;
using Rect = ::Rect<T>;
void checkArea(int wdenom, int hdenom) const {
auto w = ONE/wdenom; // could be expression templates
auto h = ONE/hdenom;
Rect const R(w, h);
T expect = w*h;
BOOST_TEST(expect == R.getArea(), "1/" << wdenom << " x " << "1/" << hdenom);
// I'd prefer explicit toleranc
BOOST_TEST(expect == R.getArea(), tt::tolerance(epsilon));
}
};
}
BOOST_AUTO_TEST_SUITE(Rectangles)
BOOST_FIXTURE_TEST_SUITE(Double, Fixture<double>, *utf::tolerance(Eps<double>))
BOOST_AUTO_TEST_CASE(check2_3) { checkArea(2, 3); }
BOOST_AUTO_TEST_CASE(check7_2) { checkArea(7, 2); }
BOOST_AUTO_TEST_CASE(check57_31) { checkArea(57, 31); }
BOOST_AUTO_TEST_SUITE_END()
BOOST_FIXTURE_TEST_SUITE(MultiPrecision, Fixture<DecFloat>, *utf::tolerance(Eps<DecFloat>))
BOOST_AUTO_TEST_CASE(check2_3) { checkArea(2, 3); }
BOOST_AUTO_TEST_CASE(check7_2) { checkArea(7, 2); }
BOOST_AUTO_TEST_CASE(check57_31) { checkArea(57, 31); }
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE_END()
版画
自从调整一些代码以启用多精度以来,我的一些单元测试开始失败。头文件:
#ifndef SCRATCH_UNITTESTBOOST_INCLUDED
#define SCRATCH_UNITTESTBOOST_INCLUDED
#include <boost/multiprecision/cpp_dec_float.hpp>
// typedef double FLOAT;
typedef boost::multiprecision::cpp_dec_float_50 FLOAT;
const FLOAT ONE(FLOAT(1));
struct Rect
{
Rect(const FLOAT &width, const FLOAT &height) : Width(width), Height(height){};
FLOAT getArea() const { return Width * Height; }
FLOAT Width, Height;
};
#endif
主要测试文件:
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/test/unit_test.hpp>
#include "SCRATCH_UnitTestBoost.h"
namespace utf = boost::unit_test;
// Failing
BOOST_AUTO_TEST_CASE(AreaTest1)
{
Rect R(ONE / 2, ONE / 3);
FLOAT expected_area = (ONE / 2) * (ONE / 3);
std::cout << std::setprecision(std::numeric_limits<FLOAT>::digits10) << std::showpoint;
std::cout << "Expected: " << expected_area << std::endl;
std::cout << "Actual : " << R.getArea() << std::endl;
// BOOST_CHECK_EQUAL(expected_area, R.getArea());
BOOST_TEST(expected_area == R.getArea());
}
// Tolerance has no effect?
BOOST_AUTO_TEST_CASE(AreaTestTol, *utf::tolerance(1e-40))
{
Rect R(ONE / 2, ONE / 3);
FLOAT expected_area = (ONE / 2) * (ONE / 3);
BOOST_TEST(expected_area == R.getArea());
}
// Passing
BOOST_AUTO_TEST_CASE(AreaTest2)
{
Rect R(ONE / 7, ONE / 2);
FLOAT expected_area = (ONE / 7) * (ONE / 2);
BOOST_CHECK_EQUAL(expected_area, R.getArea());
}
注意,当定义FLOAT
为double
类型时,所有的测试都通过了。令我困惑的是,当打印准确的预期值和实际值时(请参阅 AreaTest1),我们会看到相同的结果。但是BOOST_TEST
报错是:
error: in "AreaTest1": check expected_area == R.getArea() has failed
[0.16666666666666666666666666666666666666666666666666666666666666666666666666666666 !=
0.16666666666666666666666666666666666666666666666666666666666666666666666672236366]
使用 g++ SCRATCH_UnitTestBoost.cpp -o utb.o -lboost_unit_test_framework
编译。
问题:
- 为什么测试失败?
- 为什么在
AreaTestTol
中使用tolerance
没有给出记录的输出 here?
相关信息:
两期:
- 差异从何而来
- 如何应用epsilon?
差异从何而来
Boost Multiprecision 使用模板表达式来延迟计算。
此外,您选择了一些不能精确表示为 10 进制的有理分数(cpp_dec_float 使用小数,因此以 10 为底)。
这意味着当你这样做时
T x = 1/3;
T y = 1/7;
这实际上会不准确地近似两个分数。
这样做:
T z = 1/3 * 1/7;
实际上会计算右侧的 表达式模板 ,因此不像之前计算 x
ans y
那样的临时变量,右侧有一种类型:
expression<detail::multiplies, detail::expression<?>, detail::expression<?>, [2 * ...]>
这是实际类型的缩写:
boost::multiprecision::detail::expression<
boost::multiprecision::detail::multiplies,
boost::multiprecision::detail::expression<
boost::multiprecision::detail::divide_immediates,
boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
int, void>, (boost::multiprecision::expression_template_option)1>, int,
void, void>,
boost::multiprecision::detail::expression<
boost::multiprecision::detail::divide_immediates,
boost::multiprecision::number<boost::multiprecision::backends::cpp_dec_float<50u,
int, void>, (boost::multiprecision::expression_template_option)1>, int,
void, void>,
void, void>
长话短说,这就是您想要的,因为它可以节省您的工作并保持更好的准确性,因为表达式首先被规范化为 1/(3*7)
所以 1/21
.
这就是你与众不同的地方。通过以下任一方法修复它:
关闭表达式模板
using T = boost::multiprecision::number< boost::multiprecision::cpp_dec_float<50>, boost::multiprecision::et_off > >;
重写表达式以等效于您的实现:
T expected_area = T(ONE / 7) * T(ONE / 2); T expected_area = (ONE / 7).eval() * (ONE / 2).eval();
应用 Tole运行ce
我发现很难解析关于此的 Boost 单元测试文档,但这里有经验数据:
BOOST_CHECK_EQUAL(expected_area, R.getArea());
T const eps = std::numeric_limits<T>::epsilon();
BOOST_CHECK_CLOSE(expected_area, R.getArea(), eps);
BOOST_TEST(expected_area == R.getArea(), tt::tolerance(eps));
第一次失败,最后两次通过。确实,除此之外,下面两个也失败了:
BOOST_CHECK_EQUAL(expected_area, R.getArea());
BOOST_TEST(expected_area == R.getArea());
所以看来在 utf::tolerance
装饰器生效之前必须做一些事情。使用本地双打测试告诉我只有 BOOST_TEST
隐式应用 tole运行ce。所以深入研究预处理扩展:
::boost::unit_test::unit_test_log.set_checkpoint(
::boost::unit_test::const_string(
"/home/sehe/Projects/Whosebug/test.cpp",
sizeof("/home/sehe/Projects/Whosebug/test.cpp") - 1),
static_cast<std::size_t>(42));
::boost::test_tools::tt_detail::report_assertion(
(::boost::test_tools::assertion::seed()->*a == b).evaluate(),
(::boost::unit_test::lazy_ostream::instance()
<< ::boost::unit_test::const_string("a == b", sizeof("a == b") - 1)),
::boost::unit_test::const_string(
"/home/sehe/Projects/Whosebug/test.cpp",
sizeof("/home/sehe/Projects/Whosebug/test.cpp") - 1),
static_cast<std::size_t>(42), ::boost::test_tools::tt_detail::CHECK,
::boost::test_tools::tt_detail::CHECK_BUILT_ASSERTION, 0);
} while (::boost::test_tools::tt_detail::dummy_cond());
深入挖掘,我 运行 进入:
/*!@brief Indicates if a type can be compared using a tolerance scheme
*
* This is a metafunction that should evaluate to @c mpl::true_ if the type
* @c T can be compared using a tolerance based method, typically for floating point
* types.
*
* This metafunction can be specialized further to declare user types that are
* floating point (eg. boost.multiprecision).
*/
template <typename T>
struct tolerance_based : tolerance_based_delegate<T, !is_array<T>::value && !is_abstract_class_or_function<T>::value>::type {};
好了!但是没有,
static_assert(boost::math::fpc::tolerance_based<double>::value);
static_assert(boost::math::fpc::tolerance_based<cpp_dec_float_50>::value);
两个都已经通过了。嗯。
查看装饰器,我注意到注入到夹具上下文中的 tole运行ce 是 typed.
根据实验,我得出的结论是 tole运行ce 装饰器 需要 具有相同的 static 类型参数 比较中的ope运行ds才能生效。
这实际上可能非常有用(对于不同的浮点类型你可以有不同的隐式 tole运行ces),但它也很令人惊讶。
TL;DR
这是完整的测试集,供您欣赏:
- 考虑评估顺序和对准确性的影响
- 使用
utf::tolerance(v)
中的静态类型来匹配你的操作运行ds - 不要使用 BOOST_CHECK_EQUAL 进行基于 tole运行ce 的比较
- 我建议使用显式
test_tools::tolerance
而不是依赖“环境”来 运行ce。毕竟,我们要测试的是我们的代码,而不是测试框架
靠 Coliru 生活
template <typename T> struct Rect {
Rect(const T &width, const T &height) : width(width), height(height){};
T getArea() const { return width * height; }
private:
T width, height;
};
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE RectTest
#include <boost/multiprecision/cpp_dec_float.hpp>
using DecFloat = boost::multiprecision::cpp_dec_float_50;
#include <boost/test/unit_test.hpp>
namespace utf = boost::unit_test;
namespace tt = boost::test_tools;
namespace {
template <typename T>
static inline const T Eps = std::numeric_limits<T>::epsilon();
template <typename T> struct Fixture {
T const epsilon = Eps<T>;
T const ONE = 1;
using Rect = ::Rect<T>;
void checkArea(int wdenom, int hdenom) const {
auto w = ONE/wdenom; // could be expression templates
auto h = ONE/hdenom;
Rect const R(w, h);
T expect = w*h;
BOOST_TEST(expect == R.getArea(), "1/" << wdenom << " x " << "1/" << hdenom);
// I'd prefer explicit toleranc
BOOST_TEST(expect == R.getArea(), tt::tolerance(epsilon));
}
};
}
BOOST_AUTO_TEST_SUITE(Rectangles)
BOOST_FIXTURE_TEST_SUITE(Double, Fixture<double>, *utf::tolerance(Eps<double>))
BOOST_AUTO_TEST_CASE(check2_3) { checkArea(2, 3); }
BOOST_AUTO_TEST_CASE(check7_2) { checkArea(7, 2); }
BOOST_AUTO_TEST_CASE(check57_31) { checkArea(57, 31); }
BOOST_AUTO_TEST_SUITE_END()
BOOST_FIXTURE_TEST_SUITE(MultiPrecision, Fixture<DecFloat>, *utf::tolerance(Eps<DecFloat>))
BOOST_AUTO_TEST_CASE(check2_3) { checkArea(2, 3); }
BOOST_AUTO_TEST_CASE(check7_2) { checkArea(7, 2); }
BOOST_AUTO_TEST_CASE(check57_31) { checkArea(57, 31); }
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE_END()
版画