Path: blob/main/contrib/kyua/store/read_transaction.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 "store/read_transaction.hpp"2930extern "C" {31#include <stdint.h>32}3334#include <map>35#include <utility>3637#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/dbtypes.hpp"43#include "store/exceptions.hpp"44#include "store/read_backend.hpp"45#include "utils/datetime.hpp"46#include "utils/format/macros.hpp"47#include "utils/fs/path.hpp"48#include "utils/logging/macros.hpp"49#include "utils/noncopyable.hpp"50#include "utils/optional.ipp"51#include "utils/sanity.hpp"52#include "utils/sqlite/database.hpp"53#include "utils/sqlite/exceptions.hpp"54#include "utils/sqlite/statement.ipp"55#include "utils/sqlite/transaction.hpp"5657namespace datetime = utils::datetime;58namespace fs = utils::fs;59namespace sqlite = utils::sqlite;6061using utils::optional;626364namespace {656667/// Retrieves the environment variables of the context.68///69/// \param db The SQLite database.70///71/// \return The environment variables of the specified context.72///73/// \throw sqlite::error If there is a problem loading the variables.74static std::map< std::string, std::string >75get_env_vars(sqlite::database& db)76{77std::map< std::string, std::string > env;7879sqlite::statement stmt = db.create_statement(80"SELECT var_name, var_value FROM env_vars");8182while (stmt.step()) {83const std::string name = stmt.safe_column_text("var_name");84const std::string value = stmt.safe_column_text("var_value");85env[name] = value;86}8788return env;89}909192/// Retrieves a metadata object.93///94/// \param db The SQLite database.95/// \param metadata_id The identifier of the metadata.96///97/// \return A new metadata object.98static model::metadata99get_metadata(sqlite::database& db, const int64_t metadata_id)100{101model::metadata_builder builder;102103sqlite::statement stmt = db.create_statement(104"SELECT * FROM metadatas WHERE metadata_id == :metadata_id");105stmt.bind(":metadata_id", metadata_id);106while (stmt.step()) {107const std::string name = stmt.safe_column_text("property_name");108const std::string value = stmt.safe_column_text("property_value");109builder.set_string(name, value);110}111112return builder.build();113}114115116/// Gets a file from the database.117///118/// \param db The database to query the file from.119/// \param file_id The identifier of the file to be queried.120///121/// \return A textual representation of the file contents.122///123/// \throw integrity_error If there is any problem in the loaded data or if the124/// file cannot be found.125static std::string126get_file(sqlite::database& db, const int64_t file_id)127{128sqlite::statement stmt = db.create_statement(129"SELECT contents FROM files WHERE file_id == :file_id");130stmt.bind(":file_id", file_id);131if (!stmt.step())132throw store::integrity_error(F("Cannot find referenced file %s") %133file_id);134135try {136const sqlite::blob raw_contents = stmt.safe_column_blob("contents");137const std::string contents(138static_cast< const char *>(raw_contents.memory), raw_contents.size);139140const bool more = stmt.step();141INV(!more);142143return contents;144} catch (const sqlite::error& e) {145throw store::integrity_error(e.what());146}147}148149150/// Gets all the test cases within a particular test program.151///152/// \param db The database to query the information from.153/// \param test_program_id The identifier of the test program whose test cases154/// to query.155///156/// \return The collection of loaded test cases.157///158/// \throw integrity_error If there is any problem in the loaded data.159static model::test_cases_map160get_test_cases(sqlite::database& db, const int64_t test_program_id)161{162model::test_cases_map_builder test_cases;163164sqlite::statement stmt = db.create_statement(165"SELECT name, metadata_id "166"FROM test_cases WHERE test_program_id == :test_program_id");167stmt.bind(":test_program_id", test_program_id);168while (stmt.step()) {169const std::string name = stmt.safe_column_text("name");170const int64_t metadata_id = stmt.safe_column_int64("metadata_id");171172const model::metadata metadata = get_metadata(db, metadata_id);173LD(F("Loaded test case '%s'") % name);174test_cases.add(name, metadata);175}176177return test_cases.build();178}179180181/// Retrieves a result from the database.182///183/// \param stmt The statement with the data for the result to load.184/// \param type_column The name of the column containing the type of the result.185/// \param reason_column The name of the column containing the reason for the186/// result, if any.187///188/// \return The loaded result.189///190/// \throw integrity_error If the data in the database is invalid.191static model::test_result192parse_result(sqlite::statement& stmt, const char* type_column,193const char* reason_column)194{195try {196const model::test_result_type type =197store::column_test_result_type(stmt, type_column);198if (type == model::test_result_passed) {199if (stmt.column_type(stmt.column_id(reason_column)) !=200sqlite::type_null)201throw store::integrity_error("Result of type 'passed' has a "202"non-NULL reason");203return model::test_result(type);204} else {205return model::test_result(type,206stmt.safe_column_text(reason_column));207}208} catch (const sqlite::error& e) {209throw store::integrity_error(e.what());210}211}212213214} // anonymous namespace215216217/// Loads a specific test program from the database.218///219/// \param backend_ The store backend we are dealing with.220/// \param id The identifier of the test program to load.221///222/// \return The instantiated test program.223///224/// \throw integrity_error If the data read from the database cannot be properly225/// interpreted.226model::test_program_ptr227store::detail::get_test_program(read_backend& backend_, const int64_t id)228{229sqlite::database& db = backend_.database();230231model::test_program_ptr test_program;232sqlite::statement stmt = db.create_statement(233"SELECT * FROM test_programs WHERE test_program_id == :id");234stmt.bind(":id", id);235stmt.step();236const std::string interface = stmt.safe_column_text("interface");237test_program.reset(new model::test_program(238interface,239fs::path(stmt.safe_column_text("relative_path")),240fs::path(stmt.safe_column_text("root")),241stmt.safe_column_text("test_suite_name"),242get_metadata(db, stmt.safe_column_int64("metadata_id")),243get_test_cases(db, id)));244const bool more = stmt.step();245INV(!more);246247LD(F("Loaded test program '%s'") % test_program->relative_path());248return test_program;249}250251252/// Internal implementation for a results iterator.253struct store::results_iterator::impl : utils::noncopyable {254/// The store backend we are dealing with.255store::read_backend _backend;256257/// The statement to iterate on.258sqlite::statement _stmt;259260/// A cache for the last loaded test program.261optional< std::pair< int64_t, model::test_program_ptr > >262_last_test_program;263264/// Whether the iterator is still valid or not.265bool _valid;266267/// Constructor.268///269/// \param backend_ The store backend implementation.270impl(store::read_backend& backend_) :271_backend(backend_),272_stmt(backend_.database().create_statement(273"SELECT test_programs.test_program_id, "274" test_programs.interface, "275" test_cases.test_case_id, test_cases.name, "276" test_results.result_type, test_results.result_reason, "277" test_results.start_time, test_results.end_time "278"FROM test_programs "279" JOIN test_cases "280" ON test_programs.test_program_id = test_cases.test_program_id "281" JOIN test_results "282" ON test_cases.test_case_id = test_results.test_case_id "283"ORDER BY test_programs.absolute_path, test_cases.name"))284{285_valid = _stmt.step();286}287};288289290/// Constructor.291///292/// \param pimpl_ The internal implementation details of the iterator.293store::results_iterator::results_iterator(294std::shared_ptr< impl > pimpl_) :295_pimpl(pimpl_)296{297}298299300/// Destructor.301store::results_iterator::~results_iterator(void)302{303}304305306/// Moves the iterator forward by one result.307///308/// \return The iterator itself.309store::results_iterator&310store::results_iterator::operator++(void)311{312_pimpl->_valid = _pimpl->_stmt.step();313return *this;314}315316317/// Checks whether the iterator is still valid.318///319/// \return True if there is more elements to iterate on, false otherwise.320store::results_iterator::operator bool(void) const321{322return _pimpl->_valid;323}324325326/// Gets the test program this result belongs to.327///328/// \return The representation of a test program.329const model::test_program_ptr330store::results_iterator::test_program(void) const331{332const int64_t id = _pimpl->_stmt.safe_column_int64("test_program_id");333if (!_pimpl->_last_test_program ||334_pimpl->_last_test_program.get().first != id)335{336const model::test_program_ptr tp = detail::get_test_program(337_pimpl->_backend, id);338_pimpl->_last_test_program = std::make_pair(id, tp);339}340return _pimpl->_last_test_program.get().second;341}342343344/// Gets the name of the test case pointed by the iterator.345///346/// The caller can look up the test case data by using the find() method on the347/// test program returned by test_program().348///349/// \return A test case name, unique within the test program.350std::string351store::results_iterator::test_case_name(void) const352{353return _pimpl->_stmt.safe_column_text("name");354}355356357/// Gets the result of the test case pointed by the iterator.358///359/// \return A test case result.360model::test_result361store::results_iterator::result(void) const362{363return parse_result(_pimpl->_stmt, "result_type", "result_reason");364}365366367/// Gets the start time of the test case execution.368///369/// \return The time when the test started execution.370datetime::timestamp371store::results_iterator::start_time(void) const372{373return column_timestamp(_pimpl->_stmt, "start_time");374}375376377/// Gets the end time of the test case execution.378///379/// \return The time when the test finished execution.380datetime::timestamp381store::results_iterator::end_time(void) const382{383return column_timestamp(_pimpl->_stmt, "end_time");384}385386387/// Gets a file from a test case.388///389/// \param db The database to query the file from.390/// \param test_case_id The identifier of the test case.391/// \param filename The name of the file to be retrieved.392///393/// \return A textual representation of the file contents.394///395/// \throw integrity_error If there is any problem in the loaded data or if the396/// file cannot be found.397static std::string398get_test_case_file(sqlite::database& db, const int64_t test_case_id,399const char* filename)400{401sqlite::statement stmt = db.create_statement(402"SELECT file_id FROM test_case_files "403"WHERE test_case_id == :test_case_id AND file_name == :file_name");404stmt.bind(":test_case_id", test_case_id);405stmt.bind(":file_name", filename);406if (stmt.step())407return get_file(db, stmt.safe_column_int64("file_id"));408else409return "";410}411412413/// Gets the contents of stdout of a test case.414///415/// \return A textual representation of the stdout contents of the test case.416/// This may of course be empty if the test case didn't print anything.417std::string418store::results_iterator::stdout_contents(void) const419{420return get_test_case_file(_pimpl->_backend.database(),421_pimpl->_stmt.safe_column_int64("test_case_id"),422"__STDOUT__");423}424425426/// Gets the contents of stderr of a test case.427///428/// \return A textual representation of the stderr contents of the test case.429/// This may of course be empty if the test case didn't print anything.430std::string431store::results_iterator::stderr_contents(void) const432{433return get_test_case_file(_pimpl->_backend.database(),434_pimpl->_stmt.safe_column_int64("test_case_id"),435"__STDERR__");436}437438439/// Internal implementation for a store read-only transaction.440struct store::read_transaction::impl : utils::noncopyable {441/// The backend instance.442store::read_backend& _backend;443444/// The SQLite database this transaction deals with.445sqlite::database _db;446447/// The backing SQLite transaction.448sqlite::transaction _tx;449450/// Opens a transaction.451///452/// \param backend_ The backend this transaction is connected to.453impl(read_backend& backend_) :454_backend(backend_),455_db(backend_.database()),456_tx(backend_.database().begin_transaction())457{458}459};460461462/// Creates a new read-only transaction.463///464/// \param backend_ The backend this transaction belongs to.465store::read_transaction::read_transaction(read_backend& backend_) :466_pimpl(new impl(backend_))467{468}469470471/// Destructor.472store::read_transaction::~read_transaction(void)473{474}475476477/// Finishes the transaction.478///479/// This actually commits the result of the transaction, but because the480/// transaction is read-only, we use a different term to denote that there is no481/// distinction between commit and rollback.482///483/// \throw error If there is any problem when talking to the database.484void485store::read_transaction::finish(void)486{487try {488_pimpl->_tx.commit();489} catch (const sqlite::error& e) {490throw error(e.what());491}492}493494495/// Retrieves an context from the database.496///497/// \return The retrieved context.498///499/// \throw error If there is a problem loading the context.500model::context501store::read_transaction::get_context(void)502{503try {504sqlite::statement stmt = _pimpl->_db.create_statement(505"SELECT cwd FROM contexts");506if (!stmt.step())507throw error("Error loading context: no data");508509return model::context(fs::path(stmt.safe_column_text("cwd")),510get_env_vars(_pimpl->_db));511} catch (const sqlite::error& e) {512throw error(F("Error loading context: %s") % e.what());513}514}515516517/// Creates a new iterator to scan tests results.518///519/// \return The constructed iterator.520///521/// \throw error If there is any problem constructing the iterator.522store::results_iterator523store::read_transaction::get_results(void)524{525try {526return results_iterator(std::shared_ptr< results_iterator::impl >(527new results_iterator::impl(_pimpl->_backend)));528} catch (const sqlite::error& e) {529throw error(e.what());530}531}532533534