diff --git a/.circleci/config.yml b/.circleci/config.yml index 89ff50941..79f318f20 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,14 +26,16 @@ jobs: command: | mkdir -p release cd release - env CFLAGS="-Wdeclaration-after-statement -Werror" CXXFLAGS="-Werror" cmake .. + env CFLAGS="-Wdeclaration-after-statement -Werror" CXXFLAGS="-Werror" cmake -Werror=dev -DDOWNLOAD_GTEST=ON .. make everything + make run_tests ./teeworlds_srv shutdown - run: name: Build teeworlds with cmake in Debug mode command: | mkdir -p debug cd debug - env CFLAGS="-Wdeclaration-after-statement -Werror" CXXFLAGS="-Werror" cmake -DDEV=ON .. + env CFLAGS="-Wdeclaration-after-statement -Werror" CXXFLAGS="-Werror" cmake -Werror=dev -DDOWNLOAD_GTEST=ON -DDEV=ON .. make everything + make run_tests ./teeworlds_srv shutdown diff --git a/CMakeLists.txt b/CMakeLists.txt index 547d9851b..20e46dd55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ endif() option(CLIENT "Compile client" ON) option(DOWNLOAD_DEPENDENCIES "Download dependencies (only available on Windows)" ${AUTO_DEPENDENCIES_DEFAULT}) +option(DOWNLOAD_GTEST "Download and compile GTest if not found" ${AUTO_DEPENDENCIES_DEFAULT}) option(PREFER_BUNDLED_LIBS "Prefer bundled libraries over system libraries" ${AUTO_DEPENDENCIES_DEFAULT}) option(DEV "Don't generate stuff necessary for packaging" OFF) @@ -285,6 +286,7 @@ endif() find_package(ZLIB) find_package(Freetype) find_package(Git) +find_package(GTest) find_package(Pnglite) find_package(SDL2) find_package(Threads) @@ -346,6 +348,18 @@ endif() if(CLIENT AND NOT(SDL2_FOUND)) message(SEND_ERROR "You must install SDL2 to compile the Teeworlds client") endif() +if(NOT(GTEST_FOUND)) + if(DOWNLOAD_GTEST) + if(GIT_FOUND) + message(STATUS "Automatically downloading GTest to be able to run tests") + else() + set(DOWNLOAD_GTEST OFF) + message(WARNING "To automatically download GTest, you have to install Git") + endif() + else() + message(STATUS "To run the tests, you have to install GTest") + endif() +endif() if(TARGET_OS STREQUAL "windows") set(PLATFORM_CLIENT) @@ -371,6 +385,57 @@ else() endif() endif() +######################################################################## +# DOWNLOAD GTEST +######################################################################## + +if(NOT(GTEST_FOUND) AND DOWNLOAD_GTEST) + set(TEEWORLDS_GTEST_VERSION release-1.8.1) + configure_file(cmake/Download_GTest_CMakeLists.txt.in googletest-download/CMakeLists.txt) + execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . + RESULT_VARIABLE result + WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/googletest-download + ) + if(result) + message(WARNING "CMake step for googletest failed: ${result}") + set(DOWNLOAD_GTEST OFF) + else() + execute_process(COMMAND ${CMAKE_COMMAND} --build . + RESULT_VARIABLE result + WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/googletest-download + ) + if(result) + message(WARNING "Build step for googletest failed: ${result}") + set(DOWNLOAD_GTEST OFF) + else() + # Prevent overriding the parent project's compiler/linker settings on Windows + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + + # Add googletest directly to our build. This defines the gtest target. + add_subdirectory( + ${PROJECT_BINARY_DIR}/googletest-src + ${PROJECT_BINARY_DIR}/googletest-build + EXCLUDE_FROM_ALL + ) + + if(MSVC) + foreach(target gtest) + # `/w` disables all warnings. This is needed because `gtest` enables + # `/WX` (equivalent of `-Werror`) for some reason, breaking builds + # when MSVS adds new warnings. + target_compile_options(${target} PRIVATE $<$:/MT> $<${DBG}:/MTd> /w) + endforeach() + endif() + + set(GTEST_LIBRARIES gtest) + set(GTEST_INCLUDE_DIRS) + if(CMAKE_VERSION VERSION_LESS 2.8.11) + set(GTEST_INCLUDE_DIRS "${gtest_SOURCE_DIR}/include") + endif() + endif() + endif() +endif() + ######################################################################## # DEPENDENCY COMPILATION ######################################################################## @@ -1478,6 +1543,40 @@ list(APPEND TARGETS_LINK ${TARGETS_TOOLS}) add_custom_target(tools DEPENDS ${TARGETS_TOOLS}) add_custom_target(everything DEPENDS ${TARGETS_OWN}) +######################################################################## +# TESTS +######################################################################## + +if(GTEST_FOUND OR DOWNLOAD_GTEST) + set_src(TESTS GLOB src/test + fs.cpp + git_revision.cpp + str.cpp + test.cpp + test.h + thread.cpp + ) + set(TARGET_TESTRUNNER testrunner) + add_executable(${TARGET_TESTRUNNER} EXCLUDE_FROM_ALL + ${TESTS} + $ + $ + ${DEPS} + ) + target_link_libraries(${TARGET_TESTRUNNER} ${LIBS} ${GTEST_LIBRARIES}) + target_include_directories(${TARGET_TESTRUNNER} PRIVATE ${GTEST_INCLUDE_DIRS}) + + list(APPEND TARGETS_OWN ${TARGET_TESTRUNNER}) + list(APPEND TARGETS_LINK ${TARGET_TESTRUNNER}) + + add_custom_target(run_tests + COMMAND $ ${TESTRUNNER_ARGS} + COMMENT Running tests + DEPENDS ${TARGET_TESTRUNER} + USES_TERMINAL + ) +endif() + ######################################################################## # INSTALLATION ######################################################################## diff --git a/appveyor.yml b/appveyor.yml index 2b5e5b737..d864cf9d9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,6 +17,10 @@ build_script: - cmd: cmake --build build64 --config Release --target everything test_script: + - cmd: cmake --build build32 --config Debug --target run_tests + - cmd: cmake --build build64 --config Debug --target run_tests + - cmd: cmake --build build32 --config Release --target run_tests + - cmd: cmake --build build64 --config Release --target run_tests - cmd: | cd build32 Release\teeworlds_srv shutdown diff --git a/cmake/Download_GTest_CMakeLists.txt.in b/cmake/Download_GTest_CMakeLists.txt.in new file mode 100644 index 000000000..c1a176b02 --- /dev/null +++ b/cmake/Download_GTest_CMakeLists.txt.in @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 2.8) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG "${TEEWORLDS_GTEST_VERSION}" + SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" + TLS_VERIFY ON +) diff --git a/src/base/system.c b/src/base/system.c index 2f222a0a5..f200055a7 100644 --- a/src/base/system.c +++ b/src/base/system.c @@ -39,6 +39,7 @@ #include #include #include + #include #include #else #error NOT IMPLEMENTED @@ -99,7 +100,7 @@ void dbg_msg(const char *sys, const char *fmt, ...) va_start(args, fmt); #if defined(CONF_FAMILY_WINDOWS) - _vsnprintf(msg, sizeof(str)-len, fmt, args); + _vsprintf_p(msg, sizeof(str)-len, fmt, args); #else vsnprintf(msg, sizeof(str)-len, fmt, args); #endif @@ -421,7 +422,7 @@ unsigned io_write_newline(IOHANDLE io) int io_close(IOHANDLE io) { fclose((FILE*)io); - return 1; + return 0; } int io_flush(IOHANDLE io) @@ -1471,7 +1472,7 @@ int fs_storage_path(const char *appname, char *path, int max) char *home = getenv("APPDATA"); if(!home) return -1; - _snprintf(path, max, "%s/%s", home, appname); + str_format(path, max, "%s/%s", home, appname); return 0; #else char *home = getenv("HOME"); @@ -1483,25 +1484,25 @@ int fs_storage_path(const char *appname, char *path, int max) return -1; #if defined(CONF_PLATFORM_MACOSX) - snprintf(path, max, "%s/Library/Application Support/%s", home, appname); + str_format(path, max, "%s/Library/Application Support/%s", home, appname); return 0; #endif /* old folder location */ - snprintf(path, max, "%s/.%s", home, appname); + str_format(path, max, "%s/.%s", home, appname); for(i = strlen(home)+2; path[i]; i++) path[i] = tolower(path[i]); if(!xdgdatahome) { /* use default location */ - snprintf(xdgpath, max, "%s/.local/share/%s", home, appname); + str_format(xdgpath, max, "%s/.local/share/%s", home, appname); for(i = strlen(home)+14; xdgpath[i]; i++) xdgpath[i] = tolower(xdgpath[i]); } else { - snprintf(xdgpath, max, "%s/%s", xdgdatahome, appname); + str_format(xdgpath, max, "%s/%s", xdgdatahome, appname); for(i = strlen(xdgdatahome)+1; xdgpath[i]; i++) xdgpath[i] = tolower(xdgpath[i]); } @@ -1514,7 +1515,7 @@ int fs_storage_path(const char *appname, char *path, int max) return 0; } - snprintf(path, max, "%s", xdgpath); + str_format(path, max, "%s", xdgpath); return 0; #endif @@ -1773,7 +1774,7 @@ void str_format(char *buffer, int buffer_size, const char *format, ...) #if defined(CONF_FAMILY_WINDOWS) va_list ap; va_start(ap, format); - _vsnprintf(buffer, buffer_size, format, ap); + _vsprintf_p(buffer, buffer_size, format, ap); va_end(ap); #else va_list ap; @@ -2351,6 +2352,15 @@ void secure_random_fill(void *bytes, unsigned length) #endif } +int pid() +{ +#if defined(CONF_FAMILY_WINDOWS) + return _getpid(); +#else + return getpid(); +#endif +} + #if defined(__cplusplus) } #endif diff --git a/src/base/system.h b/src/base/system.h index bd46dc2e1..a21e4b811 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -1441,6 +1441,15 @@ int secure_random_init(); */ void secure_random_fill(void *bytes, unsigned length); +/* + Function: pid + Gets the process ID of the current process + + Returns: + The process ID of the current process. +*/ +int pid(); + #ifdef __cplusplus } #endif diff --git a/src/test/fs.cpp b/src/test/fs.cpp new file mode 100644 index 000000000..fc10c7ed4 --- /dev/null +++ b/src/test/fs.cpp @@ -0,0 +1,14 @@ +#include "test.h" +#include + +#include + +TEST(Filesystem, CreateCloseDelete) +{ + CTestInfo Info; + + IOHANDLE File = io_open(Info.m_aFilename, IOFLAG_WRITE); + ASSERT_TRUE(File); + EXPECT_FALSE(io_close(File)); + EXPECT_FALSE(fs_remove(Info.m_aFilename)); +} diff --git a/src/test/git_revision.cpp b/src/test/git_revision.cpp new file mode 100644 index 000000000..3389becd0 --- /dev/null +++ b/src/test/git_revision.cpp @@ -0,0 +1,14 @@ +#include + +#include +#include + +extern const char *GIT_SHORTREV_HASH; + +TEST(GitRevision, ExistsOrNull) +{ + if(GIT_SHORTREV_HASH) + { + ASSERT_STRNE(GIT_SHORTREV_HASH, ""); + } +} diff --git a/src/test/str.cpp b/src/test/str.cpp new file mode 100644 index 000000000..0e3884646 --- /dev/null +++ b/src/test/str.cpp @@ -0,0 +1,63 @@ +#include + +#include + +TEST(Str, Startswith) +{ + EXPECT_TRUE(str_startswith("abcdef", "abc")); + EXPECT_FALSE(str_startswith("abc", "abcdef")); + + EXPECT_TRUE(str_startswith("xyz", "")); + EXPECT_FALSE(str_startswith("", "xyz")); + + EXPECT_FALSE(str_startswith("house", "home")); + EXPECT_FALSE(str_startswith("blackboard", "board")); + + EXPECT_TRUE(str_startswith("поплавать", "по")); + EXPECT_FALSE(str_startswith("плавать", "по")); + + static const char ABCDEFG[] = "abcdefg"; + static const char ABC[] = "abc"; + EXPECT_EQ(str_startswith(ABCDEFG, ABC) - ABCDEFG, str_length(ABC)); +} + +TEST(Str, Endswith) +{ + EXPECT_TRUE(str_endswith("abcdef", "def")); + EXPECT_FALSE(str_endswith("def", "abcdef")); + + EXPECT_TRUE(str_endswith("xyz", "")); + EXPECT_FALSE(str_endswith("", "xyz")); + + EXPECT_FALSE(str_endswith("rhyme", "mine")); + EXPECT_FALSE(str_endswith("blackboard", "black")); + + EXPECT_TRUE(str_endswith("люди", "юди")); + EXPECT_FALSE(str_endswith("люди", "любовь")); + + static const char ABCDEFG[] = "abcdefg"; + static const char DEFG[] = "defg"; + EXPECT_EQ(str_endswith(ABCDEFG, DEFG) - ABCDEFG, + str_length(ABCDEFG) - str_length(DEFG)); +} + +TEST(StrFormat, Positional) +{ + char aBuf[256]; + + // normal + str_format(aBuf, sizeof(aBuf), "%s %s", "first", "second"); + EXPECT_STREQ(aBuf, "first second"); + + // normal with positional arguments + str_format(aBuf, sizeof(aBuf), "%1$s %2$s", "first", "second"); + EXPECT_STREQ(aBuf, "first second"); + + // reverse + str_format(aBuf, sizeof(aBuf), "%2$s %1$s", "first", "second"); + EXPECT_STREQ(aBuf, "second first"); + + // duplicate + str_format(aBuf, sizeof(aBuf), "%1$s %1$s %2$d %1$s %2$d", "str", 1); + EXPECT_STREQ(aBuf, "str str 1 str 1"); +} diff --git a/src/test/test.cpp b/src/test/test.cpp new file mode 100644 index 000000000..761d661f7 --- /dev/null +++ b/src/test/test.cpp @@ -0,0 +1,19 @@ +#include "test.h" +#include + +#include + +CTestInfo::CTestInfo() +{ + const ::testing::TestInfo *pTestInfo = + ::testing::UnitTest::GetInstance()->current_test_info(); + str_format(m_aFilename, sizeof(m_aFilename), "%s.%s-%d.tmp", + pTestInfo->test_case_name(), pTestInfo->name(), pid()); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + net_init(); + return RUN_ALL_TESTS(); +} diff --git a/src/test/test.h b/src/test/test.h new file mode 100644 index 000000000..71f13c9b0 --- /dev/null +++ b/src/test/test.h @@ -0,0 +1,9 @@ +#ifndef TEST_TEST_H +#define TEST_TEST_H +class CTestInfo +{ +public: + CTestInfo(); + char m_aFilename[64]; +}; +#endif // TEST_TEST_H diff --git a/src/test/thread.cpp b/src/test/thread.cpp new file mode 100644 index 000000000..4f2946d4a --- /dev/null +++ b/src/test/thread.cpp @@ -0,0 +1,48 @@ +#include + +#include + +static void Nothing(void *pUser) +{ + (void)pUser; +} + +TEST(Thread, Detach) +{ + void *pThread = thread_init(Nothing, 0); + thread_detach(pThread); +} + +static void SetToOne(void *pUser) +{ + *(int *)pUser = 1; +} + +TEST(Thread, Wait) +{ + int Integer = 0; + void *pThread = thread_init(SetToOne, &Integer); + thread_wait(pThread); + EXPECT_EQ(Integer, 1); +} + +TEST(Thread, Yield) +{ + thread_yield(); +} + +static void LockThread(void *pUser) +{ + LOCK *pLock = (LOCK *)pUser; + lock_wait(*pLock); + lock_unlock(*pLock); +} + +TEST(Thread, Lock) +{ + LOCK Lock = lock_create(); + lock_wait(Lock); + void *pThread = thread_init(LockThread, &Lock); + lock_unlock(Lock); + thread_wait(pThread); +}