C++ Utilities  4.9.1
Common C++ classes and routines used by my applications such as argument parser, IO and conversion utilities
testutils.cpp
Go to the documentation of this file.
1 #include "./testutils.h"
2 
3 #include "../application/failure.h"
4 #include "../conversion/stringbuilder.h"
5 #include "../conversion/stringconversion.h"
6 #include "../io/catchiofailure.h"
7 
8 #include <cstdlib>
9 #include <cstring>
10 #include <fstream>
11 #include <initializer_list>
12 #include <iostream>
13 #include <limits>
14 
15 #ifdef PLATFORM_UNIX
16 #include <poll.h>
17 #include <sys/stat.h>
18 #include <sys/wait.h>
19 #include <unistd.h>
20 #endif
21 
22 using namespace std;
23 using namespace ApplicationUtilities;
24 using namespace ConversionUtilities;
25 using namespace IoUtilities;
26 
30 namespace TestUtilities {
31 
32 TestApplication *TestApplication::m_instance = nullptr;
33 
44 TestApplication::TestApplication(int argc, char **argv)
45  : m_helpArg(m_parser)
46  , m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files")
47  , m_applicationPathArg("app-path", 'a', "specifies the path of the application to be tested")
48  , m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files")
49  , m_unitsArg("units", 'u', "specifies the units to test; omit to test all units")
50 {
51  // check whether there is already an instance
52  if (m_instance) {
53  throw runtime_error("only one TestApplication instance allowed at a time");
54  }
55  m_instance = this;
56 
57  // read TEST_FILE_PATH environment variable
58  if (const char *testFilesPathEnv = getenv("TEST_FILE_PATH")) {
59  if (const auto len = strlen(testFilesPathEnv)) {
60  m_testFilesPathEnvValue.reserve(len + 1);
61  m_testFilesPathEnvValue += testFilesPathEnv;
62  m_testFilesPathEnvValue += '/';
63  }
64  }
65 
66  // setup argument parser
67  for (Argument *arg : initializer_list<Argument *>{ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg }) {
68  arg->setRequiredValueCount(1);
69  arg->setValueNames({ "path" });
70  arg->setCombinable(true);
71  }
72  m_unitsArg.setRequiredValueCount(Argument::varValueCount);
73  m_unitsArg.setValueNames({ "unit1", "unit2", "unit3" });
74  m_unitsArg.setCombinable(true);
75  m_parser.setMainArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg, &m_helpArg });
76 
77  // parse arguments
78  try {
79  m_parser.parseArgs(argc, argv);
80 
81  // print help
82  if (m_helpArg.isPresent()) {
83  m_valid = false;
84  exit(0);
85  }
86 
87  // handle path for testfiles and working-copy
88  cerr << "Directories used to search for testfiles:" << endl;
89  if (m_testFilesPathArg.isPresent()) {
90  if (*m_testFilesPathArg.values().front()) {
91  cerr << ((m_testFilesPathArgValue = m_testFilesPathArg.values().front()) += '/') << endl;
92  } else {
93  cerr << (m_testFilesPathArgValue = "./") << endl;
94  }
95  }
96  if (!m_testFilesPathEnvValue.empty()) {
97  cerr << m_testFilesPathEnvValue << endl;
98  }
99  cerr << "./testfiles/" << endl << endl;
100  cerr << "Directory used to store working copies:" << endl;
101  if (m_workingDirArg.isPresent()) {
102  if (*m_workingDirArg.values().front()) {
103  (m_workingDir = m_workingDirArg.values().front()) += '/';
104  } else {
105  m_workingDir = "./";
106  }
107  } else if (const char *workingDirEnv = getenv("WORKING_DIR")) {
108  if (const auto len = strlen(workingDirEnv)) {
109  m_workingDir.reserve(len + 1);
110  m_workingDir += workingDirEnv;
111  m_workingDir += '/';
112  }
113  } else {
114  if (m_testFilesPathArg.isPresent()) {
115  m_workingDir = m_testFilesPathArgValue + "workingdir/";
116  } else if (!m_testFilesPathEnvValue.empty()) {
117  m_workingDir = m_testFilesPathEnvValue + "workingdir/";
118  } else {
119  m_workingDir = "./testfiles/workingdir/";
120  }
121  }
122  cerr << m_workingDir << endl << endl;
123 
124  // clear list of all additional profiling files created when forking the test application
125  if (const char *profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
126  ofstream(profrawListFile, ios_base::trunc);
127  }
128 
129  m_valid = true;
130  cerr << "Executing test cases ..." << endl;
131  } catch (const Failure &failure) {
132  cerr << "Invalid arguments specified: " << failure.what() << endl;
133  m_valid = false;
134  }
135 }
136 
141 {
142  m_instance = nullptr;
143 }
144 
148 string TestApplication::testFilePath(const string &name) const
149 {
150  string path;
151  fstream file; // used to check whether the file is present
152 
153  // check the path specified by command line argument
154  if (m_testFilesPathArg.isPresent()) {
155  file.open(path = m_testFilesPathArgValue + name, ios_base::in);
156  if (file.good()) {
157  return path;
158  }
159  }
160 
161  // check the path specified by environment variable
162  if (!m_testFilesPathEnvValue.empty()) {
163  file.clear();
164  file.open(path = m_testFilesPathEnvValue + name, ios_base::in);
165  if (file.good()) {
166  return path;
167  }
168  }
169 
170  // file still not found -> return default path
171  file.clear();
172  file.open(path = "./testfiles/" + name, ios_base::in);
173  if (!file.good()) {
174  cerr << "Warning: The testfile \"" << path << "\" can not be located." << endl;
175  }
176  return path;
177 }
178 
179 #ifdef PLATFORM_UNIX
180 
184 string TestApplication::workingCopyPathMode(const string &name, WorkingCopyMode mode) const
185 {
186  // create file streams
187  fstream origFile, workingCopy;
188  origFile.exceptions(ios_base::badbit | ios_base::failbit);
189  workingCopy.exceptions(ios_base::badbit | ios_base::failbit);
190 
191  // ensure working directory is present
192  struct stat currentStat;
193  if (stat(m_workingDir.c_str(), &currentStat) || !S_ISDIR(currentStat.st_mode)) {
194  if (mkdir(m_workingDir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) {
195  cerr << "Unable to create working copy for \"" << name << "\": can't create working directory." << endl;
196  return string();
197  }
198  }
199 
200  // ensure subdirectory exists
201  const auto parts = splitString<vector<string>>(name, string("/"), EmptyPartsTreat::Omit);
202  if (!parts.empty()) {
203  string currentLevel = m_workingDir;
204  for (auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
205  if (currentLevel.back() != '/') {
206  currentLevel += '/';
207  }
208  currentLevel += *i;
209  if (stat(currentLevel.c_str(), &currentStat) || !S_ISDIR(currentStat.st_mode)) {
210  if (mkdir(currentLevel.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) {
211  cerr << "Unable to create working copy for \"" << name << "\": can't create working directory." << endl;
212  return string();
213  }
214  }
215  }
216  }
217 
218  // copy file
219  if (mode != WorkingCopyMode::NoCopy) {
220  try {
221  origFile.open(testFilePath(name), ios_base::in | ios_base::binary);
222  const string path = m_workingDir + name;
223  workingCopy.open(path, ios_base::out | ios_base::binary | ios_base::trunc);
224  workingCopy << origFile.rdbuf();
225  return path;
226  } catch (...) {
227  catchIoFailure();
228  cerr << "Unable to create working copy for \"" << name << "\": an IO error occured." << endl;
229  }
230  } else {
231  return m_workingDir + name;
232  }
233  return string();
234 }
235 
236 string TestApplication::workingCopyPath(const string &name) const
237 {
238  return workingCopyPathMode(name, WorkingCopyMode::CreateCopy);
239 }
240 
245 int execAppInternal(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout,
246  const std::string &newProfilingPath)
247 {
248  // print log message
249  if (!suppressLogging) {
250  cout << '-';
251  for (const char *const *i = args; *i; ++i) {
252  cout << ' ' << *i;
253  }
254  cout << endl;
255  }
256 
257  // create pipes
258  int coutPipes[2], cerrPipes[2];
259  pipe(coutPipes), pipe(cerrPipes);
260  int readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
261  int readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
262 
263  // create child process
264  if (int child = fork()) {
265  // parent process: read stdout and stderr from child
266  close(writeCoutPipe), close(writeCerrPipe);
267 
268  try {
269  if (child == -1) {
270  throw runtime_error("Unable to create fork");
271  }
272 
273  // init file descriptor set for poll
274  struct pollfd fileDescriptorSet[2];
275  fileDescriptorSet[0].fd = readCoutPipe;
276  fileDescriptorSet[1].fd = readCerrPipe;
277  fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
278 
279  // init variables for reading
280  char buffer[512];
281  ssize_t count;
282  output.clear(), errors.clear();
283 
284  // poll as long as at least one pipe is open
285  do {
286  int retpoll = poll(fileDescriptorSet, 2, timeout);
287  if (retpoll > 0) {
288  // poll succeeds
289  if (fileDescriptorSet[0].revents & POLLIN) {
290  if ((count = read(readCoutPipe, buffer, sizeof(buffer))) > 0) {
291  output.append(buffer, count);
292  }
293  } else if (fileDescriptorSet[0].revents & POLLHUP) {
294  close(readCoutPipe);
295  fileDescriptorSet[0].fd = -1;
296  }
297  if (fileDescriptorSet[1].revents & POLLIN) {
298  if ((count = read(readCerrPipe, buffer, sizeof(buffer))) > 0) {
299  errors.append(buffer, count);
300  }
301  } else if (fileDescriptorSet[1].revents & POLLHUP) {
302  close(readCerrPipe);
303  fileDescriptorSet[1].fd = -1;
304  }
305  } else if (retpoll == 0) {
306  // timeout
307  throw runtime_error("Poll time-out");
308  } else {
309  // fail
310  throw runtime_error("Poll failed");
311  }
312  } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
313  } catch (...) {
314  // ensure all pipes are close in the error case
315  close(readCoutPipe), close(readCerrPipe);
316  throw;
317  }
318 
319  // get return code
320  int childReturnCode;
321  waitpid(child, &childReturnCode, 0);
322  return childReturnCode;
323  } else {
324  // child process
325  // -> set pipes to be used for stdout/stderr
326  dup2(writeCoutPipe, STDOUT_FILENO), dup2(writeCerrPipe, STDERR_FILENO);
327  close(readCoutPipe), close(writeCoutPipe), close(readCerrPipe), close(writeCerrPipe);
328 
329  // -> modify environment variable LLVM_PROFILE_FILE to apply new path for profiling output
330  if (!newProfilingPath.empty()) {
331  setenv("LLVM_PROFILE_FILE", newProfilingPath.data(), true);
332  }
333 
334  // -> execute application
335  execv(appPath, const_cast<char *const *>(args));
336  cerr << "Unable to execute \"" << appPath << "\": execv() failed" << endl;
337  exit(-101);
338  }
339 }
340 
350 int TestApplication::execApp(const char *const *args, string &output, string &errors, bool suppressLogging, int timeout) const
351 {
352  // increase counter used for giving profiling files unique names
353  static unsigned int invocationCount = 0;
354  ++invocationCount;
355 
356  // determine application path
357  const char *const appPath = m_applicationPathArg.firstValue();
358  if (!appPath || !*appPath) {
359  throw runtime_error("Unable to execute application to be tested: no application path specified");
360  }
361 
362  // determine new path for profiling output (to not override profiling output of parent and previous invocations)
363  string newProfilingPath;
364  if (const char *llvmProfileFile = getenv("LLVM_PROFILE_FILE")) {
365  // replace eg. "/some/path/tageditor_tests.profraw" with "/some/path/tageditor0.profraw"
366  if (const char *llvmProfileFileEnd = strstr(llvmProfileFile, ".profraw")) {
367  const string llvmProfileFileWithoutExtension(llvmProfileFile, llvmProfileFileEnd);
368  // extract application name from path
369  const char *appName = strrchr(appPath, '/');
370  appName = appName ? appName + 1 : appPath;
371  // concat new path
372  newProfilingPath = argsToString(llvmProfileFileWithoutExtension, '_', appName, invocationCount, ".profraw");
373  // append path to profiling list file
374  if (const char *profrawListFile = getenv("LLVM_PROFILE_LIST_FILE")) {
375  ofstream(profrawListFile, ios_base::app) << newProfilingPath << endl;
376  }
377  }
378  }
379 
380  return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, newProfilingPath);
381 }
382 
390 int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
391 {
392  return execAppInternal(appPath, args, output, errors, suppressLogging, timeout, string());
393 }
394 #endif
395 }
void setCombinable(bool value)
Sets whether this argument can be combined.
virtual const char * what() const USE_NOTHROW
Returns a C-style character string describing the cause of the Failure.
Definition: failure.cpp:40
Contains currently only ArgumentParser and related classes.
constexpr StringType argsToString(Args &&... args)
void setMainArguments(const ArgumentInitializerList &mainArguments)
Sets the main arguments for the parser.
STL namespace.
std::string testFilePath(const std::string &name) const
Returns the full path of the test file with the specified name.
Definition: testutils.cpp:148
void parseArgs(int argc, const char *const *argv)
Parses the specified command line arguments.
const char * firstValue() const
Returns the first parameter value of the first occurrence of the argument.
Contains utility classes helping to read and write streams.
Definition: binaryreader.h:10
Contains classes and functions utilizing creating of test applications.
Definition: testutils.h:10
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:140
const std::vector< const char * > & values(std::size_t occurrence=0) const
Returns the parameter values for the specified occurrence of argument.
Contains several functions providing conversions between different data types.
The Argument class is a wrapper for command line argument information.
WorkingCopyMode
The WorkingCopyMode enum specifies additional options to influence behavior of TestApplication::worki...
Definition: testutils.h:15
bool isPresent() const
Returns an indication whether the argument could be detected when parsing.
The Failure class is thrown by an ArgumentParser when a parsing error occurs.
Definition: failure.h:11
CPP_UTILITIES_EXPORT const char * catchIoFailure()
Provides a workaround for GCC Bug 66145.
void setRequiredValueCount(std::size_t requiredValueCount)
Sets the number of values which are required to be given for this argument.
void setValueNames(std::initializer_list< const char *> valueNames)
Sets the names of the requried values.