Научите Google-тестировать, как печатать собственную матрицу
Введение
Я пишу тесты на собственных матрицах, используя платформу Google для тестирования Google-Mock, как уже обсуждалось в другом вопросе.
С помощью следующего кода мне удалось добавить пользовательский Matcher
для соответствия матриц Eigen с заданной точностью.
MATCHER_P2(EigenApproxEqual, expect, prec,
std::string(negation ? "isn't" : "is") + " approx equal to" +
::testing::PrintToString(expect) + "\nwith precision " +
::testing::PrintToString(prec)) {
return arg.isApprox(expect, prec);
}
Это делается для сравнения двух собственных матриц по методу isApprox
, и если они не совпадают с Google-Mock, напечатать соответствующее сообщение об ошибке, которое будет содержать ожидаемые и фактические значения матриц. Или, должно быть, по крайней мере...
Проблема
Возьмем следующий простой тестовый пример:
TEST(EigenPrint, Simple) {
Eigen::Matrix2d A, B;
A << 0., 1., 2., 3.;
B << 0., 2., 1., 3.;
EXPECT_THAT(A, EigenApproxEqual(B, 1e-7));
}
Этот тест завершится неудачно, потому что A
и B
не равны. К сожалению, соответствующее сообщение об ошибке выглядит следующим образом:
gtest_eigen_print.cpp:31: Failure
Value of: A
Expected: is approx equal to32-byte object <00-00 00-00 00-00 00-00 00-00 00-00 00-00 F0-3F 00-00 00-00 00-00 00-40 00-00 00-00 00-00 08-40>
with precision 1e-07
Actual: 32-byte object <00-00 00-00 00-00 00-00 00-00 00-00 00-00 00-40 00-00 00-00 00-00 F0-3F 00-00 00-00 00-00 08-40>
Как вы можете видеть, Google-Test печатает шестнадцатеричный дамп матриц вместо лучшего представления их значений. Документация Google сообщает следующее о печати значений пользовательских типов:
Этот принтер знает, как печатать встроенные типы С++, собственные массивы, STL контейнеры и любой тип, который поддерживает < оператор. Для других типы, он печатает необработанные байты в ценности и надеется, что вы пользователь может понять это.
Собственная матрица поставляется с operator<<
. Однако Google-Test или компилятор С++ игнорируют его. По моему мнению, по следующей причине: подпись этого оператора гласит (IO.h(строка 240))
template<typename Derived>
std::ostream &operator<< (std::ostream &s, const DenseBase<Derived> &m);
т.е. он принимает значение const DenseBase<Derived>&
. С другой стороны, стандартным принтером Google-test с шестнадцатеричным дампом является реализация функции шаблона по умолчанию. Здесь вы можете найти реализацию здесь. (Следуйте за деревом вызова, начинающимся с PrintTo, чтобы убедиться, что это так, или доказать, что я ошибаюсь.;))
Итак, принтер Google-Test по умолчанию лучше, потому что он принимает const Derived &
, а не только его базовый класс const DenseBase<Derived> &
.
Мой вопрос
Мой вопрос следующий. Как я могу сказать компилятору, чтобы он предпочел Eigen-специфический operator <<
по сравнению с тестовым шестнадцатеричным дампом Google? В предположении, что я не могу изменить определение класса собственной матрицы.
Мои попытки
До сих пор я пробовал следующие вещи.
Определение функции
template <class Derived>
void PrintTo(const Eigen::DensBase<Derived> &m, std::ostream *o);
не будет работать по той же причине, для которой operator<<
не работает.
Единственное, что я нашел, что работал, это использовать механизм .
С файлом eigen_matrix_addons.hpp
:
friend void PrintTo(const Derived &m, ::std::ostream *o) {
*o << "\n" << m;
}
а следующая директива
#define EIGEN_MATRIXBASE_PLUGIN "eigen_matrix_addons.hpp"
#include <Eigen/Dense>
тест даст следующий результат:
gtest_eigen_print.cpp:31: Failure
Value of: A
Expected: is approx equal to
0 2
1 3
with precision 1e-07
Actual:
0 1
2 3
Что не так с этим?
Для собственных матриц это, вероятно, приемлемое решение. Тем не менее, я знаю, что мне придется применить одно и то же к другим классам шаблонов, очень скоро, которые, к сожалению, не предлагают плагин-механизм, такой как Eigen, и чьи определения у меня нет прямого доступа.
Следовательно, мой вопрос: есть ли способ указать компилятору на право operator<<
или PrintTo
функцию без изменения самого определения класса?
Полный код
#include <Eigen/Dense>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gmock/gmock-matchers.h>
// A GMock matcher for Eigen matrices.
MATCHER_P2(EigenApproxEqual, expect, prec,
std::string(negation ? "isn't" : "is") + " approx equal to" +
::testing::PrintToString(expect) + "\nwith precision " +
::testing::PrintToString(prec)) {
return arg.isApprox(expect, prec);
}
TEST(EigenPrint, Simple) {
Eigen::Matrix2d A, B;
A << 0., 1., 2., 3.;
B << 0., 2., 1., 3.;
EXPECT_THAT(A, EigenApproxEqual(B, 1e-7));
}
Изменить: дальнейшие попытки
Я сделал некоторый прогресс с помощью подхода SFINAE.
Сначала я определил черту для типов Eigen. С его помощью мы можем использовать std::enable_if
для предоставления функций шаблона только для типов, которые соответствуют этому признаку.
#include <type_traits>
#include <Eigen/Dense>
template <class Derived>
struct is_eigen : public std::is_base_of<Eigen::DenseBase<Derived>, Derived> {
};
Моя первая мысль заключалась в предоставлении такой версии PrintTo
. К сожалению, компилятор жалуется на двусмысленность между этой функцией и внутренним дефолтом Google-Test. Есть ли способ устранить ошибку и указать компилятор на мою функцию?
namespace Eigen {
// This function will cause the following compiler error, when defined inside
// the Eigen namespace.
// gmock-1.7.0/gtest/include/gtest/gtest-printers.h:600:5: error:
// call to 'PrintTo' is ambiguous
// PrintTo(value, os);
// ^~~~~~~
//
// It will simply be ignore when defined in the global namespace.
template <class Derived,
class = typename std::enable_if<is_eigen<Derived>::value>::type>
void PrintTo(const Derived &m, ::std::ostream *o) {
*o << "\n" << m;
}
}
Другой подход - перегрузить operator<<
для типа Eigen. Это действительно работает. Однако недостатком является то, что это глобальная перегрузка оператора ostream. Таким образом, невозможно определить какое-либо тестовое форматирование (например, дополнительную новую строку) без этих изменений, также влияющих на не тестируемый код. Следовательно, я бы предпочел специализированный PrintTo
, подобный приведенному выше.
template <class Derived,
class = typename std::enable_if<is_eigen<Derived>::value>::type>
::std::ostream &operator<<(::std::ostream &o, const Derived &m) {
o << "\n" << static_cast<const Eigen::DenseBase<Derived> &>(m);
return o;
}
Изменить: после ответа @Alex
В следующем коде я реализую решение @Alex и реализую небольшую функцию, которая преобразует ссылки собственных матриц в тип печати.
#include <Eigen/Dense>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gmock/gmock-matchers.h>
MATCHER_P(EigenEqual, expect,
std::string(negation ? "isn't" : "is") + " equal to" +
::testing::PrintToString(expect)) {
return arg == expect;
}
template <class Base>
class EigenPrintWrap : public Base {
friend void PrintTo(const EigenPrintWrap &m, ::std::ostream *o) {
*o << "\n" << m;
}
};
template <class Base>
const EigenPrintWrap<Base> &print_wrap(const Base &base) {
return static_cast<const EigenPrintWrap<Base> &>(base);
}
TEST(Eigen, Matrix) {
Eigen::Matrix2i A, B;
A << 1, 2,
3, 4;
B = A.transpose();
EXPECT_THAT(print_wrap(A), EigenEqual(print_wrap(B)));
}
Ответы
Ответ 1
Проблемы, с которыми вы сталкиваетесь, - проблемы с разрешением перегрузки.
Тест google реализует функцию шаблона
namespace testing { namespace internal {
template <typename T>
void PrintTo(const T& value, std::ostream *o) { /* do smth */ }
} }
Библиотека Eigen определяет функцию принтера, основанную на деривации. Следовательно
struct EigenBase { };
std::ostream& operator<< (std::ostream& stream, const EigenBase& m) { /* do smth */ }
struct Eigen : public EigenBase { };
void f1() {
Eigen e;
std::cout << e; // works
}
void f2() {
Eigen e;
print_to(eigen, &std::cout); // works
}
Оба имеют сомнительный дизайн.
Google Test не должен обеспечивать реализацию PrintTo
, но вместо этого должен проверять время компиляции, если пользователь предоставляет PrintTo
и в противном случае вызывает другую функцию печати по умолчанию PrintToDefault
. PrintTo
обеспечивает лучшее совпадение, чем тот, который вы предоставили (в соответствии с разрешением перегрузки).
с другой стороны, Eigen operator<<
основан на деривации, и функция шаблона также будет предпочтительной при разрешении перегрузки.
Eigen может предоставить базовый класс CRTP, который наследует operator<<
, который лучше подходит для соответствия.
Что вы можете сделать, это наследовать от пользователя и предоставлять перегрузку CRTP для вашего унаследованного класса, избегая проблемы.
#include <gtest/gtest.h>
#include <iostream>
class EigenBase {
};
std::ostream &operator<<(std::ostream &o, const EigenBase &r) {
o << "operator<< EigenBase called";
return o;
}
template <typename T>
void print_to(const T &t, std::ostream *o) {
*o << "Google Print To Called";
}
class EigenSub : public EigenBase {};
template <typename T>
struct StreamBase {
typedef T value_type;
// friend function is inline and static
friend std::ostream &operator<<(std::ostream &o, const value_type &r) {
o << "operator<< from CRTP called";
return o;
}
friend void print_to(const value_type &t, std::ostream *o) {
*o << "print_to from CRTP called";
}
};
// this is were the magic appears, because the oeprators are actually
// defined with signatures matching the MyEigenSub class.
class MyEigenSub : public EigenSub, public StreamBase<MyEigenSub> {
};
TEST(EigenBasePrint, t1) {
EigenBase e;
std::cout << e << std::endl; // works
}
TEST(EigenBasePrint, t2) {
EigenBase e;
print_to(e, &std::cout); // works
}
TEST(EigenSubPrint, t3) {
EigenSub e;
std::cout << e << std::endl; // works
}
TEST(EigenCRTPPrint, t4) {
MyEigenSub e;
std::cout << e << std::endl; // operator<< from CRTP called
}
TEST(EigenCRTPPrint, t5) {
MyEigenSub e;
print_to(e, &std::cout); // prints print_to from CRTP called
}
Ответ 2
Учитывая ответ OPs, я хочу сделать некоторые разъяснения.
В отличие от производного решения от OP я действительно хотел украсить класс, а не использовать оболочку функции внутри утверждения.
Для простоты, вместо использования предиката совпадения теста google, я перегрузил operator==
.
Идея
Вместо использования самого класса Eigen мы используем оболочку, которая является полной заменой Eigen. Поэтому всякий раз, когда мы создаем экземпляр Eigen
, вместо него создается экземпляр WrapEigen
.
Потому что мы не намерены изменять реализацию Eigen
. Прекращение вывода.
Кроме того, мы хотим добавить функции в оболочку. Я делаю это здесь с множественным наследованием функторов, таких как классы с именем StreamBase
и EqualBase
. Мы используем CRTP в этих функторах, чтобы получить правильные подписи.
Чтобы сохранить потенциальную типизацию, я использовал конструктор вариационных шаблонов в Wrapper
. Он вызывает соответствующий базовый конструктор, если он существует.
Рабочий пример
#include <gtest/gtest.h>
#include <iostream>
#include <utility>
using namespace testing::internal;
struct EigenBase {
explicit EigenBase(int i) : priv_(i) {}
friend std::ostream &operator<<(std::ostream &o, const EigenBase &r) {
o << r.priv_;
return o;
}
friend bool operator==(const EigenBase& a, const EigenBase& b) {
return a.priv_ == b.priv_;
}
int priv_;
};
struct Eigen : public EigenBase {
explicit Eigen(int i) : EigenBase(i) {}
};
template <typename T, typename U>
struct StreamBase {
typedef T value_type;
typedef const value_type &const_reference;
friend void PrintTo(const value_type &t, std::ostream *o) {
*o << static_cast<const U&>(t);
}
};
template <typename T, typename U>
struct EqualBase {
typedef T value_type;
typedef const T &const_reference;
friend bool operator==(const_reference a, const_reference b) {
return static_cast<const U&>(a)
== static_cast<const U&>(b);
}
};
template <typename T, typename U>
struct Wrapper
: public T,
public StreamBase<Wrapper<T,U>, U>,
public EqualBase<Wrapper<T,U>, U> {
template <typename... Args>
Wrapper(Args&&... args) : T(std::forward<Args>(args)...) { }
};
TEST(EigenPrint, t1) {
Eigen e(10);
Eigen f(11);
ASSERT_EQ(e,f); // calls gtest::PrintTo
}
TEST(WrapEigenPrint, t1) {
typedef Wrapper<Eigen, EigenBase> WrapEigen;
WrapEigen e(10);
WrapEigen f(11);
ASSERT_EQ(e,f); // calls our own.
}