Support testing applications

This commit is contained in:
Martchus 2016-07-30 22:34:31 +02:00
parent 822d77484b
commit 17d22be584
3 changed files with 103 additions and 10 deletions

View File

@ -9,9 +9,24 @@ endif()
# add test executable, but exclude it from the "all target" # 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}) 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) 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 the test executable to the dependencies of the check target
add_dependencies(check ${META_PROJECT_NAME}_tests) add_dependencies(check ${META_PROJECT_NAME}_tests)

View File

@ -8,8 +8,14 @@
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <initializer_list>
#include <thread>
#include <sys/stat.h> #ifdef PLATFORM_UNIX
# include <unistd.h>
# include <sys/wait.h>
# include <sys/stat.h>
#endif
using namespace std; using namespace std;
using namespace ApplicationUtilities; using namespace ApplicationUtilities;
@ -36,6 +42,7 @@ TestApplication *TestApplication::m_instance = nullptr;
TestApplication::TestApplication(int argc, char **argv) : TestApplication::TestApplication(int argc, char **argv) :
m_helpArg(m_parser), m_helpArg(m_parser),
m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files"), 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_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") 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 // setup argument parser
m_testFilesPathArg.setRequiredValueCount(1); for(Argument *arg : initializer_list<Argument *>{&m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg}) {
m_testFilesPathArg.setValueNames({"path"}); arg->setRequiredValueCount(1);
m_testFilesPathArg.setCombinable(true); arg->setValueNames({"path"});
m_workingDirArg.setRequiredValueCount(1); arg->setCombinable(true);
m_workingDirArg.setValueNames({"path"}); }
m_workingDirArg.setCombinable(true);
m_unitsArg.setRequiredValueCount(-1); m_unitsArg.setRequiredValueCount(-1);
m_unitsArg.setValueNames({"unit1", "unit2", "unit3"}); m_unitsArg.setValueNames({"unit1", "unit2", "unit3"});
m_unitsArg.setCombinable(true); 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 // parse arguments
try { try {
@ -157,6 +163,7 @@ string TestApplication::testFilePath(const string &name) const
#ifdef PLATFORM_UNIX #ifdef PLATFORM_UNIX
/*! /*!
* \brief Returns the full path to a working copy of the test file with the specified \a name. * \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 string TestApplication::workingCopyPath(const string &name) const
{ {
@ -205,6 +212,63 @@ string TestApplication::workingCopyPath(const string &name) const
} }
return string(); 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<char *const *>(args));
cerr << "Unable to execute \"" << appPath << "\": execv() failed" << endl;
exit(-101);
}
}
#endif #endif
} }

View File

@ -18,6 +18,7 @@ public:
std::string testFilePath(const std::string &name) const; std::string testFilePath(const std::string &name) const;
#ifdef PLATFORM_UNIX #ifdef PLATFORM_UNIX
std::string workingCopyPath(const std::string &name) const; std::string workingCopyPath(const std::string &name) const;
int execApp(const char *const *args, std::string &output, std::string &errors) const;
#endif #endif
bool unitsSpecified() const; bool unitsSpecified() const;
const std::vector<const char *> &units() const; const std::vector<const char *> &units() const;
@ -27,6 +28,7 @@ private:
ApplicationUtilities::ArgumentParser m_parser; ApplicationUtilities::ArgumentParser m_parser;
ApplicationUtilities::HelpArgument m_helpArg; ApplicationUtilities::HelpArgument m_helpArg;
ApplicationUtilities::Argument m_testFilesPathArg; ApplicationUtilities::Argument m_testFilesPathArg;
ApplicationUtilities::Argument m_applicationPathArg;
ApplicationUtilities::Argument m_workingDirArg; ApplicationUtilities::Argument m_workingDirArg;
ApplicationUtilities::Argument m_unitsArg; ApplicationUtilities::Argument m_unitsArg;
std::string m_testFilesPathArgValue; std::string m_testFilesPathArgValue;
@ -75,6 +77,7 @@ inline const std::vector<const char *> &TestApplication::units() const
/*! /*!
* \brief Convenience function which returns the full path of the test file with the specified \a name. * \brief Convenience function which returns the full path of the test file with the specified \a name.
* \remarks A TestApplication must be present. * \remarks A TestApplication must be present.
* \sa TestApplication::testFilePath()
*/ */
inline LIB_EXPORT std::string testFilePath(const std::string &name) 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. * \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. * \remarks A TestApplication must be present.
* \sa TestApplication::workingCopyPath()
*/ */
inline LIB_EXPORT std::string workingCopyPath(const std::string &name) inline LIB_EXPORT std::string workingCopyPath(const std::string &name)
{ {
return TestApplication::instance()->workingCopyPath(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 #endif
/*! /*!