atf-run.cpp revision 1.2.4.1
1// 2// Automated Testing Framework (atf) 3// 4// Copyright (c) 2007 The NetBSD Foundation, Inc. 5// All rights reserved. 6// 7// Redistribution and use in source and binary forms, with or without 8// modification, are permitted provided that the following conditions 9// are met: 10// 1. Redistributions of source code must retain the above copyright 11// notice, this list of conditions and the following disclaimer. 12// 2. Redistributions in binary form must reproduce the above copyright 13// notice, this list of conditions and the following disclaimer in the 14// documentation and/or other materials provided with the distribution. 15// 16// THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND 17// CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 18// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20// IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY 21// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 23// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 26// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 27// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28// 29 30extern "C" { 31#include <sys/types.h> 32#include <sys/param.h> 33#include <sys/stat.h> 34#include <sys/wait.h> 35#include <unistd.h> 36} 37 38#include <algorithm> 39#include <cassert> 40#include <cerrno> 41#include <cstdlib> 42#include <cstring> 43#include <fstream> 44#include <iostream> 45#include <map> 46#include <string> 47 48#include "application.hpp" 49#include "atffile.hpp" 50#include "config.hpp" 51#include "config_file.hpp" 52#include "env.hpp" 53#include "exceptions.hpp" 54#include "fs.hpp" 55#include "parser.hpp" 56#include "process.hpp" 57#include "requirements.hpp" 58#include "test-program.hpp" 59#include "text.hpp" 60 61namespace { 62 63typedef std::map< std::string, std::string > vars_map; 64 65} // anonymous namespace 66 67class atf_run : public tools::application::app { 68 static const char* m_description; 69 70 vars_map m_cmdline_vars; 71 72 static vars_map::value_type parse_var(const std::string&); 73 74 void process_option(int, const char*); 75 std::string specific_args(void) const; 76 options_set specific_options(void) const; 77 78 void parse_vflag(const std::string&); 79 80 std::vector< std::string > conf_args(void) const; 81 82 size_t count_tps(std::vector< std::string >) const; 83 84 int run_test(const tools::fs::path&, tools::test_program::atf_tps_writer&, 85 const vars_map&); 86 int run_test_directory(const tools::fs::path&, 87 tools::test_program::atf_tps_writer&); 88 int run_test_program(const tools::fs::path&, 89 tools::test_program::atf_tps_writer&, 90 const vars_map&); 91 92 tools::test_program::test_case_result get_test_case_result( 93 const std::string&, const tools::process::status&, 94 const tools::fs::path&) const; 95 96public: 97 atf_run(void); 98 99 int main(void); 100}; 101 102static void 103sanitize_gdb_env(void) 104{ 105 try { 106 tools::env::unset("TERM"); 107 } catch (...) { 108 // Just swallow exceptions here; they cannot propagate into C, which 109 // is where this function is called from, and even if these exceptions 110 // appear they are benign. 111 } 112} 113 114static void 115dump_stacktrace(const tools::fs::path& tp, const tools::process::status& s, 116 const tools::fs::path& workdir, 117 tools::test_program::atf_tps_writer& w) 118{ 119 assert(s.signaled() && s.coredump()); 120 121 w.stderr_tc("Test program crashed; attempting to get stack trace"); 122 123 const tools::fs::path corename = workdir / 124 (tp.leaf_name().substr(0, MAXCOMLEN) + ".core"); 125 if (!tools::fs::exists(corename)) { 126 w.stderr_tc("Expected file " + corename.str() + " not found"); 127 return; 128 } 129 130 const tools::fs::path gdb(GDB); 131 const tools::fs::path gdbout = workdir / "gdb.out"; 132 const tools::process::argv_array args(gdb.leaf_name().c_str(), "-batch", 133 "-q", "-ex", "bt", tp.c_str(), 134 corename.c_str(), NULL); 135 tools::process::status status = tools::process::exec( 136 gdb, args, 137 tools::process::stream_redirect_path(gdbout), 138 tools::process::stream_redirect_path(tools::fs::path("/dev/null")), 139 sanitize_gdb_env); 140 if (!status.exited() || status.exitstatus() != EXIT_SUCCESS) { 141 w.stderr_tc("Execution of " GDB " failed"); 142 return; 143 } 144 145 std::ifstream input(gdbout.c_str()); 146 if (input) { 147 std::string line; 148 while (std::getline(input, line).good()) 149 w.stderr_tc(line); 150 input.close(); 151 } 152 153 w.stderr_tc("Stack trace complete"); 154} 155 156const char* atf_run::m_description = 157 "atf-run is a tool that runs tests programs and collects their " 158 "results."; 159 160atf_run::atf_run(void) : 161 app(m_description, "atf-run(1)", "atf(7)") 162{ 163} 164 165void 166atf_run::process_option(int ch, const char* arg) 167{ 168 switch (ch) { 169 case 'v': 170 parse_vflag(arg); 171 break; 172 173 default: 174 std::abort(); 175 } 176} 177 178std::string 179atf_run::specific_args(void) 180 const 181{ 182 return "[test-program1 .. test-programN]"; 183} 184 185atf_run::options_set 186atf_run::specific_options(void) 187 const 188{ 189 using tools::application::option; 190 options_set opts; 191 opts.insert(option('v', "var=value", "Sets the configuration variable " 192 "`var' to `value'; overrides " 193 "values in configuration files")); 194 return opts; 195} 196 197void 198atf_run::parse_vflag(const std::string& str) 199{ 200 if (str.empty()) 201 throw std::runtime_error("-v requires a non-empty argument"); 202 203 std::vector< std::string > ws = tools::text::split(str, "="); 204 if (ws.size() == 1 && str[str.length() - 1] == '=') { 205 m_cmdline_vars[ws[0]] = ""; 206 } else { 207 if (ws.size() != 2) 208 throw std::runtime_error("-v requires an argument of the form " 209 "var=value"); 210 211 m_cmdline_vars[ws[0]] = ws[1]; 212 } 213} 214 215int 216atf_run::run_test(const tools::fs::path& tp, 217 tools::test_program::atf_tps_writer& w, 218 const vars_map& config) 219{ 220 tools::fs::file_info fi(tp); 221 222 int errcode; 223 if (fi.get_type() == tools::fs::file_info::dir_type) 224 errcode = run_test_directory(tp, w); 225 else { 226 const vars_map effective_config = 227 tools::config_file::merge_configs(config, m_cmdline_vars); 228 229 errcode = run_test_program(tp, w, effective_config); 230 } 231 return errcode; 232} 233 234int 235atf_run::run_test_directory(const tools::fs::path& tp, 236 tools::test_program::atf_tps_writer& w) 237{ 238 tools::atffile af = tools::read_atffile(tp / "Atffile"); 239 240 vars_map test_suite_vars; 241 { 242 vars_map::const_iterator iter = af.props().find("test-suite"); 243 assert(iter != af.props().end()); 244 test_suite_vars = tools::config_file::read_config_files((*iter).second); 245 } 246 247 bool ok = true; 248 for (std::vector< std::string >::const_iterator iter = af.tps().begin(); 249 iter != af.tps().end(); iter++) { 250 const bool result = run_test(tp / *iter, w, 251 tools::config_file::merge_configs(af.conf(), test_suite_vars)); 252 ok &= (result == EXIT_SUCCESS); 253 } 254 255 return ok ? EXIT_SUCCESS : EXIT_FAILURE; 256} 257 258tools::test_program::test_case_result 259atf_run::get_test_case_result(const std::string& broken_reason, 260 const tools::process::status& s, 261 const tools::fs::path& resfile) 262 const 263{ 264 using tools::text::to_string; 265 using tools::test_program::read_test_case_result; 266 using tools::test_program::test_case_result; 267 268 if (!broken_reason.empty()) { 269 test_case_result tcr; 270 271 try { 272 tcr = read_test_case_result(resfile); 273 274 if (tcr.state() == "expected_timeout") { 275 return tcr; 276 } else { 277 return test_case_result("failed", -1, broken_reason); 278 } 279 } catch (const std::runtime_error&) { 280 return test_case_result("failed", -1, broken_reason); 281 } 282 } 283 284 if (s.exited()) { 285 test_case_result tcr; 286 287 try { 288 tcr = read_test_case_result(resfile); 289 } catch (const std::runtime_error& e) { 290 return test_case_result("failed", -1, "Test case exited " 291 "normally but failed to create the results file: " + 292 std::string(e.what())); 293 } 294 295 if (tcr.state() == "expected_death") { 296 return tcr; 297 } else if (tcr.state() == "expected_exit") { 298 if (tcr.value() == -1 || s.exitstatus() == tcr.value()) 299 return tcr; 300 else 301 return test_case_result("failed", -1, "Test case was " 302 "expected to exit with a " + to_string(tcr.value()) + 303 " error code but returned " + to_string(s.exitstatus())); 304 } else if (tcr.state() == "expected_failure") { 305 if (s.exitstatus() == EXIT_SUCCESS) 306 return tcr; 307 else 308 return test_case_result("failed", -1, "Test case returned an " 309 "error in expected_failure mode but it should not have"); 310 } else if (tcr.state() == "expected_signal") { 311 return test_case_result("failed", -1, "Test case exited cleanly " 312 "but was expected to receive a signal"); 313 } else if (tcr.state() == "failed") { 314 if (s.exitstatus() == EXIT_SUCCESS) 315 return test_case_result("failed", -1, "Test case " 316 "exited successfully but reported failure"); 317 else 318 return tcr; 319 } else if (tcr.state() == "passed") { 320 if (s.exitstatus() == EXIT_SUCCESS) 321 return tcr; 322 else 323 return test_case_result("failed", -1, "Test case exited as " 324 "passed but reported an error"); 325 } else if (tcr.state() == "skipped") { 326 if (s.exitstatus() == EXIT_SUCCESS) 327 return tcr; 328 else 329 return test_case_result("failed", -1, "Test case exited as " 330 "skipped but reported an error"); 331 } 332 } else if (s.signaled()) { 333 test_case_result tcr; 334 335 try { 336 tcr = read_test_case_result(resfile); 337 } catch (const std::runtime_error&) { 338 return test_case_result("failed", -1, "Test program received " 339 "signal " + tools::text::to_string(s.termsig()) + 340 (s.coredump() ? " (core dumped)" : "")); 341 } 342 343 if (tcr.state() == "expected_death") { 344 return tcr; 345 } else if (tcr.state() == "expected_signal") { 346 if (tcr.value() == -1 || s.termsig() == tcr.value()) 347 return tcr; 348 else 349 return test_case_result("failed", -1, "Test case was " 350 "expected to exit due to a " + to_string(tcr.value()) + 351 " signal but got " + to_string(s.termsig())); 352 } else { 353 return test_case_result("failed", -1, "Test program received " 354 "signal " + tools::text::to_string(s.termsig()) + 355 (s.coredump() ? " (core dumped)" : "") + " and created a " 356 "bogus results file"); 357 } 358 } 359 std::abort(); 360 return test_case_result(); 361} 362 363int 364atf_run::run_test_program(const tools::fs::path& tp, 365 tools::test_program::atf_tps_writer& w, 366 const vars_map& config) 367{ 368 int errcode = EXIT_SUCCESS; 369 370 tools::test_program::metadata md; 371 try { 372 md = tools::test_program::get_metadata(tp, config); 373 } catch (const tools::parser::format_error& e) { 374 w.start_tp(tp.str(), 0); 375 w.end_tp("Invalid format for test case list: " + std::string(e.what())); 376 return EXIT_FAILURE; 377 } catch (const tools::parser::parse_errors& e) { 378 const std::string reason = tools::text::join(e, "; "); 379 w.start_tp(tp.str(), 0); 380 w.end_tp("Invalid format for test case list: " + reason); 381 return EXIT_FAILURE; 382 } 383 384 tools::fs::temp_dir resdir( 385 tools::fs::path(tools::config::get("atf_workdir")) / "atf-run.XXXXXX"); 386 387 w.start_tp(tp.str(), md.test_cases.size()); 388 if (md.test_cases.empty()) { 389 w.end_tp("Bogus test program: reported 0 test cases"); 390 errcode = EXIT_FAILURE; 391 } else { 392 for (std::map< std::string, vars_map >::const_iterator iter 393 = md.test_cases.begin(); iter != md.test_cases.end(); iter++) { 394 const std::string& tcname = (*iter).first; 395 const vars_map& tcmd = (*iter).second; 396 397 w.start_tc(tcname); 398 399 try { 400 const std::string& reqfail = tools::check_requirements( 401 tcmd, config); 402 if (!reqfail.empty()) { 403 w.end_tc("skipped", reqfail); 404 continue; 405 } 406 } catch (const std::runtime_error& e) { 407 w.end_tc("failed", e.what()); 408 errcode = EXIT_FAILURE; 409 continue; 410 } 411 412 const std::pair< int, int > user = tools::get_required_user( 413 tcmd, config); 414 415 tools::fs::path resfile = resdir.get_path() / "tcr"; 416 assert(!tools::fs::exists(resfile)); 417 try { 418 const bool has_cleanup = tools::text::to_bool( 419 (*tcmd.find("has.cleanup")).second); 420 421 tools::fs::temp_dir workdir(tools::fs::path(tools::config::get( 422 "atf_workdir")) / "atf-run.XXXXXX"); 423 if (user.first != -1 && user.second != -1) { 424 if (::chown(workdir.get_path().c_str(), user.first, 425 user.second) == -1) { 426 throw tools::system_error("chown(" + 427 workdir.get_path().str() + ")", "chown(2) failed", 428 errno); 429 } 430 resfile = workdir.get_path() / "tcr"; 431 } 432 433 std::pair< std::string, const tools::process::status > s = 434 tools::test_program::run_test_case( 435 tp, tcname, "body", tcmd, config, 436 resfile, workdir.get_path(), w); 437 if (s.second.signaled() && s.second.coredump()) 438 dump_stacktrace(tp, s.second, workdir.get_path(), w); 439 if (has_cleanup) 440 (void)tools::test_program::run_test_case( 441 tp, tcname, "cleanup", tcmd, 442 config, resfile, workdir.get_path(), w); 443 444 // TODO: Force deletion of workdir. 445 446 tools::test_program::test_case_result tcr = 447 get_test_case_result(s.first, s.second, resfile); 448 449 w.end_tc(tcr.state(), tcr.reason()); 450 if (tcr.state() == "failed") 451 errcode = EXIT_FAILURE; 452 } catch (...) { 453 if (tools::fs::exists(resfile)) 454 tools::fs::remove(resfile); 455 throw; 456 } 457 if (tools::fs::exists(resfile)) 458 tools::fs::remove(resfile); 459 460 } 461 w.end_tp(""); 462 } 463 464 return errcode; 465} 466 467size_t 468atf_run::count_tps(std::vector< std::string > tps) 469 const 470{ 471 size_t ntps = 0; 472 473 for (std::vector< std::string >::const_iterator iter = tps.begin(); 474 iter != tps.end(); iter++) { 475 tools::fs::path tp(*iter); 476 tools::fs::file_info fi(tp); 477 478 if (fi.get_type() == tools::fs::file_info::dir_type) { 479 tools::atffile af = tools::read_atffile(tp / "Atffile"); 480 std::vector< std::string > aux = af.tps(); 481 for (std::vector< std::string >::iterator i2 = aux.begin(); 482 i2 != aux.end(); i2++) 483 *i2 = (tp / *i2).str(); 484 ntps += count_tps(aux); 485 } else 486 ntps++; 487 } 488 489 return ntps; 490} 491 492static 493void 494call_hook(const std::string& tool, const std::string& hook) 495{ 496 const tools::fs::path sh(tools::config::get("atf_shell")); 497 const tools::fs::path hooks = 498 tools::fs::path(tools::config::get("atf_pkgdatadir")) / (tool + ".hooks"); 499 500 const tools::process::status s = 501 tools::process::exec(sh, 502 tools::process::argv_array(sh.c_str(), hooks.c_str(), 503 hook.c_str(), NULL), 504 tools::process::stream_inherit(), 505 tools::process::stream_inherit()); 506 507 508 if (!s.exited() || s.exitstatus() != EXIT_SUCCESS) 509 throw std::runtime_error("Failed to run the '" + hook + "' hook " 510 "for '" + tool + "'"); 511} 512 513int 514atf_run::main(void) 515{ 516 tools::atffile af = tools::read_atffile(tools::fs::path("Atffile")); 517 518 std::vector< std::string > tps; 519 tps = af.tps(); 520 if (m_argc >= 1) { 521 // TODO: Ensure that the given test names are listed in the 522 // Atffile. Take into account that the file can be using globs. 523 tps.clear(); 524 for (int i = 0; i < m_argc; i++) 525 tps.push_back(m_argv[i]); 526 } 527 528 // Read configuration data for this test suite. 529 vars_map test_suite_vars; 530 { 531 vars_map::const_iterator iter = af.props().find("test-suite"); 532 assert(iter != af.props().end()); 533 test_suite_vars = tools::config_file::read_config_files((*iter).second); 534 } 535 536 tools::test_program::atf_tps_writer w(std::cout); 537 call_hook("atf-run", "info_start_hook"); 538 w.ntps(count_tps(tps)); 539 540 bool ok = true; 541 for (std::vector< std::string >::const_iterator iter = tps.begin(); 542 iter != tps.end(); iter++) { 543 const bool result = run_test(tools::fs::path(*iter), w, 544 tools::config_file::merge_configs(af.conf(), test_suite_vars)); 545 ok &= (result == EXIT_SUCCESS); 546 } 547 548 call_hook("atf-run", "info_end_hook"); 549 550 return ok ? EXIT_SUCCESS : EXIT_FAILURE; 551} 552 553int 554main(int argc, char* const* argv) 555{ 556 return atf_run().run(argc, argv); 557} 558