Set up the test infrastructure

This is essentially copy-pasted from the cla-sysrepo private project.

Change-Id: Ie8c348536ab42481d79b060867f6c9f0443dba18
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bf49f98..52a3cdf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,41 @@
 find_package(docopt REQUIRED)
 find_package(spdlog REQUIRED)
 
+include(CTest)
+if(BUILD_TESTING)
+    enable_testing()
+    find_path(TROMPELOEIL_PATH trompeloeil.hpp PATH_SUFFIXES trompeloeil/)
+    if("${TROMPELOEIL_PATH}" STREQUAL "TROMPELOEIL_PATH-NOTFOUND")
+        message(FATAL_ERROR "Cannot find the \"trompeloeil.hpp\" file provided by <https://github.com/rollbear/trompeloeil>. "
+            "Please set TROMPELOEIL_PATH to where it is available.")
+    endif()
+
+    find_path(CATCH_PATH catch.hpp PATH_SUFFIXES catch/)
+    if("${CATCH_PATH}" STREQUAL "CATCH_PATH-NOTFOUND")
+        message(FATAL_ERROR "Cannot find the \"catch.hpp\" file provided by <http://catch-lib.net/>. "
+            "Please set CATCH_PATH to where it is available.")
+    endif()
+
+    add_library(TestCatchIntegration STATIC
+        tests/catch_integration.cpp
+        tests/trompeloeil_catch.h
+        )
+    target_include_directories(TestCatchIntegration SYSTEM PUBLIC ${TROMPELOEIL_PATH} ${CATCH_PATH})
+    target_include_directories(TestCatchIntegration PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/tests/ ${CMAKE_CURRENT_SOURCE_DIR}/src/)
+    target_link_libraries(TestCatchIntegration spdlog::spdlog)
+
+    macro(cli_test fname)
+        set(test_${fname}_SOURCES tests/${fname}.cpp)
+        add_executable(test_${fname} ${test_${fname}_SOURCES})
+        target_link_libraries(test_${fname} TestCatchIntegration)
+        if(NOT CMAKE_CROSSCOMPILING)
+            add_test(test_${fname} test_${fname})
+        endif()
+        target_include_directories(test_${fname} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+        target_link_libraries(test_${fname} TestCatchIntegration)
+    endmacro()
+endif()
+
 if(WITH_DOCS)
     set(doxyfile_in ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in)
     set(doxyfile ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
diff --git a/tests/catch_integration.cpp b/tests/catch_integration.cpp
new file mode 100644
index 0000000..b3143fb
--- /dev/null
+++ b/tests/catch_integration.cpp
@@ -0,0 +1,2 @@
+#define CATCH_CONFIG_MAIN
+#include <catch.hpp>
diff --git a/tests/trompeloeil_catch.h b/tests/trompeloeil_catch.h
new file mode 100644
index 0000000..c6d1131
--- /dev/null
+++ b/tests/trompeloeil_catch.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#ifdef REQUIRE
+# error This file needs to be included prior to including anything from Catch.
+#endif
+
+// clang-format off
+
+#include <ostream>
+#include <thread>
+#include <catch.hpp>
+#include <trompeloeil.hpp>
+
+// this is copy-paste from https://github.com/rollbear/trompeloeil/blob/master/docs/CookBook.md/#unit_test_frameworks
+namespace trompeloeil {
+
+/** @short Pass reports from the Trompeloeil mocker to Catch for processing as test failures */
+template <>
+struct reporter<trompeloeil::specialized>
+{
+  static void send(trompeloeil::severity s,
+                   const char* file,
+                   unsigned long line,
+                   const char* msg)
+  {
+    std::ostringstream os;
+    if (line) os << file << ':' << line << '\n';
+    os << msg;
+    auto failure = os.str();
+    if (s == severity::fatal)
+    {
+      FAIL(failure);
+    }
+    else
+    {
+      CAPTURE(failure);
+      CHECK(failure.empty());
+    }
+  }
+};
+}
+
+/** @short Wait until a given sequence of expectation is matched, and then a bit more to ensure that there's silence afterwards */
+void waitForCompletionAndBitMore(const trompeloeil::sequence& seq)
+{
+    using namespace std::literals;
+    using clock = std::chrono::steady_clock;
+
+    // We're busy-waiting a bit
+    const auto waitingStep = 30ms;
+    // Timeout after this much
+    const auto completionTimeout = 5000ms;
+    // When checking for silence afterwards, wait at least this long.
+    // We'll also wait as long as it originally took to process everything.
+    const auto minExtraWait = 100ms;
+
+    auto start = clock::now();
+    while (!seq.is_completed()) {
+        std::this_thread::sleep_for(waitingStep);
+        if (clock::now() - start > completionTimeout) {
+            break;
+        }
+    }
+    REQUIRE(seq.is_completed());
+    auto duration = std::chrono::duration<double>(clock::now() - start);
+    std::this_thread::sleep_for(std::max(duration, decltype(duration)(minExtraWait)));
+}