Nektar++
Loading...
Searching...
No Matches
Tester.cpp.in
Go to the documentation of this file.
1///////////////////////////////////////////////////////////////////////////////
2//
3// File: Tester.cpp
4//
5// For more information, please see: http://www.nektar.info
6//
7// The MIT License
8//
9// Copyright (c) 2006 Division of Applied Mathematics, Brown University (USA),
10// Department of Aeronautics, Imperial College London (UK), and Scientific
11// Computing and Imaging Institute, University of Utah (USA).
12//
13// Permission is hereby granted, free of charge, to any person obtaining a
14// copy of this software and associated documentation files (the "Software"),
15// to deal in the Software without restriction, including without limitation
16// the rights to use, copy, modify, merge, publish, distribute, sublicense,
17// and/or sell copies of the Software, and to permit persons to whom the
18// Software is furnished to do so, subject to the following conditions:
19//
20// The above copyright notice and this permission notice shall be included
21// in all copies or substantial portions of the Software.
22//
23// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
24// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29// DEALINGS IN THE SOFTWARE.
30//
31// Description: Tester executable.
32//
33///////////////////////////////////////////////////////////////////////////////
34
35/**
36 * @file Tester.cpp.in
37 * @brief This file contains the main function for the Tester program, which is
38 * a tool for testing Nektar++ executables.
39 *
40 * The main function reads command line options and parses the provided test
41 * (.tst) file. Using information provided in this file, the Tester program
42 * generates test metrics, and creates temporary subdirectories in which to run
43 * the executable. All test outputs are appended to a single @p master.out file,
44 * and errors are appended to @p master.err. These files are sent to all of the
45 * metrics for analysis. If the test fails, the output and error files are
46 * dumped to the terminal for debugging purposes.
47 *
48 * @see Metric
49 * @see Metric#Test:
50 */
51
52#include <algorithm>
53#include <chrono>
54#include <fstream>
55#include <iostream>
56#include <string>
57#include <thread>
58#include <vector>
59
60#include <Metric.h>
61#include <TestFile.h>
62
64
65#include <boost/program_options.hpp>
66
67#cmakedefine NEKTAR_TEST_FORCEMPIEXEC 1
68
69using namespace std;
70using namespace Nektar;
71
72// Define some namespace aliases
73namespace po = boost::program_options;
74
75#ifdef _WIN32
76// Define a setenv function for Windows
77int setenv(const char *name, const char *value, int overwrite)
78{
79 int errcode = 0;
80 if (!overwrite)
81 {
82 size_t envsize = 0;
83 errcode = getenv_s(&envsize, NULL, 0, name);
84 if (errcode || envsize)
85 {
86 return errcode;
87 }
88 }
89 return _putenv_s(name, value);
90}
91#endif
92
93int RunTest(TestData *test, bool verbose, po::variables_map &vm,
94 set<int> &metricGen, const fs::path &specPath,
95 const fs::path &masterDir, const fs::path &startDir)
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}
563
564int main(int argc, char *argv[])
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}
#define ASSERTL0(condition, msg)
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
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
The TestData class is responsible for parsing a test XML file and storing the data.
Definition TestFile.h:59
std::vector< TestData * > GetTests()
Definition TestFile.h:68
static std::string PortablePath(const fs::path &path)
create portable path on different platforms for std::filesystem path.
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...
MetricFactory & GetMetricFactory()
Definition Metric.cpp:42
@ eSequential
Definition TestData.h:60
@ eParallel
Definition TestData.h:61
Definition main.py:1
STL namespace.
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.