summaryrefslogtreecommitdiff
path: root/store/write_transaction.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'store/write_transaction.cpp')
-rw-r--r--store/write_transaction.cpp440
1 files changed, 440 insertions, 0 deletions
diff --git a/store/write_transaction.cpp b/store/write_transaction.cpp
new file mode 100644
index 0000000000000..134a13a304944
--- /dev/null
+++ b/store/write_transaction.cpp
@@ -0,0 +1,440 @@
+// Copyright 2011 The Kyua Authors.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * 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.
+// * Neither the name of Google Inc. nor the names of its contributors
+// may be used to endorse or promote products derived from this software
+// without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
+// OWNER 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.
+
+#include "store/write_transaction.hpp"
+
+extern "C" {
+#include <stdint.h>
+}
+
+#include <fstream>
+#include <map>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_case.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "model/types.hpp"
+#include "store/dbtypes.hpp"
+#include "store/exceptions.hpp"
+#include "store/write_backend.hpp"
+#include "utils/datetime.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/optional.ipp"
+#include "utils/sanity.hpp"
+#include "utils/stream.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+#include "utils/sqlite/statement.ipp"
+#include "utils/sqlite/transaction.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace sqlite = utils::sqlite;
+
+using utils::none;
+using utils::optional;
+
+
+namespace {
+
+
+/// Stores the environment variables of a context.
+///
+/// \param db The SQLite database.
+/// \param env The environment variables to store.
+///
+/// \throw sqlite::error If there is a problem storing the variables.
+static void
+put_env_vars(sqlite::database& db,
+ const std::map< std::string, std::string >& env)
+{
+ sqlite::statement stmt = db.create_statement(
+ "INSERT INTO env_vars (var_name, var_value) "
+ "VALUES (:var_name, :var_value)");
+ for (std::map< std::string, std::string >::const_iterator iter =
+ env.begin(); iter != env.end(); iter++) {
+ stmt.bind(":var_name", (*iter).first);
+ stmt.bind(":var_value", (*iter).second);
+ stmt.step_without_results();
+ stmt.reset();
+ }
+}
+
+
+/// Calculates the last rowid of a table.
+///
+/// \param db The SQLite database.
+/// \param table Name of the table.
+///
+/// \return The last rowid; 0 if the table is empty.
+static int64_t
+last_rowid(sqlite::database& db, const std::string& table)
+{
+ sqlite::statement stmt = db.create_statement(
+ F("SELECT MAX(ROWID) AS max_rowid FROM %s") % table);
+ stmt.step();
+ if (stmt.column_type(0) == sqlite::type_null) {
+ return 0;
+ } else {
+ INV(stmt.column_type(0) == sqlite::type_integer);
+ return stmt.column_int64(0);
+ }
+}
+
+
+/// Stores a metadata object.
+///
+/// \param db The database into which to store the information.
+/// \param md The metadata to store.
+///
+/// \return The identifier of the new metadata object.
+static int64_t
+put_metadata(sqlite::database& db, const model::metadata& md)
+{
+ const model::properties_map props = md.to_properties();
+
+ const int64_t metadata_id = last_rowid(db, "metadatas");
+
+ sqlite::statement stmt = db.create_statement(
+ "INSERT INTO metadatas (metadata_id, property_name, property_value) "
+ "VALUES (:metadata_id, :property_name, :property_value)");
+ stmt.bind(":metadata_id", metadata_id);
+
+ for (model::properties_map::const_iterator iter = props.begin();
+ iter != props.end(); ++iter) {
+ stmt.bind(":property_name", (*iter).first);
+ stmt.bind(":property_value", (*iter).second);
+ stmt.step_without_results();
+ stmt.reset();
+ }
+
+ return metadata_id;
+}
+
+
+/// Stores an arbitrary file into the database as a BLOB.
+///
+/// \param db The database into which to store the file.
+/// \param path Path to the file to be stored.
+///
+/// \return The identifier of the stored file, or none if the file was empty.
+///
+/// \throw sqlite::error If there are problems writing to the database.
+static optional< int64_t >
+put_file(sqlite::database& db, const fs::path& path)
+{
+ std::ifstream input(path.c_str());
+ if (!input)
+ throw store::error(F("Cannot open file %s") % path);
+
+ try {
+ if (utils::stream_length(input) == 0)
+ return none;
+ } catch (const std::runtime_error& e) {
+ // Skipping empty files is an optimization. If we fail to calculate the
+ // size of the file, just ignore the problem. If there are real issues
+ // with the file, the read below will fail anyway.
+ LD(F("Cannot determine if file is empty: %s") % e.what());
+ }
+
+ // TODO(jmmv): This will probably cause an unreasonable amount of memory
+ // consumption if we decide to store arbitrary files in the database (other
+ // than stdout or stderr). Should this happen, we need to investigate a
+ // better way to feel blobs into SQLite.
+ const std::string contents = utils::read_stream(input);
+
+ sqlite::statement stmt = db.create_statement(
+ "INSERT INTO files (contents) VALUES (:contents)");
+ stmt.bind(":contents", sqlite::blob(contents.c_str(), contents.length()));
+ stmt.step_without_results();
+
+ return optional< int64_t >(db.last_insert_rowid());
+}
+
+
+} // anonymous namespace
+
+
+/// Internal implementation for a store write-only transaction.
+struct store::write_transaction::impl : utils::noncopyable {
+ /// The backend instance.
+ store::write_backend& _backend;
+
+ /// The SQLite database this transaction deals with.
+ sqlite::database _db;
+
+ /// The backing SQLite transaction.
+ sqlite::transaction _tx;
+
+ /// Opens a transaction.
+ ///
+ /// \param backend_ The backend this transaction is connected to.
+ impl(write_backend& backend_) :
+ _backend(backend_),
+ _db(backend_.database()),
+ _tx(backend_.database().begin_transaction())
+ {
+ }
+};
+
+
+/// Creates a new write-only transaction.
+///
+/// \param backend_ The backend this transaction belongs to.
+store::write_transaction::write_transaction(write_backend& backend_) :
+ _pimpl(new impl(backend_))
+{
+}
+
+
+/// Destructor.
+store::write_transaction::~write_transaction(void)
+{
+}
+
+
+/// Commits the transaction.
+///
+/// \throw error If there is any problem when talking to the database.
+void
+store::write_transaction::commit(void)
+{
+ try {
+ _pimpl->_tx.commit();
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Rolls the transaction back.
+///
+/// \throw error If there is any problem when talking to the database.
+void
+store::write_transaction::rollback(void)
+{
+ try {
+ _pimpl->_tx.rollback();
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Puts a context into the database.
+///
+/// \pre The context has not been put yet.
+/// \post The context is stored into the database with a new identifier.
+///
+/// \param context The context to put.
+///
+/// \throw error If there is any problem when talking to the database.
+void
+store::write_transaction::put_context(const model::context& context)
+{
+ try {
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "INSERT INTO contexts (cwd) VALUES (:cwd)");
+ stmt.bind(":cwd", context.cwd().str());
+ stmt.step_without_results();
+
+ put_env_vars(_pimpl->_db, context.env());
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Puts a test program into the database.
+///
+/// \pre The test program has not been put yet.
+/// \post The test program is stored into the database with a new identifier.
+///
+/// \param test_program The test program to put.
+///
+/// \return The identifier of the inserted test program.
+///
+/// \throw error If there is any problem when talking to the database.
+int64_t
+store::write_transaction::put_test_program(
+ const model::test_program& test_program)
+{
+ try {
+ const int64_t metadata_id = put_metadata(
+ _pimpl->_db, test_program.get_metadata());
+
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "INSERT INTO test_programs (absolute_path, "
+ " root, relative_path, test_suite_name, "
+ " metadata_id, interface) "
+ "VALUES (:absolute_path, :root, :relative_path, "
+ " :test_suite_name, :metadata_id, :interface)");
+ stmt.bind(":absolute_path", test_program.absolute_path().str());
+ // TODO(jmmv): The root is not necessarily absolute. We need to ensure
+ // that we can recover the absolute path of the test program. Maybe we
+ // need to change the test_program to always ensure root is absolute?
+ stmt.bind(":root", test_program.root().str());
+ stmt.bind(":relative_path", test_program.relative_path().str());
+ stmt.bind(":test_suite_name", test_program.test_suite_name());
+ stmt.bind(":metadata_id", metadata_id);
+ stmt.bind(":interface", test_program.interface_name());
+ stmt.step_without_results();
+ return _pimpl->_db.last_insert_rowid();
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Puts a test case into the database.
+///
+/// \pre The test case has not been put yet.
+/// \post The test case is stored into the database with a new identifier.
+///
+/// \param test_program The program containing the test case to be stored.
+/// \param test_case_name The name of the test case to put.
+/// \param test_program_id The test program this test case belongs to.
+///
+/// \return The identifier of the inserted test case.
+///
+/// \throw error If there is any problem when talking to the database.
+int64_t
+store::write_transaction::put_test_case(const model::test_program& test_program,
+ const std::string& test_case_name,
+ const int64_t test_program_id)
+{
+ const model::test_case& test_case = test_program.find(test_case_name);
+
+ try {
+ const int64_t metadata_id = put_metadata(
+ _pimpl->_db, test_case.get_raw_metadata());
+
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "INSERT INTO test_cases (test_program_id, name, metadata_id) "
+ "VALUES (:test_program_id, :name, :metadata_id)");
+ stmt.bind(":test_program_id", test_program_id);
+ stmt.bind(":name", test_case.name());
+ stmt.bind(":metadata_id", metadata_id);
+ stmt.step_without_results();
+ return _pimpl->_db.last_insert_rowid();
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Stores a file generated by a test case into the database as a BLOB.
+///
+/// \param name The name of the file to store in the database. This needs to be
+/// unique per test case. The caller is free to decide what names to use
+/// for which files. For example, it might make sense to always call
+/// __STDOUT__ the stdout of the test case so that it is easy to locate.
+/// \param path The path to the file to be stored.
+/// \param test_case_id The identifier of the test case this file belongs to.
+///
+/// \return The identifier of the stored file, or none if the file was empty.
+///
+/// \throw store::error If there are problems writing to the database.
+optional< int64_t >
+store::write_transaction::put_test_case_file(const std::string& name,
+ const fs::path& path,
+ const int64_t test_case_id)
+{
+ LD(F("Storing %s (%s) of test case %s") % name % path % test_case_id);
+ try {
+ const optional< int64_t > file_id = put_file(_pimpl->_db, path);
+ if (!file_id) {
+ LD("Not storing empty file");
+ return none;
+ }
+
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "INSERT INTO test_case_files (test_case_id, file_name, file_id) "
+ "VALUES (:test_case_id, :file_name, :file_id)");
+ stmt.bind(":test_case_id", test_case_id);
+ stmt.bind(":file_name", name);
+ stmt.bind(":file_id", file_id.get());
+ stmt.step_without_results();
+
+ return optional< int64_t >(_pimpl->_db.last_insert_rowid());
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Puts a result into the database.
+///
+/// \pre The result has not been put yet.
+/// \post The result is stored into the database with a new identifier.
+///
+/// \param result The result to put.
+/// \param test_case_id The test case this result corresponds to.
+/// \param start_time The time when the test started to run.
+/// \param end_time The time when the test finished running.
+///
+/// \return The identifier of the inserted result.
+///
+/// \throw error If there is any problem when talking to the database.
+int64_t
+store::write_transaction::put_result(const model::test_result& result,
+ const int64_t test_case_id,
+ const datetime::timestamp& start_time,
+ const datetime::timestamp& end_time)
+{
+ try {
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "INSERT INTO test_results (test_case_id, result_type, "
+ " result_reason, start_time, "
+ " end_time) "
+ "VALUES (:test_case_id, :result_type, :result_reason, "
+ " :start_time, :end_time)");
+ stmt.bind(":test_case_id", test_case_id);
+
+ store::bind_test_result_type(stmt, ":result_type", result.type());
+ if (result.reason().empty())
+ stmt.bind(":result_reason", sqlite::null());
+ else
+ stmt.bind(":result_reason", result.reason());
+
+ store::bind_timestamp(stmt, ":start_time", start_time);
+ store::bind_timestamp(stmt, ":end_time", end_time);
+
+ stmt.step_without_results();
+ const int64_t result_id = _pimpl->_db.last_insert_rowid();
+
+ return result_id;
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}