C++ Utilities  4.8.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  m_unitsArg.setRequiredValueCount(Argument::varValueCount);
72  m_unitsArg.setValueNames({ "unit1", "unit2", "unit3" });
73  m_unitsArg.setCombinable(true);
74  m_parser.setMainArguments({ &m_testFilesPathArg, &m_applicationPathArg, &m_workingDirArg, &m_unitsArg, &m_helpArg });
75 
76  // parse arguments
77  try {
78  m_parser.parseArgs(argc, argv);
79  if (m_helpArg.isPresent()) {
80  m_valid = false;
81  exit(0);
82  }
83  cerr << "Directories used to search for testfiles:" << endl;
84  if (m_testFilesPathArg.isPresent()) {
85  if (*m_testFilesPathArg.values().front()) {
86  cerr << ((m_testFilesPathArgValue = m_testFilesPathArg.values().front()) += '/') << endl;
87  } else {
88  cerr << (m_testFilesPathArgValue = "./") << endl;
89  }
90  }
91  if (!m_testFilesPathEnvValue.empty()) {
92  cerr << m_testFilesPathEnvValue << endl;
93  }
94  cerr << "./testfiles/" << endl << endl;
95  cerr << "Directory used to store working copies:" << endl;
96  if (m_workingDirArg.isPresent()) {
97  if (*m_workingDirArg.values().front()) {
98  (m_workingDir = m_workingDirArg.values().front()) += '/';
99  } else {
100  m_workingDir = "./";
101  }
102  } else if (const char *workingDirEnv = getenv("WORKING_DIR")) {
103  if (const auto len = strlen(workingDirEnv)) {
104  m_workingDir.reserve(len + 1);
105  m_workingDir += workingDirEnv;
106  m_workingDir += '/';
107  }
108  } else {
109  if (m_testFilesPathArg.isPresent()) {
110  m_workingDir = m_testFilesPathArgValue + "workingdir/";
111  } else if (!m_testFilesPathEnvValue.empty()) {
112  m_workingDir = m_testFilesPathEnvValue + "workingdir/";
113  } else {
114  m_workingDir = "./testfiles/workingdir/";
115  }
116  }
117  cerr << m_workingDir << endl << endl;
118 
119  m_valid = true;
120  cerr << "Executing test cases ..." << endl;
121  } catch (const Failure &failure) {
122  cerr << "Invalid arguments specified: " << failure.what() << endl;
123  m_valid = false;
124  }
125 }
126 
131 {
132  m_instance = nullptr;
133 }
134 
138 string TestApplication::testFilePath(const string &name) const
139 {
140  string path;
141  fstream file; // used to check whether the file is present
142 
143  // check the path specified by command line argument
144  if (m_testFilesPathArg.isPresent()) {
145  file.open(path = m_testFilesPathArgValue + name, ios_base::in);
146  if (file.good()) {
147  return path;
148  }
149  }
150 
151  // check the path specified by environment variable
152  if (!m_testFilesPathEnvValue.empty()) {
153  file.clear();
154  file.open(path = m_testFilesPathEnvValue + name, ios_base::in);
155  if (file.good()) {
156  return path;
157  }
158  }
159 
160  // file still not found -> return default path
161  file.clear();
162  file.open(path = "./testfiles/" + 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 &output, string &errors, bool suppressLogging, int timeout) const
241 {
242  return execHelperApp(m_applicationPathArg.firstValue(), args, output, errors, suppressLogging, timeout);
243 }
244 
245 int execHelperApp(const char *appPath, const char *const *args, std::string &output, std::string &errors, bool suppressLogging, int timeout)
246 {
247  // print log message
248  if (!suppressLogging) {
249  cout << '-';
250  for (const char *const *i = args; *i; ++i) {
251  cout << ' ' << *i;
252  }
253  cout << endl;
254  }
255  // determine application path
256  if (!appPath || !*appPath) {
257  throw runtime_error("Unable to execute application to be tested: no application path specified");
258  }
259  // create pipes
260  int coutPipes[2], cerrPipes[2];
261  pipe(coutPipes), pipe(cerrPipes);
262  int readCoutPipe = coutPipes[0], writeCoutPipe = coutPipes[1];
263  int readCerrPipe = cerrPipes[0], writeCerrPipe = cerrPipes[1];
264  // create child process
265  if (int child = fork()) {
266  // parent process: read stdout and stderr from child
267  close(writeCoutPipe), close(writeCerrPipe);
268 
269  try {
270  if (child == -1) {
271  throw runtime_error("Unable to create fork");
272  }
273 
274  // init file descriptor set for poll
275  struct pollfd fileDescriptorSet[2];
276  fileDescriptorSet[0].fd = readCoutPipe;
277  fileDescriptorSet[1].fd = readCerrPipe;
278  fileDescriptorSet[0].events = fileDescriptorSet[1].events = POLLIN;
279 
280  // init variables for reading
281  char buffer[512];
282  ssize_t count;
283  output.clear(), errors.clear();
284 
285  // poll as long as at least one pipe is open
286  do {
287  int retpoll = poll(fileDescriptorSet, 2, timeout);
288  if (retpoll > 0) {
289  // poll succeeds
290  if (fileDescriptorSet[0].revents & POLLIN) {
291  if ((count = read(readCoutPipe, buffer, sizeof(buffer))) > 0) {
292  output.append(buffer, count);
293  }
294  } else if (fileDescriptorSet[0].revents & POLLHUP) {
295  close(readCoutPipe);
296  fileDescriptorSet[0].fd = -1;
297  }
298  if (fileDescriptorSet[1].revents & POLLIN) {
299  if ((count = read(readCerrPipe, buffer, sizeof(buffer))) > 0) {
300  errors.append(buffer, count);
301  }
302  } else if (fileDescriptorSet[1].revents & POLLHUP) {
303  close(readCerrPipe);
304  fileDescriptorSet[1].fd = -1;
305  }
306  } else if (retpoll == 0) {
307  // timeout
308  throw runtime_error("Poll time-out");
309  } else {
310  // fail
311  throw runtime_error("Poll failed");
312  }
313  } while (fileDescriptorSet[0].fd >= 0 || fileDescriptorSet[1].fd >= 0);
314  } catch (...) {
315  // ensure all pipes are close in the error case
316  close(readCoutPipe), close(readCerrPipe);
317  throw;
318  }
319 
320  // get return code
321  int childReturnCode;
322  waitpid(child, &childReturnCode, 0);
323  return childReturnCode;
324  } else {
325  // child process: set pipes to be used for stdout/stderr, execute application
326  dup2(writeCoutPipe, STDOUT_FILENO), dup2(writeCerrPipe, STDERR_FILENO);
327  close(readCoutPipe), close(writeCoutPipe), close(readCerrPipe), close(writeCerrPipe);
328  execv(appPath, const_cast<char *const *>(args));
329  cerr << "Unable to execute \"" << appPath << "\": execv() failed" << endl;
330  exit(-101);
331  }
332 }
333 #endif
334 }
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:138
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:130
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.