GTest не находит тесты в отдельных единицах компиляции

У меня есть программа, написанная на С++, с некоторыми вложенными папками, содержащими связанные библиотеки. Там находится SCS-скрипт верхнего уровня, который вызывает файлы SConscript в подпапках/библиотеках.

Внутри библиотеки cpp есть GTest:

TEST(X, just_a_passing_test) {
EXPECT_EQ(true, true);
}

В исходном программном источнике есть основной(), который просто вызывает GTests main и имеет еще один GTest:

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

TEST(Dummy, should_pass){
EXPECT_EQ(true, true);
}

Теперь проблема в том, что при запуске программы GTest запускает тест только в источнике main.cpp. Игнорирование теста в библиотеке. Теперь он становится странным, когда я ссылаюсь на несвязанный класс в той же библиотеке cpp в main.cpp, без какого-либо побочного эффекта (например, "SomeClass foo;" ), тест волшебным образом появляется. Я пробовал использовать -O0 и другие трюки, чтобы заставить gcc не оптимизировать код, который не вызывается. Я даже попробовал Клэнг. Я подозреваю, что это связано с тем, как GTest проверяет обнаружение во время компиляции, но я не могу найти никакой информации по этой проблеме. Я считаю, что он использует статическую инициализацию, поэтому, возможно, там происходит странное упорядочение. Любая помощь/информация очень ценится!

Обновление: Нашел раздел в FAQ, который звучит как эта проблема, несмотря на то, что он ссылается конкретно на Visual С++. Что включает в себя трюк/взлома, чтобы заставить компилятор не отбрасывать библиотеку, если не ссылаться. Он рекомендует не помещать тесты в библиотеки, но это заставляет меня задаться вопросом, как еще вы будете протестировать библиотеки, не имея исполняемого файла для каждого из них, быстро избавляя их от боли и с раздутым выходом. https://code.google.com/p/googletest/wiki/Primer#Important_note_for_Visual_C++_users

Ответ 1

Из настроек сцены собрано, что библиотека, чей тестовый пример gtest пропадает, статически связана в сборке приложения. Кроме того, Используется инструментальная цепочка GNU.

Причина проблемного поведения проста. Тест программа не содержит ссылок на что-либо в библиотеке, которая содержит TEST(X, just_a_passing_test). Поэтому компоновщику не нужно связывать какие-либо объектный файл из этой библиотеки, чтобы связать программу. Так оно и есть. Итак gtest runtime не находит этот тест в исполняемом файле, потому что его нет.

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

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

Никакой магии. Чтобы удовлетворить ссылку на этот общедоступный символ, компоновщик теперь обязано связать объектный файл с библиотекой - тот, который содержит определение символа. И ОП сообщает, что библиотека создана от a .cpp. Таким образом, в библиотеке есть только один объектный файл, и он содержит также определение тестового примера. С этим объектным файлом в связь, тестовый пример находится в программе.

OP трясет тщетно с параметрами компилятора, переключаясь с GCC на clang, в поисках более респектабельного способа достижения той же цели. Компилятор не имеет значения. GCC или clang, он получает связь с системным компоновщиком, ld (если не будут приняты необычные меры для его замены).

Есть ли более респектабельный способ получить ld для связывания объектного файла с статическая библиотека, даже если программа ссылается на отсутствие символов в этом объектном файле?

Есть. Скажем, что проблема в программе app, и проблема статической библиотеки libcool.a

Тогда обычная командная строка GCC, которая связывает app, напоминает это, в соответствующем указывает:

g++ -o app -L/path/to/the/libcool/archive -lcool

Это делегирует командную строку ld, с дополнительными параметрами компоновщика и библиотеки, которые g++ считает по умолчанию для системы, в которой он находится.

Когда компоновщик приходит к рассмотрению -lcool, он выяснит, что это запрос для архива /path/to/the/libcool/archive/libcool.a. Тогда это будет фигурировать вне зависимости от того, имеет ли он в данный момент какие-либо неразрешенные ссылки на символы в руке определения которых скомпилированы в объектных файлах в libcool.a. Если есть любой, то он свяжет эти объектные файлы с app. Если нет, то ссылки ничего от libcool.a и не переходит.

