Научите Google-Test печатать собственную матрицу

Вступление

Я пишу тесты на собственных матрицах, используя платформу тестирования Google Google-Mock, как уже обсуждалось в еще вопрос.

С помощью следующего кода я смог добавить пользовательский Matcher для сопоставления собственных матриц с заданной точностью.

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 метода, и если они not match 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 говорится следующее о печати значений пользовательских типов:

Этот принтер умеет печатать встроенные типы C ++, собственные массивы, контейнеры STL и любой тип, поддерживающий оператор ‹*. Для других типов он печатает необработанные байты в значении и надеется, что вы, пользователь, сможете это понять.

Матрица Эйгена поставляется с operator<<. Однако Google-Test или, скорее, компилятор C ++ игнорирует это. Насколько я понимаю, по следующей причине: подпись этого оператора гласит (IO.h (строка 240))

template<typename Derived>
std::ostream &operator<< (std::ostream &s, const DenseBase<Derived> &m);

Т.е. требуется const DenseBase<Derived>&. С другой стороны, принтер Google-test hex-dump по умолчанию является реализацией функции шаблона по умолчанию. Вы можете найти реализацию здесь. (Следуйте дереву вызовов, начиная с PrintTo, чтобы убедиться, что это так, или докажите, что я не прав.;))

Таким образом, принтер Google-Test по умолчанию лучше подходит, потому что он принимает const Derived &, а не только его базовый класс const DenseBase<Derived> &.


Мой вопрос

У меня следующий вопрос. Как я могу сказать компилятору, что предпочтение отдается специфичному для Eigen operator <<, а не шестнадцатеричному дампу Google-test? При условии, что я не могу изменить определение класса матрицы Eigen.


Мои попытки

Пока что пробовал следующее.

Определение функции

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;
}

и следующая директива include

#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.

Во-первых, я определил черту для собственных типов. С его помощью мы можем использовать 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)));
}

person Lemming    schedule 05.08.2014    source источник
comment
Как насчет создания подклассов из пустой базы и использования enable_if, чтобы проверить, был ли ваш класс производным от этой пустой базы, а затем предложить соответствующую перегрузку?   -  person Alexander Oh    schedule 07.08.2014
comment
@Alex Спасибо за ваш комментарий. Я не уверен, правильно ли я вас понял. Вы имеете в виду, что я должен обернуть матрицу Eigen в другой класс, который наследуется от пустой базы? В этом случае у меня был бы полный контроль над этим конкретным классом, и, используя CRTP, я мог бы добавить перегрузку PrintTo так же, как и с плагином Eigen. Но мне кажется, что обертка - это крайнее средство. Поскольку обычно очень утомительно следить за тем, чтобы он правильно воспроизводил интерфейс.   -  person Lemming    schedule 07.08.2014
comment
Чтобы добавить к окончательному решению @ Lemming #define EXPECT_EIGEN_EQ(A, B) EXPECT_THAT(print_wrap(A), EigenEqual(print_wrap(B))); помогает   -  person    schedule 22.07.2016


Ответы (3)


Проблемы, с которыми вы сталкиваетесь, связаны с разрешением перегрузки.

google test реализует функцию шаблона

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 обеспечивает лучшее соответствие, чем тот, который вы предоставили (в зависимости от разрешения перегрузки).

с другой стороны, operator<< Эйгена основывается на выводе, и функция шаблона также будет предпочтительнее с точки зрения разрешения перегрузки.

Eigen мог бы предоставить базовый класс CRTP, который наследует operator<<, который лучше соответствует типу.

Что вы можете сделать, так это унаследовать от eigen и предоставить перегрузку 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
}
person Alexander Oh    schedule 12.08.2014
comment
Спасибо за Ваш ответ. Насколько я понимаю, это в основном тот же паттерн, который я ввожу в Eigen::MatrixBase с помощью EIGEN_MATRIXBASE_PLUGIN, но с внешним классом-оболочкой, правильно? Похоже, что это будет единственный вариант для классов, которые не поддерживают расширение так же, как предлагает Eigen. - person Lemming; 22.08.2014
comment
Я также добавил окончательный код после вашего ответа в самом конце моего вопроса. На всякий случай... - person Lemming; 22.08.2014

Учитывая ответ OP, я хочу сделать некоторые пояснения. В отличие от решения, производного от OP, я действительно хотел украсить класс вместо того, чтобы использовать оболочку функции внутри утверждения.

Для простоты вместо использования предиката Google test match я перегрузил 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.
}
person Alexander Oh    schedule 26.08.2014
comment
Спасибо за разъяснения. Думаю, теперь я понимаю, что вы имели в виду. Это должно работать достаточно хорошо для таких типов, как boost:multi_array. К сожалению, он очень легко ломается при использовании с типами Eigen. Eigen использует шаблоны выражений, например, e + f относится к другому типу, и его нужно будет снова обернуть. Это было мотивацией для моего подхода с отдельной функцией-оболочкой. Однако обе версии совместимы. - person Lemming; 28.08.2014
comment
@Lemming Я вижу, что ваш подход более подходит, если вы не хотите полностью обернуть публичный интерфейс. - person Alexander Oh; 28.08.2014
comment
Это кажется. Обертывание всего общедоступного интерфейса было бы излишним, поскольку мне это нужно только внутри тестов. - person Lemming; 28.08.2014

Я чувствую себя обязанным дать новый ответ, который, на мой взгляд, проще и лучше, чем другие, хотя он настолько прост, что я, возможно, что-то упустил. Это очень похоже на решения, которые вы уже пробовали, но не совсем то же самое.

По сути, вам не нужно прыгать через обручи плагинов для изменения класса. Предостережение: да, вы должны определить функцию PrintTo для каждого типа (Matrix2d, Matrix3d и т. Д.); шаблон функции не будет работать. Но поскольку это модульный тест, я предполагаю, что вы знаете, каковы все ваши типы, и это не проблема.

Так что по сути возьмите свой код из плагина и просто поместите его в модульный тест, как вы пытались сделать с шаблоном с поддержкой SFINAE:

namespace Eigen
{
    void PrintTo(const Matrix2d &m, std::ostream *os)
    {
      *os << std::endl << m << std::endl;
    }
}

Ничего фантастического. Это работает для меня и должно делать то, что вы хотите, в соответствии с вашим тестовым примером и вопросом.

person Jeremy W. Murphy    schedule 23.05.2018