C++ Utilities  5.6.0
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 
33 using namespace std;
34 using namespace CppUtilities::EscapeCodes;
35 
39 namespace CppUtilities {
40 
42 bool 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 
57 bool 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 
72 bool 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 
87 bool 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 
101 TestApplication *TestApplication::s_instance = nullptr;
102 
115 TestApplication::TestApplication()
116  : TestApplication(0, nullptr)
117 {
118 }
119 
124 TestApplication::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 
236 string TestApplication::testFilePath(const string &relativeTestFilePath) const
237 {
238  string path;
239  for (const auto &testFilesPath : m_testFilesPaths) {
240  if (fileExists(path = testFilesPath + relativeTestFilePath)) {
241  return path;
242  }
243  }
244  throw runtime_error("The testfile \"" % relativeTestFilePath % "\" can not be located. Was looking under:"
245  + joinStrings(m_testFilesPaths, "\n", false, string(), relativeTestFilePath));
246 }
247 
255 string TestApplication::workingCopyPath(const string &relativeTestFilePath, WorkingCopyMode mode) const
256 {
257  return workingCopyPathAs(relativeTestFilePath, relativeTestFilePath, mode);
258 }
259 
275  const std::string &relativeTestFilePath, const std::string &relativeWorkingCopyPath, WorkingCopyMode mode) const
276 {
277  // ensure working directory is present
278  if (!dirExists(m_workingDir) && !makeDir(m_workingDir)) {
279  cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": can't create working directory \""
280  << m_workingDir << "\"." << Phrases::EndFlush;
281  return string();
282  }
283 
284  // ensure subdirectory exists
285  const auto parts = splitString<vector<string>>(relativeWorkingCopyPath, "/", EmptyPartsTreat::Omit);
286  if (!parts.empty()) {
287  // create subdirectory level by level
288  string currentLevel;
289  currentLevel.reserve(m_workingDir.size() + relativeWorkingCopyPath.size() + 1);
290  currentLevel.assign(m_workingDir);
291  for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
292  if (currentLevel.back() != '/') {
293  currentLevel += '/';
294  }
295  currentLevel += *i;
296 
297  // continue if subdirectory level already exists or we can successfully create the directory
298  if (dirExists(currentLevel) || makeDir(currentLevel)) {
299  continue;
300  }
301  // fail otherwise
302  cerr << Phrases::Error << "Unable to create working copy for \"" << relativeWorkingCopyPath << "\": can't create directory \""
303  << currentLevel << "\" (inside working directory)." << Phrases::EndFlush;
304  return string();
305  }
306  }
307 
308  // just return the path if we don't want to actually create a copy
309  if (mode == WorkingCopyMode::NoCopy) {
310  return m_workingDir + relativeWorkingCopyPath;
311  }
312 
313  // copy the file
314  const auto origFilePath(testFilePath(relativeTestFilePath));
315  auto workingCopyPath(m_workingDir + relativeWorkingCopyPath);
316  size_t workingCopyPathAttempt = 0;
317  NativeFileStream origFile, workingCopy;
318  origFile.open(origFilePath, ios_base::in | ios_base::binary);
319  if (origFile.fail()) {
320  cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
321  << "\": an IO error occurred when opening original file \"" << origFilePath << "\"." << Phrases::EndFlush;
322  cerr << "error: " << strerror(errno) << endl;
323  return string();
324  }
325  workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
326  while (workingCopy.fail() && fileSystemItemExists(workingCopyPath)) {
327  // adjust the working copy path if the target file already exists and can not be truncated
328  workingCopyPath = argsToString(m_workingDir, relativeWorkingCopyPath, '.', ++workingCopyPathAttempt);
329  workingCopy.clear();
330  workingCopy.open(workingCopyPath, ios_base::out | ios_base::binary | ios_base::trunc);
331  }
332  if (workingCopy.fail()) {
333  cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath
334  << "\": an IO error occurred when opening target file \"" << workingCopyPath << "\"." << Phrases::EndFlush;
335  cerr << "error: " << strerror(errno) << endl;
336  return string();
337  }
338  workingCopy << origFile.rdbuf();
339  workingCopy.close();
340  if (!origFile.fail() && !workingCopy.fail()) {
341  return workingCopyPath;
342  }
343 
344  cerr << Phrases::Error << "Unable to create working copy for \"" << relativeTestFilePath << "\": ";
345  if (origFile.fail()) {
346  cerr << "an IO error occurred when reading original file \"" << origFilePath << "\"";
347  return string();
348  }
349  if (workingCopy.fail()) {
350  if (origFile.fail()) {
351  cerr << " and ";
352  }
353  cerr << " an IO error occurred when writing to target file \"" << workingCopyPath << "\".";
354  }
355  cerr << "error: " << strerror(errno) << endl;
356  return string();
357 }
358 
359 #ifdef PLATFORM_UNIX
360 
364 static int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
365  const std::string &newProfilingPath, bool enableSearchPath = false)
366 {
367  // print log message
368  if (!suppressLogging) {
369  // print actual appPath and skip first argument instead
370  cout << '-' << ' ' << appPath;
371  if (*args) {
372  for (const char *const *i = args + 1; *i; ++i) {
373  cout << ' ' << *i;
374  }
375  }
376  cout << endl;
377  }
378 
379  // create pipes
380  int coutPipes[2], cerrPipes[2];
381  pipe(coutPipes);
382  pipe(cerrPipes);
383  const auto readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
384  const auto readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
385 
386  // create child process
387  if (const auto child = fork()) {
388  // parent process: read stdout and stderr from child
389  close(writeCoutPipe);
390  close(writeCerrPipe);
391 
392  try {
393  if (child == -1) {
394  throw runtime_error("Unable to create fork");
395  }
396 
397  // init file descriptor set for poll
398  struct pollfd fileDescriptorSet[2];
399  fileDescriptorSet[0].fd = readCoutPipe;
400  fileDescriptorSet[1].fd = readCerrPipe;
401  fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
402 
403  // init variables for reading
404  char buffer[512];
405  output.clear();
406  errors.clear();
407 
408  // poll as long as at least one pipe is open
409  do {
410  const auto retpoll = poll(fileDescriptorSet, 2, timeout);
411  if (retpoll == 0) {
412  throw runtime_error("Poll time-out");
413  }
414  if (retpoll < 0) {
415  throw runtime_error("Poll failed");
416  }
417  if (fileDescriptorSet[0].revents & POLLIN) {
418  const auto count = read(readCoutPipe, buffer, sizeof(buffer));
419  if (count > 0) {
420  output.append(buffer, static_cast<size_t>(count));
421  }
422  } else if (fileDescriptorSet[0].revents & POLLHUP) {
423  close(readCoutPipe);
424  fileDescriptorSet[0].fd = -1;
425  }
426  if (fileDescriptorSet[1].revents & POLLIN) {
427  const auto count = read(readCerrPipe, buffer, sizeof(buffer));
428  if (count > 0) {
429  errors.append(buffer, static_cast<size_t>(count));
430  }
431  } else if (fileDescriptorSet[1].revents & POLLHUP) {
432  close(readCerrPipe);
433  fileDescriptorSet[1].fd = -1;
434  }
435  } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
436  } catch (...) {
437  // ensure all pipes are closed in the error case
438  close(readCoutPipe);
439  close(readCerrPipe);
440  throw;
441  }
442 
443  // get return code
444  int childReturnCode;
445  waitpid(child, &childReturnCode, 0);
446  return childReturnCode;
447  } else {
448  // child process
449  // -> set pipes to be used for stdout/stderr
450  dup2(writeCoutPipe, STDOUT_FILENO);
451  dup2(writeCerrPipe, STDERR_FILENO);
452  close(readCoutPipe);
453  close(writeCoutPipe);
454  close(readCerrPipe);
455  close(writeCerrPipe);
456 
457  // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
458  if (!newProfilingPath.empty()) {
459  setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
460  }
461 
462  // -> execute application
463  if (enableSearchPath) {
464  execvp(appPath, const_cast<char *const *>(args));
465 
466  } else {
467  execv(appPath, const_cast<char *const *>(args));
468  }
469  cerr << Phrases::Error << "Unable to execute \"" << appPath << "\": execv() failed" << Phrases::EndFlush;
470  exit(-101);
471  }
472 }
473 
483 int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
484 {
485  // increase counter used for giving profiling files unique names
486  static unsigned int invocationCount = 0;
487  ++invocationCount;
488 
489  // determine the path of the application to be tested
490  const char *appPath = m_applicationPathArg.firstValue();
491  string fallbackAppPath;
492  if (!appPath || !*appPath) {
493  // try to find the path by removing "_tests"-suffix from own executable path
494  // (the own executable path is the path of the test application and its name is usually the name of the application
495  // to be tested with "_tests"-suffix)
496  const char *const testAppPath = m_parser.executable();
497  const size_t testAppPathLength = strlen(testAppPath);
498  if (testAppPathLength > 6 && !strcmp(testAppPath + testAppPathLength - 6, "_tests")) {
499  fallbackAppPath.assign(testAppPath, testAppPathLength - 6);
500  appPath = fallbackAppPath.data();
501  // TODO: it would not hurt to verify whether "fallbackAppPath" actually exists and is executalbe
502  } else {
503  throw runtime_error("Unable to execute application to be tested: no application path specified");
504  }
505  }
506 
507  // determine new path for profiling output (to not override profiling output of parent and previous invocations)
508  const auto newProfilingPath = [appPath] {
509  string newProfilingPath;
510  const char *const llvmProfileFile = getenv("LLVM_PROFILE_FILE");
511  if (!llvmProfileFile) {
512  return newProfilingPath;
513  }
514  // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
515  const char *const llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw");
516  if (!llvmProfileFileEnd) {
517  return newProfilingPath;
518  }
519  const string llvmProfileFileWithoutExtension(llvmProfileFile, llvmProfileFileEnd);
520  // extract application name from path
521  const char *appName = strrchr(appPath, '/');
522  appName = appName ? appName + 1 : appPath;
523  // concat new path
524  newProfilingPath = argsToString(llvmProfileFileWithoutExtension, '_', appName, invocationCount, ".profraw");
525  // append path to profiling list file
526  if (const char *const profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
527  ofstream(profrawListFile, ios_base::app) << newProfilingPath << endl;
528  }
529  return newProfilingPath;
530  }();
531 
532  return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
533 }
534 
542 int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
543 {
544  return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
545 }
546 
557 int execHelperAppInSearchPath(
558  const char *appName, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
559 {
560  return execAppInternal(appName, args, output, errors, suppressLogging, timeout, string(), true);
561 }
562 #endif // PLATFORM_UNIX
563 
567 string TestApplication::readTestfilePathFromEnv()
568 {
569  const char *const testFilesPathEnv = getenv("TEST_FILE_PATH");
570  if (!testFilesPathEnv || !*testFilesPathEnv) {
571  return string();
572  }
573  return argsToString(testFilesPathEnv, '/');
574 }
575 
581 string TestApplication::readTestfilePathFromSrcRef()
582 {
583  // find the path of the current executable on platforms supporting "/proc/self/exe"; otherwise assume the current working directory
584  // is the executable path
585  std::string binaryPath;
586 #if defined(CPP_UTILITIES_USE_STANDARD_FILESYSTEM) && defined(PLATFORM_UNIX)
587  try {
588  binaryPath = std::filesystem::read_symlink("/proc/self/exe").parent_path();
589  binaryPath += '/';
590  } catch (const std::filesystem::filesystem_error &e) {
591  cerr << Phrases::Warning << "Unable to detect binary path for finding \"srcdirref\": " << e.what() << Phrases::EndFlush;
592  }
593 #endif
594  try {
595  // read "srcdirref" file which should contain the path of the source directory
596  auto srcDirContent(readFile(binaryPath + "srcdirref", 2 * 1024));
597  if (srcDirContent.empty()) {
598  cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
599  return string();
600  }
601  srcDirContent += "/testfiles/";
602 
603  // check whether the referenced source directory contains a "testfiles" directory
604  if (!dirExists(srcDirContent)) {
605  cerr << Phrases::Warning
606  << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
607  << Phrases::End << "Referenced source directory: " << srcDirContent << endl;
608  return string();
609  }
610  return srcDirContent;
611 
612  } catch (const std::ios_base::failure &) {
613  cerr << Phrases::Warning << "The file \"srcdirref\" can not be opened. It likely just doesn't exist in the working directory."
614  << Phrases::EndFlush;
615  }
616  return string();
617 }
618 } // namespace CppUtilities
CppUtilities::TestApplication::~TestApplication
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:219
CppUtilities::Argument::varValueCount
static constexpr std::size_t varValueCount
Denotes a variable number of values.
Definition: argumentparser.h:361
CppUtilities::WorkingCopyMode::NoCopy
@ NoCopy
CppUtilities::NativeFileStream
std::fstream NativeFileStream
Definition: nativefilestream.h:108
CppUtilities::readFile
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
CppUtilities::TestApplication
The TestApplication class simplifies writing test applications that require opening test files.
Definition: testutils.h:21
CppUtilities::EmptyPartsTreat::Omit
@ Omit
CppUtilities::TestApplication::TestApplication
TestApplication()
Constructs a TestApplication instance without further arguments.
Definition: testutils.cpp:115
CppUtilities::joinStrings
ReturnType joinStrings(const Container &strings, const typename Container::value_type &delimiter=typename Container::value_type(), bool omitEmpty=false, const typename Container::value_type &leftClosure=typename Container::value_type(), const typename Container::value_type &rightClosure=typename Container::value_type())
Joins the given strings using the specified delimiter.
Definition: stringconversion.h:75
CppUtilities::argsToString
StringType argsToString(Args &&... args)
Definition: stringbuilder.h:258
CppUtilities::Argument::firstValue
const char * firstValue() const
Returns the first parameter value of the first occurrence of the argument.
Definition: argumentparser.cpp:491
CppUtilities::TestApplication::workingCopyPath
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:255
CppUtilities::TestApplication::workingCopyPathAs
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:274
CppUtilities::TestApplication::testFilePath
std::string testFilePath(const std::string &relativeTestFilePath) const
Returns the full path of the test file with the specified relativeTestFilePath.
Definition: testutils.cpp:236
CppUtilities
Contains all utilities provides by the c++utilities library.
Definition: argumentparser.h:17
i
constexpr int i
Definition: traitstests.cpp:106
CppUtilities::EscapeCodes
Encapsulates functions for formatted terminal output using ANSI escape codes.
Definition: ansiescapecodes.h:12
CppUtilities::WorkingCopyMode
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
Definition: testutils.h:16
CppUtilities::ArgumentParser::executable
const char * executable() const
Returns the name of the current executable.
Definition: argumentparser.h:1104
testutils.h
CppUtilities::TestApplication::appPath
static const char * appPath()
Returns the application path or an empty string if no application path has been set.
Definition: testutils.h:89
CppUtilities::ParseArgumentBehavior::CheckConstraints
@ CheckConstraints
CppUtilities::ParseArgumentBehavior::InvokeCallbacks
@ InvokeCallbacks