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