Nektar++
Macros | Functions
Tester.cpp.in File Reference

This file contains the main function for the Tester program, which is a tool for testing Nektar++ executables. More...

#include <algorithm>
#include <chrono>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <Metric.h>
#include <TestData.h>
#include <LibUtilities/BasicUtils/Filesystem.hpp>
#include <boost/program_options.hpp>

Go to the source code of this file.

Macros

#define NEKTAR_TEST_FORCEMPIEXEC   1
 

Functions

int main (int argc, char *argv[])
 

Detailed Description

This file contains the main function for the Tester program, which is a tool for testing Nektar++ executables.

The main function reads command line options and parses the provided test (.tst) file. Using information provided in this file, the Tester program generates test metrics, and creates temporary subdirectories in which to run the executable. All test outputs are appended to a single master.out file, and errors are appended to master.err. These files are sent to all of the metrics for analysis. If the test fails, the output and error files are dumped to the terminal for debugging purposes.

See also
Metric
Metric::Test:

Definition in file Tester.cpp.in.

Macro Definition Documentation

◆ NEKTAR_TEST_FORCEMPIEXEC

#define NEKTAR_TEST_FORCEMPIEXEC   1

Definition at line 67 of file Tester.cpp.in.

Function Documentation

◆ main()

int main ( int  argc,
char *  argv[] 
)

Definition at line 91 of file Tester.cpp.in.

