From 17d22be5844c14e11bf61ef99c91a3d76cc1fd3f Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 30 Jul 2016 22:34:31 +0200 Subject: [PATCH] Support testing applications --- cmake/modules/TestTarget.cmake | 19 +++++++- tests/testutils.cpp | 80 ++++++++++++++++++++++++++++++---- tests/testutils.h | 14 ++++++ 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/cmake/modules/TestTarget.cmake b/cmake/modules/TestTarget.cmake index e4d8534..a81cf49 100644 --- a/cmake/modules/TestTarget.cmake +++ b/cmake/modules/TestTarget.cmake @@ -9,9 +9,24 @@ endif() # add test executable, but exclude it from the "all target" add_executable(${META_PROJECT_NAME}_tests EXCLUDE_FROM_ALL ${TEST_HEADER_FILES} ${TEST_SRC_FILES}) -target_link_libraries(${META_PROJECT_NAME}_tests ${META_PROJECT_NAME} ${TEST_LIBRARIES} cppunit) + +# always link test applications against c++utilities, cppunit and pthreads +find_library(CPP_UNIT_LIB cppunit) +find_library(PTHREAD_LIB pthread) +list(APPEND TEST_LIBRARIES ${CPP_UTILITIES_SHARED_LIB} ${CPP_UNIT_LIB} ${PTHREAD_LIB}) + +# test applications of my projects always use c++utilities and cppunit +if(NOT META_PROJECT_TYPE OR "${META_PROJECT_TYPE}" STREQUAL "library") # default project type is library + # when testing a library, the test application always needs to link against it + list(APPEND TEST_LIBRARIES ${META_PROJECT_NAME}) +else() + # otherwise, the tests application needs the path of the application to be tested + set(APPLICATION_PATH "-a ${CMAKE_CURRENT_BINARY_DIR}/${META_PROJECT_NAME}") +endif() + +target_link_libraries(${META_PROJECT_NAME}_tests ${TEST_LIBRARIES}) set_target_properties(${META_PROJECT_NAME}_tests PROPERTIES CXX_STANDARD 11) -add_test(NAME ${META_PROJECT_NAME}_cppunit COMMAND ${META_PROJECT_NAME}_tests -p "${CMAKE_CURRENT_SOURCE_DIR}/testfiles") +add_test(NAME ${META_PROJECT_NAME}_cppunit COMMAND ${META_PROJECT_NAME}_tests -p "${CMAKE_CURRENT_SOURCE_DIR}/testfiles" ${APPLICATION_PATH}) # add the test executable to the dependencies of the check target add_dependencies(check ${META_PROJECT_NAME}_tests) diff --git a/tests/testutils.cpp b/tests/testutils.cpp index adcfc50..1aafd84 100644 --- a/tests/testutils.cpp +++ b/tests/testutils.cpp @@ -8,8 +8,14 @@ #include #include #include +#include +#include -#include +#ifdef PLATFORM_UNIX +# include +# include +# include +#endif using namespace std; using namespace ApplicationUtilities; @@ -36,6 +42,7 @@ TestApplication *TestApplication::m_instance = nullptr; TestApplication::TestApplication(int argc, char **argv) : m_helpArg(m_parser), m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files"), + m_applicationPathArg("app-path",'a', "specifies the path of the application to be tested"), m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files"), m_unitsArg("units", 'u', "specifies the units to test; omit to test all units") { @@ -55,16 +62,15 @@ TestApplication::TestApplication(int argc, char **argv) : } // setup argument parser - m_testFilesPathArg.setRequiredValueCount(1); - m_testFilesPathArg.setValueNames({"path"}); - m_testFilesPathArg.setCombinable(true); - m_workingDirArg.setRequiredValueCount(1); - m_workingDirArg.setValueNames({"path"}); - m_workingDirArg.setCombinable(true); + for(Argument *arg : initializer_list{&m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg}) { + arg->setRequiredValueCount(1); + arg->setValueNames({"path"}); + arg->setCombinable(true); + } m_unitsArg.setRequiredValueCount(-1); m_unitsArg.setValueNames({"unit1", "unit2", "unit3"}); m_unitsArg.setCombinable(true); - m_parser.setMainArguments({&m_testFilesPathArg, &m_workingDirArg, &m_unitsArg, &m_helpArg}); + m_parser.setMainArguments({&m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg, &m_helpArg}); // parse arguments try { @@ -157,6 +163,7 @@ string TestApplication::testFilePath(const string &name) const #ifdef PLATFORM_UNIX /*! * \brief Returns the full path to a working copy of the test file with the specified \a name. + * \remarks Currently only available under UNIX. */ string TestApplication::workingCopyPath(const string &name) const { @@ -205,6 +212,63 @@ string TestApplication::workingCopyPath(const string &name) const } return string(); } + +/*! + * \brief Executes the application to be tested with the specified \a args and stores the standard output and + * errors in \a stdout and \a stderr. + * \throws Throws std::runtime_error when the application can not be executed. + * \remarks + * - The specified \a args must be 0 terminated. The first argument is the application name. + * - Currently only available under UNIX. + */ +int TestApplication::execApp(const char *const *args, string &stdout, string &stderr) const +{ + // determine application path + const char *appPath = m_applicationPathArg.firstValue(); + if(!appPath || !*appPath) { + throw runtime_error("Unable to execute application to be tested: no application path specified"); + } + // create pipes + int coutPipes[2], cerrPipes[2]; + pipe(coutPipes), pipe(cerrPipes); + int readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1]; + int readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1]; + // create child process + if(int child = fork()) { + // parent process: read stdout and stderr from child + close(writeCoutPipe), close(writeCerrPipe); + thread readCoutThread([readCoutPipe, &stdout] { + char buffer[512]; + ssize_t count; + stdout.clear(); + while((count = read(readCoutPipe, buffer, sizeof(buffer))) > 0) { + stdout.append(buffer, count); + } + close(readCoutPipe); + }); + thread readCerrThread([readCerrPipe, &stderr] { + char buffer[512]; + ssize_t count; + stderr.clear(); + while((count = read(readCerrPipe, buffer, sizeof(buffer))) > 0) { + stderr.append(buffer, count); + } + close(readCerrPipe); + }); + readCoutThread.join(); + readCerrThread.join(); + int childReturnCode; + waitpid(child, &childReturnCode, 0); + return childReturnCode; + } else { + // child process: set pipes to be used for stdout/stderr, execute application + dup2(writeCoutPipe, STDOUT_FILENO), dup2(writeCerrPipe, STDERR_FILENO); + close(readCoutPipe), close(writeCoutPipe), close(readCerrPipe), close(writeCerrPipe); + execv(appPath, const_cast(args)); + cerr << "Unable to execute \"" << appPath << "\": execv() failed" << endl; + exit(-101); + } +} #endif } diff --git a/tests/testutils.h b/tests/testutils.h index 3e0dd9b..31a757a 100644 --- a/tests/testutils.h +++ b/tests/testutils.h @@ -18,6 +18,7 @@ public: std::string testFilePath(const std::string &name) const; #ifdef PLATFORM_UNIX std::string workingCopyPath(const std::string &name) const; + int execApp(const char *const *args, std::string &output, std::string &errors) const; #endif bool unitsSpecified() const; const std::vector &units() const; @@ -27,6 +28,7 @@ private: ApplicationUtilities::ArgumentParser m_parser; ApplicationUtilities::HelpArgument m_helpArg; ApplicationUtilities::Argument m_testFilesPathArg; + ApplicationUtilities::Argument m_applicationPathArg; ApplicationUtilities::Argument m_workingDirArg; ApplicationUtilities::Argument m_unitsArg; std::string m_testFilesPathArgValue; @@ -75,6 +77,7 @@ inline const std::vector &TestApplication::units() const /*! * \brief Convenience function which returns the full path of the test file with the specified \a name. * \remarks A TestApplication must be present. + * \sa TestApplication::testFilePath() */ inline LIB_EXPORT std::string testFilePath(const std::string &name) { @@ -85,11 +88,22 @@ inline LIB_EXPORT std::string testFilePath(const std::string &name) /*! * \brief Convenience function which returns the full path to a working copy of the test file with the specified \a name. * \remarks A TestApplication must be present. + * \sa TestApplication::workingCopyPath() */ inline LIB_EXPORT std::string workingCopyPath(const std::string &name) { return TestApplication::instance()->workingCopyPath(name); } + +/*! + * \brief Convenience function which executes the application to be tested with the specified \a args. + * \remarks A TestApplication must be present. + * \sa TestApplication::execApp() + */ +inline LIB_EXPORT int execApp(const char *const *args, std::string &output, std::string &errors) +{ + return TestApplication::instance()->execApp(args, output, errors); +} #endif /*!