Но мы знаем, что в libcool.a есть определения символов, которые мы хотим link, хотя app не ссылается на них. В этом случае мы можем сказать компоновщик, чтобы связать объектные файлы с libcool.a, даже если они не упоминается. Точнее, мы можем сказать g++ сказать компоновщику, чтобы это сделать, так:

g++ -o app -L/path/to/the/libcool/archive -Wl,--whole-archive -lcool -Wl,-no-whole-archive

Те опции -Wl,... указывают g++ передать параметры ... в ld. --whole-archive option сообщает ld связать все объектные файлы с последующими архивами, независимо от того, ссылаются или нет, до дальнейшего уведомления. -no-whole-archive указывает ld, чтобы прекратить это делать и возобновить работу, как обычно.

Может показаться, что -Wl,-no-whole-archive является избыточным, так как это последнее, что на g++ командной строки. Но это не так. Помните, что g++ добавляет системные библиотеки по умолчанию до командной строки, за кулисами, прежде чем передать его в ld. Вы определенно не хотите, чтобы --whole-archive действовал, когда эти библиотеки по умолчанию связаны. (Связь не с ошибками определения).

Примените это решение к задаче и TEST(X, just_a_passing_test) будет выполняться без взлома, чтобы заставить программу сделать некоторые операции no-op ссылку в объектный файл, который определяет этот тест.

В этом случае очевидный недостаток этого решения. Если произойдет, что библиотека из которые мы хотим принудительно связать с каким-либо объектным файлом без ссылок, содержит кучу других файлов с объектами без ссылок, которые нам действительно не нужны. --whole-archive связывает их все они тоже, и они просто раздуваются в программе.

Решение --whole-archive может быть более респектабельным, чем ссылка no-op взломать, но это не респектабельно. Он даже не выглядит респектабельным.

Реальное решение здесь - это сделать разумную вещь. Если вы хотите компоновщик, чтобы связать определение чего-то в вашей программе, тогда не держите это в секрете от компоновщик. По крайней мере, объявите вещь в каждом блоке компиляции, где вы ожидайте, что его определение будет использовано.

Выполнение разумной вещи при тестировании gtest предполагает понимание того, что Макрос gtest, подобный TEST(X, just_a_passing_test), расширяется до определения класса, в этом случае:

class X_just_a_passing_test_Test : public ::testing::Test {
public: 
    X_just_a_passing_test_Test() {} 
private: 
    virtual void TestBody(); 
    static ::testing::TestInfo* const test_info_ __attribute__ ((unused)); 
    X_just_a_passing_test_Test(X_just_a_passing_test_Test const &); 
    void operator=(X_just_a_passing_test_Test const &);
};

(плюс статический инициализатор для test_info_ и определение для TestBody()).

Аналогично для вариантов TEST_F, TEST_P. Следовательно, вы можете развернуть эти макросов в вашем коде с теми же ограничениями и ожиданиями, которые применяются к определениям классов.

В этом свете, если у вас есть библиотека libcool, определенная в cool.h, реализована в cool.cpp и хотите gtest модульные тесты для него, выполняемые тестовой программой tests который реализован в tests.cpp, разумная вещь: -

  • Введите заголовочный файл, cool_test.h
  • #include "cool.h" в нем
  • #include <gtest/gtest.h>.
  • Затем определите в нем libcool тестовые примеры
  • #include "cool_test.h" в tests.cpp,
  • Скомпилировать и связать tests.cpp с libcool и libgtest

И это очевидно, почему вы не сделали бы то, что сделал OP. Вы бы не определили классы, которые необходимы tests.cpp и не нужны cool.cpp, в пределах cool.cpp а не в tests.cpp.

ОП не просил совета по определению тестовых случаев в библиотеке потому что:

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

Как правило, я бы рекомендовал практиковать сохранение gtest исполняемого файла на каждую библиотеку, подлежащую тестированию на единицу: быстро запускать их безболезненно с помощью обычных инструментов автоматизации такой make, и гораздо лучше получить приемочный/ошибочный вердикт на каждую библиотеку, чем просто вердикт для кучки библиотек. Но если вы не хотите этого делать, возражение:

// tests.cpp
#include "cool_test.h"
#include "cooler_test.h"
#include "coolest_test.h"

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Скомпилировать и связать с libcool, libcooler, libcoolest и libgtest