From 1c10872ea2b59ccbe9aee3229e6d357057ec4fbb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 12 Mar 2018 15:26:14 -0400
Subject: [PATCH] tests: add unit tests structure to opendht

+ Unit testing requires cppunit.
+ add options for CMake and Autotools to build opendht_unit_tests.
+ add sample tests for InfoHash
+ modify Dockerfiles and Travis build to run `make test`
+ Change lowbit signature to return an int instead of an unsigned.
---
 .gitignore                   |   4 +
 .travis.yml                  |   2 +-
 CMakeLists.txt               |  27 ++++++-
 Makefile.am                  |   2 +-
 cmake/FindCppunit.cmake      |  28 +++++++
 configure.ac                 |   6 ++
 docker/DockerfileDeps        |   2 +-
 docker/DockerfileDepsLlvm    |   4 +-
 docker/DockerfileTravis      |   6 +-
 docker/DockerfileTravisLlvm  |   6 +-
 docker/DockerfileTravisProxy |   3 +
 include/opendht/infohash.h   |   2 +-
 tests/Makefile.am            |   9 +++
 tests/infohashtester.cpp     | 145 +++++++++++++++++++++++++++++++++++
 tests/infohashtester.h       |  70 +++++++++++++++++
 tests/tests_runner.cpp       |  35 +++++++++
 16 files changed, 342 insertions(+), 9 deletions(-)
 create mode 100644 cmake/FindCppunit.cmake
 create mode 100644 tests/Makefile.am
 create mode 100644 tests/infohashtester.cpp
 create mode 100644 tests/infohashtester.h
 create mode 100644 tests/tests_runner.cpp

diff --git a/.gitignore b/.gitignore
index c79e0d15..0dd6ebd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,7 @@ Makefile.in
 
 # Python
 *.pyc
+*.stamp
 python/opendht.cpp
 python/setup.py
 
@@ -60,3 +61,6 @@ doc/Doxyfile
 # vim swap files
 *.swp
 *.swo
+
+# build dir
+build
diff --git a/.travis.yml b/.travis.yml
index daaf24c0..de9c3720 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -58,7 +58,7 @@ script:
       else
         options+='-DOPENDHT_PUSH_NOTIFICATIONS=OFF ';
       fi
