Path: blob/main/contrib/kyua/drivers/run_tests.cpp
39478 views
// Copyright 2011 The Kyua Authors.1// All rights reserved.2//3// Redistribution and use in source and binary forms, with or without4// modification, are permitted provided that the following conditions are5// met:6//7// * Redistributions of source code must retain the above copyright8// notice, this list of conditions and the following disclaimer.9// * Redistributions in binary form must reproduce the above copyright10// notice, this list of conditions and the following disclaimer in the11// documentation and/or other materials provided with the distribution.12// * Neither the name of Google Inc. nor the names of its contributors13// may be used to endorse or promote products derived from this software14// without specific prior written permission.15//16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.2728#include "drivers/run_tests.hpp"2930#include <utility>3132#include "engine/config.hpp"33#include "engine/filters.hpp"34#include "engine/kyuafile.hpp"35#include "engine/scanner.hpp"36#include "engine/scheduler.hpp"37#include "model/context.hpp"38#include "model/metadata.hpp"39#include "model/test_case.hpp"40#include "model/test_program.hpp"41#include "model/test_result.hpp"42#include "store/write_backend.hpp"43#include "store/write_transaction.hpp"44#include "utils/config/tree.ipp"45#include "utils/datetime.hpp"46#include "utils/defs.hpp"47#include "utils/format/macros.hpp"48#include "utils/logging/macros.hpp"49#include "utils/noncopyable.hpp"50#include "utils/optional.ipp"51#include "utils/passwd.hpp"52#include "utils/text/operations.ipp"5354namespace config = utils::config;55namespace datetime = utils::datetime;56namespace fs = utils::fs;57namespace passwd = utils::passwd;58namespace scheduler = engine::scheduler;59namespace text = utils::text;6061using utils::none;62using utils::optional;636465namespace {666768/// Map of test program identifiers (relative paths) to their identifiers in the69/// database. We need to keep this in memory because test programs can be70/// returned by the scanner in any order, and we only want to put each test71/// program once.72typedef std::map< fs::path, int64_t > path_to_id_map;737475/// Map of in-flight PIDs to their corresponding test case IDs.76typedef std::map< int, int64_t > pid_to_id_map;777879/// Pair of PID to a test case ID.80typedef pid_to_id_map::value_type pid_and_id_pair;818283/// Puts a test program in the store and returns its identifier.84///85/// This function is idempotent: we maintain a side cache of already-put test86/// programs so that we can return their identifiers without having to put them87/// again.88/// TODO(jmmv): It's possible that the store module should offer this89/// functionality and not have to do this ourselves here.90///91/// \param test_program The test program being put.92/// \param [in,out] tx Writable transaction on the store.93/// \param [in,out] ids_cache Cache of already-put test programs.94///95/// \return A test program identifier.96static int64_t97find_test_program_id(const model::test_program_ptr test_program,98store::write_transaction& tx,99path_to_id_map& ids_cache)100{101const fs::path& key = test_program->relative_path();102std::map< fs::path, int64_t >::const_iterator iter = ids_cache.find(key);103if (iter == ids_cache.end()) {104const int64_t id = tx.put_test_program(*test_program);105ids_cache.insert(std::make_pair(key, id));106return id;107} else {108return (*iter).second;109}110}111112113/// Stores the result of an execution in the database.114///115/// \param test_case_id Identifier of the test case in the database.116/// \param result The result of the execution.117/// \param [in,out] tx Writable transaction where to store the result data.118static void119put_test_result(const int64_t test_case_id,120const scheduler::test_result_handle& result,121store::write_transaction& tx)122{123tx.put_result(result.test_result(), test_case_id,124result.start_time(), result.end_time());125tx.put_test_case_file("__STDOUT__", result.stdout_file(), test_case_id);126tx.put_test_case_file("__STDERR__", result.stderr_file(), test_case_id);127128}129130131/// Cleans up a test case and folds any errors into the test result.132///133/// \param handle The result handle for the test.134///135/// \return The test result if the cleanup succeeds; a broken test result136/// otherwise.137model::test_result138safe_cleanup(scheduler::test_result_handle handle) throw()139{140try {141handle.cleanup();142return handle.test_result();143} catch (const std::exception& e) {144return model::test_result(145model::test_result_broken,146F("Failed to clean up test case's work directory %s: %s") %147handle.work_directory() % e.what());148}149}150151152/// Starts a test asynchronously.153///154/// \param handle Scheduler handle.155/// \param match Test program and test case to start.156/// \param [in,out] tx Writable transaction to obtain test IDs.157/// \param [in,out] ids_cache Cache of already-put test cases.158/// \param user_config The end-user configuration properties.159/// \param hooks The hooks for this execution.160///161/// \returns The PID for the started test and the test case's identifier in the162/// store.163pid_and_id_pair164start_test(scheduler::scheduler_handle& handle,165const engine::scan_result& match,166store::write_transaction& tx,167path_to_id_map& ids_cache,168const config::tree& user_config,169drivers::run_tests::base_hooks& hooks)170{171const model::test_program_ptr test_program = match.first;172const std::string& test_case_name = match.second;173174hooks.got_test_case(*test_program, test_case_name);175176const int64_t test_program_id = find_test_program_id(177test_program, tx, ids_cache);178const int64_t test_case_id = tx.put_test_case(179*test_program, test_case_name, test_program_id);180181const scheduler::exec_handle exec_handle = handle.spawn_test(182test_program, test_case_name, user_config);183return std::make_pair(exec_handle, test_case_id);184}185186187/// Processes the completion of a test.188///189/// \param [in,out] result_handle The completion handle of the test subprocess.190/// \param test_case_id Identifier of the test case as returned by start_test().191/// \param [in,out] tx Writable transaction to put the test results.192/// \param hooks The hooks for this execution.193///194/// \post result_handle is cleaned up. The caller cannot clean it up again.195void196finish_test(scheduler::result_handle_ptr result_handle,197const int64_t test_case_id,198store::write_transaction& tx,199drivers::run_tests::base_hooks& hooks)200{201const scheduler::test_result_handle* test_result_handle =202dynamic_cast< const scheduler::test_result_handle* >(203result_handle.get());204205put_test_result(test_case_id, *test_result_handle, tx);206207const model::test_result test_result = safe_cleanup(*test_result_handle);208hooks.got_result(209*test_result_handle->test_program(),210test_result_handle->test_case_name(),211test_result_handle->test_result(),212result_handle->end_time() - result_handle->start_time());213}214215216/// Extracts the keys of a pid_to_id_map and returns them as a string.217///218/// \param map The PID to test ID map from which to get the PIDs.219///220/// \return A user-facing string with the collection of PIDs.221static std::string222format_pids(const pid_to_id_map& map)223{224std::set< pid_to_id_map::key_type > pids;225for (pid_to_id_map::const_iterator iter = map.begin(); iter != map.end();226++iter) {227pids.insert(iter->first);228}229return text::join(pids, ",");230}231232233} // anonymous namespace234235236/// Pure abstract destructor.237drivers::run_tests::base_hooks::~base_hooks(void)238{239}240241242/// Executes the operation.243///244/// \param kyuafile_path The path to the Kyuafile to be loaded.245/// \param build_root If not none, path to the built test programs.246/// \param store_path The path to the store to be used.247/// \param filters The test case filters as provided by the user.248/// \param user_config The end-user configuration properties.249/// \param hooks The hooks for this execution.250///251/// \returns A structure with all results computed by this driver.252drivers::run_tests::result253drivers::run_tests::drive(const fs::path& kyuafile_path,254const optional< fs::path > build_root,255const fs::path& store_path,256const std::set< engine::test_filter >& filters,257const config::tree& user_config,258base_hooks& hooks)259{260scheduler::scheduler_handle handle = scheduler::setup();261262const engine::kyuafile kyuafile = engine::kyuafile::load(263kyuafile_path, build_root, user_config, handle);264store::write_backend db = store::write_backend::open_rw(store_path);265store::write_transaction tx = db.start_write();266267{268const model::context context = scheduler::current_context();269(void)tx.put_context(context);270}271272engine::scanner scanner(kyuafile.test_programs(), filters);273274path_to_id_map ids_cache;275pid_to_id_map in_flight;276std::vector< engine::scan_result > exclusive_tests;277278const std::size_t slots = user_config.lookup< config::positive_int_node >(279"parallelism");280INV(slots >= 1);281do {282INV(in_flight.size() <= slots);283284// Spawn as many jobs as needed to fill our execution slots. We do this285// first with the assumption that the spawning is faster than any single286// job, so we want to keep as many jobs in the background as possible.287while (in_flight.size() < slots) {288optional< engine::scan_result > match = scanner.yield();289if (!match)290break;291const model::test_program_ptr test_program = match.get().first;292const std::string& test_case_name = match.get().second;293294const model::test_case& test_case = test_program->find(295test_case_name);296if (test_case.get_metadata().is_exclusive()) {297// Exclusive tests get processed later, separately.298exclusive_tests.push_back(match.get());299continue;300}301302const pid_and_id_pair pid_id = start_test(303handle, match.get(), tx, ids_cache, user_config, hooks);304INV_MSG(in_flight.find(pid_id.first) == in_flight.end(),305F("Spawned test has PID of still-tracked process %s") %306pid_id.first);307in_flight.insert(pid_id);308}309310// If there are any used slots, consume any at random and return the311// result. We consume slots one at a time to give preference to the312// spawning of new tests as detailed above.313if (!in_flight.empty()) {314scheduler::result_handle_ptr result_handle = handle.wait_any();315316const pid_to_id_map::iterator iter = in_flight.find(317result_handle->original_pid());318INV_MSG(iter != in_flight.end(),319F("Lost track of in-flight PID %s; tracking %s") %320result_handle->original_pid() % format_pids(in_flight));321const int64_t test_case_id = (*iter).second;322in_flight.erase(iter);323324finish_test(result_handle, test_case_id, tx, hooks);325}326} while (!in_flight.empty() || !scanner.done());327328// Run any exclusive tests that we spotted earlier sequentially.329for (std::vector< engine::scan_result >::const_iterator330iter = exclusive_tests.begin(); iter != exclusive_tests.end();331++iter) {332const pid_and_id_pair data = start_test(333handle, *iter, tx, ids_cache, user_config, hooks);334scheduler::result_handle_ptr result_handle = handle.wait_any();335finish_test(result_handle, data.second, tx, hooks);336}337338tx.commit();339340handle.cleanup();341342return result(scanner.unused_filters());343}344345346