diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8a1b6..4607212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,9 +121,9 @@ jobs: - name: Run tests id: test shell: msys2 {0} - working-directory: build/tests + working-directory: build run: | - ./test_libdisplaydevice.exe --gtest_color=yes + ./tests/test_libdisplaydevice --gtest_color=yes - name: Generate gcov report # any except canceled or skipped diff --git a/.gitignore b/.gitignore index 5073c82..c75a6b1 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ # build directories build/ cmake-*/ + +# CTest +Testing/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 90c291f..8947e6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,67 +1,34 @@ +# +# Project configuration +# cmake_minimum_required(VERSION 3.13) # todo: what is the minimum version required? - project(libdisplaydevice DESCRIPTION "Library to modify display devices." - HOMEPAGE_URL "https://app.lizardbyte.dev") + HOMEPAGE_URL "https://app.lizardbyte.dev" + LANGUAGES CXX) set(PROJECT_LICENSE "GPL-3.0") +set(CMAKE_CXX_STANDARD 20) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Release' as none was specified.") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() -set(CMAKE_CXX_STANDARD 20) - -# set the module path, used for includes -set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") - -# options +# +# Project optional configuration +# option(BUILD_TESTS "Build tests" ON) -option(CODE_QL_ANALYSIS "Build is for codeql analysis" OFF) - -# auto-set CODE_QL_ANALYSIS based on env variable -if(DEFINED ENV{GITHUB_CODEQL_BUILD}) - set(CODE_QL_ANALYSIS ON) -endif() -# include directories (for including header files) -include_directories("${CMAKE_SOURCE_DIR}") +# +# Library code is located here +# +add_subdirectory(src) -# glob src files, excluding platf directory -file(GLOB_RECURSE DD_COMMON_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp") -list(FILTER DD_COMMON_TARGET_FILES EXCLUDE REGEX ".*/platf/.*") - -if(WIN32) - file(GLOB_RECURSE DD_PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platf/windows/*.cpp") - include(${CMAKE_MODULE_PATH}/windows.cmake) -elseif(APPLE) - if(NOT CODE_QL_ANALYSIS) - message(FATAL_ERROR "MacOS is not supported yet.") - endif() - file(GLOB_RECURSE DD_PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platf/macos/*.cpp") - include(${CMAKE_MODULE_PATH}/macos.cmake) -elseif(UNIX) - if(NOT CODE_QL_ANALYSIS) - message(FATAL_ERROR "Linux is not supported yet.") - endif() - file(GLOB_RECURSE DD_PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platf/linux/*.cpp") - include(${CMAKE_MODULE_PATH}/linux.cmake) -else() - message(FATAL_ERROR "Unsupported platform") -endif() - -message(STATUS "Common source files: ${DD_COMMON_TARGET_FILES}") -message(STATUS "Platform source files: ${DD_PLATFORM_TARGET_FILES}") - -# Combine common and platform-specific source files -set(DD_TARGET_FILES ${DD_COMMON_TARGET_FILES} ${DD_PLATFORM_TARGET_FILES}) - -# tests -if(BUILD_TESTS) +# +# Testing only available if this is the main project +# +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTS) + enable_testing() add_subdirectory(tests) endif() - -# lib -add_library(${PROJECT_NAME} ${DD_TARGET_FILES}) -target_include_directories(${PROJECT_NAME} PUBLIC include) diff --git a/cmake/linux.cmake b/cmake/linux.cmake deleted file mode 100644 index addf2c1..0000000 --- a/cmake/linux.cmake +++ /dev/null @@ -1,3 +0,0 @@ -# Linux specific setup -file(GLOB_RECURSE DD_TARGET_FILES - "${CMAKE_SOURCE_DIR}/src/platf/linux/*.cpp") diff --git a/cmake/macos.cmake b/cmake/macos.cmake deleted file mode 100644 index e1c5e94..0000000 --- a/cmake/macos.cmake +++ /dev/null @@ -1,3 +0,0 @@ -# macOS specific setup -file(GLOB_RECURSE DD_TARGET_FILES - "${CMAKE_SOURCE_DIR}/src/platf/macos/*.cpp") diff --git a/cmake/windows.cmake b/cmake/windows.cmake deleted file mode 100644 index 606fc02..0000000 --- a/cmake/windows.cmake +++ /dev/null @@ -1,3 +0,0 @@ -# Windows specific setup -file(GLOB_RECURSE DD_TARGET_FILES - "${CMAKE_SOURCE_DIR}/src/platf/windows/*.cpp") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..984f6ef --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,19 @@ +# This a shared common library for other libraries +add_subdirectory(common) + +# This is a platform-specific library +if(WIN32) + add_subdirectory(windows) +elseif(APPLE) + message(WARNING "MacOS is not supported yet.") +elseif(UNIX) + message(WARNING "Linux is not supported yet.") +else() + message(FATAL_ERROR "Unsupported platform") +endif() + +# This is a platform-specific library that loads the correct library for the OS +add_subdirectory(platf) + +# This is the main library +add_subdirectory(displaydevice) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt new file mode 100644 index 0000000..01afea8 --- /dev/null +++ b/src/common/CMakeLists.txt @@ -0,0 +1,12 @@ +# A global identifier for the library +set(MODULE libcommon) + +# Globing headers (so that they appear in some IDEs) and sources +file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/displaydevice/*.h") +file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") + +# Automatic library - will be static or dynamic based on user setting +add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) + +# Provide the includes together with this library +target_include_directories(${MODULE} PUBLIC include) diff --git a/src/logging.h b/src/common/include/displaydevice/logging.h similarity index 100% rename from src/logging.h rename to src/common/include/displaydevice/logging.h diff --git a/src/logging.cpp b/src/common/logging.cpp similarity index 98% rename from src/logging.cpp rename to src/common/logging.cpp index f78d46e..2b54ad1 100644 --- a/src/logging.cpp +++ b/src/common/logging.cpp @@ -1,5 +1,5 @@ // class header include -#include "logging.h" +#include "displaydevice/logging.h" // system includes #include diff --git a/src/displaydevice/CMakeLists.txt b/src/displaydevice/CMakeLists.txt new file mode 100644 index 0000000..223d660 --- /dev/null +++ b/src/displaydevice/CMakeLists.txt @@ -0,0 +1,15 @@ +# A global identifier for the library +set(MODULE libdisplaydevice) + +# Globing headers (so that they appear in some IDEs) and sources +file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/displaydevice/*.h") +file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") + +# Automatic library - will be static or dynamic based on user setting +add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) + +# Provide the includes together with this library +target_include_directories(${MODULE} PUBLIC include) + +# Required libraries +target_link_libraries(${MODULE} PUBLIC libcommon PRIVATE libplatf) diff --git a/src/displaydevice/include/displaydevice/libddplaceholder.h b/src/displaydevice/include/displaydevice/libddplaceholder.h new file mode 100644 index 0000000..e69de29 diff --git a/src/displaydevice/libddplaceholder.cpp b/src/displaydevice/libddplaceholder.cpp new file mode 100644 index 0000000..d1f244d --- /dev/null +++ b/src/displaydevice/libddplaceholder.cpp @@ -0,0 +1,6 @@ +#include "displaydevice/libddplaceholder.h" + +int +ddplaceholder() { + return 0; +} diff --git a/src/platf/CMakeLists.txt b/src/platf/CMakeLists.txt new file mode 100644 index 0000000..be702ce --- /dev/null +++ b/src/platf/CMakeLists.txt @@ -0,0 +1,26 @@ +# A global identifier for the library +set(MODULE libplatf) + +# Globing headers (so that they appear in some IDEs) and sources +file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/displaydevice/*.h") +file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") + +# Automatic library - will be static or dynamic based on user setting +add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) + +# Provide the includes together with this library +target_include_directories(${MODULE} PUBLIC include) + +# Shared libraries between platforms +target_link_libraries(${MODULE} PRIVATE libcommon) + +# Link the platform specific library privately +if(WIN32) + target_link_libraries(${MODULE} PRIVATE libwindows) +elseif(APPLE) + message(WARNING "MacOS is not supported yet.") +elseif(UNIX) + message(WARNING "Linux is not supported yet.") +else() + message(FATAL_ERROR "Unsupported platform") +endif() diff --git a/src/platf/include/displaydevice/libplatfplaceholder.h b/src/platf/include/displaydevice/libplatfplaceholder.h new file mode 100644 index 0000000..e69de29 diff --git a/src/platf/libplatfplaceholder.cpp b/src/platf/libplatfplaceholder.cpp new file mode 100644 index 0000000..8754fc4 --- /dev/null +++ b/src/platf/libplatfplaceholder.cpp @@ -0,0 +1,6 @@ +#include "displaydevice/libplatfplaceholder.h" + +int +plaftplaceholder() { + return 0; +} diff --git a/src/windows/CMakeLists.txt b/src/windows/CMakeLists.txt new file mode 100644 index 0000000..2af2f2f --- /dev/null +++ b/src/windows/CMakeLists.txt @@ -0,0 +1,15 @@ +# A global identifier for the library +set(MODULE libwindows) + +# Globing headers (so that they appear in some IDEs) and sources +file(GLOB HEADER_LIST CONFIGURE_DEPENDS "include/libdisplaydevice/windows/*.h") +file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") + +# Automatic library - will be static or dynamic based on user setting +add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) + +# Provide the includes together with this library +target_include_directories(${MODULE} PUBLIC include) + +# Link the additional libraries +target_link_libraries(${MODULE} PRIVATE libcommon) diff --git a/src/platf/windows/winapilayer.h b/src/windows/include/displaydevice/windows/winapilayer.h similarity index 100% rename from src/platf/windows/winapilayer.h rename to src/windows/include/displaydevice/windows/winapilayer.h diff --git a/src/platf/windows/winapilayerinterface.h b/src/windows/include/displaydevice/windows/winapilayerinterface.h similarity index 100% rename from src/platf/windows/winapilayerinterface.h rename to src/windows/include/displaydevice/windows/winapilayerinterface.h diff --git a/src/platf/windows/winapilayer.cpp b/src/windows/winapilayer.cpp similarity index 99% rename from src/platf/windows/winapilayer.cpp rename to src/windows/winapilayer.cpp index 49a762b..dbc1da4 100644 --- a/src/platf/windows/winapilayer.cpp +++ b/src/windows/winapilayer.cpp @@ -1,11 +1,11 @@ // class header include -#include "winapilayer.h" +#include "displaydevice/windows/winapilayer.h" // system includes #include // local includes -#include "src/logging.h" +#include "displaydevice/logging.h" namespace display_device { namespace { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8cbf236..e314595 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,75 +1,65 @@ -cmake_minimum_required(VERSION 3.13) -# https://github.com/google/oss-policies-info/blob/main/foundational-cxx-support-matrix.md#foundational-c-support - -project(test_libdisplaydevice) - -include_directories("${CMAKE_SOURCE_DIR}") - -enable_testing() - -# Add GoogleTest directory to the project -set(GTEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/third-party/googletest") +# +# Setup google test +# set(INSTALL_GTEST OFF) set(INSTALL_GMOCK OFF) -add_subdirectory("${GTEST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/googletest") -include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR}") +include(GoogleTest) +add_subdirectory("${PROJECT_SOURCE_DIR}/third-party/googletest" "third-party/googletest") -# coverage -# https://gcovr.com/en/stable/guide/compiling.html#compiler-options -set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -O1") -set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -O1") - -# if windows if (WIN32) # For Windows: Prevent overriding the parent project's compiler/linker settings - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # cmake-lint: disable=C0103 + set(gtest_force_shared_crt ON CACHE BOOL "Always use msvcrt.dll" FORCE) # cmake-lint: disable=C0103 endif () -set(TEST_PATTERNS test_* mock_*) -file(GLOB_RECURSE TEST_SOURCES - "${CMAKE_SOURCE_DIR}/tests/conftest.cpp" - "${CMAKE_SOURCE_DIR}/tests/utils.cpp") -set(TEST_PLATFORM_SOURCES) +# A helper function to setup the dependencies for the test executable +function(add_dd_test_dir) + set(options "") + set(oneValueArgs "") + set(multiValueArgs ADDITIONAL_LIBRARIES) + cmake_parse_arguments(FN_VARS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Get the current sources and libraries + get_property(sources GLOBAL PROPERTY DD_TEST_SOURCES) + get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES) + + # Gather new data + file(GLOB test_files CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp") -foreach(pattern ${TEST_PATTERNS}) - file(GLOB_RECURSE CURRENT_TEST_SOURCES - "${CMAKE_SOURCE_DIR}/tests/${pattern}.cpp") - list(APPEND TEST_SOURCES ${CURRENT_TEST_SOURCES}) + list(APPEND sources ${test_files}) + list(APPEND libraries ${FN_VARS_ADDITIONAL_LIBRARIES}) - if(WIN32) - file(GLOB_RECURSE CURRENT_TEST_PLATFORM_SOURCES - "${CMAKE_SOURCE_DIR}/tests/*/platf/windows/${pattern}.cpp") - elseif(__APPLE__) - file(GLOB_RECURSE CURRENT_TEST_PLATFORM_SOURCES - "${CMAKE_SOURCE_DIR}/tests/*/platf/macos/${pattern}.cpp") - elseif(UNIX) - file(GLOB_RECURSE CURRENT_TEST_PLATFORM_SOURCES - "${CMAKE_SOURCE_DIR}/tests/*/platf/linux/${pattern}.cpp") - else() - message(FATAL_ERROR "Unsupported platform") - endif() + # Update the global variables + set_property(GLOBAL PROPERTY DD_TEST_SOURCES "${sources}") + set_property(GLOBAL PROPERTY DD_TEST_LIBRARIES "${libraries}") +endfunction() - list(APPEND TEST_PLATFORM_SOURCES ${CURRENT_TEST_PLATFORM_SOURCES}) -endforeach() +# +# Add subdirectories +# +add_subdirectory(fixtures) +add_subdirectory(unit) -list(FILTER TEST_SOURCES EXCLUDE REGEX ".*/platf/.*") -message(STATUS "Common Test source files: ${TEST_SOURCES}") -message(STATUS "Platform Test source files: ${TEST_PLATFORM_SOURCES}") +# +# Setup the final test binary +# +set(TEST_BINARY test_libdisplaydevice) +get_property(sources GLOBAL PROPERTY DD_TEST_SOURCES) +get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES) -set(DD_SOURCES - ${DD_TARGET_FILES}) +add_executable(${TEST_BINARY} ${sources}) +target_link_libraries(${TEST_BINARY} + gtest_main # if we use this we don't need our own main function + libdisplaydevice # we are always testing at least the public API so it's safe to always link this + libfixtures # these are our fixtures/helpers for the tests + ${libraries} # additional libraries if needed +) -add_executable(${PROJECT_NAME} - ${TEST_SOURCES} - ${TEST_PLATFORM_SOURCES} - ${DD_SOURCES}) -set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) -target_link_libraries(${PROJECT_NAME} - ${DD_EXTERNAL_LIBRARIES} - gtest - gtest_main) # if we use this we don't need our own main function -target_compile_definitions(${PROJECT_NAME} PUBLIC ${DD_DEFINITIONS} ${TEST_DEFINITIONS}) -target_compile_options(${PROJECT_NAME} PRIVATE $<$:${DD_COMPILE_OPTIONS}> -std=c++20) -target_link_options(${PROJECT_NAME} PRIVATE) +# Add the test to CTest +gtest_discover_tests(${TEST_BINARY}) -add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test) +# +# Additional setup for coverage +# https://gcovr.com/en/stable/guide/compiling.html#compiler-options +# +set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -O1") +set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -O1") diff --git a/tests/conftest.cpp b/tests/conftest.cpp deleted file mode 100644 index f06eca1..0000000 --- a/tests/conftest.cpp +++ /dev/null @@ -1,177 +0,0 @@ -// system includes -#include -#include - -// local includes -#include "src/logging.h" -#include "tests/utils.h" - -// Undefine the original TEST macro -#undef TEST - -// Redefine TEST to use our BaseTest class, to automatically use our BaseTest fixture -#define TEST(test_case_name, test_name) \ - GTEST_TEST_(test_case_name, test_name, ::BaseTest, \ - ::testing::internal::GetTypeId<::BaseTest>()) - -/** - * @brief Base class for tests. - * - * This class provides a base test fixture for all tests. - * - * ``cout``, ``stderr``, and ``stdout`` are redirected to a buffer, and the buffer is printed if the test fails. - * - * @todo Retain the color of the original output. - */ -class BaseTest: public ::testing::Test { -protected: - // https://stackoverflow.com/a/58369622/11214013 - - // we can possibly use some internal googletest functions to capture stdout and stderr, but I have not tested this - // https://stackoverflow.com/a/33186201/11214013 - - BaseTest(): - sbuf { nullptr }, pipe_stdout { nullptr }, pipe_stderr { nullptr } { - // intentionally empty - } - - ~BaseTest() override = default; - - void - SetUp() override { - // todo: only run this one time, instead of every time a test is run - // see: https://stackoverflow.com/questions/2435277/googletest-accessing-the-environment-from-a-test - // get command line args from the test executable - testArgs = ::testing::internal::GetArgvs(); - - // then get the directory of the test executable - // std::string path = ::testing::internal::GetArgvs()[0]; - testBinary = testArgs[0]; - - // get the directory of the test executable - testBinaryDir = std::filesystem::path(testBinary).parent_path(); - - // If testBinaryDir is empty or `.` then set it to the current directory - // maybe some better options here: https://stackoverflow.com/questions/875249/how-to-get-current-directory - if (testBinaryDir.empty() || testBinaryDir.string() == ".") { - testBinaryDir = std::filesystem::current_path(); - } - - sbuf = std::cout.rdbuf(); // save cout buffer (std::cout) - std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout) - - // Default to the verbose level in case some test fails - display_device::logger_t::get().set_log_level(display_device::logger_t::log_level_e::verbose); - } - - void - TearDown() override { - display_device::logger_t::get().set_custom_callback(nullptr); // restore the default callback to avoid potential leaks - std::cout.rdbuf(sbuf); // restore cout buffer - - // get test info - const ::testing::TestInfo *const test_info = ::testing::UnitTest::GetInstance()->current_test_info(); - - if (test_info->result()->Failed()) { - std::cout << std::endl - << "Test failed: " << test_info->name() << std::endl - << std::endl - << "Captured cout:" << std::endl - << cout_buffer.str() << std::endl - << "Captured stdout:" << std::endl - << stdout_buffer.str() << std::endl - << "Captured stderr:" << std::endl - << stderr_buffer.str() << std::endl; - } - - sbuf = nullptr; // clear sbuf - if (pipe_stdout) { - pclose(pipe_stdout); - pipe_stdout = nullptr; - } - if (pipe_stderr) { - pclose(pipe_stderr); - pipe_stderr = nullptr; - } - } - - // functions and variables - std::vector testArgs; // CLI arguments used - std::filesystem::path testBinary; // full path of this binary - std::filesystem::path testBinaryDir; // full directory of this binary - std::stringstream cout_buffer; // declare cout_buffer - std::stringstream stdout_buffer; // declare stdout_buffer - std::stringstream stderr_buffer; // declare stderr_buffer - std::streambuf *sbuf; - FILE *pipe_stdout; - FILE *pipe_stderr; - - int - exec(const char *cmd) { - std::array buffer {}; - pipe_stdout = popen((std::string(cmd) + " 2>&1").c_str(), "r"); - pipe_stderr = popen((std::string(cmd) + " 2>&1").c_str(), "r"); - if (!pipe_stdout || !pipe_stderr) { - throw std::runtime_error("popen() failed!"); - } - while (fgets(buffer.data(), buffer.size(), pipe_stdout) != nullptr) { - stdout_buffer << buffer.data(); - } - while (fgets(buffer.data(), buffer.size(), pipe_stderr) != nullptr) { - stderr_buffer << buffer.data(); - } - int returnCode = pclose(pipe_stdout); - pipe_stdout = nullptr; - if (returnCode != 0) { - std::cout << "Error: " << stderr_buffer.str() << std::endl - << "Return code: " << returnCode << std::endl; - } - return returnCode; - } -}; - -class LinuxTest: public BaseTest { -protected: - void - SetUp() override { -#ifndef __linux__ - GTEST_SKIP_("Skipping, this test is for Linux only."); -#endif - } - - void - TearDown() override { - BaseTest::TearDown(); - } -}; - -class MacOSTest: public BaseTest { -protected: - void - SetUp() override { -#if !defined(__APPLE__) || !defined(__MACH__) - GTEST_SKIP_("Skipping, this test is for macOS only."); -#endif - } - - void - TearDown() override { - BaseTest::TearDown(); - } -}; - -class WindowsTest: public BaseTest { -protected: - void - SetUp() override { -#ifndef _WIN32 - GTEST_SKIP_("Skipping, this test is for Windows only."); -#endif - BaseTest::SetUp(); - } - - void - TearDown() override { - BaseTest::TearDown(); - } -}; diff --git a/tests/fixtures/CMakeLists.txt b/tests/fixtures/CMakeLists.txt new file mode 100644 index 0000000..dd70433 --- /dev/null +++ b/tests/fixtures/CMakeLists.txt @@ -0,0 +1,15 @@ +# A global identifier for the library +set(MODULE libfixtures) + +# Globing headers (so that they appear in some IDEs) and sources +file(GLOB HEADER_LIST CONFIGURE_DEPENDS "*.h") +file(GLOB SOURCE_LIST CONFIGURE_DEPENDS "*.cpp") + +# Automatic library - will be static or dynamic based on user setting +add_library(${MODULE} ${HEADER_LIST} ${SOURCE_LIST}) + +# Provide the includes together with this library +target_include_directories(${MODULE} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Link the additional libraries +target_link_libraries(${MODULE} PRIVATE gtest libcommon) diff --git a/tests/fixtures/fixtures.cpp b/tests/fixtures/fixtures.cpp new file mode 100644 index 0000000..8f78815 --- /dev/null +++ b/tests/fixtures/fixtures.cpp @@ -0,0 +1,142 @@ +// header include +#include "fixtures.h" + +// local includes +#include "displaydevice/logging.h" + +BaseTest::BaseTest(): + sbuf { nullptr }, pipe_stdout { nullptr }, pipe_stderr { nullptr } { + // intentionally empty +} + +void +BaseTest::SetUp() { + // todo: only run this one time, instead of every time a test is run + // see: https://stackoverflow.com/questions/2435277/googletest-accessing-the-environment-from-a-test + // get command line args from the test executable + testArgs = ::testing::internal::GetArgvs(); + + // then get the directory of the test executable + // std::string path = ::testing::internal::GetArgvs()[0]; + testBinary = testArgs[0]; + + // get the directory of the test executable + testBinaryDir = std::filesystem::path(testBinary).parent_path(); + + // If testBinaryDir is empty or `.` then set it to the current directory + // maybe some better options here: https://stackoverflow.com/questions/875249/how-to-get-current-directory + if (testBinaryDir.empty() || testBinaryDir.string() == ".") { + testBinaryDir = std::filesystem::current_path(); + } + + sbuf = std::cout.rdbuf(); // save cout buffer (std::cout) + std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout) + + // Default to the verbose level in case some test fails + display_device::logger_t::get().set_log_level(display_device::logger_t::log_level_e::verbose); +} + +void +BaseTest::TearDown() { + display_device::logger_t::get().set_custom_callback(nullptr); // restore the default callback to avoid potential leaks + std::cout.rdbuf(sbuf); // restore cout buffer + + // get test info + const ::testing::TestInfo *const test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + + if (test_info->result()->Failed()) { + std::cout << std::endl + << "Test failed: " << test_info->name() << std::endl + << std::endl + << "Captured cout:" << std::endl + << cout_buffer.str() << std::endl + << "Captured stdout:" << std::endl + << stdout_buffer.str() << std::endl + << "Captured stderr:" << std::endl + << stderr_buffer.str() << std::endl; + } + + sbuf = nullptr; // clear sbuf + if (pipe_stdout) { + pclose(pipe_stdout); + pipe_stdout = nullptr; + } + if (pipe_stderr) { + pclose(pipe_stderr); + pipe_stderr = nullptr; + } +} + +int +BaseTest::exec(const char *cmd) { + std::array buffer {}; + pipe_stdout = popen((std::string(cmd) + " 2>&1").c_str(), "r"); + pipe_stderr = popen((std::string(cmd) + " 2>&1").c_str(), "r"); + if (!pipe_stdout || !pipe_stderr) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), buffer.size(), pipe_stdout) != nullptr) { + stdout_buffer << buffer.data(); + } + while (fgets(buffer.data(), buffer.size(), pipe_stderr) != nullptr) { + stderr_buffer << buffer.data(); + } + int returnCode = pclose(pipe_stdout); + pipe_stdout = nullptr; + if (returnCode != 0) { + std::cout << "Error: " << stderr_buffer.str() << std::endl + << "Return code: " << returnCode << std::endl; + } + return returnCode; +} + +void +LinuxTest::SetUp() { +#ifndef __linux__ + GTEST_SKIP_("Skipping, this test is for Linux only."); +#endif + BaseTest::SetUp(); +} + +void +LinuxTest::TearDown() { +#ifndef __linux__ + // This a noop case to skip the teardown + return; +#endif + BaseTest::TearDown(); +} + +void +MacOSTest::SetUp() { +#if !defined(__APPLE__) || !defined(__MACH__) + GTEST_SKIP_("Skipping, this test is for macOS only."); +#endif + BaseTest::SetUp(); +} + +void +MacOSTest::TearDown() { +#if !defined(__APPLE__) || !defined(__MACH__) + // This a noop case to skip the teardown + return; +#endif + BaseTest::TearDown(); +} + +void +WindowsTest::SetUp() { +#ifndef _WIN32 + GTEST_SKIP_("Skipping, this test is for Windows only."); +#endif + BaseTest::SetUp(); +} + +void +WindowsTest::TearDown() { +#ifndef _WIN32 + // This a noop case to skip the teardown + return; +#endif + BaseTest::TearDown(); +} diff --git a/tests/fixtures/fixtures.h b/tests/fixtures/fixtures.h new file mode 100644 index 0000000..ced4276 --- /dev/null +++ b/tests/fixtures/fixtures.h @@ -0,0 +1,83 @@ +#pragma once + +// system includes +#include +#include + +// local includes +#include "utils.h" + +// Undefine the original TEST macro +#undef TEST + +// Redefine TEST to use our BaseTest class, to automatically use our BaseTest fixture +#define TEST(test_case_name, test_name) \ + GTEST_TEST_(test_case_name, test_name, ::BaseTest, \ + ::testing::internal::GetTypeId<::BaseTest>()) + +/** + * @brief Base class for tests. + * + * This class provides a base test fixture for all tests. + * + * ``cout``, ``stderr``, and ``stdout`` are redirected to a buffer, and the buffer is printed if the test fails. + * + * @todo Retain the color of the original output. + */ +class BaseTest: public ::testing::Test { +protected: + // https://stackoverflow.com/a/58369622/11214013 + + // we can possibly use some internal googletest functions to capture stdout and stderr, but I have not tested this + // https://stackoverflow.com/a/33186201/11214013 + + BaseTest(); + ~BaseTest() override = default; + + void + SetUp() override; + + void + TearDown() override; + + int + exec(const char *cmd); + + // functions and variables + std::vector testArgs; // CLI arguments used + std::filesystem::path testBinary; // full path of this binary + std::filesystem::path testBinaryDir; // full directory of this binary + std::stringstream cout_buffer; // declare cout_buffer + std::stringstream stdout_buffer; // declare stdout_buffer + std::stringstream stderr_buffer; // declare stderr_buffer + std::streambuf *sbuf; + FILE *pipe_stdout; + FILE *pipe_stderr; +}; + +class LinuxTest: public BaseTest { +protected: + void + SetUp() override; + + void + TearDown() override; +}; + +class MacOSTest: public BaseTest { +protected: + void + SetUp() override; + + void + TearDown() override; +}; + +class WindowsTest: public BaseTest { +protected: + void + SetUp() override; + + void + TearDown() override; +}; diff --git a/tests/utils.cpp b/tests/fixtures/utils.cpp similarity index 100% rename from tests/utils.cpp rename to tests/fixtures/utils.cpp diff --git a/tests/utils.h b/tests/fixtures/utils.h similarity index 100% rename from tests/utils.h rename to tests/fixtures/utils.h diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..20e84f5 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,13 @@ +# General platform-agnostic tests (or without hard platform dependencies) +add_subdirectory(general) + +# Platform specific tests +if(WIN32) + add_subdirectory(windows) +elseif(APPLE) + message(WARNING "MacOS is not supported yet.") +elseif(UNIX) + message(WARNING "Linux is not supported yet.") +else() + message(FATAL_ERROR "Unsupported platform") +endif() diff --git a/tests/unit/general/CMakeLists.txt b/tests/unit/general/CMakeLists.txt new file mode 100644 index 0000000..39a9a07 --- /dev/null +++ b/tests/unit/general/CMakeLists.txt @@ -0,0 +1,2 @@ +# Add the test files in this directory +add_dd_test_dir() diff --git a/tests/unit/test_logging.cpp b/tests/unit/general/test_logging.cpp similarity index 99% rename from tests/unit/test_logging.cpp rename to tests/unit/general/test_logging.cpp index d312df7..f99f0df 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/general/test_logging.cpp @@ -1,5 +1,6 @@ // local includes -#include "tests/conftest.cpp" +#include "displaydevice/logging.h" +#include "fixtures.h" TEST(LoggingTest, LogLevelVerbose) { using level = display_device::logger_t::log_level_e; diff --git a/tests/unit/test_sample.cpp b/tests/unit/general/test_sample.cpp similarity index 84% rename from tests/unit/test_sample.cpp rename to tests/unit/general/test_sample.cpp index 0fbd3a2..c494f28 100644 --- a/tests/unit/test_sample.cpp +++ b/tests/unit/general/test_sample.cpp @@ -1,4 +1,5 @@ -#include +// local includes +#include "fixtures.h" TEST(HelloWorldTest, HelloWorld) { EXPECT_TRUE(true); diff --git a/tests/unit/platf/linux/test_linux_sample.cpp b/tests/unit/platf/linux/test_linux_sample.cpp deleted file mode 100644 index b186cfc..0000000 --- a/tests/unit/platf/linux/test_linux_sample.cpp +++ /dev/null @@ -1 +0,0 @@ -// TODO: remove this file diff --git a/tests/unit/platf/macos/test_macos_sample.cpp b/tests/unit/platf/macos/test_macos_sample.cpp deleted file mode 100644 index b186cfc..0000000 --- a/tests/unit/platf/macos/test_macos_sample.cpp +++ /dev/null @@ -1 +0,0 @@ -// TODO: remove this file diff --git a/tests/unit/windows/CMakeLists.txt b/tests/unit/windows/CMakeLists.txt new file mode 100644 index 0000000..11ab7f0 --- /dev/null +++ b/tests/unit/windows/CMakeLists.txt @@ -0,0 +1,2 @@ +# Add the test files in this directory +add_dd_test_dir(ADDITIONAL_LIBRARIES libwindows) diff --git a/tests/unit/platf/windows/test_winapilayer.cpp b/tests/unit/windows/test_winapilayer.cpp similarity index 93% rename from tests/unit/platf/windows/test_winapilayer.cpp rename to tests/unit/windows/test_winapilayer.cpp index 0f92c37..bee9d91 100644 --- a/tests/unit/platf/windows/test_winapilayer.cpp +++ b/tests/unit/windows/test_winapilayer.cpp @@ -1,8 +1,8 @@ // local includes -#include "src/platf/windows/winapilayer.h" -#include "tests/conftest.cpp" +#include "displaydevice/windows/winapilayer.h" +#include "fixtures.h" -TEST(LoggingTest, GetErrorString) { +TEST(WinApiLayer, GetErrorString) { const display_device::WinApiLayer layer; EXPECT_TRUE(test_regex(layer.get_error_string(ERROR_INVALID_PARAMETER), R"(\[code: ERROR_INVALID_PARAMETER, .+?\])")); @@ -14,7 +14,7 @@ TEST(LoggingTest, GetErrorString) { EXPECT_TRUE(test_regex(layer.get_error_string(ERROR_ACCOUNT_DISABLED), R"(\[code: )" + std::to_string(ERROR_ACCOUNT_DISABLED) + R"(, .+?\])")); } -TEST(LoggingTest, QueryDisplayConfigPathAndModeCount) { +TEST(WinApiLayer, QueryDisplayConfigPathAndModeCount) { const display_device::WinApiLayer layer; const auto active_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::Active) }; @@ -29,7 +29,7 @@ TEST(LoggingTest, QueryDisplayConfigPathAndModeCount) { EXPECT_TRUE(all_devices->modes.size() == active_devices->modes.size()); } -TEST(LoggingTest, QueryDisplayConfigPathActivePaths) { +TEST(WinApiLayer, QueryDisplayConfigPathActivePaths) { const display_device::WinApiLayer layer; const auto active_devices { layer.query_display_config(display_device::WinApiLayer::query_type_e::Active) }; @@ -40,7 +40,7 @@ TEST(LoggingTest, QueryDisplayConfigPathActivePaths) { } } -TEST(LoggingTest, QueryDisplayConfigModeIndexValidity) { +TEST(WinApiLayer, QueryDisplayConfigModeIndexValidity) { // The MS docs is not clear when to access the index union struct or not. It appears that union struct is available, // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying (always in our case). //