C++ Utilities  5.10.5
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 std::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 
254 std::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 
273 string 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
382 static 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 
501 int 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 
560 int 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 
575 int 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 
585 string 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 
599 string 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  std::string binaryPath;
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  try {
613  // read "srcdirref" file which should contain the path of the source directory
614  auto srcDirContent(readFile(binaryPath + "srcdirref", 2 * 1024));
615  if (srcDirContent.empty()) {
616  cerr << Phrases::Warning << "The file \"srcdirref\" is empty." << Phrases::EndFlush;
617  return string();
618  }
619  srcDirContent += "/testfiles/";
620 
621  // check whether the referenced source directory contains a "testfiles" directory
622  if (!dirExists(srcDirContent)) {
623  cerr << Phrases::Warning
624  << "The source directory referenced by the file \"srcdirref\" does not contain a \"testfiles\" directory or does not exist."
625  << Phrases::End << "Referenced source directory: " << srcDirContent << endl;
626  return string();
627  }
628  return srcDirContent;
629 
630  } catch (const std::ios_base::failure &) {
631  cerr << Phrases::Warning << "The file \"srcdirref\" can not be opened. It likely just doesn't exist in the working directory."
632  << 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
constexpr int i