Nektar++
Loading...
Searching...
No Matches
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 <TestFile.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 RunTest (TestData *test, bool verbose, po::variables_map &vm, set< int > &metricGen, const fs::path &specPath, const fs::path &masterDir, const fs::path &startDir)
 
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 564 of file Tester.cpp.in.

565{
566 int status = 0;
567
568 // Set up command line options.
569 po::options_description desc("Available options");
570 desc.add_options()("help,h", "Produce this help message.")(
571 "verbose,v", "Turn on verbosity.")("generate-metric,g",
572 po::value<vector<int>>(),
573 "Generate a single metric.")(
574 "generate-all-metrics,a", "Generate all metrics.")(
575 "executable,e", po::value<string>(), "Use specified executable.");
576
577 po::options_description hidden("Hidden options");
578 hidden.add_options()("input-file", po::value<string>(), "Input filename");
579
580 po::options_description cmdline_options("Command-line options");
581 cmdline_options.add(hidden).add(desc);
582
583 po::options_description visible("Allowed options");
584 visible.add(desc);
585
586 po::positional_options_description p;
587 p.add("input-file", -1);
588
589 po::variables_map vm;
590
591 try
592 {
593 po::store(po::command_line_parser(argc, argv)
594 .options(cmdline_options)
595 .positional(p)
596 .run(),
597 vm);
598 po::notify(vm);
599 }
600 catch (const exception &e)
601 {
602 cerr << e.what() << endl;
603 cerr << desc;
604 return 1;
605 }
606
607 if (vm.count("help") || vm.count("input-file") != 1)
608 {
609 cerr << "Usage: Tester [options] input-file.tst" << endl;
610 cout << desc;
611 return 1;
612 }
613
614 bool verbose = vm.count("verbose");
615
616 // Set up set containing metrics to be generated.
617 vector<int> metricGenVec;
618 if (vm.count("generate-metric"))
619 {
620 metricGenVec = vm["generate-metric"].as<vector<int>>();
621 }
622 set<int> metricGen(metricGenVec.begin(), metricGenVec.end());
623
624 // Path to test definition file
625 const fs::path specFile(vm["input-file"].as<string>());
626
627 // Parent path of test definition file containing dependent files
628 fs::path specPath = specFile.parent_path();
629
630 if (specPath.empty())
631 {
632 specPath = fs::current_path();
633 }
634
635 string specFileStem = specFile.stem().string();
636
637 // Temporary master directory to create which holds master output and error
638 // files, and the working directories for each run
639 const fs::path masterDir =
640 fs::current_path() / LibUtilities::UniquePath(specFileStem);
641
642 // The current directory
643 const fs::path startDir = fs::current_path();
644
645 try
646 {
647 if (verbose)
648 {
649 cerr << "Reading test file definition: " << specFile << endl;
650 }
651
652 // Parse the test file
653 TestFile testFile(specFile, vm);
654
655 std::vector<TestData *> tests = testFile.GetTests();
656
657 for (int i = 0; i < tests.size(); ++i)
658 {
659 TestData *test = tests[i];
660
661 if (verbose)
662 {
663 std::cerr << "Running test # " << i << ":"
664 << test->GetDescription() << std::endl
665 << std::endl;
666 }
667
668 status = RunTest(test, verbose, vm, metricGen, specPath, masterDir,
669 startDir);
670
671 if (status != 0)
672 {
673 return status;
674 }
675 }
676
677 // Save any changes.
678 if (vm.count("generate-metric") > 0 ||
679 vm.count("generate-all-metrics") > 0)
680 {
681 testFile.SaveFile();
682 }
683
684 return status;
685 }
686 catch (const fs::filesystem_error &e)
687 {
688 cerr << "Filesystem operation error occurred:" << endl;
689 cerr << " " << e.what() << endl;
690 cerr << " Files left in " << masterDir.string() << endl;
691 }
692 catch (const TesterException &e)
693 {
694 cerr << "Error occurred during test:" << endl;
695 cerr << " " << e.what() << endl;
696 cerr << " Files left in " << masterDir.string() << endl;
697 }
698 catch (const std::exception &e)
699 {
700 cerr << "Unhandled exception during test:" << endl;
701 cerr << " " << e.what() << endl;
702 cerr << " Files left in " << masterDir.string() << endl;
703 }
704 catch (...)
705 {
706 cerr << "Unknown error during test" << endl;
707 cerr << " Files left in " << masterDir.string() << endl;
708 }
709
710 // If a system error, return 2
711 return 2;
712}
int RunTest(TestData *test, bool verbose, po::variables_map &vm, set< int > &metricGen, const fs::path &specPath, const fs::path &masterDir, const fs::path &startDir)
Definition Tester.cpp.in:93
The TestData class is responsible for parsing a test XML file and storing the data.
Definition TestData.h:79
The TestData class is responsible for parsing a test XML file and storing the data.
Definition TestFile.h:59
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...
std::vector< double > p(NPUPPER)
Subclass of std::runtime_error to handle exceptions raised by Tester.

