C++ Utilities 5.11.2
Useful C++ classes and routines such as argument parser, IO and conversion utilities
testutils.cpp
Go to the documentation of this file.
1#include "./testutils.h"
2
3#include "../conversion/stringbuilder.h"
4#include "../conversion/stringconversion.h"
5#include "../io/ansiescapecodes.h"
6#include "../io/misc.h"
7#include "../io/nativefilestream.h"
8#include "../io/path.h"
9#include "../misc/parseerror.h"
10
11#include <cerrno>
12#include <cstdlib>
13#include <cstring>
14#include <fstream>
15#include <initializer_list>
16#include <iostream>
17#include <limits>
18
19#ifdef PLATFORM_UNIX
20#ifdef CPP_UTILITIES_USE_STANDARD_FILESYSTEM
21#include <filesystem>
22#endif
23#include <poll.h>
24#include <sys/stat.h>
25#include <sys/wait.h>
26#include <unistd.h>
27#endif
28
29#ifdef PLATFORM_WINDOWS
30#include <windows.h>
31#endif
32
33using namespace std;
34using namespace CppUtilities::EscapeCodes;
35
39namespace CppUtilities {
40
42bool fileSystemItemExists(const string &path)
43{
44#ifdef PLATFORM_UNIX
45 struct stat res;
46 return stat(path.data(), &res) == 0;
47#else
48 const auto widePath(convertMultiByteToWide(path));
49 if (!widePath.first) {
50 return false;
51 }
52 const auto fileType(GetFileAttributesW(widePath.first.get()));
53 return fileType != INVALID_FILE_ATTRIBUTES;
54#endif
55}
56
57bool fileExists(const string &path)
58{
59#ifdef PLATFORM_UNIX
60 struct stat res;
61 return stat(path.data(), &res) == 0 && !S_ISDIR(res.st_mode);
62#else
63 const auto widePath(convertMultiByteToWide(path));
64 if (!widePath.first) {
65 return false;
66 }
67 const auto fileType(GetFileAttributesW(widePath.first.get()));
68 return (fileType != INVALID_FILE_ATTRIBUTES) && !(fileType & FILE_ATTRIBUTE_DIRECTORY) && !(fileType & FILE_ATTRIBUTE_DEVICE);
69#endif
70}
71
72bool dirExists(const string &path)
73{
74#ifdef PLATFORM_UNIX
75 struct stat res;
76 return stat(path.data(), &res) == 0 && S_ISDIR(res.st_mode);
77#else
78 const auto widePath(convertMultiByteToWide(path));
79 if (!widePath.first) {
80 return false;
81 }
82 const auto fileType(GetFileAttributesW(widePath.first.get()));
83 return (fileType != INVALID_FILE_ATTRIBUTES) && (fileType & FILE_ATTRIBUTE_DIRECTORY);
84#endif
85}
86
87bool makeDir(const string &path)
88{
89#ifdef PLATFORM_UNIX
90 return mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0;
91#else
92 const auto widePath(convertMultiByteToWide(path));
93 if (!widePath.first) {
94 return false;
95 }
96 return CreateDirectoryW(widePath.first.get(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS;
97#endif
98}
100
101TestApplication *TestApplication::s_instance = nullptr;
102
116 : TestApplication(0, nullptr)
117{
118}
119
124TestApplication::TestApplication(int argc, const char *const *argv)
125 : m_listArg("list", 'l', "lists available test units")
126 , m_runArg("run", 'r', "runs the tests")
127 , m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files", { "path" })
128 , m_applicationPathArg("app-path", 'a', "specifies the path of the application to be tested", { "path" })
129 , m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files", { "path" })
130 , m_unitsArg("units", 'u', "specifies the units to test; omit to test all units", { "unit1", "unit2", "unit3" })
131{
132 // check whether there is already an instance
133 if (s_instance) {
134 throw runtime_error("only one TestApplication instance allowed at a time");
135 }
136 s_instance = this;
137
138 // handle specified arguments (if present)
139 if (argc && argv) {
140 // setup argument parser
141 m_testFilesPathArg.setRequiredValueCount(Argument::varValueCount);
142 m_unitsArg.setRequiredValueCount(Argument::varValueCount);
143 m_runArg.setImplicit(true);
144 m_runArg.setSubArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg });
145 m_parser.setMainArguments({ &m_runArg, &m_listArg, &m_parser.noColorArg(), &m_parser.helpArg() });
146
147 // parse arguments
148 try {
150 } catch (const ParseError &failure) {
151 cerr << failure;
152 m_valid = false;
153 return;
154 }
155
156 // print help
157 if (m_parser.helpArg().isPresent()) {
158 exit(0);
159 }
160 }
161
162 // set paths for testfiles
163 // -> set paths set via CLI argument
164 if (m_testFilesPathArg.isPresent()) {
165 for (const char *const testFilesPath : m_testFilesPathArg.values()) {
166 if (*testFilesPath) {
167 m_testFilesPaths.emplace_back(argsToString(testFilesPath, '/'));
168 } else {
169 m_testFilesPaths.emplace_back("./");
170 }
171 }
172 }
173 // -> read TEST_FILE_PATH environment variable
174 bool hasTestFilePathFromEnv;
175 if (auto testFilePathFromEnv = readTestfilePathFromEnv(); (hasTestFilePathFromEnv = !testFilePathFromEnv.empty())) {
176 m_testFilesPaths.emplace_back(move(testFilePathFromEnv));
177 }
178 // -> find source directory
179 if (auto testFilePathFromSrcDirRef = readTestfilePathFromSrcRef(); !testFilePathFromSrcDirRef.empty()) {
180 m_testFilesPaths.emplace_back(move(testFilePathFromSrcDirRef));
181 }
182 // -> try testfiles directory in working directory
183 m_testFilesPaths.emplace_back("./testfiles/");
184 for (const auto &testFilesPath : m_testFilesPaths) {
185 cerr << testFilesPath << '\n';
186 }
187
188 // set path for working-copy
189 if (m_workingDirArg.isPresent()) {
190 if (*m_workingDirArg.values().front()) {
191 (m_workingDir = m_workingDirArg.values().front()) += '/';
192 } else {
193 m_workingDir = "./";
194 }
195 } else if (const char *const workingDirEnv = getenv("WORKING_DIR")) {
196 if (*workingDirEnv) {
197 m_workingDir = argsToString(workingDirEnv, '/');
198 }
199 } else {
200 if ((m_testFilesPathArg.isPresent() && !m_testFilesPathArg.values().empty()) || hasTestFilePathFromEnv) {
201 m_workingDir = m_testFilesPaths.front() + "workingdir/";
202 } else {
203 m_workingDir = "./testfiles/workingdir/";
204 }
205 }
206 cerr << "Directory used to store working copies:\n" << m_workingDir << '\n';
207
208 // clear list of all additional profiling files created when forking the test application
209 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
210 ofstream(profrawListFile, ios_base::trunc);
211 }
212
213 m_valid = true;
214}
215
220{
221 s_instance = nullptr;
222}
223
236std::string TestApplication::testFilePath(const std::string &relativeTestFilePath) const
237{
238 std::string path;
239 for (const auto &testFilesPath : m_testFilesPaths) {
240 if (fileExists(path = testFilesPath + relativeTestFilePath)) {
241 return path;
242 }
243 }
244 throw std::runtime_error("The test file \"" % relativeTestFilePath % "\" can not be located. Was looking under:\n"
245 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestFilePath));
246}
247
254std::string TestApplication::testDirPath(const std::string &relativeTestDirPath) const
255{
256 std::string path;
257 for (const auto &testFilesPath : m_testFilesPaths) {
258 if (dirExists(path = testFilesPath + relativeTestDirPath)) {
259 return path;
260 }
261 }
262 throw std::runtime_error("The test directory \"" % relativeTestDirPath % "\" can not be located. Was looking under:\n"
263 + joinStrings(m_testFilesPaths, "\n", false, " - ", relativeTestDirPath));
264}
265
273string TestApplication::workingCopyPath(const string &relativeTestFilePath, WorkingCopyMode mode) const
274{
275 return workingCopyPathAs(relativeTestFilePath, relativeTestFilePath, mode);
276}
277
293 const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode) const
294{
295 // ensure working directory is present
296 if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
297 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": can't create working directory \""
298 << m_workingDir << "\"." << Phrases::EndFlush;
299 return string();
300 }
301
302 // ensure subdirectory exists
303 const auto parts = splitString<vector<string>>(relativeWorkingCopyPath, "/", EmptyPartsTreat::Omit);
304 if (!parts.empty()) {
305 // create subdirectory level by level
306 string currentLevel;
307 currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
308 currentLevel.assign(m_workingDir);
309 for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
310 if (currentLevel.back() != '/') {
311 currentLevel += '/';
312 }
313 currentLevel += *i;
314
315 // continue if subdirectory level already exists or we can successfully create the directory
316 if (dirExists(currentLevel) || makeDir(currentLevel)) {
317 continue;
318 }
319 // fail otherwise
320 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeWorkingCopyPath << "\": can't create directory \""
321 << currentLevel << "\" (inside working directory)." << Phrases::EndFlush;
322 return string();
323 }
324 }
325
326 // just return the path if we don't want to actually create a copy
327 if (mode == WorkingCopyMode::NoCopy) {
328 return m_workingDir + relativeWorkingCopyPath;
329 }
330
331 // copy the file
332 const auto origFilePath(testFilePath(relativeTestFilePath));
333 auto workingCopyPath(m_workingDir + relativeWorkingCopyPath);
334 size_t workingCopyPathAttempt = 0;
335 NativeFileStream origFile, workingCopy;
336 origFile.open(origFilePath, ios_base::in | ios_base::binary);
337 if (origFile.fail()) {
338 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
339 << "\": an IO error occurred when opening original file \"" << origFilePath << "\"." << Phrases::EndFlush;
340 cerr << "error: " << strerror(errno) << endl;
341 return string();
342 }
343 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
344 while (workingCopy.fail() && fileSystemItemExists(workingCopyPath)) {
345 // adjust the working copy path if the target file already exists and can not be truncated
346 workingCopyPath = argsToString(m_workingDir, relativeWorkingCopyPath, '.', ++workingCopyPathAttempt);
347 workingCopy.clear();
348 workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
349 }
350 if (workingCopy.fail()) {
351 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
352 << "\": an IO error occurred when opening target file \"" << workingCopyPath << "\"." << Phrases::EndFlush;
353 cerr << "error: " << strerror(errno) << endl;
354 return string();
355 }
356 workingCopy << origFile.rdbuf();
357 workingCopy.close();
358 if (!origFile.fail() && !workingCopy.fail()) {
359 return workingCopyPath;
360 }
361
362 cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": ";
363 if (origFile.fail()) {
364 cerr << "an IO error occurred when reading original file \"" << origFilePath << "\"";
365 return string();
366 }
367 if (workingCopy.fail()) {
368 if (origFile.fail()) {
369 cerr << " and ";
370 }
371 cerr << " an IO error occurred when writing to target file \"" << workingCopyPath << "\".";
372 }
373 cerr << "error: " << strerror(errno) << endl;
374 return string();
375}
376
377#ifdef PLATFORM_UNIX
382static int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
383 const std::string &newProfilingPath, bool enableSearchPath = false)
384{
385 // print log message
386 if (!suppressLogging) {
387 // print actual appPath and skip first argument instead
388 cout << '-' << ' ' << appPath;
389 if (*args) {
390 for (const char *const *i = args + 1; *i; ++i) {
391 cout << ' ' << *i;
392 }
393 }
394 cout << endl;
395 }
396
397 // create pipes
398 int coutPipes[2], cerrPipes[2];
399 pipe(coutPipes);
400 pipe(cerrPipes);
401 const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
402 const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
403
404 // create child process
405 if (const auto child = fork()) {
406 // parent process: read stdout and stderr from child
407 close(writeCoutPipe);
408 close(writeCerrPipe);
409
410 try {
411 if (child == -1) {
412 throw runtime_error("Unable to create fork");
413 }
414
415 // init file descriptor set for poll
416 struct pollfd fileDescriptorSet[2];
417 fileDescriptorSet[0].fd = readCoutPipe;
418 fileDescriptorSet[1].fd = readCerrPipe;
419 fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
420
421 // init variables for reading
422 char buffer[512];
423 output.clear();
424 errors.clear();
425
426 // poll as long as at least one pipe is open
427 do {
428 const auto retpoll = poll(fileDescriptorSet, 2, timeout);
429 if (retpoll == 0) {
430 throw runtime_error("Poll time-out");
431 }
432 if (retpoll < 0) {
433 throw runtime_error("Poll failed");
434 }
435 if (fileDescriptorSet[0].revents & POLLIN) {
436 const auto count = read(readCoutPipe, buffer, sizeof(buffer));
437 if (count > 0) {
438 output.append(buffer, static_cast<size_t>(count));
439 }
440 } else if (fileDescriptorSet[0].revents & POLLHUP) {
441 close(readCoutPipe);
442 fileDescriptorSet[0].fd = -1;
443 }
444 if (fileDescriptorSet[1].revents & POLLIN) {
445 const auto count = read(readCerrPipe, buffer, sizeof(buffer));
446 if (count > 0) {
447 errors.append(buffer, static_cast<size_t>(count));
448 }
449 } else if (fileDescriptorSet[1].revents & POLLHUP) {
450 close(readCerrPipe);
451 fileDescriptorSet[1].fd = -1;
452 }
453 } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
454 } catch (...) {
455 // ensure all pipes are closed in the error case
456 close(readCoutPipe);
457 close(readCerrPipe);
458 throw;
459 }
460
461 // get return code
462 int childReturnCode;
463 waitpid(child, &childReturnCode, 0);
464 return childReturnCode;
465 } else {
466 // child process
467 // -> set pipes to be used for stdout/stderr
468 dup2(writeCoutPipe, STDOUT_FILENO);
469 dup2(writeCerrPipe, STDERR_FILENO);
470 close(readCoutPipe);
471 close(writeCoutPipe);
472 close(readCerrPipe);
473 close(writeCerrPipe);
474
475 // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
476 if (!newProfilingPath.empty()) {
477 setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
478 }
479
480 // -> execute application
481 if (enableSearchPath) {
482 execvp(appPath, const_cast<char *const *>(args));
483
484 } else {
485 execv(appPath, const_cast<char *const *>(args));
486 }
487 cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": execv() failed" << Phrases::EndFlush;
488 exit(-101);
489 }
490}
491
501int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
502{
503 // increase counter used for giving profiling files unique names
504 static unsigned int invocationCount = 0;
505 ++invocationCount;
506
507 // determine the path of the application to be tested
508 const char *appPath = m_applicationPathArg.firstValue();
509 auto fallbackAppPath = string();
510 if (!appPath || !*appPath) {
511 // try to find the path by removing "_tests"-suffix from own executable path
512 // (the own executable path is the path of the test application and its name is usually the name of the application
513 // to be tested with "_tests"-suffix)
514 const char *const testAppPath = m_parser.executable();
515 const auto testAppPathLength = strlen(testAppPath);
516 if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6, "_tests")) {
517 fallbackAppPath.assign(testAppPath, testAppPathLength - 6);
518 appPath = fallbackAppPath.data();
519 // TODO: it would not hurt to verify whether "fallbackAppPath" actually exists and is executable
520 } else {
521 throw runtime_error("Unable to execute application to be tested: no application path specified");
522 }
523 }
524
525 // determine new path for profiling output (to not override profiling output of parent and previous invocations)
526 const auto newProfilingPath = [appPath] {
527 auto path = string();
528 const char *const llvmProfileFile = getenv("LLVM_PROFILE_FILE");
529 if (!llvmProfileFile) {
530 return path;
531 }
532 // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
533 const char *const llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw");
534 if (!llvmProfileFileEnd) {
535 return path;
536 }
537 const auto llvmProfileFileWithoutExtension = string(llvmProfileFile, llvmProfileFileEnd);
538 // extract application name from path
539 const char *appName = strrchr(appPath, '/');
540 appName = appName ? appName + 1 : appPath;
541 // concat new path
542 path = argsToString(llvmProfileFileWithoutExtension, '_', appName, invocationCount, ".profraw");
543 // append path to profiling list file
544 if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
545 ofstream(profrawListFile, ios_base::app) << path << endl;
546 }
547 return path;
548 }();
549
550 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
551}
552
560int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
561{
562 return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
563}
564
575int execHelperAppInSearchPath(
576 const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
577{
578 return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true);
579}
580#endif // PLATFORM_UNIX
581
585string TestApplication::readTestfilePathFromEnv()
586{
587 const char *const testFilesPathEnv = getenv("TEST_FILE_PATH");
588 if (!testFilesPathEnv || !*testFilesPathEnv) {
589 return string();
590 }
591 return argsToString(testFilesPathEnv, '/');
592}
593
599string TestApplication::readTestfilePathFromSrcRef()
600{
601 // find the path of the current executable on platforms supporting "/proc/self/exe"; otherwise assume the current working directory
602 // is the executable path
603 auto binaryPath = std::string();
604#if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
605 try {
606 binaryPath = std::filesystem::read_symlink("/proc/self/exe").parent_path();
607 binaryPath += '/';
608 } catch (const std::filesystem::filesystem_error &e) {
609 cerr << Phrases::Warning << "Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
610 }
611#endif
612 const auto srcdirrefPath = binaryPath + "srcdirref";
613 try {
614 // read "srcdirref" file which should contain the path of the source directory
615 auto srcDirContent(readFile(srcdirrefPath, 2 * 1024));
616 if (srcDirContent.empty()) {
617 cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
618 return string();
619 }
620 srcDirContent += "/testfiles/";
621
622 // check whether the referenced source directory contains a "testfiles" directory
623 if (!dirExists(srcDirContent)) {
624 cerr << Phrases::Warning
625 << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
626 << Phrases::End << "Referenced source directory: " << srcDirContent << endl;
627 return string();
628 }
629 return srcDirContent;
630
631 } catch (const std::ios_base::failure &e) {
632 cerr << Phrases::Warning << "The file \"" << srcdirrefPath << "\" can not be opened: " << e.what() << Phrases::EndFlush;
633 }
634 return string();
635}
636} // 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 TestApplication class simplifies writing test applications that require opening test files.
Definition: testutils.h:21
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.
Definition: testutils.cpp:273
std::string testFilePath(const std::string &relativeTestFilePath) const
Returns the full path of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:236
static const char * appPath()
Returns the application path or an empty string if no application path has been set.
Definition: testutils.h:90
TestApplication()
Constructs a TestApplication instance without further arguments.
Definition: testutils.cpp:115
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.
Definition: testutils.cpp:292
std::string testDirPath(const std::string &relativeTestDirPath) const
Returns the full path of the test directory with the specified relativeTestDirPath.
Definition: testutils.cpp:254
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:219
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:16
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
Definition: testutils.h:16
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.
StringType argsToString(Args &&...args)
std::fstream NativeFileStream
STL namespace.
constexpr int i