/// Parses a user-provided test filter. /// /// \param str The user-provided string representing a filter for tests. Must /// be of the form <test_program%gt;[:<test_case%gt;]. /// /// \return The parsed filter. /// /// \throw std::runtime_error If the provided filter is invalid. engine::test_filter engine::test_filter::parse(const std::string& str) { if (str.empty()) throw std::runtime_error("Test filter cannot be empty"); const std::string::size_type pos = str.find(':'); if (pos == 0) throw std::runtime_error(F("Program name component in '%s' is empty") % str); if (pos == str.length() - 1) throw std::runtime_error(F("Test case component in '%s' is empty") % str); try { const fs::path test_program_(str.substr(0, pos)); if (test_program_.is_absolute()) throw std::runtime_error(F("Program name '%s' must be relative " "to the test suite, not absolute") % test_program_.str()); if (pos == std::string::npos) { LD(F("Parsed user filter '%s': test program '%s', no test case") % str % test_program_.str()); return test_filter(test_program_, ""); } else { const std::string test_case_(str.substr(pos + 1)); LD(F("Parsed user filter '%s': test program '%s', test case '%s'") % str % test_program_.str() % test_case_); return test_filter(test_program_, test_case_); } } catch (const fs::error& e) { throw std::runtime_error(F("Invalid path in filter '%s': %s") % str % e.what()); } }
/// Executes an external binary and replaces the current process. /// /// This differs from process::exec() in that this function reports errors /// caused by the exec(2) system call to let the caller decide how to handle /// them. /// /// This function must not use any of the logging features so that the output /// of the subprocess is not "polluted" by our own messages. /// /// This function must also not affect the global state of the current process /// as otherwise we would not be able to use vfork(). Only state stored in the /// stack can be touched. /// /// \param program The binary to execute. /// \param args The arguments to pass to the binary, without the program name. /// /// \throw system_error If the exec(2) call fails. void process::exec_unsafe(const fs::path& program, const args_vector& args) { PRE(args.size() < MAX_ARGS); int original_errno = 0; try { const char* argv[MAX_ARGS + 1]; argv[0] = program.c_str(); for (args_vector::size_type i = 0; i < args.size(); i++) argv[1 + i] = args[i].c_str(); argv[1 + args.size()] = NULL; const int ret = ::execv(program.c_str(), (char* const*)(unsigned long)(const void*)argv); original_errno = errno; INV(ret == -1); std::cerr << "Failed to execute " << program << ": " << std::strerror(original_errno) << "\n"; } catch (const std::runtime_error& error) { std::cerr << "Failed to execute " << program << ": " << error.what() << "\n"; std::abort(); } catch (...) { std::cerr << "Failed to execute " << program << "; got unexpected " "exception during exec\n"; std::abort(); } // We must do this here to prevent our exception from being caught by the // generic handlers above. INV(original_errno != 0); throw system_error("Failed to execute " + program.str(), original_errno); }
/// Initializes an empty database. /// /// \param db The database to initialize. /// /// \return The metadata record written into the new database. /// /// \throw store::error If there is a problem initializing the database. store::metadata store::detail::initialize(sqlite::database& db) { PRE(empty_database(db)); const fs::path schema = schema_file(); std::ifstream input(schema.c_str()); if (!input) throw error(F("Cannot open database schema '%s'") % schema); LI(F("Populating new database with schema from %s") % schema); const std::string schema_string = utils::read_stream(input); try { db.exec(schema_string); const metadata metadata = metadata::fetch_latest(db); LI(F("New metadata entry %s") % metadata.timestamp()); if (metadata.schema_version() != detail::current_schema_version) { UNREACHABLE_MSG(F("current_schema_version is out of sync with " "%s") % schema); } return metadata; } catch (const store::integrity_error& e) { // Could be raised by metadata::fetch_latest. UNREACHABLE_MSG("Inconsistent code while creating a database"); } catch (const sqlite::error& e) { throw error(F("Failed to initialize database: %s") % e.what()); } }
/// Computes the path to a new database for the given test suite. /// /// \param root Path to the root of the test suite being run; needed to properly /// autogenerate the identifiers. /// \param when Timestamp for the test suite being run; needed to properly /// autogenerate the identifiers. /// /// \return Identifier of the created results file, if applicable, and the path /// to such file. fs::path layout::new_db_for_migration(const fs::path& root, const datetime::timestamp& when) { const std::string generated_id = new_id(test_suite_for_path(root), when); const fs::path path = query_store_dir() / ( F("results.%s.db") % generated_id); fs::mkdir_p(path.branch_path(), 0755); return path; }
/// Parses a test suite configuration file. /// /// \param file The file to parse. /// \param user_build_root If not none, specifies a path to a directory /// containing the test programs themselves. The layout of the build root /// must match the layout of the source root (which is just the directory /// from which the Kyuafile is being read). /// /// \return High-level representation of the configuration file. /// /// \throw load_error If there is any problem loading the file. This includes /// file access errors and syntax errors. engine::kyuafile engine::kyuafile::load(const fs::path& file, const optional< fs::path > user_build_root) { const fs::path source_root_ = file.branch_path(); const fs::path build_root_ = user_build_root ? user_build_root.get() : source_root_; return kyuafile(source_root_, build_root_, parser(source_root_, build_root_, fs::path(file.leaf_name())).parse()); }
/// Returns the test suite name for the current directory. /// /// \return The identifier of the current test suite. std::string layout::test_suite_for_path(const fs::path& path) { std::string test_suite; if (path.is_absolute()) test_suite = path.str(); else test_suite = path.to_absolute().str(); PRE(!test_suite.empty() && test_suite[0] == '/'); std::replace(test_suite.begin(), test_suite.end(), '/', '_'); test_suite.erase(0, 1); return test_suite; }
/// Creates a temporary file. /// /// The temporary file is created using mkstemp(3) using the provided template. /// This should be most likely used in conjunction with fs::auto_file. /// /// \param path_template The template for the temporary path, which is a /// basename that is created within the TMPDIR. Must contain the XXXXXX /// pattern, which is atomically replaced by a random unique string. /// /// \return The generated path for the temporary directory. /// /// \throw fs::system_error If the call to mkstemp(3) fails. fs::path fs::mkstemp(const std::string& path_template) { PRE(path_template.find("XXXXXX") != std::string::npos); const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); const fs::path full_template = tmpdir / path_template; utils::auto_array< char > buf(new char[full_template.str().length() + 1]); std::strcpy(buf.get(), full_template.c_str()); if (::mkstemp(buf.get()) == -1) { const int original_errno = errno; throw fs::system_error(F("Cannot create temporary file using template " "%s") % full_template, original_errno); } return fs::path(buf.get()); }
/// Concatenates this path with another path. /// /// \param rest The path to concatenate to this one. Cannot be absolute. /// /// \return A new path containing the concatenation of this path and the other /// path. /// /// \throw utils::fs::join_error If the join operation is invalid because the /// two paths are incompatible. fs::path fs::path::operator/(const fs::path& rest) const { if (rest.is_absolute()) throw fs::join_error(_repr, rest._repr, "Cannot concatenate a path to an absolute path"); return fs::path(_repr + '/' + rest._repr); }
/// Creates a directory. /// /// \param dir The path to the directory to create. /// \param mode The permissions for the new directory. /// /// \throw system_error If the call to mkdir(2) fails. void fs::mkdir(const fs::path& dir, const int mode) { if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { const int original_errno = errno; throw fs::system_error(F("Failed to create directory %s") % dir, original_errno); } }
/// Locates a file in the PATH. /// /// \param name The file to locate. /// /// \return The path to the located file or none if it was not found. The /// returned path is always absolute. optional< fs::path > fs::find_in_path(const char* name) { const optional< std::string > current_path = utils::getenv("PATH"); if (!current_path || current_path.get().empty()) return none; std::istringstream path_input(current_path.get() + ":"); std::string path_component; while (std::getline(path_input, path_component, ':').good()) { const fs::path candidate = path_component.empty() ? fs::path(name) : (fs::path(path_component) / name); if (exists(candidate)) { if (candidate.is_absolute()) return utils::make_optional(candidate); else return utils::make_optional(candidate.to_absolute()); } } return none; }
/// Creates a directory and any missing parents. /// /// This is separate from the fs::mkdir function to clearly differentiate the /// libc wrapper from the more complex algorithm implemented here. /// /// \param dir The path to the directory to create. /// \param mode The permissions for the new directories. /// /// \throw system_error If any call to mkdir(2) fails. void fs::mkdir_p(const fs::path& dir, const int mode) { try { fs::mkdir(dir, mode); } catch (const fs::system_error& e) { if (e.original_errno() == ENOENT) { fs::mkdir_p(dir.branch_path(), mode); fs::mkdir(dir, mode); } else if (e.original_errno() != EEXIST) throw e; } }
/// Constructor. /// /// \param interface_name_ Name of the test program interface. /// \param binary_ The name of the test program binary relative to root_. /// \param root_ The root of the test suite containing the test program. /// \param test_suite_name_ The name of the test suite this program /// belongs to. /// \param md_ Metadata of the test program. impl(const std::string& interface_name_, const fs::path& binary_, const fs::path& root_, const std::string& test_suite_name_, const metadata& md_) : interface_name(interface_name_), binary(binary_), root(root_), test_suite_name(test_suite_name_), md(md_) { PRE_MSG(!binary.is_absolute(), F("The program '%s' must be relative to the root of the test " "suite '%s'") % binary % root); }
/// Backs up a database for schema migration purposes. /// /// \todo We should probably use the SQLite backup API instead of doing a raw /// file copy. We issue our backup call with the database already open, but /// because it is quiescent, it's OK to do so. /// /// \param source Location of the database to be backed up. /// \param old_version Version of the database's CURRENT schema, used to /// determine the name of the backup file. /// /// \throw error If there is a problem during the backup. void store::detail::backup_database(const fs::path& source, const int old_version) { const fs::path target(F("%s.v%s.backup") % source.str() % old_version); LI(F("Backing up database %s to %s") % source % target); std::ifstream input(source.c_str()); if (!input) throw error(F("Cannot open database file %s") % source); std::ofstream output(target.c_str()); if (!output) throw error(F("Cannot create database backup file %s") % target); char buffer[1024]; while (input.good()) { input.read(buffer, sizeof(buffer)); if (input.good() || input.eof()) output.write(buffer, input.gcount()); } if (!input.good() && !input.eof()) throw error(F("Error while reading input file %s") % source); }
/// Recursively removes a directory. /// /// This operation simulates a "rm -r". No effort is made to forcibly delete /// files and no attention is paid to mount points. /// /// \param directory The directory to remove. /// /// \throw fs::error If there is a problem removing any directory or file. void fs::rm_r(const fs::path& directory) { DIR* dirp = ::opendir(directory.c_str()); if (dirp == NULL) { const int original_errno = errno; throw fs::system_error(F("Failed to open directory %s") % directory.str(), original_errno); } try { ::dirent* dp; while ((dp = ::readdir(dirp)) != NULL) { const std::string name = dp->d_name; if (name == "." || name == "..") continue; const fs::path entry = directory / dp->d_name; const struct ::stat sb = safe_stat(entry); if (S_ISDIR(sb.st_mode)) { LD(F("Descending into %s") % entry); fs::rm_r(entry); } else { LD(F("Removing file %s") % entry); fs::unlink(entry); } } } catch (...) { ::closedir(dirp); throw; } ::closedir(dirp); LD(F("Removing empty directory %s") % directory); fs::rmdir(directory); }
/// Constructor. /// /// \param interface_name_ Name of the test program interface. /// \param binary_ The name of the test program binary relative to root_. /// \param root_ The root of the test suite containing the test program. /// \param test_suite_name_ The name of the test suite this program /// belongs to. /// \param md_ Metadata of the test program. /// \param test_cases_ The collection of test cases in the test program. impl(const std::string& interface_name_, const fs::path& binary_, const fs::path& root_, const std::string& test_suite_name_, const model::metadata& md_, const model::test_cases_map& test_cases_) : interface_name(interface_name_), binary(binary_), root(root_), test_suite_name(test_suite_name_), md(md_), test_cases(test_cases_) { PRE_MSG(!binary.is_absolute(), F("The program '%s' must be relative to the root of the test " "suite '%s'") % binary % root); for (model::test_cases_map::const_iterator iter = test_cases.begin(); iter != test_cases.end(); ++iter) { PRE_MSG((*iter).first == (*iter).second.name(), F("The test case '%s' has been registered with the " "non-matching name '%s'") % (*iter).first % (*iter).second.name()); } }
/// Computes the test cases list of a test program. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param stdout_path Path to the file containing the stdout of the test. /// \param stderr_path Path to the file containing the stderr of the test. /// /// \return A list of test cases. /// /// \throw error If there is a problem parsing the test case list. model::test_cases_map engine::atf_interface::parse_list(const optional< process::status >& status, const fs::path& stdout_path, const fs::path& stderr_path) const { const std::string stderr_contents = utils::read_file(stderr_path); if (!stderr_contents.empty()) LW("Test case list wrote to stderr: " + stderr_contents); if (!status) throw engine::error("Test case list timed out"); if (status.get().exited()) { const int exitstatus = status.get().exitstatus(); if (exitstatus == EXIT_SUCCESS) { // Nothing to do; fall through. } else if (exitstatus == exit_eacces) { throw engine::error("Permission denied to run test program"); } else if (exitstatus == exit_enoent) { throw engine::error("Cannot find test program"); } else if (exitstatus == exit_enoexec) { throw engine::error("Invalid test program format"); } else { throw engine::error("Test program did not exit cleanly"); } } else { throw engine::error("Test program received signal"); } std::ifstream input(stdout_path.c_str()); if (!input) throw engine::load_error(stdout_path, "Cannot open file for read"); const model::test_cases_map test_cases = parse_atf_list(input); if (!stderr_contents.empty()) throw engine::error("Test case list wrote to stderr"); return test_cases; }
/// Gets the absolute path to the test program. /// /// \return The absolute path to the test program binary. const fs::path model::test_program::absolute_path(void) const { const fs::path full_path = _pimpl->root / _pimpl->binary; return full_path.is_absolute() ? full_path : full_path.to_absolute(); }
/// Checks if a file exists. /// /// Be aware that this is racy in the same way as access(2) is. /// /// \param path The file to check the existance of. /// /// \return True if the file exists; false otherwise. bool fs::exists(const fs::path& path) { return ::access(path.c_str(), F_OK) == 0; }
/// Waits for completion of any forked test case. /// /// Note that if the terminated test case has a cleanup routine, this function /// is the one in charge of spawning the cleanup routine asynchronously. /// /// \return The result of the execution of a subprocess. This is a dynamically /// allocated object because the scheduler can spawn subprocesses of various /// types and, at wait time, we don't know upfront what we are going to get. scheduler::result_handle_ptr scheduler::scheduler_handle::wait_any(void) { _pimpl->generic.check_interrupt(); executor::exit_handle handle = _pimpl->generic.wait_any(); const exec_data_map::iterator iter = _pimpl->all_exec_data.find( handle.original_pid()); exec_data_ptr& data = (*iter).second; utils::dump_stacktrace_if_available(data->test_program->absolute_path(), _pimpl->generic, handle); optional< model::test_result > result; try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); test_data->exit_handle = handle; const model::test_case& test_case = test_data->test_program->find( test_data->test_case_name); result = test_case.fake_result(); if (!result && handle.status() && handle.status().get().exited() && handle.status().get().exitstatus() == exit_skipped) { // If the test's process terminated with our magic "exit_skipped" // status, there are two cases to handle. The first is the case // where the "skipped cookie" exists, in which case we never got to // actually invoke the test program; if that's the case, handle it // here. The second case is where the test case actually decided to // exit with the "exit_skipped" status; in that case, just fall back // to the regular status handling. const fs::path skipped_cookie_path = handle.control_directory() / skipped_cookie; std::ifstream input(skipped_cookie_path.c_str()); if (input) { result = model::test_result(model::test_result_skipped, utils::read_stream(input)); input.close(); // If we determined that the test needs to be skipped, we do not // want to run the cleanup routine because doing so could result // in errors. However, we still want to run the cleanup routine // if the test's body reports a skip (because actions could have // already been taken). test_data->needs_cleanup = false; } } if (!result) { result = test_data->interface->compute_result( handle.status(), handle.control_directory(), handle.stdout_file(), handle.stderr_file()); } INV(result); if (!result.get().good()) { append_files_listing(handle.work_directory(), handle.stderr_file()); } if (test_data->needs_cleanup) { INV(test_case.get_metadata().has_cleanup()); // The test body has completed and we have processed it. If there // is a cleanup routine, trigger it now and wait for any other test // completion. The caller never knows about cleanup routines. _pimpl->spawn_cleanup(test_data->test_program, test_data->test_case_name, test_data->user_config, handle, result.get()); test_data->needs_cleanup = false; // TODO(jmmv): Chaining this call is ugly. We'd be better off by // looping over terminated processes until we got a result suitable // for user consumption. For the time being this is good enough and // not a problem because the call chain won't get big: the majority // of test cases do not have cleanup routines. return wait_any(); } } catch (const std::bad_cast& e) { const cleanup_exec_data* cleanup_data = &dynamic_cast< const cleanup_exec_data& >(*data.get()); // Handle the completion of cleanup subprocesses internally: the caller // is not aware that these exist so, when we return, we must return the // data for the original test that triggered this routine. For example, // because the caller wants to see the exact same exec_handle that was // returned by spawn_test. const model::test_result& body_result = cleanup_data->body_result; if (body_result.good()) { if (!handle.status()) { result = model::test_result(model::test_result_broken, "Test case cleanup timed out"); } else { if (!handle.status().get().exited() || handle.status().get().exitstatus() != EXIT_SUCCESS) { result = model::test_result( model::test_result_broken, "Test case cleanup did not terminate successfully"); } else { result = body_result; } } } else { result = body_result; } handle = cleanup_data->body_exit_handle; } INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( new result_handle::bimpl(handle, _pimpl->all_exec_data)); std::shared_ptr< test_result_handle::impl > test_result_handle_impl( new test_result_handle::impl( data->test_program, data->test_case_name, result.get())); return result_handle_ptr(new test_result_handle(result_handle_bimpl, test_result_handle_impl)); }