C++ Utilities  4.6.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/stringconversion.h"
5 #include "../io/catchiofailure.h"
6 
7 #include <cstdlib>
8 #include <cstring>
9 #include <iostream>
10 #include <fstream>
11 #include <initializer_list>
12 
13 #ifdef PLATFORM_UNIX
14 # include <unistd.h>
15 # include <poll.h>
16 # include <sys/wait.h>
17 # include <sys/stat.h>
18 #endif
19 
20 using namespace std;
21 using namespace ApplicationUtilities;
22 using namespace ConversionUtilities;
23 using namespace IoUtilities;
24 
28 namespace TestUtilities {
29 
30 TestApplication *TestApplication::m_instance = nullptr;
31 
42 TestApplication::TestApplication(int argc, char **argv) :
43  m_helpArg(m_parser),
44  m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files"),
45  m_applicationPathArg("app-path",'a', "specifies the path of the application to be tested"),
46  m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files"),
47  m_unitsArg("units", 'u', "specifies the units to test; omit to test all units")
48 {
49  // check whether there is already an instance
50  if(m_instance) {
51  throw runtime_error("only one TestApplication instance allowed at a time");
52  }
53  m_instance = this;
54 
55  // read TEST_FILE_PATH environment variable
56  if(const char *testFilesPathEnv = getenv("TEST_FILE_PATH")) {
57  if(const auto len = strlen(testFilesPathEnv)) {
58  m_testFilesPathEnvValue.reserve(len + 1);
59  m_testFilesPathEnvValue += testFilesPathEnv;
60  m_testFilesPathEnvValue += '/';
61  }
62  }
63 
64  // setup argument parser
65  for(Argument *arg : initializer_list<Argument *>{&m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg}) {
66  arg->setRequiredValueCount(1);
67  arg->setValueNames({"path"});
68  arg->setCombinable(true);
69  }
70  m_unitsArg.setRequiredValueCount(-1);
71  m_unitsArg.setValueNames({"unit1", "unit2", "unit3"});
72  m_unitsArg.setCombinable(true);
73  m_parser.setMainArguments({&m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg, &m_helpArg});
74 
75  // parse arguments
76  try {
77  m_parser.parseArgs(argc, argv);
78  if(m_helpArg.isPresent()) {
79  m_valid = false;
80  exit(0);
81  }
82  cerr << "Directories used to search for testfiles:" << endl;
83  if(m_testFilesPathArg.isPresent()) {
84  if(*m_testFilesPathArg.values().front()) {
85  cerr << ((m_testFilesPathArgValue = m_testFilesPathArg.values().front()) += '/') << endl;
86  } else {
87  cerr << (m_testFilesPathArgValue = "./") << endl;
88  }
89  }
90  if(!m_testFilesPathEnvValue.empty()) {
91  cerr << m_testFilesPathEnvValue << endl;
92  }
93  cerr << "./testfiles/" << endl << endl;
94  cerr << "Directory used to store working copies:" << endl;
95  if(m_workingDirArg.isPresent()) {
96  if(*m_workingDirArg.values().front()) {
97  (m_workingDir = m_workingDirArg.values().front()) += '/';
98  } else {
99  m_workingDir = "./";
100  }
101  } else if(const char *workingDirEnv = getenv("WORKING_DIR")) {
102  if(const auto len = strlen(workingDirEnv)) {
103  m_workingDir.reserve(len + 1);
104  m_workingDir += workingDirEnv;
105  m_workingDir += '/';
106  }
107  } else {
108  if(m_testFilesPathArg.isPresent()) {
109  m_workingDir = m_testFilesPathArgValue + "workingdir/";
110  } else if(!m_testFilesPathEnvValue.empty()) {
111  m_workingDir = m_testFilesPathEnvValue + "workingdir/";
112  } else {
113  m_workingDir = "./testfiles/workingdir/";
114  }
115  }
116  cerr << m_workingDir << endl << endl;
117 
118  m_valid = true;
119  cerr << "Executing test cases ..." << endl;
120  } catch(const Failure &failure) {
121  cerr << "Invalid arguments specified: " << failure.what() << endl;
122  m_valid = false;
123  }
124 }
125 
130 {
131  m_instance = nullptr;
132 }
133 
137 string TestApplication::testFilePath(const string &name) const
138 {
139  string path;
140  fstream file; // used to check whether the file is present
141 
142  // check the path specified by command line argument
143  if(m_testFilesPathArg.isPresent()) {
144  file.open(path = m_testFilesPathArgValue + name, ios_base::in);
145  if(file.good()) {
146  return path;
147  }
148  }
149 
150  // check the path specified by environment variable
151  if(!m_testFilesPathEnvValue.empty()) {
152  file.clear();
153  file.open(path = m_testFilesPathEnvValue + name, ios_base::in);
154  if(file.good()) {
155  return path;
156  }
157  }
158 
159  // file still not found -> return default path
160  path = "./testfiles/" + name;
161  file.clear();
162  file.open(path = m_testFilesPathEnvValue + name, ios_base::in);
163  if(!file.good()) {
164  cerr << "Warning: The testfile \"" << path << "\" can not be located." << endl;
165  }
166  return path;
167 }
168 
169 #ifdef PLATFORM_UNIX
170 
174 string TestApplication::workingCopyPathMode(const string &name, WorkingCopyMode mode) const
175 {
176  // create file streams
177  fstream origFile, workingCopy;
178  origFile.exceptions(ios_base::badbit | ios_base::failbit);
179  workingCopy.exceptions(ios_base::badbit | ios_base::failbit);
180 
181  // ensure working directory is present
182  struct stat currentStat;
183  if(stat(m_workingDir.c_str(), &currentStat) || !S_ISDIR(currentStat.st_mode)) {
184  if(mkdir(m_workingDir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) {
185  cerr << "Unable to create working copy for \"" << name << "\": can't create working directory." << endl;
186  return string();
187  }
188  }
189 
190  // ensure subdirectory exists
191  const auto parts = splitString<vector<string> >(name, string("/"), EmptyPartsTreat::Omit);
192  if(!parts.empty()) {
193  string currentLevel = m_workingDir;
194  for(auto i = parts.cbegin(), end = parts.end() - 1; i != end; ++i) {
195  if(currentLevel.back() != '/') {
196  currentLevel += '/';
197  }
198  currentLevel += *i;
199  if(stat(currentLevel.c_str(), &currentStat) || !S_ISDIR(currentStat.st_mode)) {
200  if(mkdir(currentLevel.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) {
201  cerr << "Unable to create working copy for \"" << name << "\": can't create working directory." << endl;
202  return string();
203  }
204  }
205  }
206  }
207 
208  // copy file
209  if(mode != WorkingCopyMode::NoCopy) {
210  try {
211  origFile.open(testFilePath(name), ios_base::in | ios_base::binary);
212  const string path = m_workingDir + name;
213  workingCopy.open(path, ios_base::out | ios_base::binary | ios_base::trunc);
214  workingCopy << origFile.rdbuf();
215  return path;
216  } catch(...) {
217  catchIoFailure();
218  cerr << "Unable to create working copy for \"" << name << "\": an IO error occured." << endl;
219  }
220  } else {
221  return m_workingDir + name;
222  }
223  return string();
224 }
225 
226 string TestApplication::workingCopyPath(const string &name) const
227 {
228  return workingCopyPathMode(name, WorkingCopyMode::CreateCopy);
229 }
230 
240 int TestApplication::execApp(const char *const *args, string &stdout, string &stderr, bool suppressLogging, int timeout) const
241 {
242  // print log message
243  if(!suppressLogging) {
244  cout << '-';
245  for(const char *const *i = args; *i; ++i) {
246  cout << ' ' << *i;
247  }
248  cout << endl;
249  }
250  // determine application path
251  const char *appPath = m_applicationPathArg.firstValue();
252  if(!appPath || !*appPath) {
253  throw runtime_error("Unable to execute application to be tested: no application path specified");
254  }
255  // create pipes
256  int coutPipes[2], cerrPipes[2];
257  pipe(coutPipes), pipe(cerrPipes);
258  int readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
259  int readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
260  // create child process
261  if(int child = fork()) {
262  // parent process: read stdout and stderr from child
263  close(writeCoutPipe), close(writeCerrPipe);
264 
265  try {
266  if(child == -1) {
267  throw runtime_error("Unable to create fork");
268  }
269 
270  // init file descriptor set for poll
271  struct pollfd fileDescriptorSet[2];
272  fileDescriptorSet[0].fd = readCoutPipe;
273  fileDescriptorSet[1].fd = readCerrPipe;
274  fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
275 
276  // init variables for reading
277  char buffer[512];
278  ssize_t count;
279  stdout.clear(), stderr.clear();
280 
281  // poll as long as at least one pipe is open
282  do {
283  int retpoll = poll(fileDescriptorSet, 2, timeout);
284  if(retpoll > 0) {
285  // poll succeeds
286  if(fileDescriptorSet[0].revents & POLLIN) {
287  if((count = read(readCoutPipe, buffer, sizeof(buffer))) > 0) {
288  stdout.append(buffer, count);
289  }
290  } else if(fileDescriptorSet[0].revents & POLLHUP) {
291  close(readCoutPipe);
292  fileDescriptorSet[0].fd = -1;
293  }
294  if(fileDescriptorSet[1].revents & POLLIN) {
295  if((count = read(readCerrPipe, buffer, sizeof(buffer))) > 0) {
296  stderr.append(buffer, count);
297  }
298  } else if(fileDescriptorSet[1].revents & POLLHUP) {
299  close(readCerrPipe);
300  fileDescriptorSet[1].fd = -1;
301  }
302  } else if(retpoll == 0) {
303  // timeout
304  throw runtime_error("Poll time-out");
305  } else {
306  // fail
307  throw runtime_error("Poll failed");
308  }
309  } while(fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
310  } catch(...) {
311  // ensure all pipes are close in the error case
312  close(readCoutPipe), close(readCerrPipe);
313  throw;
314  }
315 
316  // get return code
317  int childReturnCode;
318  waitpid(child, &childReturnCode, 0);
319  return childReturnCode;
320  } else {
321  // child process: set pipes to be used for stdout/stderr, execute application
322  dup2(writeCoutPipe, STDOUT_FILENO), dup2(writeCerrPipe, STDERR_FILENO);
323  close(readCoutPipe), close(writeCoutPipe), close(readCerrPipe), close(writeCerrPipe);
324  execv(appPath, const_cast<char *const *>(args));
325  cerr << "Unable to execute \"" << appPath << "\": execv() failed" << endl;
326  exit(-101);
327  }
328 }
329 #endif
330 
331 }
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:37
Contains currently only ArgumentParser and related classes.
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:137
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:9
~TestApplication()
Destroys the TestApplication.
Definition: testutils.cpp:129
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:14
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.