Как правильно скомпилировать несколько тестовых исходников с помощью Catch2?

У меня есть следующая структура проекта:

test_main.cc

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

test1.cc

#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test1", "[test1]") {
  REQUIRE(1 == 1);
}

test2.cc

#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test2", "[test2]") {
  REQUIRE(2 == 2);
}

test_utils.hpp

#pragma once
#include <iostream>

void something_great() {
  std::cout << ":)\n";
}

Если я компилирую, используя что-то вроде clang++ -std=c++17 test_main.cc test1.cc test2.cc, функция something_great определена как в test1.o, так и в test2.o. Это приводит к ошибке вроде

duplicate symbol __Z15something_greatv in:
    test1.cc.o
    test2.cc.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

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

Используйте столько дополнительных файлов cpp (или того, что вы называете своими файлами реализации), сколько вам нужно для ваших тестов, но разбитых на разделы имеет смысл для вашего способа работы. Для каждого дополнительного файла нужно только #include "catch.hpp"

но в разделе примеры документации я не вижу такого варианта использования, как мой. Я прочитал эту запись в блоге, в которой описывается три решения, которые мне не нравятся: определение функций как макросов или создание функций static или inline.

Есть ли другой способ скомпилировать эти файлы, чтобы получить один исполняемый файл с основной функцией, определенной test_main.cc?


person Collin    schedule 14.03.2019    source источник
comment
Добавьте встроенный сюда. inline void something_great() {. Это проблема повторяющихся символов. В остальном все хорошо. Или переместите его в test_utils.cc и укажите только объявление в заголовке.   -  person balki    schedule 15.03.2019


Ответы (2)


На самом деле это не имеет ничего общего с Catch или тестированием. Когда вы #include создаете файл на C++, он дословно копируется в #include строку. Если вы поместите определения бесплатных функций в заголовки, вы увидите эту проблему при построении вашей реальной программы и т. д.

Основная проблема заключается в том, что #include — это не тот же тип директивы import-a-module, что и эквивалентная директива (import, require и т. д.) в большинстве языков, которые делают разумную вещь в подобной ситуации (подтвердите, что заголовок тот же самый, который мы уже видели, и игнорируем повторяющееся определение метода).

Комментатор, который предложил вам написать inline, технически верен в том смысле, что это «решит вашу проблему», потому что ваш компилятор не будет генерировать объектный код для метода несколько раз. Однако на самом деле это не объясняет, что происходит, и не решает основную проблему.


Чистое решение:

  • В test_utils.hpp замените определение метода объявлением метода: void something_great();.
  • Создайте test_utils.cc с определением метода (который у вас сейчас есть в .hpp).
  • clang++ -std=c++17 test1.cc -c
  • clang++ -std=c++17 test2.cc -c
  • clang++ -std=c++17 test_main.cc -c
  • clang++ -std=c++17 test_utils.cc -c
  • clang++ -std=c++17 test1.o test2.o test_utils.o test_main.o

Я также рекомендую вам прочитать это: В чем разница между определение и объявление?

Явно:

// test_utils.hpp
#pragma once

// This tells the compiler that when the final executable is linked,
// there will be a method named something_great which takes no arguments
// and returns void defined; the definition lives in test_utils.o in our
// case, although in practice the definition could live in any .o file
// in the final linking clang++ call.
void something_great();

А также:

// test_utils.cpp
#include "test_utils.hpp"
#include <iostream>

// Generates a DEFINITION for something_great, which
// will get put in test_utils.o.
void something_great() { std::cout << "Hi\n"; }

Кажется, вы беспокоитесь о «перекомпиляции Catch» каждый раз, когда вносите изменения в тест. Мне не хочется вас разочаровывать, но вы сейчас находитесь в стране C++: вы будете много и бессмысленно перекомпилировать материал. Библиотеки только для заголовков, такие как Catch, ДОЛЖНЫ быть «перекомпилированы» в некоторой степени, когда исходный файл, включающий их, изменяется, потому что, к лучшему или к худшему, если исходный файл или файл заголовка, включенный транзитивно из исходного файла, включает catch2.hpp, тогда исходный код catch2.hpp будет проанализирован компилятором при чтении этого исходного файла.

person Andrey Mishchenko    schedule 15.03.2019

После некоторых экспериментов я нашел разумное решение, которое не требует от вас полной перекомпиляции Catch каждый раз, когда вы вносите изменения в тест.

Определите test_main.cc так же, как и раньше:

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

Добавьте еще один файл .cc, test_root, который включает ваши тестовые файлы в виде заголовков:

#include "test1.hpp"
#include "test2.hpp"

Измените источники тестов на заголовки:

test1.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test1", "[test1]") {
  REQUIRE(1 == 1);
}

test2.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test2", "[test2]") {
  REQUIRE(2 == 2);
}

Компилировать отдельно

clang++ -std=c++17 test_main.cc -c
clang++ -std=c++17 test_root.cc -c
clang++ test_main.o test_root.o

Где test_main.cc нужно скомпилировать только один раз. test_root.cc нужно будет перекомпилировать всякий раз, когда вы изменяете свои тесты, и, конечно же, вы должны повторно связать два объектных файла.

Я оставлю этот ответ непринятым на данный момент, если есть лучшие решения.

person Collin    schedule 14.03.2019
comment
Мне любопытно, почему за это проголосовали. Было ли что-то не так с этим подходом? - person Collin; 15.03.2019
comment
возможно, потому что в учебнике по catch2 сказано, что вы не должны писать свои тесты в файлах заголовков: github.com/catchorg/Catch2/blob/master/docs/ - person John Doe; 08.09.2020