Подтвердить что ты не робот

Научите 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)));
}
4b9b3361

Ответ 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.
}