-      docker run opendht-proxy /bin/sh -c "cd /root/opendht && mkdir build && cd build && cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=ON -DOPENDHT_LTO=ON $options .. && make -j8 && make install";
+      docker run opendht-proxy /bin/sh -c "cd /root/opendht && mkdir build && cd build && cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=ON -DOPENDHT_LTO=ON -DOPENDHT_TESTS=ON $options .. && make -j8 && ./opendht_unit_tests && make install";
     fi
 
   - |
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cdd49bf7..443a9963 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,7 +20,9 @@ option (OPENDHT_PROXY_SERVER "Enable DHT proxy server, use Restbed and jsoncpp"
 option (OPENDHT_PUSH_NOTIFICATIONS "Enable push notifications support" OFF)
 option (OPENDHT_PROXY_SERVER_IDENTITY "Allow clients to use the node identity" OFF)
 option (OPENDHT_PROXY_CLIENT "Enable DHT proxy client, use Restbed and jsoncpp" OFF)
-option (OPENDHT_INDEX "Build DHT indexation feature" ON)
+option (OPENDHT_INDEX "Build DHT indexation feature" OFF)
+option (OPENDHT_TESTS "Add unit tests executable" OFF)
+
 
 find_package(Doxygen)
 option (OPENDHT_DOCUMENTATION "Create and install the HTML based API documentation (requires Doxygen)" ${DOXYGEN_FOUND})
@@ -292,3 +294,26 @@ install (DIRECTORY include DESTINATION ${CMAKE_INSTALL_PREFIX})
 install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendht.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
 install (EXPORT opendht DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/opendht FILE opendhtConfig.cmake)
 install (FILES ${CMAKE_CURRENT_BINARY_DIR}/opendhtConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/opendht)
+
+# Unit tests
+IF(OPENDHT_TESTS)
+  FIND_PACKAGE(Cppunit REQUIRED)
+  # unit testing
+    add_executable(opendht_unit_tests
+      tests/tests_runner.cpp
+      tests/infohashtester.h
+      tests/infohashtester.cpp
+    )
+    if (OPENDHT_SHARED)
+      TARGET_LINK_LIBRARIES(opendht_unit_tests opendht)
+    else ()
+    	TARGET_LINK_LIBRARIES(opendht_unit_tests opendht-static)
+    endif ()
+    TARGET_LINK_LIBRARIES(opendht_unit_tests
+       ${CMAKE_THREAD_LIBS_INIT}
+       ${CPPUNIT_LIBRARIES}
+       ${GNUTLS_LIBRARIES}
+    )
+    enable_testing()
+    add_test(TEST opendht_unit_tests)
+ENDIF()
diff --git a/Makefile.am b/Makefile.am
index 37485650..d16800ad 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -12,7 +12,7 @@ if USE_CYTHON
 SUBDIRS += python
 endif
 
-SUBDIRS += doc
+SUBDIRS += doc tests
 
 ACLOCAL_AMFLAGS = -I m4
 
diff --git a/cmake/FindCppunit.cmake b/cmake/FindCppunit.cmake
new file mode 100644
index 00000000..d734a73d
--- /dev/null
+++ b/cmake/FindCppunit.cmake
@@ -0,0 +1,28 @@
+INCLUDE(FindPkgConfig)
+PKG_CHECK_MODULES(PC_CPPUNIT "cppunit")
+
+FIND_PATH(CPPUNIT_INCLUDE_DIRS
+    NAMES cppunit/TestCase.h
+    HINTS ${PC_CPPUNIT_INCLUDE_DIR}
+    ${CMAKE_INSTALL_PREFIX}/include
+    PATHS
+    /usr/local/include
+    /usr/include
+)
+
+FIND_LIBRARY(CPPUNIT_LIBRARIES
+    NAMES cppunit
+    HINTS ${PC_CPPUNIT_LIBDIR}
+    ${CMAKE_INSTALL_PREFIX}/lib
+    ${CMAKE_INSTALL_PREFIX}/lib64
+    PATHS
+    ${CPPUNIT_INCLUDE_DIRS}/../lib
+    /usr/local/lib
+    /usr/lib
+)
+
+LIST(APPEND CPPUNIT_LIBRARIES ${CMAKE_DL_LIBS})
+
+INCLUDE(FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS(CPPUNIT DEFAULT_MSG CPPUNIT_LIBRARIES CPPUNIT_INCLUDE_DIRS)
+MARK_AS_ADVANCED(CPPUNIT_LIBRARIES CPPUNIT_INCLUDE_DIRS)
diff --git a/configure.ac b/configure.ac
index 604d985e..26112757 100644
--- a/configure.ac
+++ b/configure.ac
@@ -182,6 +182,11 @@ AM_COND_IF([PROXY_CLIENT_OR_SERVER], [
 	LDFLAGS="${LDFLAGS} ${Jsoncpp_LIBS} -lrestbed"
 ], [])
 
+AC_ARG_ENABLE([tests], AS_HELP_STRING([--enable-tests], [Enable tests]), build_tests=yes, build_tests=no)
+AM_CONDITIONAL(OPENDHT_TESTS, test x$build_tests == xyes)
+AM_COND_IF([OPENDHT_TESTS], [
+	PKG_CHECK_MODULES([CppUnit], [cppunit >= 1.12])
+])
 
 AC_CONFIG_FILES([doc/Doxyfile doc/Makefile])
 
@@ -197,5 +202,6 @@ AM_COND_IF([USE_CYTHON], [
 AC_CONFIG_FILES([Makefile
                  src/Makefile
                  tools/Makefile
+								 tests/Makefile
                  opendht.pc])
 AC_OUTPUT
diff --git a/docker/DockerfileDeps b/docker/DockerfileDeps
index ed0f5582..9b7dc466 100644
--- a/docker/DockerfileDeps
+++ b/docker/DockerfileDeps
@@ -1,3 +1,3 @@
 FROM ubuntu:17.04
 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
-RUN apt-get update && apt-get install -y build-essential cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libargon2-0-dev cython3 python3-dev python3-setuptools && apt-get clean
\ No newline at end of file
+RUN apt-get update && apt-get install -y build-essential cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libargon2-0-dev cython3 python3-dev libcppunit-dev python3-setuptools && apt-get clean
diff --git a/docker/DockerfileDepsLlvm b/docker/DockerfileDepsLlvm
index ab2dedc3..5dc9fb56 100644
--- a/docker/DockerfileDepsLlvm
+++ b/docker/DockerfileDepsLlvm
@@ -1,7 +1,7 @@
 FROM ubuntu:17.04
 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
 RUN apt-get update \
-	&& apt-get install -y llvm llvm-dev clang make cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libargon2-0-dev cython3 python3-dev python3-setuptools \
+	&& apt-get install -y llvm llvm-dev clang make cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libargon2-0-dev cython3 python3-dev python3-setuptools libcppunit-dev \
 	&& apt-get remove -y gcc g++ && apt-get autoremove -y && apt-get clean
 ENV CC cc
-ENV CXX c++
\ No newline at end of file
+ENV CXX c++
diff --git a/docker/DockerfileTravis b/docker/DockerfileTravis
index 693827eb..840e4ccd 100644
--- a/docker/DockerfileTravis
+++ b/docker/DockerfileTravis
@@ -1,5 +1,9 @@
 FROM aberaud/opendht-deps
 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+RUN apt-get install -y libcppunit-dev # temp while aberaud/opendht-deps doesn't have this
+
 COPY . /root/opendht
 RUN cd /root/opendht && mkdir build && cd build \
-	&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_LTO=On .. && make -j8 && make install
+	&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_LTO=On -DOPENDHT_TESTS=ON ..  \
+	&& make -j8 && ./opendht_unit_tests && make install
diff --git a/docker/DockerfileTravisLlvm b/docker/DockerfileTravisLlvm
index c32a9fd8..b4a443e6 100644
--- a/docker/DockerfileTravisLlvm
+++ b/docker/DockerfileTravisLlvm
@@ -1,5 +1,9 @@
 FROM aberaud/opendht-deps-llvm
 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+RUN apt-get install -y libcppunit-dev # temp while aberaud/opendht-deps doesn't have this
+
 COPY . /root/opendht
 RUN cd /root/opendht && mkdir build && cd build \
-	&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On .. && make -j8 && make install
+	&& cmake -DCMAKE_INSTALL_PREFIX=/usr -DOPENDHT_PYTHON=On -DOPENDHT_TESTS=ON .. \
+	&& make -j8 && ./opendht_unit_tests && make install
diff --git a/docker/DockerfileTravisProxy b/docker/DockerfileTravisProxy
index b543e61a..551217b7 100644
--- a/docker/DockerfileTravisProxy
+++ b/docker/DockerfileTravisProxy
@@ -1,3 +1,6 @@
 FROM opendht-deps-proxy
 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+
+RUN apt-get install -y libcppunit-dev # temp while aberaud/opendht-deps doesn't have this
+
 COPY . /root/opendht
diff --git a/include/opendht/infohash.h b/include/opendht/infohash.h
index ade90dc3..e82613f5 100644
--- a/include/opendht/infohash.h
+++ b/include/opendht/infohash.h
@@ -129,7 +129,7 @@ public:
      * Find the lowest 1 bit in an id.
      * Result will allways be lower than 8*N
      */
-    inline unsigned lowbit() const {
+    inline int lowbit() const {
         int i, j;
         for(i = N-1; i >= 0; i--)
             if(data_[i] != 0)
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 00000000..16660000
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,9 @@
+if OPENDHT_TESTS
+bin_PROGRAMS = opendht_unit_tests
+
+AM_CPPFLAGS = -I../include
+
+opendht_unit_tests_SOURCES = tests_runner.cpp infohashtester.cpp
+opendht_unit_tests_HEADERS = infohashtester.h
+opendht_unit_tests_LDFLAGS = -lopendht -lcppunit  -L@top_builddir@/src/.libs @Argon2_LDFLAGS@ @GnuTLS_LIBS@
+endif
diff --git a/tests/infohashtester.cpp b/tests/infohashtester.cpp
new file mode 100644
index 00000000..1d8f116c
--- /dev/null
+++ b/tests/infohashtester.cpp
@@ -0,0 +1,145 @@
+/*
+ *  Copyright (C) 2018 Savoir-faire Linux Inc.
+ *
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "infohashtester.h"
+
+// std
+#include <iostream>
+#include <string>
+
+// opendht
+#include "infohash.h"
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(InfoHashTester);
+
+void
+InfoHashTester::setUp() {
+
+}
+
+void
+InfoHashTester::testConstructors() {
+    // Default constructor creates a null infohash
+    auto nullHash = dht::InfoHash();
+    CPPUNIT_ASSERT(nullHash.size() == 20);
+    CPPUNIT_ASSERT(!nullHash);
+    // Build from a uint8_t. if length to short, should get a null infohash
+    uint8_t to_short[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8};
+    auto infohash = dht::InfoHash(to_short, 8);
+    CPPUNIT_ASSERT(infohash.size() == 20);
+    CPPUNIT_ASSERT_EQUAL(infohash.toString(),
+        std::string("0000000000000000000000000000000000000000"));
+    // Build from a uint8_t. if length is enough, data should contains the uint8_t
+    uint8_t enough[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa,
+                        0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa};
+    infohash = dht::InfoHash(enough, 20);
+    CPPUNIT_ASSERT(infohash.size() == 20);
+    const auto* data = infohash.data();
+    for (auto i = 0; i < 20; ++i) {
+        CPPUNIT_ASSERT_EQUAL(enough[i], data[i]);
+    }
+    // if too long, should be cutted to 20
+    uint8_t tooLong[] = {0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa,
+                        0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb0};
+    infohash = dht::InfoHash(tooLong, 21);
+    CPPUNIT_ASSERT(infohash.size() == 20);
+    const auto* data2 = infohash.data();
+    for (auto i = 0; i < 20; ++i) {
+        CPPUNIT_ASSERT_EQUAL(enough[i], data2[i]);
+    }
+    // Build from string
+    auto infohashFromStr = dht::InfoHash("0102030405060708090A0102030405060708090A");
+    CPPUNIT_ASSERT(infohashFromStr.size() == 20);
+    const auto* dataStr = infohashFromStr.data();
+    for (auto i = 0; i < 20; ++i) {
+        CPPUNIT_ASSERT_EQUAL((int)dataStr[i], (int)data[i]);
+    }
+}
+
+void
+InfoHashTester::testComperators() {
+    auto nullHash = dht::InfoHash();
+    auto minHash = dht::InfoHash("0000000000000000000000000000000000111110");
+    auto maxHash = dht::InfoHash("0111110000000000000000000000000000000000");
+    // operator ==
+    CPPUNIT_ASSERT_EQUAL(minHash, minHash);
+    CPPUNIT_ASSERT_EQUAL(minHash, dht::InfoHash("0000000000000000000000000000000000111110"));
+	CPPUNIT_ASSERT(!(minHash == maxHash));
+    // operator !=
+    CPPUNIT_ASSERT(!(minHash != minHash));
+    CPPUNIT_ASSERT(!(minHash != dht::InfoHash("0000000000000000000000000000000000111110")));
+	CPPUNIT_ASSERT(minHash != maxHash);
+    // operator<
+    CPPUNIT_ASSERT(nullHash < minHash);
+    CPPUNIT_ASSERT(nullHash < maxHash);
+    CPPUNIT_ASSERT(minHash < maxHash);
+    CPPUNIT_ASSERT(!(minHash < nullHash));
+    CPPUNIT_ASSERT(!(maxHash < nullHash));
+    CPPUNIT_ASSERT(!(maxHash < minHash));
+    CPPUNIT_ASSERT(!(minHash < minHash));
+    // bool()
+    CPPUNIT_ASSERT(maxHash);
+    CPPUNIT_ASSERT(!nullHash);
+
+}
+
+void
+InfoHashTester::testLowBit() {
+    auto nullHash = dht::InfoHash();
+    auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+    auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+    CPPUNIT_ASSERT_EQUAL(nullHash.lowbit(), -1);
+    CPPUNIT_ASSERT_EQUAL(minHash.lowbit(), 155);
+    CPPUNIT_ASSERT_EQUAL(maxHash.lowbit(), 7);
+}
+
+void
+InfoHashTester::testCommonBits() {
+    auto nullHash = dht::InfoHash();
+    auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+    auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+    CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, nullHash), (unsigned)160);
+    CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, minHash), (unsigned)155);
+    CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(nullHash, maxHash), (unsigned)7);
+    CPPUNIT_ASSERT_EQUAL(dht::InfoHash::commonBits(minHash, maxHash), (unsigned)7);
+}
+
+void
+InfoHashTester::testXorCmp() {
+    auto nullHash = dht::InfoHash();
+    auto minHash = dht::InfoHash("0000000000000000000000000000000000000010");
+    auto maxHash = dht::InfoHash("0100000000000000000000000000000000000000");
+    CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(nullHash, maxHash), -1);
+    CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(maxHash, nullHash), 1);
+    CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(minHash, maxHash), -1);
+    CPPUNIT_ASSERT_EQUAL(minHash.xorCmp(maxHash, minHash), 1);
+    CPPUNIT_ASSERT_EQUAL(nullHash.xorCmp(minHash, maxHash), -1);
+    CPPUNIT_ASSERT_EQUAL(nullHash.xorCmp(maxHash, minHash), 1);
+    // Because hashes are circular in distance.
+    CPPUNIT_ASSERT_EQUAL(maxHash.xorCmp(nullHash, minHash), -1);
+    CPPUNIT_ASSERT_EQUAL(maxHash.xorCmp(minHash, nullHash), 1);
+}
+
+void
+InfoHashTester::tearDown() {
+
+}
+}  // namespace test
diff --git a/tests/infohashtester.h b/tests/infohashtester.h
new file mode 100644
index 00000000..2558d3af
--- /dev/null
+++ b/tests/infohashtester.h
@@ -0,0 +1,70 @@
+/*
+ *  Copyright (C) 2018 Savoir-faire Linux Inc.
+ *
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+// cppunit
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace test {
+
+class InfoHashTester : public CppUnit::TestFixture {
+    CPPUNIT_TEST_SUITE(InfoHashTester);
+    CPPUNIT_TEST(testConstructors);
+    CPPUNIT_TEST(testComperators);
+    CPPUNIT_TEST(testLowBit);
+    CPPUNIT_TEST(testCommonBits);
+    CPPUNIT_TEST(testXorCmp);
+    CPPUNIT_TEST_SUITE_END();
+
+ public:
+    /**
+     * Method automatically called before each test by CppUnit
+     */
+    void setUp();
+    /**
+     * Method automatically called after each test CppUnit
+     */
+    void tearDown();
+    /**
+     * Test the differents behaviors of constructors
+     */
+    void testConstructors();
+    /**
+     * Test compare operators
+     */
+    void testComperators();
+    /**
+     * Test lowbit method
+     */
+    void testLowBit();
+    /**
+     * Test commonBits method
+     */
+    void testCommonBits();
+    /**
+     * Test xorCmp operators
+     */
+    void testXorCmp();
+
+};
+
+}  // namespace test
diff --git a/tests/tests_runner.cpp b/tests/tests_runner.cpp
new file mode 100644
index 00000000..dffdc8c9
--- /dev/null
+++ b/tests/tests_runner.cpp
@@ -0,0 +1,35 @@
+/*
+ *  Copyright (C) 2017-2018 Savoir-faire Linux Inc.
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <cppunit/ui/text/TestRunner.h>
+#include <cppunit/CompilerOutputter.h>
+#include <iostream>
+
+int main(int argc, char** argv) {
+    CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry();
+    CppUnit::Test *suite = registry.makeTest();
+    if (suite->countTestCases() == 0) {
+        std::cout << "No test cases specified for suite" << std::endl;
+        return 1;
+    }
+    CppUnit::TextUi::TestRunner runner;
+    runner.addTest(suite);
+    auto result = runner.run() ? 0 : 1;
+    return result;
+}
-- 
GitLab