References Nektar::TestFile::GetTests(), RunTest(), Nektar::TestFile::SaveFile(), and Nektar::LibUtilities::UniquePath().

◆ RunTest()

int RunTest ( TestData test,
bool  verbose,
po::variables_map &  vm,
set< int > &  metricGen,
const fs::path &  specPath,
const fs::path &  masterDir,
const fs::path &  startDir 
)

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

96{
97 int status = 0;
98 string command;
99
100 if (verbose && test->GetNumMetrics() > 0)
101 {
102 cerr << "Creating metrics:" << endl;
103 }
104
105 // Generate the metric objects
106 vector<MetricSharedPtr> metrics;
107 for (unsigned int i = 0; i < test->GetNumMetrics(); ++i)
108 {
109 set<int>::iterator it = metricGen.find(test->GetMetricId(i));
110 bool genMetric =
111 it != metricGen.end() || (vm.count("generate-all-metrics") > 0);
112
113 metrics.push_back(GetMetricFactory().CreateInstance(
114 test->GetMetricType(i), test->GetMetric(i), genMetric));
115
116 if (verbose)
117 {
118 cerr << " - ID " << metrics.back()->GetID() << ": "
119 << metrics.back()->GetType() << endl;
120 }
121
122 if (it != metricGen.end())
123 {
124 metricGen.erase(it);
125 }
126 }
127
128 if (metricGen.size() != 0)
129 {
130 string s = metricGen.size() == 1 ? "s" : "";
131 set<int>::iterator it;
132 cerr << "Unable to find metric" + s + " with ID" + s + " ";
133 for (it = metricGen.begin(); it != metricGen.end(); ++it)
134 {
135 cerr << *it << " ";
136 }
137 cerr << endl;
138 return 1;
139 }
140
141 // Remove the master directory if left from a previous test
142 if (fs::exists(masterDir))
143 {
144 fs::remove_all(masterDir);
145 }
146
147 if (verbose)
148 {
149 cerr << "Creating master directory: " << masterDir << endl;
150 }
151
152 // Create the master directory
153 fs::create_directory(masterDir);
154
155 // Change working directory to the master directory
156 fs::current_path(masterDir);
157
158 // Create a master output and error test-> Output and error files from
159 // all runs will be appended to these files.
160 fstream masterOut("master.out", ios::out | ios::in | ios::trunc);
161 fstream masterErr("master.err", ios::out | ios::in | ios::trunc);
162
163 if (masterOut.bad() || masterErr.bad())
164 {
165 cerr << "One or more master output files are unreadable." << endl;
166 throw 1;
167 }
168
169 // Vector of temporary subdirectories to create and conduct tests in
170 vector<fs::path> tmpWorkingDirs;
171 string line;
172
173 for (unsigned int i = 0; i < test->GetNumRuns(); ++i)
174 {
175 command = "";
176
177 if (verbose)
178 {
179 cerr << "Starting run " << i << "." << endl;
180 }
181
182 // Temporary directory to create and in which to hold the run
183 const fs::path tmpDir = masterDir / fs::path("run" + std::to_string(i));
184 tmpWorkingDirs.push_back(tmpDir);
185
186 if (verbose)
187 {
188 cerr << "Creating working directory: " << tmpDir << endl;
189 }
190
191 // Create temporary directory
192 fs::create_directory(tmpDir);
193
194 // Change working directory to the temporary directory
195 fs::current_path(tmpDir);
196
197 if (verbose && test->GetNumDependentFiles())
198 {
199 cerr << "Copying required files: " << endl;
200 }
201
202 // Copy required files for this test from the test definition
203 // directory to the temporary directory.
204 for (unsigned int j = 0; j < test->GetNumDependentFiles(); ++j)
205 {
206 fs::path source_file(test->GetDependentFile(j).m_filename);
207
208 fs::path source = specPath / source_file;
209 fs::path dest = tmpDir / source_file.filename();
210 if (verbose)
211 {
212 cerr << " - " << source << " -> " << dest << endl;
213 }
214
215 if (fs::is_directory(source))
216 {
217 fs::create_directory(dest);
218 // If source is a directory, then only directory name is
219 // created, so call copy again to copy files.
220 for (const auto &dirEnt :
221 fs::recursive_directory_iterator{source})
222 {
223 fs::path newdest = dest / dirEnt.path().filename();
224 fs::copy_file(dirEnt.path(), newdest);
225 }
226 }
227 else
228 {
229 fs::copy_file(source, dest);
230 }
231 }
232
233 // Copy opt file if exists to to the temporary directory.
234 fs::path source_file("test.opt");
235 fs::path source = specPath / source_file;
236 bool HaveOptFile = false;
237 if (fs::exists(source))
238 {
239 fs::path dest = tmpDir / source_file.filename();
240 if (verbose)
241 {
242 cerr << " - " << source << " -> " << dest << endl;
243 }
244
245 if (fs::is_directory(source))
246 {
247 fs::create_directory(dest);
248 // If source is a directory, then only directory name is
249 // created, so call copy again to copy files.
250 for (const auto &dirEnt :
251 fs::recursive_directory_iterator{source})
252 {
253 fs::path newdest = dest / dirEnt.path().filename();
254 fs::copy_file(dirEnt.path(), newdest);
255 }
256 }
257 else
258 {
259 fs::copy_file(source, dest);
260 }
261
262 HaveOptFile = true;
263 }
264
265 // If we're Python, copy script too.
266
267 // Set PYTHONPATH environment variable in case Python is run inside
268 // any of our tests. For non-Python tests this will do nothing.
269 setenv("PYTHONPATH", "@NEKPY_BASE_DIR@", true);
270
271 // Construct test command to run. Output from stdout and stderr are
272 // directed to the files output.out and output.err, respectively.
273
274 bool mpiAdded = false;
275 for (unsigned int j = 0; j < test->GetNumCommands(); ++j)
276 {
277 Command cmd = test->GetCommand(j);
278
279#ifdef NEKTAR_TEST_FORCEMPIEXEC
280#else
281 if (cmd.m_processes > 1 ||
282 (test->GetNumCommands() > 1 && cmd.m_commandType == eParallel))
283#endif
284 {
285 if (mpiAdded)
286 {
287 continue;
288 }
289
290 command += "\"@MPIEXEC@\" ";
291 if (std::string("@NEKTAR_TEST_USE_HOSTFILE@") == "ON")
292 {
293 command += "-hostfile hostfile ";
294#if (NEKTAR_MPI_TYPE == 1) // MPICH
295 if (system("echo 'localhost:12' > hostfile"))
296#else
297 if (system("echo 'localhost slots=12' > hostfile"))
298#endif
299 {
300 cerr << "Unable to write 'hostfile' in path '"
301 << fs::current_path() << endl;
302 status = 1;
303 }
304 }
305
306 if (test->GetNumCommands() > 1)
307 {
308#if (NEKTAR_MPI_TYPE == 1)
309 // MPICH prepends the rank to each cout, causing the
310 // rank annotation to appear in the middle of the output
311 // and not just at the beginning of lines.
312 // This causes tests not to pass as the tester fails to
313 // parse the output correctly.
314 // command += "--prepend-rank ";
315#else
316 command += "--tag-output ";
317#endif
318 }
319
320 mpiAdded = true;
321 }
322 }
323
324 // Parse commands.
325 for (unsigned int j = 0; j < test->GetNumCommands(); ++j)
326 {
327 Command cmd = test->GetCommand(j);
328
329 // If running with multiple commands simultaneously, separate
330 // with colon.
331 if (j > 0 && cmd.m_commandType == eParallel)
332 {
333 command += " : ";
334 }
335 else if (j > 0 && cmd.m_commandType == eSequential)
336 {
337 command += " && ";
338 if (cmd.m_processes > 1)
339 {
340 command += "\"@MPIEXEC@\" ";
341 if (std::string("@NEKTAR_TEST_USE_HOSTFILE@") == "ON")
342 {
343 command += "-hostfile hostfile ";
344 }
345 }
346 }
347
348 // Add -n where appropriate.
349 if (cmd.m_processes > 1 ||
350 (test->GetNumCommands() > 1 && cmd.m_commandType == eParallel))
351 {
352 command += "@MPIEXEC_NUMPROC_FLAG@ ";
353 command += std::to_string(cmd.m_processes) + " ";
354 }
355
356 // Look for executable or Python script.
357 fs::path execPath = startDir / cmd.m_executable;
358 if (!fs::exists(execPath))
359 {
360 ASSERTL0(!cmd.m_pythonTest, "Python script not found.");
361 execPath = cmd.m_executable;
362 }
363
364 // Prepend script name with Python executable path if this is a
365 // Python test.
366 if (cmd.m_pythonTest)
367 {
368 command += "@Python3_EXECUTABLE@ ";
369 }
370
371 std::string pathString = LibUtilities::PortablePath(execPath);
372 command += pathString;
373 if (HaveOptFile && cmd.m_executable.filename().string().find(
374 "FieldConvert") == std::string::npos)
375 {
376 command += " --use-opt-file test.opt ";
377 }
378
379 command += " ";
380 command += cmd.m_parameters;
381 command += " 1>output.out 2>output.err";
382 }
383
384 status = 0;
385
386 if (verbose)
387 {
388 cerr << "Running command: " << command << endl;
389 }
390
391 // Run executable to perform test.
392 if (system(command.c_str()))
393 {
394 cerr << "Error occurred running test:" << endl;
395 cerr << "Command: " << command << endl;
396 status = 1;
397 }
398
399 // Check output files exist
400 if (!(fs::exists("output.out") && fs::exists("output.err")))
401 {
402 cerr << "One or more test output files are missing." << endl;
403 throw 1;
404 }
405
406 // Open output files and check they are readable
407 ifstream vStdout("output.out");
408 ifstream vStderr("output.err");
409 if (vStdout.bad() || vStderr.bad())
410 {
411 cerr << "One or more test output files are unreadable." << endl;
412 throw 1;
413 }
414
415 // Append output to the master output and error files.
416 if (verbose)
417 {
418 cerr << "Appending run " << i << " output and error to master."
419 << endl;
420 }
421
422 while (getline(vStdout, line))
423 {
424 masterOut << line << endl;
425 }
426
427 while (getline(vStderr, line))
428 {
429 masterErr << line << endl;
430 }
431
432 vStdout.close();
433 vStderr.close();
434 }
435
436 // Warn user if any metrics don't support multiple runs.
437 for (int i = 0; i < metrics.size(); ++i)
438 {
439 if (!metrics[i]->SupportsAverage() && test->GetNumRuns() > 1)
440 {
441 cerr << "WARNING: Metric " << metrics[i]->GetType()
442 << " does not support multiple runs. Test may yield "
443 "unexpected results."
444 << endl;
445 }
446 }
447
448 // Test against all metrics
449 if (status == 0)
450 {
451 if (verbose && metrics.size())
452 {
453 cerr << "Checking metrics:" << endl;
454 }
455
456 for (int i = 0; i < metrics.size(); ++i)
457 {
458 bool gen = metricGen.find(metrics[i]->GetID()) != metricGen.end() ||
459 (vm.count("generate-all-metrics") > 0);
460
461 masterOut.clear();
462 masterErr.clear();
463 masterOut.seekg(0, ios::beg);
464 masterErr.seekg(0, ios::beg);
465
466 if (verbose)
467 {
468 cerr << " - " << (gen ? "generating" : "checking")
469 << " metric " << metrics[i]->GetID() << " ("
470 << metrics[i]->GetType() << ")... ";
471 }
472
473 if (!metrics[i]->Test(masterOut, masterErr))
474 {
475 status = 1;
476 if (verbose)
477 {
478 cerr << "failed!" << endl;
479 }
480 }
481 else if (verbose)
482 {
483 cerr << "passed" << endl;
484 }
485 }
486 }
487
488 if (verbose)
489 {
490 cerr << endl << endl;
491 }
492
493 // Dump output files to terminal for debugging purposes on fail.
494 if (status == 1 || verbose)
495 {
496 masterOut.clear();
497 masterErr.clear();
498 masterOut.seekg(0, ios::beg);
499 masterErr.seekg(0, ios::beg);
500
501 cout << "Output from test: " << test->GetDescription() << endl << endl;
502
503 cout << "=== Output ===" << endl;
504 while (masterOut.good())
505 {
506 getline(masterOut, line);
507 cout << line << endl;
508 }
509 cout << "=== Errors ===" << endl;
510 while (masterErr.good())
511 {
512 getline(masterErr, line);
513 cout << line << endl;
514 }
515 }
516
517 // Close output files.
518 masterOut.close();
519 masterErr.close();
520
521 // Change back to the original path and delete temporary directory.
522 fs::current_path(startDir);
523
524 if (verbose)
525 {
526 cerr << "Removing working directory" << endl;
527 }
528
529 // Repeatedly try deleting directory with sleep for filesystems which
530 // work asynchronously. This allows time for the filesystem to register
531 // the output files are closed so they can be deleted and not cause a
532 // filesystem failure. Attempts made for 1 second.
533 int i = 1000;
534 while (i > 0)
535 {
536 try
537 {
538 // If delete successful, stop trying.
539 fs::remove_all(masterDir);
540 break;
541 }
542 catch (const fs::filesystem_error &e)
543 {
544 using namespace std::chrono_literals;
545 std::this_thread::sleep_for(1ms);
546 i--;
547 if (i > 0)
548 {
549 cout << "Locked files encountered. "
550 << "Retrying after 1ms..." << endl;
551 }
552 else
553 {
554 // If still failing after 1sec, we consider it a permanent
555 // filesystem error and abort.
556 throw e;
557 }
558 }
559 }
560
561 return status;
562}
#define ASSERTL0(condition, msg)
MetricSharedPtr CreateInstance(std::string key, TiXmlElement *elmt, bool generate)
Definition Metric.h:139
static std::string PortablePath(const fs::path &path)
create portable path on different platforms for std::filesystem path.
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

References ASSERTL0, Nektar::MetricFactory::CreateInstance(), Nektar::eParallel, Nektar::eSequential, Nektar::GetMetricFactory(), Nektar::Command::m_commandType, Nektar::Command::m_executable, Nektar::Command::m_parameters, Nektar::Command::m_processes, Nektar::Command::m_pythonTest, and Nektar::LibUtilities::PortablePath().

Referenced by main().