// // Automated Testing Framework (atf) // // Copyright (c) 2007 The NetBSD Foundation, Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND // CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. // IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN // IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // #if defined(HAVE_CONFIG_H) #include "bconfig.h" #endif extern "C" { #include #include #include #include #include } #include #include #include #include #include #include #include #include #include "atf-c++/detail/application.hpp" #include "atf-c++/config.hpp" #include "atf-c++/tests.hpp" #include "atf-c++/detail/env.hpp" #include "atf-c++/detail/exceptions.hpp" #include "atf-c++/detail/fs.hpp" #include "atf-c++/detail/parser.hpp" #include "atf-c++/detail/process.hpp" #include "atf-c++/detail/sanity.hpp" #include "atf-c++/detail/text.hpp" #include "atffile.hpp" #include "config.hpp" #include "fs.hpp" #include "requirements.hpp" #include "test-program.hpp" namespace impl = atf::atf_run; #if defined(MAXCOMLEN) static const std::string::size_type max_core_name_length = MAXCOMLEN; #else static const std::string::size_type max_core_name_length = std::string::npos; #endif class atf_run : public atf::application::app { static const char* m_description; atf::tests::vars_map m_cmdline_vars; static atf::tests::vars_map::value_type parse_var(const std::string&); void process_option(int, const char*); std::string specific_args(void) const; options_set specific_options(void) const; void parse_vflag(const std::string&); std::vector< std::string > conf_args(void) const; size_t count_tps(std::vector< std::string >) const; int run_test(const atf::fs::path&, impl::atf_tps_writer&, const atf::tests::vars_map&); int run_test_directory(const atf::fs::path&, impl::atf_tps_writer&); int run_test_program(const atf::fs::path&, impl::atf_tps_writer&, const atf::tests::vars_map&); impl::test_case_result get_test_case_result(const std::string&, const atf::process::status&, const atf::fs::path&) const; public: atf_run(void); int main(void); }; static void sanitize_gdb_env(void) { try { atf::env::unset("TERM"); } catch (...) { // Just swallow exceptions here; they cannot propagate into C, which // is where this function is called from, and even if these exceptions // appear they are benign. } } static void dump_stacktrace(const atf::fs::path& tp, const atf::process::status& s, const atf::fs::path& workdir, impl::atf_tps_writer& w) { PRE(s.signaled() && s.coredump()); w.stderr_tc("Test program crashed; attempting to get stack trace"); const atf::fs::path corename = workdir / (tp.leaf_name().substr(0, max_core_name_length) + ".core"); if (!atf::fs::exists(corename)) { w.stderr_tc("Expected file " + corename.str() + " not found"); return; } const atf::fs::path gdb(GDB); const atf::fs::path gdbout = workdir / "gdb.out"; const atf::process::argv_array args(gdb.leaf_name().c_str(), "-batch", "-q", "-ex", "bt", tp.c_str(), corename.c_str(), NULL); atf::process::status status = atf::process::exec( gdb, args, atf::process::stream_redirect_path(gdbout), atf::process::stream_redirect_path(atf::fs::path("/dev/null")), sanitize_gdb_env); if (!status.exited() || status.exitstatus() != EXIT_SUCCESS) { w.stderr_tc("Execution of " GDB " failed"); return; } std::ifstream input(gdbout.c_str()); if (input) { std::string line; while (std::getline(input, line).good()) w.stderr_tc(line); input.close(); } w.stderr_tc("Stack trace complete"); } const char* atf_run::m_description = "atf-run is a tool that runs tests programs and collects their " "results."; atf_run::atf_run(void) : app(m_description, "atf-run(1)", "atf(7)") { } void atf_run::process_option(int ch, const char* arg) { switch (ch) { case 'v': parse_vflag(arg); break; default: UNREACHABLE; } } std::string atf_run::specific_args(void) const { return "[test-program1 .. test-programN]"; } atf_run::options_set atf_run::specific_options(void) const { using atf::application::option; options_set opts; opts.insert(option('v', "var=value", "Sets the configuration variable " "`var' to `value'; overrides " "values in configuration files")); return opts; } void atf_run::parse_vflag(const std::string& str) { if (str.empty()) throw std::runtime_error("-v requires a non-empty argument"); std::vector< std::string > ws = atf::text::split(str, "="); if (ws.size() == 1 && str[str.length() - 1] == '=') { m_cmdline_vars[ws[0]] = ""; } else { if (ws.size() != 2) throw std::runtime_error("-v requires an argument of the form " "var=value"); m_cmdline_vars[ws[0]] = ws[1]; } } int atf_run::run_test(const atf::fs::path& tp, impl::atf_tps_writer& w, const atf::tests::vars_map& config) { atf::fs::file_info fi(tp); int errcode; if (fi.get_type() == atf::fs::file_info::dir_type) errcode = run_test_directory(tp, w); else { const atf::tests::vars_map effective_config = impl::merge_configs(config, m_cmdline_vars); errcode = run_test_program(tp, w, effective_config); } return errcode; } int atf_run::run_test_directory(const atf::fs::path& tp, impl::atf_tps_writer& w) { impl::atffile af = impl::read_atffile(tp / "Atffile"); atf::tests::vars_map test_suite_vars; { atf::tests::vars_map::const_iterator iter = af.props().find("test-suite"); INV(iter != af.props().end()); test_suite_vars = impl::read_config_files((*iter).second); } bool ok = true; for (std::vector< std::string >::const_iterator iter = af.tps().begin(); iter != af.tps().end(); iter++) { const bool result = run_test(tp / *iter, w, impl::merge_configs(af.conf(), test_suite_vars)); ok &= (result == EXIT_SUCCESS); } return ok ? EXIT_SUCCESS : EXIT_FAILURE; } impl::test_case_result atf_run::get_test_case_result(const std::string& broken_reason, const atf::process::status& s, const atf::fs::path& resfile) const { using atf::text::to_string; using impl::read_test_case_result; using impl::test_case_result; if (!broken_reason.empty()) { test_case_result tcr; try { tcr = read_test_case_result(resfile); if (tcr.state() == "expected_timeout") { return tcr; } else { return test_case_result("failed", -1, broken_reason); } } catch (const std::runtime_error&) { return test_case_result("failed", -1, broken_reason); } } if (s.exited()) { test_case_result tcr; try { tcr = read_test_case_result(resfile); } catch (const std::runtime_error& e) { return test_case_result("failed", -1, "Test case exited " "normally but failed to create the results file: " + std::string(e.what())); } if (tcr.state() == "expected_death") { return tcr; } else if (tcr.state() == "expected_exit") { if (tcr.value() == -1 || s.exitstatus() == tcr.value()) return tcr; else return test_case_result("failed", -1, "Test case was " "expected to exit with a " + to_string(tcr.value()) + " error code but returned " + to_string(s.exitstatus())); } else if (tcr.state() == "expected_failure") { if (s.exitstatus() == EXIT_SUCCESS) return tcr; else return test_case_result("failed", -1, "Test case returned an " "error in expected_failure mode but it should not have"); } else if (tcr.state() == "expected_signal") { return test_case_result("failed", -1, "Test case exited cleanly " "but was expected to receive a signal"); } else if (tcr.state() == "failed") { if (s.exitstatus() == EXIT_SUCCESS) return test_case_result("failed", -1, "Test case " "exited successfully but reported failure"); else return tcr; } else if (tcr.state() == "passed") { if (s.exitstatus() == EXIT_SUCCESS) return tcr; else return test_case_result("failed", -1, "Test case exited as " "passed but reported an error"); } else if (tcr.state() == "skipped") { if (s.exitstatus() == EXIT_SUCCESS) return tcr; else return test_case_result("failed", -1, "Test case exited as " "skipped but reported an error"); } } else if (s.signaled()) { test_case_result tcr; try { tcr = read_test_case_result(resfile); } catch (const std::runtime_error&) { return test_case_result("failed", -1, "Test program received " "signal " + atf::text::to_string(s.termsig()) + (s.coredump() ? " (core dumped)" : "")); } if (tcr.state() == "expected_death") { return tcr; } else if (tcr.state() == "expected_signal") { if (tcr.value() == -1 || s.termsig() == tcr.value()) return tcr; else return test_case_result("failed", -1, "Test case was " "expected to exit due to a " + to_string(tcr.value()) + " signal but got " + to_string(s.termsig())); } else { return test_case_result("failed", -1, "Test program received " "signal " + atf::text::to_string(s.termsig()) + (s.coredump() ? " (core dumped)" : "") + " and created a " "bogus results file"); } } UNREACHABLE; return test_case_result(); } int atf_run::run_test_program(const atf::fs::path& tp, impl::atf_tps_writer& w, const atf::tests::vars_map& config) { int errcode = EXIT_SUCCESS; impl::metadata md; try { md = impl::get_metadata(tp, config); } catch (const atf::parser::format_error& e) { w.start_tp(tp.str(), 0); w.end_tp("Invalid format for test case list: " + std::string(e.what())); return EXIT_FAILURE; } catch (const atf::parser::parse_errors& e) { const std::string reason = atf::text::join(e, "; "); w.start_tp(tp.str(), 0); w.end_tp("Invalid format for test case list: " + reason); return EXIT_FAILURE; } impl::temp_dir resdir(atf::fs::path(atf::config::get("atf_workdir")) / "atf-run.XXXXXX"); w.start_tp(tp.str(), md.test_cases.size()); if (md.test_cases.empty()) { w.end_tp("Bogus test program: reported 0 test cases"); errcode = EXIT_FAILURE; } else { for (std::map< std::string, atf::tests::vars_map >::const_iterator iter = md.test_cases.begin(); iter != md.test_cases.end(); iter++) { const std::string& tcname = (*iter).first; const atf::tests::vars_map& tcmd = (*iter).second; w.start_tc(tcname); try { const std::string& reqfail = impl::check_requirements( tcmd, config); if (!reqfail.empty()) { w.end_tc("skipped", reqfail); continue; } } catch (const std::runtime_error& e) { w.end_tc("failed", e.what()); errcode = EXIT_FAILURE; continue; } const std::pair< int, int > user = impl::get_required_user( tcmd, config); atf::fs::path resfile = resdir.get_path() / "tcr"; INV(!atf::fs::exists(resfile)); try { const bool has_cleanup = atf::text::to_bool( (*tcmd.find("has.cleanup")).second); impl::temp_dir workdir(atf::fs::path(atf::config::get( "atf_workdir")) / "atf-run.XXXXXX"); if (user.first != -1 && user.second != -1) { if (::chown(workdir.get_path().c_str(), user.first, user.second) == -1) { throw atf::system_error("chown(" + workdir.get_path().str() + ")", "chown(2) failed", errno); } resfile = workdir.get_path() / "tcr"; } std::pair< std::string, const atf::process::status > s = impl::run_test_case(tp, tcname, "body", tcmd, config, resfile, workdir.get_path(), w); if (s.second.signaled() && s.second.coredump()) dump_stacktrace(tp, s.second, workdir.get_path(), w); if (has_cleanup) (void)impl::run_test_case(tp, tcname, "cleanup", tcmd, config, resfile, workdir.get_path(), w); // TODO: Force deletion of workdir. impl::test_case_result tcr = get_test_case_result(s.first, s.second, resfile); w.end_tc(tcr.state(), tcr.reason()); if (tcr.state() == "failed") errcode = EXIT_FAILURE; } catch (...) { if (atf::fs::exists(resfile)) atf::fs::remove(resfile); throw; } if (atf::fs::exists(resfile)) atf::fs::remove(resfile); } w.end_tp(""); } return errcode; } size_t atf_run::count_tps(std::vector< std::string > tps) const { size_t ntps = 0; for (std::vector< std::string >::const_iterator iter = tps.begin(); iter != tps.end(); iter++) { atf::fs::path tp(*iter); atf::fs::file_info fi(tp); if (fi.get_type() == atf::fs::file_info::dir_type) { impl::atffile af = impl::read_atffile(tp / "Atffile"); std::vector< std::string > aux = af.tps(); for (std::vector< std::string >::iterator i2 = aux.begin(); i2 != aux.end(); i2++) *i2 = (tp / *i2).str(); ntps += count_tps(aux); } else ntps++; } return ntps; } static void call_hook(const std::string& tool, const std::string& hook) { const atf::fs::path sh(atf::config::get("atf_shell")); const atf::fs::path hooks = atf::fs::path(atf::config::get("atf_pkgdatadir")) / (tool + ".hooks"); const atf::process::status s = atf::process::exec(sh, atf::process::argv_array(sh.c_str(), hooks.c_str(), hook.c_str(), NULL), atf::process::stream_inherit(), atf::process::stream_inherit()); if (!s.exited() || s.exitstatus() != EXIT_SUCCESS) throw std::runtime_error("Failed to run the '" + hook + "' hook " "for '" + tool + "'"); } int atf_run::main(void) { impl::atffile af = impl::read_atffile(atf::fs::path("Atffile")); std::vector< std::string > tps; tps = af.tps(); if (m_argc >= 1) { // TODO: Ensure that the given test names are listed in the // Atffile. Take into account that the file can be using globs. tps.clear(); for (int i = 0; i < m_argc; i++) tps.push_back(m_argv[i]); } // Read configuration data for this test suite. atf::tests::vars_map test_suite_vars; { atf::tests::vars_map::const_iterator iter = af.props().find("test-suite"); INV(iter != af.props().end()); test_suite_vars = impl::read_config_files((*iter).second); } impl::atf_tps_writer w(std::cout); call_hook("atf-run", "info_start_hook"); w.ntps(count_tps(tps)); bool ok = true; for (std::vector< std::string >::const_iterator iter = tps.begin(); iter != tps.end(); iter++) { const bool result = run_test(atf::fs::path(*iter), w, impl::merge_configs(af.conf(), test_suite_vars)); ok &= (result == EXIT_SUCCESS); } call_hook("atf-run", "info_end_hook"); return ok ? EXIT_SUCCESS : EXIT_FAILURE; } int main(int argc, char* const* argv) { return atf_run().run(argc, argv); }