92{
93 int status = 0;
94 string command;
95
96 // Set up command line options.
97 po::options_description desc("Available options");
98 desc.add_options()("help,h", "Produce this help message.")(
99 "verbose,v", "Turn on verbosity.")("generate-metric,g",
100 po::value<vector<int>>(),
101 "Generate a single metric.")(
102 "generate-all-metrics,a", "Generate all metrics.")(
103 "executable,e", po::value<string>(), "Use specified executable.");
104
105 po::options_description hidden("Hidden options");
106 hidden.add_options()("input-file", po::value<string>(), "Input filename");
107
108 po::options_description cmdline_options("Command-line options");
109 cmdline_options.add(hidden).add(desc);
110
111 po::options_description visible("Allowed options");
112 visible.add(desc);
113
114 po::positional_options_description p;
115 p.add("input-file", -1);
116
117 po::variables_map vm;
118
119 try
120 {
121 po::store(po::command_line_parser(argc, argv)
122 .options(cmdline_options)
123 .positional(p)
124 .run(),
125 vm);
126 po::notify(vm);
127 }
128 catch (const exception &e)
129 {
130 cerr << e.what() << endl;
131 cerr << desc;
132 return 1;
133 }
134
135 if (vm.count("help") || vm.count("input-file") != 1)
136 {
137 cerr << "Usage: Tester [options] input-file.tst" << endl;
138 cout << desc;
139 return 1;
140 }
141
142 bool verbose = vm.count("verbose");
143
144 // Set up set containing metrics to be generated.
145 vector<int> metricGenVec;
146 if (vm.count("generate-metric"))
147 {
148 metricGenVec = vm["generate-metric"].as<vector<int>>();
149 }
150 set<int> metricGen(metricGenVec.begin(), metricGenVec.end());
151
152 // Path to test definition file
153 const fs::path specFile(vm["input-file"].as<string>());
154
155 // Parent path of test definition file containing dependent files
156 fs::path specPath = specFile.parent_path();
157
158 if (specPath.empty())
159 {
160 specPath = fs::current_path();
161 }
162
163 string specFileStem = specFile.stem().string();
164
165 // Temporary master directory to create which holds master output and error
166 // files, and the working directories for each run
167 const fs::path masterDir =
168 fs::current_path() / LibUtilities::UniquePath(specFileStem);
169
170 // The current directory
171 const fs::path startDir = fs::current_path();
172
173 try
174 {
175 if (verbose)
176 {
177 cerr << "Reading test file definition: " << specFile << endl;
178 }
179
180 // Parse the test file
181 TestData file(specFile, vm);
182
183 if (verbose && file.GetNumMetrics() > 0)
184 {
185 cerr << "Creating metrics:" << endl;
186 }
187
188 // Generate the metric objects
189 vector<MetricSharedPtr> metrics;
190 for (unsigned int i = 0; i < file.GetNumMetrics(); ++i)
191 {
192 set<int>::iterator it = metricGen.find(file.GetMetricId(i));
193 bool genMetric =
194 it != metricGen.end() || (vm.count("generate-all-metrics") > 0);
195
196 metrics.push_back(GetMetricFactory().CreateInstance(
197 file.GetMetricType(i), file.GetMetric(i), genMetric));
198
199 if (verbose)
200 {
201 cerr << " - ID " << metrics.back()->GetID() << ": "
202 << metrics.back()->GetType() << endl;
203 }
204
205 if (it != metricGen.end())
206 {
207 metricGen.erase(it);
208 }
209 }
210
211 if (metricGen.size() != 0)
212 {
213 string s = metricGen.size() == 1 ? "s" : "";
214 set<int>::iterator it;
215 cerr << "Unable to find metric" + s + " with ID" + s + " ";
216 for (it = metricGen.begin(); it != metricGen.end(); ++it)
217 {
218 cerr << *it << " ";
219 }
220 cerr << endl;
221 return 1;
222 }
223
224 // Remove the master directory if left from a previous test
225 if (fs::exists(masterDir))
226 {
227 fs::remove_all(masterDir);
228 }
229
230 if (verbose)
231 {
232 cerr << "Creating master directory: " << masterDir << endl;
233 }
234
235 // Create the master directory
236 fs::create_directory(masterDir);
237
238 // Change working directory to the master directory
239 fs::current_path(masterDir);
240
241 // Create a master output and error file. Output and error files from
242 // all runs will be appended to these files.
243 fstream masterOut("master.out", ios::out | ios::in | ios::trunc);
244 fstream masterErr("master.err", ios::out | ios::in | ios::trunc);
245
246 if (masterOut.bad() || masterErr.bad())
247 {
248 cerr << "One or more master output files are unreadable." << endl;
249 throw 1;
250 }
251
252 // Vector of temporary subdirectories to create and conduct tests in
253 vector<fs::path> tmpWorkingDirs;
254 string line;
255
256 for (unsigned int i = 0; i < file.GetNumRuns(); ++i)
257 {
258 command = "";
259
260 if (verbose)
261 {
262 cerr << "Starting run " << i << "." << endl;
263 }
264
265 // Temporary directory to create and in which to hold the run
266 const fs::path tmpDir =
267 masterDir / fs::path("run" + std::to_string(i));
268 tmpWorkingDirs.push_back(tmpDir);
269
270 if (verbose)
271 {
272 cerr << "Creating working directory: " << tmpDir << endl;
273 }
274
275 // Create temporary directory
276 fs::create_directory(tmpDir);
277
278 // Change working directory to the temporary directory
279 fs::current_path(tmpDir);
280
281 if (verbose && file.GetNumDependentFiles())
282 {
283 cerr << "Copying required files: " << endl;
284 }
285
286 // Copy required files for this test from the test definition
287 // directory to the temporary directory.
288 for (unsigned int j = 0; j < file.GetNumDependentFiles(); ++j)
289 {
290 fs::path source_file(file.GetDependentFile(j).m_filename);
291
292 fs::path source = specPath / source_file;
293 fs::path dest = tmpDir / source_file.filename();
294 if (verbose)
295 {
296 cerr << " - " << source << " -> " << dest << endl;
297 }
298
299 if (fs::is_directory(source))
300 {
301 fs::create_directory(dest);
302 // If source is a directory, then only directory name is
303 // created, so call copy again to copy files.
304 for (const auto &dirEnt :
305 fs::recursive_directory_iterator{source})
306 {
307 fs::path newdest = dest / dirEnt.path().filename();
308 fs::copy_file(dirEnt.path(), newdest);
309 }
310 }
311 else
312 {
313 fs::copy_file(source, dest);
314 }
315 }
316
317 // Copy opt file if exists to to the temporary directory.
318 fs::path source_file("test.opt");
319 fs::path source = specPath / source_file;
320 bool HaveOptFile = false;
321 if (fs::exists(source))
322 {
323 fs::path dest = tmpDir / source_file.filename();
324 if (verbose)
325 {
326 cerr << " - " << source << " -> " << dest << endl;
327 }
328
329 if (fs::is_directory(source))
330 {
331 fs::create_directory(dest);
332 // If source is a directory, then only directory name is
333 // created, so call copy again to copy files.
334 for (const auto &dirEnt :
335 fs::recursive_directory_iterator{source})
336 {
337 fs::path newdest = dest / dirEnt.path().filename();
338 fs::copy_file(dirEnt.path(), newdest);
339 }
340 }
341 else
342 {
343 fs::copy_file(source, dest);
344 }
345
346 HaveOptFile = true;
347 }
348
349 // If we're Python, copy script too.
350
351 // Set PYTHONPATH environment variable in case Python is run inside
352 // any of our tests. For non-Python tests this will do nothing.
353 setenv("PYTHONPATH", "@NEKPY_BASE_DIR@", true);
354
355 // Construct test command to run. Output from stdout and stderr are
356 // directed to the files output.out and output.err, respectively.
357
358 bool mpiAdded = false;
359 for (unsigned int j = 0; j < file.GetNumCommands(); ++j)
360 {
361 Command cmd = file.GetCommand(j);
362
363#ifdef NEKTAR_TEST_FORCEMPIEXEC
364#else
365 if (cmd.m_processes > 1 || (file.GetNumCommands() > 1 &&
366 cmd.m_commandType == eParallel))
367#endif
368 {
369 if (mpiAdded)
370 {
371 continue;
372 }
373
374 command += "\"@MPIEXEC@\" ";
375 if (std::string("@NEKTAR_TEST_USE_HOSTFILE@") == "ON")
376 {
377 command += "-hostfile hostfile ";
378#if (NEKTAR_MPI_TYPE == 1) // MPICH
379 if (system("echo 'localhost:12' > hostfile"))
380#else
381 if (system("echo 'localhost slots=12' > hostfile"))
382#endif
383 {
384 cerr << "Unable to write 'hostfile' in path '"
385 << fs::current_path() << endl;
386 status = 1;
387 }
388 }
389
390 if (file.GetNumCommands() > 1)
391 {
392#if (NEKTAR_MPI_TYPE == 1)
393 // MPICH prepends the rank to each cout, causing the
394 // rank annotation to appear in the middle of the output
395 // and not just at the beginning of lines.
396 // This causes tests not to pass as the tester fails to
397 // parse the output correctly.
398// command += "--prepend-rank ";
399#else
400 command += "--tag-output ";
401#endif
402 }
403
404 mpiAdded = true;
405 }
406 }
407
408 // Parse commands.
409 for (unsigned int j = 0; j < file.GetNumCommands(); ++j)
410 {
411 Command cmd = file.GetCommand(j);
412
413 // If running with multiple commands simultaneously, separate
414 // with colon.
415 if (j > 0 && cmd.m_commandType == eParallel)
416 {
417 command += " : ";
418 }
419 else if (j > 0 && cmd.m_commandType == eSequential)
420 {
421 command += " && ";
422 if (cmd.m_processes > 1)
423 {
424 command += "\"@MPIEXEC@\" ";
425 if (std::string("@NEKTAR_TEST_USE_HOSTFILE@") == "ON")
426 {
427 command += "-hostfile hostfile ";
428 }
429 }
430 }
431
432 // Add -n where appropriate.
433 if (cmd.m_processes > 1 || (file.GetNumCommands() > 1 &&
434 cmd.m_commandType == eParallel))
435 {
436 command += "@MPIEXEC_NUMPROC_FLAG@ ";
437 command += std::to_string(cmd.m_processes) + " ";
438 }
439
440 // Look for executable or Python script.
441 fs::path execPath = startDir / cmd.m_executable;
442 if (!fs::exists(execPath))
443 {
444 ASSERTL0(!cmd.m_pythonTest, "Python script not found.");
445 execPath = cmd.m_executable;
446 }
447
448 // Prepend script name with Python executable path if this is a
449 // Python test.
450 if (cmd.m_pythonTest)
451 {
452 command += "@Python3_EXECUTABLE@ ";
453 }
454
455 std::string pathString = LibUtilities::PortablePath(execPath);
456 command += pathString;
457 if (HaveOptFile)
458 {
459 command += " --use-opt-file test.opt ";
460 }
461
462 command += " ";
463 command += cmd.m_parameters;
464 command += " 1>output.out 2>output.err";
465 }
466
467 status = 0;
468
469 if (verbose)
470 {
471 cerr << "Running command: " << command << endl;
472 }
473
474 // Run executable to perform test.
475 if (system(command.c_str()))
476 {
477 cerr << "Error occurred running test:" << endl;
478 cerr << "Command: " << command << endl;
479 status = 1;
480 }
481
482 // Check output files exist
483 if (!(fs::exists("output.out") && fs::exists("output.err")))
484 {
485 cerr << "One or more test output files are missing." << endl;
486 throw 1;
487 }
488
489 // Open output files and check they are readable
490 ifstream vStdout("output.out");
491 ifstream vStderr("output.err");
492 if (vStdout.bad() || vStderr.bad())
493 {
494 cerr << "One or more test output files are unreadable." << endl;
495 throw 1;
496 }
497
498 // Append output to the master output and error files.
499 if (verbose)
500 {
501 cerr << "Appending run " << i << " output and error to master."
502 << endl;
503 }
504
505 while (getline(vStdout, line))
506 {
507 masterOut << line << endl;
508 }
509
510 while (getline(vStderr, line))
511 {
512 masterErr << line << endl;
513 }
514
515 vStdout.close();
516 vStderr.close();
517 }
518
519 // Warn user if any metrics don't support multiple runs.
520 for (int i = 0; i < metrics.size(); ++i)
521 {
522 if (!metrics[i]->SupportsAverage() && file.GetNumRuns() > 1)
523 {
524 cerr << "WARNING: Metric " << metrics[i]->GetType()
525 << " does not support multiple runs. Test may yield "
526 "unexpected results."
527 << endl;
528 }
529 }
530
531 // Test against all metrics
532 if (status == 0)
533 {
534 if (verbose && metrics.size())
535 {
536 cerr << "Checking metrics:" << endl;
537 }
538
539 for (int i = 0; i < metrics.size(); ++i)
540 {
541 bool gen =
542 metricGen.find(metrics[i]->GetID()) != metricGen.end() ||
543 (vm.count("generate-all-metrics") > 0);
544
545 masterOut.clear();
546 masterErr.clear();
547 masterOut.seekg(0, ios::beg);
548 masterErr.seekg(0, ios::beg);
549
550 if (verbose)
551 {
552 cerr << " - " << (gen ? "generating" : "checking")
553 << " metric " << metrics[i]->GetID() << " ("
554 << metrics[i]->GetType() << ")... ";
555 }
556
557 if (!metrics[i]->Test(masterOut, masterErr))
558 {
559 status = 1;
560 if (verbose)
561 {
562 cerr << "failed!" << endl;
563 }
564 }
565 else if (verbose)
566 {
567 cerr << "passed" << endl;
568 }
569 }
570 }
571
572 if (verbose)
573 {
574 cerr << endl << endl;
575 }
576
577 // Dump output files to terminal for debugging purposes on fail.
578 if (status == 1 || verbose)
579 {
580 masterOut.clear();
581 masterErr.clear();
582 masterOut.seekg(0, ios::beg);
583 masterErr.seekg(0, ios::beg);
584
585 cout << "=== Output ===" << endl;
586 while (masterOut.good())
587 {
588 getline(masterOut, line);
589 cout << line << endl;
590 }
591 cout << "=== Errors ===" << endl;
592 while (masterErr.good())
593 {
594 getline(masterErr, line);
595 cout << line << endl;
596 }
597 }
598
599 // Close output files.
600 masterOut.close();
601 masterErr.close();
602
603 // Change back to the original path and delete temporary directory.
604 fs::current_path(startDir);
605
606 if (verbose)
607 {
608 cerr << "Removing working directory" << endl;
609 }
610
611 // Repeatedly try deleting directory with sleep for filesystems which
612 // work asynchronously. This allows time for the filesystem to register
613 // the output files are closed so they can be deleted and not cause a
614 // filesystem failure. Attempts made for 1 second.
615 int i = 1000;
616 while (i > 0)
617 {
618 try
619 {
620 // If delete successful, stop trying.
621 fs::remove_all(masterDir);
622 break;
623 }
624 catch (const fs::filesystem_error &e)
625 {
626 using namespace std::chrono_literals;
627 std::this_thread::sleep_for(1ms);
628 i--;
629 if (i > 0)
630 {
631 cout << "Locked files encountered. "
632 << "Retrying after 1ms..." << endl;
633 }
634 else
635 {
636 // If still failing after 1sec, we consider it a permanent
637 // filesystem error and abort.
638 throw e;
639 }
640 }
641 }
642
643 // Save any changes.
644 if (vm.count("generate-metric") > 0 ||
645 vm.count("generate-all-metrics") > 0)
646 {
647 file.SaveFile();
648 }
649
650 // Return status of test. 0 = PASS, 1 = FAIL
651 return status;
652 }
653 catch (const fs::filesystem_error &e)
654 {
655 cerr << "Filesystem operation error occurred:" << endl;
656 cerr << " " << e.what() << endl;
657 cerr << " Files left in " << masterDir.string() << endl;
658 }
659 catch (const TesterException &e)
660 {
661 cerr << "Error occurred during test:" << endl;
662 cerr << " " << e.what() << endl;
663 cerr << " Files left in " << masterDir.string() << endl;
664 }
665 catch (const std::exception &e)
666 {
667 cerr << "Unhandled exception during test:" << endl;
668 cerr << " " << e.what() << endl;
669 cerr << " Files left in " << masterDir.string() << endl;
670 }
671 catch (...)
672 {
673 cerr << "Unknown error during test" << endl;
674 cerr << " Files left in " << masterDir.string() << endl;
675 }
676
677 // If a system error, return 2
678 return 2;
679}
#define ASSERTL0(condition, msg)
Definition: ErrorUtil.hpp:208
MetricSharedPtr CreateInstance(std::string key, TiXmlElement *elmt, bool generate)
Definition: Metric.h:139
The TestData class is responsible for parsing a test XML file and storing the data.
Definition: TestData.h:79
static std::string PortablePath(const fs::path &path)
create portable path on different platforms for std::filesystem path.
Definition: Filesystem.hpp:66
static fs::path UniquePath(std::string specFileStem)
Create a unique (random) path, based on an input stem string. The returned string is a filename or di...
Definition: Filesystem.hpp:79
MetricFactory & GetMetricFactory()
Definition: Metric.cpp:42
@ eSequential
Definition: TestData.h:60
@ eParallel
Definition: TestData.h:61
bool m_pythonTest
Definition: TestData.h:69
fs::path m_executable
Definition: TestData.h:66
std::string m_parameters
Definition: TestData.h:67
CommandType m_commandType
Definition: TestData.h:70
unsigned int m_processes
Definition: TestData.h:68
Subclass of std::runtime_error to handle exceptions raised by Tester.

References ASSERTL0, Nektar::MetricFactory::CreateInstance(), Nektar::eParallel, Nektar::eSequential, Nektar::TestData::GetCommand(), Nektar::TestData::GetDependentFile(), Nektar::TestData::GetMetric(), Nektar::GetMetricFactory(), Nektar::TestData::GetMetricId(), Nektar::TestData::GetMetricType(), Nektar::TestData::GetNumCommands(), Nektar::TestData::GetNumDependentFiles(), Nektar::TestData::GetNumMetrics(), Nektar::TestData::GetNumRuns(), Nektar::Command::m_commandType, Nektar::Command::m_executable, Nektar::DependentFile::m_filename, Nektar::Command::m_parameters, Nektar::Command::m_processes, Nektar::Command::m_pythonTest, CellMLToNektar.cellml_metadata::p, Nektar::LibUtilities::PortablePath(), CellMLToNektar.translators::run(), Nektar::TestData::SaveFile(), and Nektar::LibUtilities::UniquePath().