15#include <initializer_list>
19#ifdef CPP_UTILITIES_USE_STANDARD_FILESYSTEM
28#ifdef PLATFORM_WINDOWS
29#ifdef CPP_UTILITIES_USE_STANDARD_FILESYSTEM
34#ifdef CPP_UTILITIES_BOOST_PROCESS
35#include <boost/asio/buffers_iterator.hpp>
38#include <boost/asio/io_context.hpp>
40#include <boost/asio/streambuf.hpp>
41#if BOOST_VERSION >= 108600
42#include <boost/process/v1/async.hpp>
43#include <boost/process/v1/child.hpp>
44#include <boost/process/v1/env.hpp>
45#include <boost/process/v1/environment.hpp>
46#include <boost/process/v1/group.hpp>
47#include <boost/process/v1/io.hpp>
48#include <boost/process/v1/search_path.hpp>
50#include <boost/process/async.hpp>
51#include <boost/process/child.hpp>
52#include <boost/process/env.hpp>
53#include <boost/process/environment.hpp>
54#include <boost/process/group.hpp>
55#include <boost/process/io.hpp>
56#include <boost/process/search_path.hpp>
57namespace boost::process {
58namespace v1 = boost::process;
63#ifdef PLATFORM_WINDOWS
76static bool fileSystemItemExists(
const string &path)
80 return stat(path.data(), &res) == 0;
82 const auto widePath(convertMultiByteToWide(path));
83 if (!widePath.first) {
86 const auto fileType(GetFileAttributesW(widePath.first.get()));
87 return fileType != INVALID_FILE_ATTRIBUTES;
91static bool fileExists(
const string &path)
95 return stat(path.data(), &res) == 0 && !S_ISDIR(res.st_mode);
97 const auto widePath(convertMultiByteToWide(path));
98 if (!widePath.first) {
101 const auto fileType(GetFileAttributesW(widePath.first.get()));
102 return (fileType != INVALID_FILE_ATTRIBUTES) && !(fileType & FILE_ATTRIBUTE_DIRECTORY) && !(fileType & FILE_ATTRIBUTE_DEVICE);
106static bool dirExists(
const string &path)
110 return stat(path.data(), &res) == 0 && S_ISDIR(res.st_mode);
112 const auto widePath(convertMultiByteToWide(path));
113 if (!widePath.first) {
116 const auto fileType(GetFileAttributesW(widePath.first.get()));
117 return (fileType != INVALID_FILE_ATTRIBUTES) && (fileType & FILE_ATTRIBUTE_DIRECTORY);
121static bool makeDir(
const string &path)
124 return mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0;
126 const auto widePath(convertMultiByteToWide(path));
127 if (!widePath.first) {
130 return CreateDirectoryW(widePath.first.get(),
nullptr) || GetLastError() == ERROR_ALREADY_EXISTS;
159 : m_listArg(
"list",
'l',
"lists available test units")
160 , m_runArg(
"run",
'r',
"runs the tests")
161 , m_testFilesPathArg(
"test-files-path",
'p',
"specifies the path of the directory with test files", {
"path" })
162 , m_applicationPathArg(
"app-path",
'a',
"specifies the path of the application to be tested", {
"path" })
163 , m_workingDirArg(
"working-dir",
'w',
"specifies the directory to store working copies of test files", {
"path" })
164 , m_unitsArg(
"units",
'u',
"specifies the units to test; omit to test all units", {
"unit1",
"unit2",
"unit3" })
168 throw runtime_error(
"only one TestApplication instance allowed at a time");
177 m_runArg.setImplicit(
true);
178 m_runArg.setSubArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg });
179 m_parser.setMainArguments({ &m_runArg, &m_listArg, &m_parser.noColorArg(), &m_parser.helpArg() });
191 if (m_parser.helpArg().isPresent()) {
198 if (m_testFilesPathArg.isPresent()) {
199 for (
const char *
const testFilesPath : m_testFilesPathArg.values()) {
200 if (*testFilesPath) {
201 m_testFilesPaths.emplace_back(
argsToString(testFilesPath,
'/'));
203 m_testFilesPaths.emplace_back(
"./");
208 bool hasTestFilePathFromEnv;
209 if (
auto testFilePathFromEnv = readTestfilePathFromEnv(); (hasTestFilePathFromEnv = !testFilePathFromEnv.empty())) {
210 m_testFilesPaths.emplace_back(std::move(testFilePathFromEnv));
213 if (
auto testFilePathFromSrcDirRef = readTestfilePathFromSrcRef(); !testFilePathFromSrcDirRef.empty()) {
214 m_testFilesPaths.insert(m_testFilesPaths.end(), std::make_move_iterator(testFilePathFromSrcDirRef.begin()),
215 std::make_move_iterator(testFilePathFromSrcDirRef.end()));
218 m_testFilesPaths.emplace_back(
"./testfiles/");
219 for (
const auto &testFilesPath : m_testFilesPaths) {
220 cerr << testFilesPath <<
'\n';
224 if (m_workingDirArg.isPresent()) {
225 if (*m_workingDirArg.values().front()) {
226 (m_workingDir = m_workingDirArg.values().front()) +=
'/';
230 }
else if (
const char *
const workingDirEnv = getenv(
"WORKING_DIR")) {
231 if (*workingDirEnv) {
235 if ((m_testFilesPathArg.isPresent() && !m_testFilesPathArg.values().empty()) || hasTestFilePathFromEnv) {
236 m_workingDir = m_testFilesPaths.front() +
"workingdir/";
238 m_workingDir =
"./testfiles/workingdir/";
241 cerr <<
"Directory used to store working copies:\n" << m_workingDir <<
'\n';
244 if (
const char *
const profrawListFile = getenv(
"LLVM_PROFILE_LIST_FILE")) {
245 ofstream(profrawListFile, ios_base::trunc);
256 s_instance =
nullptr;
274 for (
const auto &testFilesPath : m_testFilesPaths) {
275 if (fileExists(path = testFilesPath + relativeTestFilePath)) {
279 throw std::runtime_error(
"The test file \"" % relativeTestFilePath %
"\" can not be located. Was looking under:\n"
280 +
joinStrings(m_testFilesPaths,
"\n",
false,
" - ", relativeTestFilePath));
292 for (
const auto &testFilesPath : m_testFilesPaths) {
293 if (dirExists(path = testFilesPath + relativeTestDirPath)) {
297 throw std::runtime_error(
"The test directory \"" % relativeTestDirPath %
"\" can not be located. Was looking under:\n"
298 +
joinStrings(m_testFilesPaths,
"\n",
false,
" - ", relativeTestDirPath));
328 const std::string &relativeTestFilePath,
const std::string &relativeWorkingCopyPath,
WorkingCopyMode mode)
const
332 if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
333 cerr <<
Phrases::Error <<
"Unable to create working copy for \"" << relativeTestFilePath <<
"\": can't create working directory \""
340 if (!parts.empty()) {
343 currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
344 currentLevel.assign(m_workingDir);
345 for (
auto i = parts.cbegin(), end = parts.end() - 1;
i != end; ++
i) {
346 if (currentLevel.back() !=
'/') {
352 if (dirExists(currentLevel) || makeDir(currentLevel)) {
356 cerr <<
Phrases::Error <<
"Unable to create working copy for \"" << relativeWorkingCopyPath <<
"\": can't create directory \""
370 const auto error = std::strerror(errno);
379 const auto origFilePath =
testFilePath(relativeTestFilePath);
380 size_t workingCopyPathAttempt = 0;
382 origFile.open(origFilePath, ios_base::in | ios_base::binary);
383 if (origFile.fail()) {
384 cerr <<
Phrases::Error <<
"Unable to create working copy for \"" << relativeTestFilePath
385 <<
"\": an IO error occurred when opening original file \"" << origFilePath <<
"\"." <<
Phrases::EndFlush;
386 cerr <<
"error: " << std::strerror(errno) << endl;
390 workingCopy.open(
workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
395 workingCopy.open(
workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
397 if (workingCopy.fail()) {
398 cerr <<
Phrases::Error <<
"Unable to create working copy for \"" << relativeTestFilePath
400 cerr <<
"error: " << strerror(errno) << endl;
404 workingCopy << origFile.rdbuf();
406 if (!origFile.fail() && !workingCopy.fail()) {
410 cerr <<
Phrases::Error <<
"Unable to create working copy for \"" << relativeTestFilePath <<
"\": ";
411 if (origFile.fail()) {
412 cerr <<
"an IO error occurred when reading original file \"" << origFilePath <<
"\"";
416 if (workingCopy.fail()) {
417 if (origFile.fail()) {
420 cerr <<
" an IO error occurred when writing to target file \"" <<
workingCopyPath <<
"\".";
422 cerr <<
"error: " << strerror(errno) << endl;
427#ifdef CPP_UTILITIES_HAS_EXEC_APP
429#if defined(CPP_UTILITIES_BOOST_PROCESS)
430inline static std::string streambufToString(boost::asio::streambuf &buf)
432 const auto begin = boost::asio::buffers_begin(buf.data());
433 return std::string(begin, begin +
static_cast<std::ptrdiff_t
>(buf.size()));
441static int execAppInternal(
const char *appPath,
const char *
const *args, std::string &output, std::string &errors,
bool suppressLogging,
int timeout,
442 const std::string &newProfilingPath,
bool enableSearchPath =
false)
445 if (!suppressLogging) {
447 cout <<
'-' <<
' ' << appPath;
449 for (
const char *
const *
i = args + 1; *
i; ++
i) {
456#if defined(CPP_UTILITIES_BOOST_PROCESS)
457 auto path = enableSearchPath ? boost::process::v1::search_path(appPath) : boost::process::v1::filesystem::path(appPath);
458 auto ctx = boost::asio::io_context();
459 auto group = boost::process::v1::group();
461#if defined(PLATFORM_WINDOWS)
462 std::vector<std::wstring>();
464 std::vector<std::string>();
467 for (
const char *
const *arg = args + 1; *arg; ++arg) {
468#if defined(PLATFORM_WINDOWS)
469 auto ec = std::error_code();
470 argsAsVector.emplace_back(convertMultiByteToWide(ec, std::string_view(*arg)));
472 throw std::runtime_error(
argsToString(
"unable to convert arg \"", *arg,
"\" to wide string"));
475 argsAsVector.emplace_back(*arg);
479 auto outputBuffer = boost::asio::streambuf(), errorBuffer = boost::asio::streambuf();
480 auto env = boost::process::v1::environment(boost::this_process::environment());
481 if (!newProfilingPath.empty()) {
482 env[
"LLVM_PROFILE_FILE"] = newProfilingPath;
484 auto child = boost::process::v1::child(
485 ctx, group, path, argsAsVector, env, boost::process::v1::std_out > outputBuffer, boost::process::v1::std_err > errorBuffer);
487 ctx.run_for(std::chrono::milliseconds(timeout));
491 output = streambufToString(outputBuffer);
492 errors = streambufToString(errorBuffer);
495 return child.exit_code();
497#elif defined(PLATFORM_UNIX)
499 int coutPipes[2], cerrPipes[2];
500 if (pipe(coutPipes) != 0 || pipe(cerrPipes) != 0) {
501 throw std::runtime_error(
argsToString(
"Unable to create pipe: ", std::strerror(errno)));
503 const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
504 const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
507 if (
const auto child = fork()) {
509 close(writeCoutPipe);
510 close(writeCerrPipe);
514 throw std::runtime_error(
argsToString(
"Unable to create fork: ", std::strerror(errno)));
518 struct pollfd fileDescriptorSet[2];
519 fileDescriptorSet[0].fd = readCoutPipe;
520 fileDescriptorSet[1].fd = readCerrPipe;
521 fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
530 const auto retpoll = poll(fileDescriptorSet, 2, timeout);
532 throw std::runtime_error(
"Poll timed out");
535 throw std::runtime_error(
argsToString(
"Poll failed: ", std::strerror(errno)));
537 if (fileDescriptorSet[0].revents & POLLIN) {
538 const auto count = read(readCoutPipe, buffer,
sizeof(buffer));
540 output.append(buffer,
static_cast<size_t>(count));
542 }
else if (fileDescriptorSet[0].revents & POLLHUP) {
544 fileDescriptorSet[0].fd = -1;
546 if (fileDescriptorSet[1].revents & POLLIN) {
547 const auto count = read(readCerrPipe, buffer,
sizeof(buffer));
549 errors.append(buffer,
static_cast<size_t>(count));
551 }
else if (fileDescriptorSet[1].revents & POLLHUP) {
553 fileDescriptorSet[1].fd = -1;
555 }
while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
565 waitpid(child, &childReturnCode, 0);
566 waitpid(-child,
nullptr, 0);
567 return childReturnCode;
571 if (dup2(writeCoutPipe, STDOUT_FILENO) == -1 || dup2(writeCerrPipe, STDERR_FILENO) == -1) {
572 std::cerr << Phrases::Error <<
"Unable to duplicate file descriptor: " << std::strerror(errno) << Phrases::EndFlush;
573 std::exit(EXIT_FAILURE);
576 close(writeCoutPipe);
578 close(writeCerrPipe);
582 cerr << Phrases::Error <<
"Unable create process group: " << std::strerror(errno) << Phrases::EndFlush;
587 if (!newProfilingPath.empty()) {
588 setenv(
"LLVM_PROFILE_FILE", newProfilingPath.data(),
true);
592 if (enableSearchPath) {
593 execvp(appPath,
const_cast<char *
const *
>(args));
595 execv(appPath,
const_cast<char *
const *
>(args));
597 cerr << Phrases::Error <<
"Unable to execute \"" << appPath <<
"\": " << std::strerror(errno) << Phrases::EndFlush;
602 throw std::runtime_error(
"lauching test applications is not supported on this platform");
614int TestApplication::execApp(
const char *
const *args,
string &output,
string &errors,
bool suppressLogging,
int timeout)
const
617 static unsigned int invocationCount = 0;
621 const char *
appPath = m_applicationPathArg.firstValue();
622 auto fallbackAppPath = string();
627 const char *
const testAppPath = m_parser.executable();
628 const auto testAppPathLength = strlen(testAppPath);
629 if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6,
"_tests")) {
630 fallbackAppPath.assign(testAppPath, testAppPathLength - 6);
631 appPath = fallbackAppPath.data();
634 throw runtime_error(
"Unable to execute application to be tested: no application path specified");
639 const auto newProfilingPath = [
appPath] {
640 auto path = string();
641 const char *
const llvmProfileFile = getenv(
"LLVM_PROFILE_FILE");
642 if (!llvmProfileFile) {
646 const char *
const llvmProfileFileEnd = strstr(llvmProfileFile,
".profraw");
647 if (!llvmProfileFileEnd) {
650 const auto llvmProfileFileWithoutExtension = string(llvmProfileFile, llvmProfileFileEnd);
652 const char *appName = strrchr(
appPath,
'/');
653 appName = appName ? appName + 1 :
appPath;
655 path =
argsToString(llvmProfileFileWithoutExtension,
'_', appName, invocationCount,
".profraw");
657 if (
const char *
const profrawListFile = getenv(
"LLVM_PROFILE_LIST_FILE")) {
658 ofstream(profrawListFile, ios_base::app) << path << endl;
663 return execAppInternal(
appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
672int execHelperApp(
const char *appPath,
const char *
const *args, std::string &output, std::string &errors,
bool suppressLogging,
int timeout)
674 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout,
string());
686int execHelperAppInSearchPath(
687 const char *appName,
const char *
const *args, std::string &output, std::string &errors,
bool suppressLogging,
int timeout)
689 return execAppInternal(appName, args, output, errors, suppressLogging, timeout,
string(),
true);
696string TestApplication::readTestfilePathFromEnv()
698 const char *
const testFilesPathEnv = getenv(
"TEST_FILE_PATH");
699 if (!testFilesPathEnv || !*testFilesPathEnv) {
710std::vector<std::string> TestApplication::readTestfilePathFromSrcRef()
714 auto res = std::vector<std::string>();
715 auto binaryPath = std::string();
716#if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
718 binaryPath = std::filesystem::read_symlink(
"/proc/self/exe").parent_path();
719 }
catch (
const std::filesystem::filesystem_error &e) {
720 cerr << Phrases::Warning <<
"Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
722#elif defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_WINDOWS)
723 auto binaryPathBuffer = std::vector<wchar_t>();
724 auto copied = DWORD();
726 binaryPathBuffer.resize(binaryPathBuffer.size() + MAX_PATH);
727 copied = GetModuleFileNameW(0, binaryPathBuffer.data(),
static_cast<DWORD
>(binaryPathBuffer.size()));
728 }
while (copied >= binaryPathBuffer.size());
729 binaryPath = std::filesystem::path(binaryPathBuffer.begin(), binaryPathBuffer.begin() + copied, std::filesystem::path::native_format)
733 const auto srcdirrefPath = binaryPath.empty() ?
"srcdirref" : binaryPath +
"/srcdirref";
736 const auto srcDirContent =
readFile(srcdirrefPath, 1024 * 1024);
737 if (srcDirContent.empty()) {
738 cerr << Phrases::Warning <<
"The file \"srcdirref\" is empty." << Phrases::EndFlush;
744 for (
const auto &srcPath : srcPaths) {
745 auto testfilesPath =
argsToString(srcPath,
"/testfiles/");
746 if (dirExists(testfilesPath)) {
747 res.emplace_back(std::move(testfilesPath));
749 cerr << Phrases::Warning
750 <<
"The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
751 << Phrases::End <<
"Referenced source directory: " << testfilesPath << endl;
756 }
catch (
const std::ios_base::failure &e) {
757 cerr << Phrases::Warning <<
"The file \"" << srcdirrefPath <<
"\" can not be opened: " << e.what() << Phrases::EndFlush;
#define CPP_UTILITIES_WARNING_PUSH
Causes GCC/Clang to remember the state of the diagnostics.
#define CPP_UTILITIES_WARNING_POP
Causes GCC/Clang to restore the previous state of the diagnostics.
#define CPP_UTILITIES_WARNING_DISARM(text)
Causes GCC/Clang to treat the specified diagnostic as warning (even though otherwise treated as error...
static constexpr std::size_t varValueCount
Denotes a variable number of values.
The ParseError class is thrown by an ArgumentParser when a parsing error occurs.
The TestApplication class simplifies writing test applications that require opening test files.
std::string workingCopyPath(const std::string &relativeTestFilePath, WorkingCopyMode mode=WorkingCopyMode::CreateCopy) const
Returns the full path to a working copy of the test file with the specified relativeTestFilePath.
std::string testFilePath(const std::string &relativeTestFilePath) const
Returns the full path of the test file with the specified relativeTestFilePath.
static const char * appPath()
Returns the application path or an empty string if no application path has been set.
TestApplication()
Constructs a TestApplication instance without further arguments.
std::string workingCopyPathAs(const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode=WorkingCopyMode::CreateCopy) const
Returns the full path to a working copy of the test file with the specified relativeTestFilePath.
std::string testDirPath(const std::string &relativeTestDirPath) const
Returns the full path of the test directory with the specified relativeTestDirPath.
~TestApplication()
Destroys the TestApplication.
Encapsulates functions for formatted terminal output using ANSI escape codes.
Contains all utilities provided by the c++utilities library.
CPP_UTILITIES_EXPORT std::string readFile(const std::string &path, std::string::size_type maxSize=std::string::npos)
Reads all contents of the specified file in a single call.
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
ReturnType joinStrings(const Container &strings, Detail::StringParamForContainer< Container > delimiter=Detail::StringParamForContainer< Container >(), bool omitEmpty=false, Detail::StringParamForContainer< Container > leftClosure=Detail::StringParamForContainer< Container >(), Detail::StringParamForContainer< Container > rightClosure=Detail::StringParamForContainer< Container >())
Joins the given strings using the specified delimiter.
std::fstream NativeFileStream
Container splitStringSimple(Detail::StringParamForContainer< Container > string, Detail::StringParamForContainer< Container > delimiter, int maxParts=-1)
Splits the given string (which might also be a string view) at the specified delimiter.
StringType argsToString(Args &&...args)
Container splitString(Detail::StringParamForContainer< Container > string, Detail::StringParamForContainer< Container > delimiter, EmptyPartsTreat emptyPartsRole=EmptyPartsTreat::Keep, int maxParts=-1)
Splits the given string at the specified delimiter.