C++ Utilities 5.24.8
Useful C++ classes and routines such as argument parser, IO and conversion utilities
Loading...
Searching...
No Matches
testutils.cpp
Go to the documentation of this file.
1#include "./testutils.h"
2
6#include "../io/misc.h"
8#include "../io/path.h"
10
11#include <cerrno>
12#include <cstdio>
13#include <cstdlib>
14#include <cstring>
15#include <fstream>
16#include <initializer_list>
17#include <iostream>
18#include <limits>
19
20#ifdef PLATFORM_UNIX
21#ifdef CPP_UTILITIES_USE_STANDARD_FILESYSTEM
22#include <filesystem>
23#endif
24#include <poll.h>
25#include <sys/stat.h>
26#include <sys/wait.h>
27#include <unistd.h>
28#endif
29
30#ifdef CPP_UTILITIES_BOOST_PROCESS
31#include <boost/asio/buffers_iterator.hpp>
32#include <boost/asio/io_context.hpp>
33#include <boost/asio/streambuf.hpp>
34#include <boost/process/async.hpp>
35#include <boost/process/child.hpp>
36#include <boost/process/env.hpp>
37#include <boost/process/environment.hpp>
38#include <boost/process/group.hpp>
39#include <boost/process/io.hpp>
40#include <boost/process/search_path.hpp>
41#endif
42
43#ifdef PLATFORM_WINDOWS
44#include <windows.h>
45#endif
46
47using namespace std;
48using namespace CppUtilities::EscapeCodes;
49
53namespace CppUtilities {
54
56static bool fileSystemItemExists(const string &path)
57{
58#ifdef PLATFORM_UNIX
59 struct stat res;
60 return stat(path.data(), &res) == 0;
61#else
62 const auto widePath(convertMultiByteToWide(path));
63 if (!widePath.first) {
64 return false;
65 }
66 const auto fileType(GetFileAttributesW(widePath.first.get()));
68#endif
69}
70
71static bool fileExists(const string &path)
72{
73#ifdef PLATFORM_UNIX
74 struct stat res;
75 return stat(path.data(), &res) == 0 && !S_ISDIR(res.st_mode);
76#else
77 const auto widePath(convertMultiByteToWide(path));
78 if (!widePath.first) {
79 return false;
80 }
81 const auto fileType(GetFileAttributesW(widePath.first.get()));
83#endif
84}
85
86static bool dirExists(const string &path)
87{
88#ifdef PLATFORM_UNIX
89 struct stat res;
90 return stat(path.data(), &res) == 0 && S_ISDIR(res.st_mode);
91#else
92 const auto widePath(convertMultiByteToWide(path));
93 if (!widePath.first) {
94 return false;
95 }
96 const auto fileType(GetFileAttributesW(widePath.first.get()));
98#endif
99}
100
101static bool makeDir(const string &path)
102{
103#ifdef PLATFORM_UNIX
104 return mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0;
105#else
106 const auto widePath(convertMultiByteToWide(path));
107 if (!widePath.first) {
108 return false;
109 }
110 return CreateDirectoryW(widePath.first.get(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS;
111#endif
112}
114
115TestApplication *TestApplication::s_instance = nullptr;
116
133
138TestApplication::TestApplication(int argc, const char *const *argv)
139 : m_listArg("list", 'l', "lists available test units")
140 , m_runArg("run", 'r', "runs the tests")
141 , m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files", { "path" })
142 , m_applicationPathArg("app-path", 'a', "specifies the path of the application to be tested", { "path" })
143 , m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files", { "path" })
144 , m_unitsArg("units", 'u', "specifies the units to test; omit to test all units", { "unit1", "unit2", "unit3" })
145{
146 // check whether there is already an instance
147 if (s_instance) {
148 throw runtime_error("only one TestApplication instance allowed at a time");
149 }
150 s_instance = this;
151
152 // handle specified arguments (if present)
153 if (argc && argv) {
154 // setup argument parser
155 m_testFilesPathArg.setRequiredValueCount(Argument::varValueCount);
156 m_unitsArg.setRequiredValueCount(Argument::varValueCount);
157 m_runArg.setImplicit(true);
158 m_runArg.setSubArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg });
159 m_parser.setMainArguments({ &m_runArg, &m_listArg, &m_parser.noColorArg(), &m_parser.helpArg() });
160
161 // parse arguments
162 try {
164 } catch (const ParseError &failure) {
165 cerr << failure;
166 m_valid = false;
167 return;
168 }
169
170 // print help
171 if (m_parser.helpArg().isPresent()) {
172 exit(0);
173 }
174 }
175
176 // set paths for testfiles
177 // -> set paths set via CLI argument
178 if (m_testFilesPathArg.isPresent()) {
179 for (const char *const testFilesPath : m_testFilesPathArg.values()) {
180 if (*testFilesPath) {
181 m_testFilesPaths.emplace_back(argsToString(testFilesPath, '/'));
182 } else {
183 m_testFilesPaths.emplace_back("./");
184 }
185 }
186 }
187 // -> read TEST_FILE_PATH environment variable
189 if (auto testFilePathFromEnv = readTestfilePathFromEnv(); (hasTestFilePathFromEnv = !testFilePathFromEnv.empty())) {
190 m_testFilesPaths.emplace_back(std::move(testFilePathFromEnv));
191 }
192 // -> find source directory
193 if (auto testFilePathFromSrcDirRef = readTestfilePathFromSrcRef(); !testFilePathFromSrcDirRef.empty()) {
194 m_testFilesPaths.insert(m_testFilesPaths.end(), std::make_move_iterator(testFilePathFromSrcDirRef.begin()),
195 std::make_move_iterator(testFilePathFromSrcDirRef.end()));
196 }
197 // -> try testfiles directory in working directory
198 m_testFilesPaths.emplace_back("./testfiles/");
199 for (const auto &testFilesPath : m_testFilesPaths) {
200 cerr << testFilesPath << '\n';
201 }
202
203 // set path for working-copy
204 if (m_workingDirArg.isPresent()) {
205 if (*m_workingDirArg.values().front()) {
206 (m_workingDir = m_workingDirArg.values().front()) += '/';
207 } else {
208 m_workingDir = "./";
209 }
210 } else if (const char *const workingDirEnv = getenv("WORKING_DIR")) {
211 if (*workingDirEnv) {
212 m_workingDir = argsToString(workingDirEnv, '/');
213 }
214 } else {
215 if ((m_testFilesPathArg.isPresent() && !m_testFilesPathArg.values().empty()) || hasTestFilePathFromEnv) {
216 m_workingDir = m_testFilesPaths.front() + "workingdir/";
217 } else {
218 m_workingDir = "./testfiles/workingdir/";
219 }
220 }
221 cerr << "Directory used to store working copies:\n" << m_workingDir << '\n';
222
223 // clear list of all additional profiling files created when forking the test application
224 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
225 ofstream(profrawListFile, ios_base::trunc);
226 }
227
228 m_valid = true;
229}
230
235{
236 s_instance = nullptr;
237}
238
251std::string TestApplication::testFilePath(const std::string &relativeTestFilePath) const
252{
253 std::string path;
254 for (const auto &testFilesPath : m_testFilesPaths) {
256 return path;
257 }
258 }
259 throw std::runtime_error("The test file \"" % relativeTestFilePath % "\" can not be located. Was looking under:\n"
260 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestFilePath));
261}
262
269std::string TestApplication::testDirPath(const std::string &relativeTestDirPath) const
270{
271 std::string path;
272 for (const auto &testFilesPath : m_testFilesPaths) {
274 return path;
275 }
276 }
277 throw std::runtime_error("The test directory \"" % relativeTestDirPath % "\" can not be located. Was looking under:\n"
278 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestDirPath));
279}
280
292
308 const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode) const
309{
310 // ensure working directory is present
311 auto workingCopyPath = std::string();
312 if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
313 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": can't create working directory \""
314 << m_workingDir << "\"." << Phrases::EndFlush;
315 return workingCopyPath;
316 }
317
318 // ensure subdirectory exists
320 if (!parts.empty()) {
321 // create subdirectory level by level
322 string currentLevel;
323 currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
324 currentLevel.assign(m_workingDir);
325 for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
326 if (currentLevel.back() != '/') {
327 currentLevel += '/';
328 }
329 currentLevel += *i;
330
331 // continue if subdirectory level already exists or we can successfully create the directory
333 continue;
334 }
335 // fail otherwise
336 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeWorkingCopyPath << "\": can't create directory \""
337 << currentLevel << "\" (inside working directory)." << Phrases::EndFlush;
338 return workingCopyPath;
339 }
340 }
341
343 switch (mode) {
345 // just return the path if we don't want to actually create a copy
346 return workingCopyPath;
348 // ensure the file does not exist in cleanup mode
349 if (std::remove(workingCopyPath.data()) != 0 && errno != ENOENT) {
350 const auto error = std::strerror(errno);
351 cerr << Phrases::Error << "Unable to delete \"" << workingCopyPath << "\": " << error << Phrases::EndFlush;
352 workingCopyPath.clear();
353 }
354 return workingCopyPath;
355 default:;
356 }
357
358 // copy the file
360 size_t workingCopyPathAttempt = 0;
362 origFile.open(origFilePath, ios_base::in | ios_base::binary);
363 if (origFile.fail()) {
364 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
365 << "\": an IO error occurred when opening original file \"" << origFilePath << "\"." << Phrases::EndFlush;
366 cerr << "error: " << std::strerror(errno) << endl;
367 workingCopyPath.clear();
368 return workingCopyPath;
369 }
370 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
372 // adjust the working copy path if the target file already exists and can not be truncated
374 workingCopy.clear();
375 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
376 }
377 if (workingCopy.fail()) {
378 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
379 << "\": an IO error occurred when opening target file \"" << workingCopyPath << "\"." << Phrases::EndFlush;
380 cerr << "error: " << strerror(errno) << endl;
381 workingCopyPath.clear();
382 return workingCopyPath;
383 }
384 workingCopy << origFile.rdbuf();
385 workingCopy.close();
386 if (!origFile.fail() && !workingCopy.fail()) {
387 return workingCopyPath;
388 }
389
390 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": ";
391 if (origFile.fail()) {
392 cerr << "an IO error occurred when reading original file \"" << origFilePath << "\"";
393 workingCopyPath.clear();
394 return workingCopyPath;
395 }
396 if (workingCopy.fail()) {
397 if (origFile.fail()) {
398 cerr << " and ";
399 }
400 cerr << " an IO error occurred when writing to target file \"" << workingCopyPath << "\".";
401 }
402 cerr << "error: " << strerror(errno) << endl;
403 workingCopyPath.clear();
404 return workingCopyPath;
405}
406
407#ifdef CPP_UTILITIES_HAS_EXEC_APP
408
409#if defined(CPP_UTILITIES_BOOST_PROCESS)
410inline static std::string streambufToString(boost::asio::streambuf &buf)
411{
412 const auto begin = boost::asio::buffers_begin(buf.data());
413 return std::string(begin, begin + static_cast<std::ptrdiff_t>(buf.size()));
414}
415#endif
416
421static int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
422 const std::string &newProfilingPath, bool enableSearchPath = false)
423{
424 // print log message
425 if (!suppressLogging) {
426 // print actual appPath and skip first argument instead
427 cout << '-' << ' ' << appPath;
428 if (*args) {
429 for (const char *const *i = args + 1; *i; ++i) {
430 cout << ' ' << *i;
431 }
432 }
433 cout << endl;
434 }
435
436#if defined(CPP_UTILITIES_BOOST_PROCESS)
437 auto path = enableSearchPath ? boost::process::search_path(appPath) : boost::process::filesystem::path(appPath);
438 auto ctx = boost::asio::io_context();
439 auto group = boost::process::group();
440 auto argsAsVector =
441#if defined(PLATFORM_WINDOWS)
442 std::vector<std::wstring>();
443#else
444 std::vector<std::string>();
445#endif
446 if (*args) {
447 for (const char *const *arg = args + 1; *arg; ++arg) {
448#if defined(PLATFORM_WINDOWS)
449 auto ec = std::error_code();
450 argsAsVector.emplace_back(convertMultiByteToWide(ec, std::string_view(*arg)));
451 if (ec) {
452 throw std::runtime_error(argsToString("unable to convert arg \"", *arg, "\" to wide string"));
453 }
454#else
455 argsAsVector.emplace_back(*arg);
456#endif
457 }
458 }
459 auto outputBuffer = boost::asio::streambuf(), errorBuffer = boost::asio::streambuf();
460 auto env = boost::process::environment(boost::this_process::environment());
461 if (!newProfilingPath.empty()) {
462 env["LLVM_PROFILE_FILE"] = newProfilingPath;
463 }
464 auto child
465 = boost::process::child(ctx, group, path, argsAsVector, env, boost::process::std_out > outputBuffer, boost::process::std_err > errorBuffer);
466 if (timeout > 0) {
467 ctx.run_for(std::chrono::milliseconds(timeout));
468 } else {
469 ctx.run();
470 }
473 child.wait();
474 group.wait();
475 return child.exit_code();
476
477#elif defined(PLATFORM_UNIX)
478 // create pipes
479 int coutPipes[2], cerrPipes[2];
482 const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
483 const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
484
485 // create child process
486 if (const auto child = fork()) {
487 // parent process: read stdout and stderr from child
490
491 try {
492 if (child == -1) {
493 throw runtime_error("Unable to create fork");
494 }
495
496 // init file descriptor set for poll
497 struct pollfd fileDescriptorSet[2];
500 fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
501
502 // init variables for reading
503 char buffer[512];
504 output.clear();
505 errors.clear();
506
507 // poll as long as at least one pipe is open
508 do {
509 const auto retpoll = poll(fileDescriptorSet, 2, timeout);
510 if (retpoll == 0) {
511 throw runtime_error("Poll time-out");
512 }
513 if (retpoll < 0) {
514 throw runtime_error("Poll failed");
515 }
517 const auto count = read(readCoutPipe, buffer, sizeof(buffer));
518 if (count > 0) {
519 output.append(buffer, static_cast<size_t>(count));
520 }
521 } else if (fileDescriptorSet[0].revents & POLLHUP) {
523 fileDescriptorSet[0].fd = -1;
524 }
526 const auto count = read(readCerrPipe, buffer, sizeof(buffer));
527 if (count > 0) {
528 errors.append(buffer, static_cast<size_t>(count));
529 }
530 } else if (fileDescriptorSet[1].revents & POLLHUP) {
532 fileDescriptorSet[1].fd = -1;
533 }
534 } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
535 } catch (...) {
536 // ensure all pipes are closed in the error case
539 throw;
540 }
541
542 // get return code
543 int childReturnCode;
545 waitpid(-child, nullptr, 0);
546 return childReturnCode;
547 } else {
548 // child process
549 // -> set pipes to be used for stdout/stderr
556
557 // -> create process group
558 if (setpgid(0, 0)) {
559 cerr << Phrases::Error << "Unable create process group: " << std::strerror(errno) << Phrases::EndFlush;
561 }
562
563 // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
564 if (!newProfilingPath.empty()) {
565 setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
566 }
567
568 // -> execute application
569 if (enableSearchPath) {
570 execvp(appPath, const_cast<char *const *>(args));
571 } else {
572 execv(appPath, const_cast<char *const *>(args));
573 }
574 cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": " << std::strerror(errno) << Phrases::EndFlush;
576 }
577
578#else
579 throw std::runtime_error("lauching test applications is not supported on this platform");
580#endif
581}
582
591int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
592{
593 // increase counter used for giving profiling files unique names
594 static unsigned int invocationCount = 0;
596
597 // determine the path of the application to be tested
598 const char *appPath = m_applicationPathArg.firstValue();
599 auto fallbackAppPath = string();
600 if (!appPath || !*appPath) {
601 // try to find the path by removing "_tests"-suffix from own executable path
602 // (the own executable path is the path of the test application and its name is usually the name of the application
603 // to be tested with "_tests"-suffix)
604 const char *const testAppPath = m_parser.executable();
606 if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6, "_tests")) {
608 appPath = fallbackAppPath.data();
609 // TODO: it would not hurt to verify whether "fallbackAppPath" actually exists and is executable
610 } else {
611 throw runtime_error("Unable to execute application to be tested: no application path specified");
612 }
613 }
614
615 // determine new path for profiling output (to not override profiling output of parent and previous invocations)
616 const auto newProfilingPath = [appPath] {
617 auto path = string();
618 const char *const llvmProfileFile = getenv("LLVM_PROFILE_FILE");
619 if (!llvmProfileFile) {
620 return path;
621 }
622 // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
623 const char *const llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw");
624 if (!llvmProfileFileEnd) {
625 return path;
626 }
628 // extract application name from path
629 const char *appName = strrchr(appPath, '/');
630 appName = appName ? appName + 1 : appPath;
631 // concat new path
633 // append path to profiling list file
634 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
635 ofstream(profrawListFile, ios_base::app) << path << endl;
636 }
637 return path;
638 }();
639
641}
642
649int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
650{
651 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
652}
653
664 const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
665{
666 return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true);
667}
668#endif
669
673string TestApplication::readTestfilePathFromEnv()
674{
675 const char *const testFilesPathEnv = getenv("TEST_FILE_PATH");
677 return string();
678 }
679 return argsToString(testFilesPathEnv, '/');
680}
681
687std::vector<std::string> TestApplication::readTestfilePathFromSrcRef()
688{
689 // find the path of the current executable on platforms supporting "/proc/self/exe"; otherwise assume the current working directory
690 // is the executable path
691 auto res = std::vector<std::string>();
692 auto binaryPath = std::string();
693#if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
694 try {
695 binaryPath = std::filesystem::read_symlink("/proc/self/exe").parent_path();
696 binaryPath += '/';
697 } catch (const std::filesystem::filesystem_error &e) {
698 cerr << Phrases::Warning << "Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
699 }
700#endif
701 const auto srcdirrefPath = binaryPath + "srcdirref";
702 try {
703 // read "srcdirref" file which should contain the path of the source directory
704 const auto srcDirContent = readFile(srcdirrefPath, 2 * 1024);
705 if (srcDirContent.empty()) {
706 cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
707 return res;
708 }
709
710 // check whether the referenced source directories contain a "testfiles" directory
712 for (const auto &srcPath : srcPaths) {
713 auto testfilesPath = argsToString(srcPath, "/testfiles/");
715 res.emplace_back(std::move(testfilesPath));
716 } else {
717 cerr << Phrases::Warning
718 << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
719 << Phrases::End << "Referenced source directory: " << testfilesPath << endl;
720 }
721 }
722 return res;
723
724 } catch (const std::ios_base::failure &e) {
725 cerr << Phrases::Warning << "The file \"" << srcdirrefPath << "\" can not be opened: " << e.what() << Phrases::EndFlush;
726 }
727 return res;
728}
729} // namespace CppUtilities
const char * executable() const
Returns the name of the current executable.
static constexpr std::size_t varValueCount
Denotes a variable number of values.
const char * firstValue() const
Returns the first parameter value of the first occurrence of the argument.
The ParseError class is thrown by an ArgumentParser when a parsing error occurs.
Definition parseerror.h:11
The TestApplication class simplifies writing test applications that require opening test files.
Definition testutils.h:34
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.
Definition testutils.h:103
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 provides 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.
Definition misc.cpp:17
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
Definition testutils.h:28
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.
IntegralType stringToNumber(const StringType &string, BaseType base=10)
Converts the given string to an unsigned/signed number assuming string uses the specified base.
std::fstream NativeFileStream
StringType argsToString(Args &&...args)
STL namespace.
constexpr int i