5

I have the following project structure:

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

If I compile using something like clang++ -std=c++17 test_main.cc test1.cc test2.cc, the function something_great is defined in both test1.o and test2.o. This leads to an error like

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)

In the Scaling Up section of the Catch2 documentation, they mention that in order to split up your tests you may want to

Use as many additional cpp files (or whatever you call your implementation files) as you need for your tests, partitioned however makes most sense for your way of working. Each additional file need only #include "catch.hpp"

but in the examples section of the documentation I don't see a use case like mine. I read this blog post which describes three solutions which don't appeal to me: defining functions as macros, or making functions static or inline.

Is there another way to compile these files which yield a single executable with the main function defined by test_main.cc?

Collin
  • 1,607
  • 2
  • 26
  • 39
  • 2
    Add inline here. `inline void something_great() {`. This is the duplicate symbol problem. Everything else is good. Or move it to a `test_utils.cc` and only have declaration in header. – balki Mar 14 '19 at 23:36

2 Answers2

5

This actually has nothing to do with Catch or testing. When you #include a file in C++, it gets copy-pasted at the #include line verbatim. If you put free function definitions in headers, you would see this problem building your actual program, etc.

The underlying problem is that #include is not the same kind of import-a-module directive as is the equivalent directive (import, require, etc.) in most languages, which do the sane thing in a situation like this (confirm that the header is the same one we've already seen and ignore the repeated method definition).

The commenter that suggested you write inline is technically correct, in the sense that this will "solve your problem" because your compiler won't generate object code for the method multiple times. However, it doesn't really explain what's going on or address the underlying issue.


The clean solution is:

  • In test_utils.hpp, replace the method definition with a method declaration: void something_great();.
  • Create test_utils.cc with the definition of the method (which you currently have in the .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

I also recommend you read this: What is the difference between a definition and a declaration?

Explicitly:

// 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();

And:

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

It seems you are worried about "recompiling Catch" every time you make a change to a test. I hate to break it to you, but you are in C++ land now: you are going to be recompiling stuff pointlessly a lot. Header-only libraries like Catch MUST be "recompiled" to some extent when a source file including them changes, because for better or worse, if the source file or a header file included transitively from the source file includes catch2.hpp, then the source code of catch2.hpp will get parsed by the compiler when that source file is read.

Andrey Mishchenko
  • 3,490
  • 1
  • 15
  • 36
-1

After some experimentation, I found a reasonable solution which doesn't require you to fully recompile Catch any time you make a change to a test.

Define test_main.cc in the same way as before:

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

Add another .cc file, test_root which includes your test files as headers:

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

Change your test sources to headers:

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

Compile separately

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

Where test_main.cc needs only be compiled once. test_root.cc will need to be recompiled whenever you change your tests and of course you must relink the two object files.

I will leave this answer unaccepted for now in case there are better solutions.

Collin
  • 1,607
  • 2
  • 26
  • 39
  • 2
    I'm curious why this was downvoted. Was there something "wrong" with this approach? – Collin Mar 15 '19 at 19:08
  • maybe because the catch2 tutorial says that you should not write your tests in header files: https://github.com/catchorg/Catch2/blob/master/docs/tutorial.md#scaling-up – John Doe Sep 08 '20 at 16:55