diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bb186a..57a41d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,7 @@ include(3rdParty) use_iconv(AUTO_LINKAGE REQUIRED) # configure use of native file buffer and its backend implementation if enabled +set(REQUIRED_BOOST_COMPONENTS "") set(USE_NATIVE_FILE_BUFFER_BY_DEFAULT OFF) if (WIN32 OR ANDROID @@ -157,7 +158,7 @@ if (USE_NATIVE_FILE_BUFFER) endforeach () else () message(STATUS "Using boost::iostreams::stream_buffer for NativeFileStream") - use_package(TARGET_NAME Boost::iostreams PACKAGE_NAME Boost PACKAGE_ARGS "REQUIRED;COMPONENTS;iostreams") + list(APPEND REQUIRED_BOOST_COMPONENTS iostreams) foreach (NATIVE_FILE_STREAM_IMPL_FILE ${NATIVE_FILE_STREAM_IMPL_FILES}) set_property( SOURCE ${NATIVE_FILE_STREAM_IMPL_FILE} @@ -169,6 +170,26 @@ else () message(STATUS "Using std::fstream for NativeFileStream") endif () +# configure use of Boost.Process for launching test applications on Windows +if (WIN32) + option(USE_BOOST_PROCESS "enables use of Boost.Process to launch test applications" ON) + if (USE_BOOST_PROCESS) + list(APPEND REQUIRED_BOOST_COMPONENTS filesystem) + list(APPEND META_PUBLIC_COMPILE_DEFINITIONS ${META_PROJECT_VARNAME}_BOOST_PROCESS) + list(APPEND PRIVATE_LIBRARIES ws2_32) # needed by Boost.Asio + use_package(TARGET_NAME Threads::Threads PACKAGE_NAME Threads PACKAGE_ARGS REQUIRED) + endif () +endif () + +# configure usage of Boost +if (REQUIRED_BOOST_COMPONENTS) + set(BOOST_ARGS REQUIRED COMPONENTS ${REQUIRED_BOOST_COMPONENTS}) + use_package(TARGET_NAME Boost::boost PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}") + foreach (COMPONENT ${REQUIRED_BOOST_COMPONENTS}) + use_package(TARGET_NAME Boost::${COMPONENT} PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}") + endforeach () +endif () + # configure required libraries for std::filesystem option(USE_STANDARD_FILESYSTEM "uses std::filesystem; if disabled Bash completion for files and directories is not working" ON) diff --git a/README.md b/README.md index 09e1e8e..37e454d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ These build instructions apply to `c++utilities` but also to my other projects u - libstdc++ under GNU/Linux and Windows - libc++ under GNU/Linux and Android * glibc with iconv support or standalone iconv library - * libstdc++ or Boost.Iostreams for `NativeFileStream` (optional) + * libstdc++ or Boost.Iostreams for `NativeFileStream` (optional, use `USE_NATIVE_FILE_BUFFER` to disable) + * Boost.Process for `execApp()` test helper under Windows (optional, use `USE_BOOST_PROCESS=OFF` to disable) * My other projects have further dependencies such as Qt. Checkout the README of these projects for further details. diff --git a/tests/testutils.cpp b/tests/testutils.cpp index 5ecb66b..5dee94b 100644 --- a/tests/testutils.cpp +++ b/tests/testutils.cpp @@ -27,6 +27,19 @@ #include #endif +#ifdef CPP_UTILITIES_BOOST_PROCESS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + #ifdef PLATFORM_WINDOWS #include #endif @@ -391,7 +404,16 @@ string TestApplication::workingCopyPathAs( return workingCopyPath; } -#ifdef PLATFORM_UNIX +#ifdef CPP_UTILITIES_HAS_EXEC_APP + +#if defined(CPP_UTILITIES_BOOST_PROCESS) +inline static std::string streambufToString(boost::asio::streambuf &buf) +{ + const auto begin = boost::asio::buffers_begin(buf.data()); + return std::string(begin, begin + static_cast(buf.size())); +} +#endif + /*! * \brief Executes an application with the specified \a args. * \remarks Provides internal implementation of execApp() and execHelperApp(). @@ -411,6 +433,34 @@ static int execAppInternal(const char *appPath, const char *const *args, std::st cout << endl; } +#if defined(CPP_UTILITIES_BOOST_PROCESS) + auto path = enableSearchPath ? boost::process::search_path(appPath) : boost::process::filesystem::path(appPath); + auto ctx = boost::asio::io_context(); + auto group = boost::process::group(); + auto argsAsVector = std::vector(); + if (*args) { + for (const char *const *arg = args + 1; *arg; ++arg) { + argsAsVector.emplace_back(*arg); + } + } + auto outputBuffer = boost::asio::streambuf(), errorBuffer = boost::asio::streambuf(); + auto env = boost::process::environment(boost::this_process::environment()); + if (!newProfilingPath.empty()) { + env["LLVM_PROFILE_FILE"] = newProfilingPath; + } + auto child = boost::process::child(ctx, group, path, argsAsVector, env, boost::process::std_out > outputBuffer, boost::process::std_err > errorBuffer); + if (timeout > 0) { + ctx.run_for(std::chrono::milliseconds(timeout)); + } else { + ctx.run(); + } + output = streambufToString(outputBuffer); + errors = streambufToString(errorBuffer); + child.wait(); + group.wait(); + return child.exit_code(); + +#elif defined(PLATFORM_UNIX) // create pipes int coutPipes[2], cerrPipes[2]; pipe(coutPipes); @@ -504,6 +554,10 @@ static int execAppInternal(const char *appPath, const char *const *args, std::st cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": execv() failed" << Phrases::EndFlush; exit(-101); } + +#else + throw std::runtime_error("lauching test applications is not supported on this platform"); +#endif } /*! @@ -594,7 +648,7 @@ int execHelperAppInSearchPath( { return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true); } -#endif // PLATFORM_UNIX +#endif /*! * \brief Reads the path of the test file directory from the environment variable TEST_FILE_PATH. diff --git a/tests/testutils.h b/tests/testutils.h index 9c87bf8..47d6e65 100644 --- a/tests/testutils.h +++ b/tests/testutils.h @@ -9,6 +9,10 @@ #include #include +#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_BOOST_PROCESS) +#define CPP_UTILITIES_HAS_EXEC_APP +#endif + namespace CppUtilities { /*! @@ -34,7 +38,7 @@ public: std::string workingCopyPath(const std::string &relativeTestFilePath, WorkingCopyMode mode = WorkingCopyMode::CreateCopy) const; std::string workingCopyPathAs(const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode = WorkingCopyMode::CreateCopy) const; -#ifdef PLATFORM_UNIX +#ifdef CPP_UTILITIES_HAS_EXEC_APP int execApp(const char *const *args, std::string &output, std::string &errors, bool suppressLogging = false, int timeout = -1) const; #endif @@ -180,7 +184,7 @@ inline CPP_UTILITIES_EXPORT std::string workingCopyPathAs( return TestApplication::instance()->workingCopyPathAs(relativeTestFilePath, relativeWorkingCopyPath, mode); } -#ifdef PLATFORM_UNIX +#ifdef CPP_UTILITIES_HAS_EXEC_APP /*! * \brief Convenience function which executes the application to be tested with the specified \a args. * \remarks A TestApplication must be present. @@ -195,7 +199,7 @@ CPP_UTILITIES_EXPORT int execHelperApp( const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging = false, int timeout = -1); CPP_UTILITIES_EXPORT int execHelperAppInSearchPath( const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging = false, int timeout = -1); -#endif // PLATFORM_UNIX +#endif /*! * \brief Allows printing std::optional objects so those can be asserted using CPPUNIT_ASSERT_EQUAL. @@ -287,6 +291,16 @@ template > * = nullptr> const * * \remarks Requires cppunit. */ +#ifdef CPP_UTILITIES_BOOST_PROCESS +#define TESTUTILS_ASSERT_EXEC_EXIT_STATUS(args, expectedExitStatus) \ + { \ + const auto status = execApp(args, stdout, stderr); \ + if (status != expectedExitStatus) { \ + CPPUNIT_FAIL(::CppUtilities::argsToString( \ + "app exited with status ", status, " (expected ", expectedExitStatus, ")\nstdout: ", stdout, "\nstderr: ", stderr)); \ + } \ + } +#else #define TESTUTILS_ASSERT_EXEC_EXIT_STATUS(args, expectedExitStatus) \ { \ const auto status = execApp(args, stdout, stderr); \ @@ -298,6 +312,7 @@ template > * = nullptr> const "app exited with status ", exitStatus, " (expected ", expectedExitStatus, ")\nstdout: ", stdout, "\nstderr: ", stderr)); \ } \ } +#endif /*! * \brief Asserts whether the specified \a string matches the specified \a regex.