Path: blob/main/contrib/kyua/store/write_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/write_transaction.hpp"2930extern "C" {31#include <stdint.h>32}3334#include <fstream>35#include <map>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 "model/types.hpp"43#include "store/dbtypes.hpp"44#include "store/exceptions.hpp"45#include "store/write_backend.hpp"46#include "utils/datetime.hpp"47#include "utils/format/macros.hpp"48#include "utils/fs/path.hpp"49#include "utils/logging/macros.hpp"50#include "utils/noncopyable.hpp"51#include "utils/optional.ipp"52#include "utils/sanity.hpp"53#include "utils/stream.hpp"54#include "utils/sqlite/database.hpp"55#include "utils/sqlite/exceptions.hpp"56#include "utils/sqlite/statement.ipp"57#include "utils/sqlite/transaction.hpp"5859namespace datetime = utils::datetime;60namespace fs = utils::fs;61namespace sqlite = utils::sqlite;6263using utils::none;64using utils::optional;656667namespace {686970/// Stores the environment variables of a context.71///72/// \param db The SQLite database.73/// \param env The environment variables to store.74///75/// \throw sqlite::error If there is a problem storing the variables.76static void77put_env_vars(sqlite::database& db,78const std::map< std::string, std::string >& env)79{80sqlite::statement stmt = db.create_statement(81"INSERT INTO env_vars (var_name, var_value) "82"VALUES (:var_name, :var_value)");83for (std::map< std::string, std::string >::const_iterator iter =84env.begin(); iter != env.end(); iter++) {85stmt.bind(":var_name", (*iter).first);86stmt.bind(":var_value", (*iter).second);87stmt.step_without_results();88stmt.reset();89}90}919293/// Calculates the last rowid of a table.94///95/// \param db The SQLite database.96/// \param table Name of the table.97///98/// \return The last rowid; 0 if the table is empty.99static int64_t100last_rowid(sqlite::database& db, const std::string& table)101{102sqlite::statement stmt = db.create_statement(103F("SELECT MAX(ROWID) AS max_rowid FROM %s") % table);104stmt.step();105if (stmt.column_type(0) == sqlite::type_null) {106return 0;107} else {108INV(stmt.column_type(0) == sqlite::type_integer);109return stmt.column_int64(0);110}111}112113114/// Stores a metadata object.115///116/// \param db The database into which to store the information.117/// \param md The metadata to store.118///119/// \return The identifier of the new metadata object.120static int64_t121put_metadata(sqlite::database& db, const model::metadata& md)122{123const model::properties_map props = md.to_properties();124125const int64_t metadata_id = last_rowid(db, "metadatas");126127sqlite::statement stmt = db.create_statement(128"INSERT INTO metadatas (metadata_id, property_name, property_value) "129"VALUES (:metadata_id, :property_name, :property_value)");130stmt.bind(":metadata_id", metadata_id);131132for (model::properties_map::const_iterator iter = props.begin();133iter != props.end(); ++iter) {134stmt.bind(":property_name", (*iter).first);135stmt.bind(":property_value", (*iter).second);136stmt.step_without_results();137stmt.reset();138}139140return metadata_id;141}142143144/// Stores an arbitrary file into the database as a BLOB.145///146/// \param db The database into which to store the file.147/// \param path Path to the file to be stored.148///149/// \return The identifier of the stored file, or none if the file was empty.150///151/// \throw sqlite::error If there are problems writing to the database.152static optional< int64_t >153put_file(sqlite::database& db, const fs::path& path)154{155std::ifstream input(path.c_str());156if (!input)157throw store::error(F("Cannot open file %s") % path);158159try {160if (utils::stream_length(input) == 0)161return none;162} catch (const std::runtime_error& e) {163// Skipping empty files is an optimization. If we fail to calculate the164// size of the file, just ignore the problem. If there are real issues165// with the file, the read below will fail anyway.166LD(F("Cannot determine if file is empty: %s") % e.what());167}168169// TODO(jmmv): This will probably cause an unreasonable amount of memory170// consumption if we decide to store arbitrary files in the database (other171// than stdout or stderr). Should this happen, we need to investigate a172// better way to feel blobs into SQLite.173const std::string contents = utils::read_stream(input);174175sqlite::statement stmt = db.create_statement(176"INSERT INTO files (contents) VALUES (:contents)");177stmt.bind(":contents", sqlite::blob(contents.c_str(), contents.length()));178stmt.step_without_results();179180return optional< int64_t >(db.last_insert_rowid());181}182183184} // anonymous namespace185186187/// Internal implementation for a store write-only transaction.188struct store::write_transaction::impl : utils::noncopyable {189/// The backend instance.190store::write_backend& _backend;191192/// The SQLite database this transaction deals with.193sqlite::database _db;194195/// The backing SQLite transaction.196sqlite::transaction _tx;197198/// Opens a transaction.199///200/// \param backend_ The backend this transaction is connected to.201impl(write_backend& backend_) :202_backend(backend_),203_db(backend_.database()),204_tx(backend_.database().begin_transaction())205{206}207};208209210/// Creates a new write-only transaction.211///212/// \param backend_ The backend this transaction belongs to.213store::write_transaction::write_transaction(write_backend& backend_) :214_pimpl(new impl(backend_))215{216}217218219/// Destructor.220store::write_transaction::~write_transaction(void)221{222}223224225/// Commits the transaction.226///227/// \throw error If there is any problem when talking to the database.228void229store::write_transaction::commit(void)230{231try {232_pimpl->_tx.commit();233} catch (const sqlite::error& e) {234throw error(e.what());235}236}237238239/// Rolls the transaction back.240///241/// \throw error If there is any problem when talking to the database.242void243store::write_transaction::rollback(void)244{245try {246_pimpl->_tx.rollback();247} catch (const sqlite::error& e) {248throw error(e.what());249}250}251252253/// Puts a context into the database.254///255/// \pre The context has not been put yet.256/// \post The context is stored into the database with a new identifier.257///258/// \param context The context to put.259///260/// \throw error If there is any problem when talking to the database.261void262store::write_transaction::put_context(const model::context& context)263{264try {265sqlite::statement stmt = _pimpl->_db.create_statement(266"INSERT INTO contexts (cwd) VALUES (:cwd)");267stmt.bind(":cwd", context.cwd().str());268stmt.step_without_results();269270put_env_vars(_pimpl->_db, context.env());271} catch (const sqlite::error& e) {272throw error(e.what());273}274}275276277/// Puts a test program into the database.278///279/// \pre The test program has not been put yet.280/// \post The test program is stored into the database with a new identifier.281///282/// \param test_program The test program to put.283///284/// \return The identifier of the inserted test program.285///286/// \throw error If there is any problem when talking to the database.287int64_t288store::write_transaction::put_test_program(289const model::test_program& test_program)290{291try {292const int64_t metadata_id = put_metadata(293_pimpl->_db, test_program.get_metadata());294295sqlite::statement stmt = _pimpl->_db.create_statement(296"INSERT INTO test_programs (absolute_path, "297" root, relative_path, test_suite_name, "298" metadata_id, interface) "299"VALUES (:absolute_path, :root, :relative_path, "300" :test_suite_name, :metadata_id, :interface)");301stmt.bind(":absolute_path", test_program.absolute_path().str());302// TODO(jmmv): The root is not necessarily absolute. We need to ensure303// that we can recover the absolute path of the test program. Maybe we304// need to change the test_program to always ensure root is absolute?305stmt.bind(":root", test_program.root().str());306stmt.bind(":relative_path", test_program.relative_path().str());307stmt.bind(":test_suite_name", test_program.test_suite_name());308stmt.bind(":metadata_id", metadata_id);309stmt.bind(":interface", test_program.interface_name());310stmt.step_without_results();311return _pimpl->_db.last_insert_rowid();312} catch (const sqlite::error& e) {313throw error(e.what());314}315}316317318/// Puts a test case into the database.319///320/// \pre The test case has not been put yet.321/// \post The test case is stored into the database with a new identifier.322///323/// \param test_program The program containing the test case to be stored.324/// \param test_case_name The name of the test case to put.325/// \param test_program_id The test program this test case belongs to.326///327/// \return The identifier of the inserted test case.328///329/// \throw error If there is any problem when talking to the database.330int64_t331store::write_transaction::put_test_case(const model::test_program& test_program,332const std::string& test_case_name,333const int64_t test_program_id)334{335const model::test_case& test_case = test_program.find(test_case_name);336337try {338const int64_t metadata_id = put_metadata(339_pimpl->_db, test_case.get_raw_metadata());340341sqlite::statement stmt = _pimpl->_db.create_statement(342"INSERT INTO test_cases (test_program_id, name, metadata_id) "343"VALUES (:test_program_id, :name, :metadata_id)");344stmt.bind(":test_program_id", test_program_id);345stmt.bind(":name", test_case.name());346stmt.bind(":metadata_id", metadata_id);347stmt.step_without_results();348return _pimpl->_db.last_insert_rowid();349} catch (const sqlite::error& e) {350throw error(e.what());351}352}353354355/// Stores a file generated by a test case into the database as a BLOB.356///357/// \param name The name of the file to store in the database. This needs to be358/// unique per test case. The caller is free to decide what names to use359/// for which files. For example, it might make sense to always call360/// __STDOUT__ the stdout of the test case so that it is easy to locate.361/// \param path The path to the file to be stored.362/// \param test_case_id The identifier of the test case this file belongs to.363///364/// \return The identifier of the stored file, or none if the file was empty.365///366/// \throw store::error If there are problems writing to the database.367optional< int64_t >368store::write_transaction::put_test_case_file(const std::string& name,369const fs::path& path,370const int64_t test_case_id)371{372LD(F("Storing %s (%s) of test case %s") % name % path % test_case_id);373try {374const optional< int64_t > file_id = put_file(_pimpl->_db, path);375if (!file_id) {376LD("Not storing empty file");377return none;378}379380sqlite::statement stmt = _pimpl->_db.create_statement(381"INSERT INTO test_case_files (test_case_id, file_name, file_id) "382"VALUES (:test_case_id, :file_name, :file_id)");383stmt.bind(":test_case_id", test_case_id);384stmt.bind(":file_name", name);385stmt.bind(":file_id", file_id.get());386stmt.step_without_results();387388return optional< int64_t >(_pimpl->_db.last_insert_rowid());389} catch (const sqlite::error& e) {390throw error(e.what());391}392}393394395/// Puts a result into the database.396///397/// \pre The result has not been put yet.398/// \post The result is stored into the database with a new identifier.399///400/// \param result The result to put.401/// \param test_case_id The test case this result corresponds to.402/// \param start_time The time when the test started to run.403/// \param end_time The time when the test finished running.404///405/// \return The identifier of the inserted result.406///407/// \throw error If there is any problem when talking to the database.408int64_t409store::write_transaction::put_result(const model::test_result& result,410const int64_t test_case_id,411const datetime::timestamp& start_time,412const datetime::timestamp& end_time)413{414try {415sqlite::statement stmt = _pimpl->_db.create_statement(416"INSERT INTO test_results (test_case_id, result_type, "417" result_reason, start_time, "418" end_time) "419"VALUES (:test_case_id, :result_type, :result_reason, "420" :start_time, :end_time)");421stmt.bind(":test_case_id", test_case_id);422423store::bind_test_result_type(stmt, ":result_type", result.type());424if (result.reason().empty())425stmt.bind(":result_reason", sqlite::null());426else427stmt.bind(":result_reason", result.reason());428429store::bind_timestamp(stmt, ":start_time", start_time);430store::bind_timestamp(stmt, ":end_time", end_time);431432stmt.step_without_results();433const int64_t result_id = _pimpl->_db.last_insert_rowid();434435return result_id;436} catch (const sqlite::error& e) {437throw error(e.what());438}439}440441442