aboutsummaryrefslogtreecommitdiffstats
path: root/store
diff options
context:
space:
mode:
authorBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
committerBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
commit08334c51dbb99d9ecd2bb86a2d94ed06da9e167a (patch)
treec43eb24d59bd5c963583a5190caef80fc8387322 /store
downloadsrc-08334c51dbb99d9ecd2bb86a2d94ed06da9e167a.tar.gz
src-08334c51dbb99d9ecd2bb86a2d94ed06da9e167a.zip
Import the kyua testing framework for infrastructure softwarevendor/kyua/0.13-a685f91vendor/kyua
Imported at 0.13 plus assumulated changes to git hash a685f91. Obtained from: https://github.com/jmmv/kyua Sponsored by: DARPA
Notes
Notes: svn path=/vendor/kyua/dist/; revision=359042 svn path=/vendor/kyua/0.13-a685f91/; revision=359043; tag=vendor/kyua/0.13-a685f91
Diffstat (limited to 'store')
-rw-r--r--store/Kyuafile15
-rw-r--r--store/Makefile.am.inc145
-rw-r--r--store/dbtypes.cpp255
-rw-r--r--store/dbtypes.hpp68
-rw-r--r--store/dbtypes_test.cpp234
-rw-r--r--store/exceptions.cpp88
-rw-r--r--store/exceptions.hpp72
-rw-r--r--store/exceptions_test.cpp65
-rw-r--r--store/layout.cpp264
-rw-r--r--store/layout.hpp84
-rw-r--r--store/layout_fwd.hpp54
-rw-r--r--store/layout_test.cpp350
-rw-r--r--store/metadata.cpp137
-rw-r--r--store/metadata.hpp68
-rw-r--r--store/metadata_fwd.hpp43
-rw-r--r--store/metadata_test.cpp154
-rw-r--r--store/migrate.cpp287
-rw-r--r--store/migrate.hpp55
-rw-r--r--store/migrate_test.cpp132
-rw-r--r--store/migrate_v1_v2.sql357
-rw-r--r--store/migrate_v2_v3.sql120
-rw-r--r--store/read_backend.cpp160
-rw-r--r--store/read_backend.hpp77
-rw-r--r--store/read_backend_fwd.hpp43
-rw-r--r--store/read_backend_test.cpp152
-rw-r--r--store/read_transaction.cpp532
-rw-r--r--store/read_transaction.hpp120
-rw-r--r--store/read_transaction_fwd.hpp44
-rw-r--r--store/read_transaction_test.cpp262
-rw-r--r--store/schema_inttest.cpp492
-rw-r--r--store/schema_v1.sql314
-rw-r--r--store/schema_v2.sql293
-rw-r--r--store/schema_v3.sql255
-rw-r--r--store/testdata_v1.sql330
-rw-r--r--store/testdata_v2.sql462
-rw-r--r--store/testdata_v3_1.sql42
-rw-r--r--store/testdata_v3_2.sql190
-rw-r--r--store/testdata_v3_3.sql171
-rw-r--r--store/testdata_v3_4.sql141
-rw-r--r--store/transaction_test.cpp170
-rw-r--r--store/write_backend.cpp208
-rw-r--r--store/write_backend.hpp81
-rw-r--r--store/write_backend_fwd.hpp52
-rw-r--r--store/write_backend_test.cpp204
-rw-r--r--store/write_transaction.cpp440
-rw-r--r--store/write_transaction.hpp89
-rw-r--r--store/write_transaction_fwd.hpp43
-rw-r--r--store/write_transaction_test.cpp416
48 files changed, 8830 insertions, 0 deletions
diff --git a/store/Kyuafile b/store/Kyuafile
new file mode 100644
index 000000000000..ada2f7c0e88c
--- /dev/null
+++ b/store/Kyuafile
@@ -0,0 +1,15 @@
+syntax(2)
+
+test_suite("kyua")
+
+atf_test_program{name="dbtypes_test"}
+atf_test_program{name="exceptions_test"}
+atf_test_program{name="layout_test"}
+atf_test_program{name="metadata_test"}
+atf_test_program{name="migrate_test"}
+atf_test_program{name="read_backend_test"}
+atf_test_program{name="read_transaction_test"}
+atf_test_program{name="schema_inttest"}
+atf_test_program{name="transaction_test"}
+atf_test_program{name="write_backend_test"}
+atf_test_program{name="write_transaction_test"}
diff --git a/store/Makefile.am.inc b/store/Makefile.am.inc
new file mode 100644
index 000000000000..13c7c70a10d9
--- /dev/null
+++ b/store/Makefile.am.inc
@@ -0,0 +1,145 @@
+# Copyright 2010 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.
+
+STORE_CFLAGS = $(MODEL_CFLAGS) $(UTILS_CFLAGS)
+STORE_LIBS = libstore.a $(MODEL_LIBS) $(UTILS_LIBS)
+
+noinst_LIBRARIES += libstore.a
+libstore_a_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\"
+libstore_a_CPPFLAGS += $(UTILS_CFLAGS)
+libstore_a_SOURCES = store/dbtypes.cpp
+libstore_a_SOURCES += store/dbtypes.hpp
+libstore_a_SOURCES += store/exceptions.cpp
+libstore_a_SOURCES += store/exceptions.hpp
+libstore_a_SOURCES += store/layout.cpp
+libstore_a_SOURCES += store/layout.hpp
+libstore_a_SOURCES += store/layout_fwd.hpp
+libstore_a_SOURCES += store/metadata.cpp
+libstore_a_SOURCES += store/metadata.hpp
+libstore_a_SOURCES += store/metadata_fwd.hpp
+libstore_a_SOURCES += store/migrate.cpp
+libstore_a_SOURCES += store/migrate.hpp
+libstore_a_SOURCES += store/read_backend.cpp
+libstore_a_SOURCES += store/read_backend.hpp
+libstore_a_SOURCES += store/read_backend_fwd.hpp
+libstore_a_SOURCES += store/read_transaction.cpp
+libstore_a_SOURCES += store/read_transaction.hpp
+libstore_a_SOURCES += store/read_transaction_fwd.hpp
+libstore_a_SOURCES += store/write_backend.cpp
+libstore_a_SOURCES += store/write_backend.hpp
+libstore_a_SOURCES += store/write_backend_fwd.hpp
+libstore_a_SOURCES += store/write_transaction.cpp
+libstore_a_SOURCES += store/write_transaction.hpp
+libstore_a_SOURCES += store/write_transaction_fwd.hpp
+
+dist_store_DATA = store/migrate_v1_v2.sql
+dist_store_DATA += store/migrate_v2_v3.sql
+dist_store_DATA += store/schema_v3.sql
+
+if WITH_ATF
+tests_storedir = $(pkgtestsdir)/store
+
+tests_store_DATA = store/Kyuafile
+tests_store_DATA += store/schema_v1.sql
+tests_store_DATA += store/schema_v2.sql
+tests_store_DATA += store/testdata_v1.sql
+tests_store_DATA += store/testdata_v2.sql
+tests_store_DATA += store/testdata_v3_1.sql
+tests_store_DATA += store/testdata_v3_2.sql
+tests_store_DATA += store/testdata_v3_3.sql
+tests_store_DATA += store/testdata_v3_4.sql
+EXTRA_DIST += $(tests_store_DATA)
+
+tests_store_PROGRAMS = store/dbtypes_test
+store_dbtypes_test_SOURCES = store/dbtypes_test.cpp
+store_dbtypes_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_dbtypes_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/exceptions_test
+store_exceptions_test_SOURCES = store/exceptions_test.cpp
+store_exceptions_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_exceptions_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/layout_test
+store_layout_test_SOURCES = store/layout_test.cpp
+store_layout_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS)
+store_layout_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/metadata_test
+store_metadata_test_SOURCES = store/metadata_test.cpp
+store_metadata_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_metadata_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/migrate_test
+store_migrate_test_SOURCES = store/migrate_test.cpp
+store_migrate_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\"
+store_migrate_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS)
+store_migrate_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/read_backend_test
+store_read_backend_test_SOURCES = store/read_backend_test.cpp
+store_read_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_read_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/read_transaction_test
+store_read_transaction_test_SOURCES = store/read_transaction_test.cpp
+store_read_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_read_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/schema_inttest
+store_schema_inttest_SOURCES = store/schema_inttest.cpp
+store_schema_inttest_CPPFLAGS = -DKYUA_STORETESTDATADIR=\"$(tests_storedir)\"
+store_schema_inttest_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_schema_inttest_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/transaction_test
+store_transaction_test_SOURCES = store/transaction_test.cpp
+store_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/write_backend_test
+store_write_backend_test_SOURCES = store/write_backend_test.cpp
+store_write_backend_test_CPPFLAGS = -DKYUA_STOREDIR=\"$(storedir)\"
+store_write_backend_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_write_backend_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) $(ATF_CXX_LIBS)
+
+tests_store_PROGRAMS += store/write_transaction_test
+store_write_transaction_test_SOURCES = store/write_transaction_test.cpp
+store_write_transaction_test_CXXFLAGS = $(STORE_CFLAGS) $(ENGINE_CFLAGS) \
+ $(ATF_CXX_CFLAGS)
+store_write_transaction_test_LDADD = $(STORE_LIBS) $(ENGINE_LIBS) \
+ $(ATF_CXX_LIBS)
+endif
diff --git a/store/dbtypes.cpp b/store/dbtypes.cpp
new file mode 100644
index 000000000000..3ff755aa3307
--- /dev/null
+++ b/store/dbtypes.cpp
@@ -0,0 +1,255 @@
+// 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/dbtypes.hpp"
+
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/exceptions.hpp"
+#include "utils/datetime.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/sanity.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace datetime = utils::datetime;
+namespace sqlite = utils::sqlite;
+
+
+/// Binds a boolean value to a statement parameter.
+///
+/// \param stmt The statement to which to bind the parameter.
+/// \param field The name of the parameter; must exist.
+/// \param value The value to bind.
+void
+store::bind_bool(sqlite::statement& stmt, const char* field, const bool value)
+{
+ stmt.bind(field, value ? "true" : "false");
+}
+
+
+/// Binds a time delta to a statement parameter.
+///
+/// \param stmt The statement to which to bind the parameter.
+/// \param field The name of the parameter; must exist.
+/// \param delta The value to bind.
+void
+store::bind_delta(sqlite::statement& stmt, const char* field,
+ const datetime::delta& delta)
+{
+ stmt.bind(field, static_cast< int64_t >(delta.to_microseconds()));
+}
+
+
+/// Binds a string to a statement parameter.
+///
+/// If the string is not empty, this binds the string itself. Otherwise, it
+/// binds a NULL value.
+///
+/// \param stmt The statement to which to bind the parameter.
+/// \param field The name of the parameter; must exist.
+/// \param str The string to bind.
+void
+store::bind_optional_string(sqlite::statement& stmt, const char* field,
+ const std::string& str)
+{
+ if (str.empty())
+ stmt.bind(field, sqlite::null());
+ else
+ stmt.bind(field, str);
+}
+
+
+/// Binds a test result type to a statement parameter.
+///
+/// \param stmt The statement to which to bind the parameter.
+/// \param field The name of the parameter; must exist.
+/// \param type The result type to bind.
+void
+store::bind_test_result_type(sqlite::statement& stmt, const char* field,
+ const model::test_result_type& type)
+{
+ switch (type) {
+ case model::test_result_broken:
+ stmt.bind(field, "broken");
+ break;
+
+ case model::test_result_expected_failure:
+ stmt.bind(field, "expected_failure");
+ break;
+
+ case model::test_result_failed:
+ stmt.bind(field, "failed");
+ break;
+
+ case model::test_result_passed:
+ stmt.bind(field, "passed");
+ break;
+
+ case model::test_result_skipped:
+ stmt.bind(field, "skipped");
+ break;
+
+ default:
+ UNREACHABLE;
+ }
+}
+
+
+/// Binds a timestamp to a statement parameter.
+///
+/// \param stmt The statement to which to bind the parameter.
+/// \param field The name of the parameter; must exist.
+/// \param timestamp The value to bind.
+void
+store::bind_timestamp(sqlite::statement& stmt, const char* field,
+ const datetime::timestamp& timestamp)
+{
+ stmt.bind(field, timestamp.to_microseconds());
+}
+
+
+/// Queries a boolean value from a statement.
+///
+/// \param stmt The statement from which to get the column.
+/// \param column The name of the column holding the value.
+///
+/// \return The parsed value if all goes well.
+///
+/// \throw integrity_error If the value in the specified column is invalid.
+bool
+store::column_bool(sqlite::statement& stmt, const char* column)
+{
+ const int id = stmt.column_id(column);
+ if (stmt.column_type(id) != sqlite::type_text)
+ throw store::integrity_error(F("Boolean value in column %s is not a "
+ "string") % column);
+ const std::string value = stmt.column_text(id);
+ if (value == "true")
+ return true;
+ else if (value == "false")
+ return false;
+ else
+ throw store::integrity_error(F("Unknown boolean value '%s'") % value);
+}
+
+
+/// Queries a time delta from a statement.
+///
+/// \param stmt The statement from which to get the column.
+/// \param column The name of the column holding the value.
+///
+/// \return The parsed value if all goes well.
+///
+/// \throw integrity_error If the value in the specified column is invalid.
+datetime::delta
+store::column_delta(sqlite::statement& stmt, const char* column)
+{
+ const int id = stmt.column_id(column);
+ if (stmt.column_type(id) != sqlite::type_integer)
+ throw store::integrity_error(F("Time delta in column %s is not an "
+ "integer") % column);
+ return datetime::delta::from_microseconds(stmt.column_int64(id));
+}
+
+
+/// Queries an optional string from a statement.
+///
+/// \param stmt The statement from which to get the column.
+/// \param column The name of the column holding the value.
+///
+/// \return The parsed value if all goes well.
+///
+/// \throw integrity_error If the value in the specified column is invalid.
+std::string
+store::column_optional_string(sqlite::statement& stmt, const char* column)
+{
+ const int id = stmt.column_id(column);
+ switch (stmt.column_type(id)) {
+ case sqlite::type_text:
+ return stmt.column_text(id);
+ case sqlite::type_null:
+ return "";
+ default:
+ throw integrity_error(F("Invalid string type in column %s") % column);
+ }
+}
+
+
+/// Queries a test result type from a statement.
+///
+/// \param stmt The statement from which to get the column.
+/// \param column The name of the column holding the value.
+///
+/// \return The parsed value if all goes well.
+///
+/// \throw integrity_error If the value in the specified column is invalid.
+model::test_result_type
+store::column_test_result_type(sqlite::statement& stmt, const char* column)
+{
+ const int id = stmt.column_id(column);
+ if (stmt.column_type(id) != sqlite::type_text)
+ throw store::integrity_error(F("Result type in column %s is not a "
+ "string") % column);
+ const std::string type = stmt.column_text(id);
+ if (type == "passed") {
+ return model::test_result_passed;
+ } else if (type == "broken") {
+ return model::test_result_broken;
+ } else if (type == "expected_failure") {
+ return model::test_result_expected_failure;
+ } else if (type == "failed") {
+ return model::test_result_failed;
+ } else if (type == "skipped") {
+ return model::test_result_skipped;
+ } else {
+ throw store::integrity_error(F("Unknown test result type %s") % type);
+ }
+}
+
+
+/// Queries a timestamp from a statement.
+///
+/// \param stmt The statement from which to get the column.
+/// \param column The name of the column holding the value.
+///
+/// \return The parsed value if all goes well.
+///
+/// \throw integrity_error If the value in the specified column is invalid.
+datetime::timestamp
+store::column_timestamp(sqlite::statement& stmt, const char* column)
+{
+ const int id = stmt.column_id(column);
+ if (stmt.column_type(id) != sqlite::type_integer)
+ throw store::integrity_error(F("Timestamp in column %s is not an "
+ "integer") % column);
+ const int64_t value = stmt.column_int64(id);
+ if (value < 0)
+ throw store::integrity_error(F("Timestamp in column %s must be "
+ "positive") % column);
+ return datetime::timestamp::from_microseconds(value);
+}
diff --git a/store/dbtypes.hpp b/store/dbtypes.hpp
new file mode 100644
index 000000000000..919d088d0ecd
--- /dev/null
+++ b/store/dbtypes.hpp
@@ -0,0 +1,68 @@
+// 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.
+
+/// \file store/dbtypes.hpp
+/// Functions to internalize/externalize various types.
+///
+/// These helper functions are only provided to help in the implementation of
+/// other modules. Therefore, this header file should never be included from
+/// other header files.
+
+#if defined(STORE_DBTYPES_HPP)
+# error "Do not include dbtypes.hpp multiple times"
+#endif // !defined(STORE_DBTYPES_HPP)
+#define STORE_DBTYPES_HPP
+
+#include <string>
+
+#include "model/test_result_fwd.hpp"
+#include "utils/datetime_fwd.hpp"
+#include "utils/sqlite/statement_fwd.hpp"
+
+namespace store {
+
+
+void bind_bool(utils::sqlite::statement&, const char*, const bool);
+void bind_delta(utils::sqlite::statement&, const char*,
+ const utils::datetime::delta&);
+void bind_optional_string(utils::sqlite::statement&, const char*,
+ const std::string&);
+void bind_test_result_type(utils::sqlite::statement&, const char*,
+ const model::test_result_type&);
+void bind_timestamp(utils::sqlite::statement&, const char*,
+ const utils::datetime::timestamp&);
+bool column_bool(utils::sqlite::statement&, const char*);
+utils::datetime::delta column_delta(utils::sqlite::statement&, const char*);
+std::string column_optional_string(utils::sqlite::statement&, const char*);
+model::test_result_type column_test_result_type(
+ utils::sqlite::statement&, const char*);
+utils::datetime::timestamp column_timestamp(utils::sqlite::statement&,
+ const char*);
+
+
+} // namespace store
diff --git a/store/dbtypes_test.cpp b/store/dbtypes_test.cpp
new file mode 100644
index 000000000000..abe229eab2b6
--- /dev/null
+++ b/store/dbtypes_test.cpp
@@ -0,0 +1,234 @@
+// 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/dbtypes.hpp"
+
+#include <atf-c++.hpp>
+
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/exceptions.hpp"
+#include "utils/datetime.hpp"
+#include "utils/optional.ipp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace sqlite = utils::sqlite;
+
+using utils::none;
+
+
+namespace {
+
+
+/// Validates that a particular bind_x/column_x sequence works.
+///
+/// \param bind The store::bind_* function to put the value.
+/// \param value The value to store and validate.
+/// \param column The store::column_* function to get the value.
+template< typename Type1, typename Type2, typename Type3 >
+static void
+do_ok_test(void (*bind)(sqlite::statement&, const char*, Type1),
+ Type2 value,
+ Type3 (*column)(sqlite::statement&, const char*))
+{
+ sqlite::database db = sqlite::database::in_memory();
+ db.exec("CREATE TABLE test (column DONTCARE)");
+
+ sqlite::statement insert = db.create_statement("INSERT INTO test "
+ "VALUES (:v)");
+ bind(insert, ":v", value);
+ insert.step_without_results();
+
+ sqlite::statement query = db.create_statement("SELECT * FROM test");
+ ATF_REQUIRE(query.step());
+ ATF_REQUIRE(column(query, "column") == value);
+ ATF_REQUIRE(!query.step());
+}
+
+
+/// Validates an error condition of column_*.
+///
+/// \param value The invalid value to insert into the database.
+/// \param column The store::column_* function to get the value.
+/// \param error_regexp The expected message in the raised integrity_error.
+template< typename Type1, typename Type2 >
+static void
+do_invalid_test(Type1 value,
+ Type2 (*column)(sqlite::statement&, const char*),
+ const std::string& error_regexp)
+{
+ sqlite::database db = sqlite::database::in_memory();
+ db.exec("CREATE TABLE test (column DONTCARE)");
+
+ sqlite::statement insert = db.create_statement("INSERT INTO test "
+ "VALUES (:v)");
+ insert.bind(":v", value);
+ insert.step_without_results();
+
+ sqlite::statement query = db.create_statement("SELECT * FROM test");
+ ATF_REQUIRE(query.step());
+ ATF_REQUIRE_THROW_RE(store::integrity_error, error_regexp,
+ column(query, "column"));
+ ATF_REQUIRE(!query.step());
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(bool__ok);
+ATF_TEST_CASE_BODY(bool__ok)
+{
+ do_ok_test(store::bind_bool, true, store::column_bool);
+ do_ok_test(store::bind_bool, false, store::column_bool);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_type);
+ATF_TEST_CASE_BODY(bool__get_invalid_type)
+{
+ do_invalid_test(123, store::column_bool, "not a string");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(bool__get_invalid_value);
+ATF_TEST_CASE_BODY(bool__get_invalid_value)
+{
+ do_invalid_test("foo", store::column_bool, "Unknown boolean.*foo");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(delta__ok);
+ATF_TEST_CASE_BODY(delta__ok)
+{
+ do_ok_test(store::bind_delta, datetime::delta(15, 34), store::column_delta);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(delta__get_invalid_type);
+ATF_TEST_CASE_BODY(delta__get_invalid_type)
+{
+ do_invalid_test(15.6, store::column_delta, "not an integer");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(optional_string__ok);
+ATF_TEST_CASE_BODY(optional_string__ok)
+{
+ do_ok_test(store::bind_optional_string, "", store::column_optional_string);
+ do_ok_test(store::bind_optional_string, "a", store::column_optional_string);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(optional_string__get_invalid_type);
+ATF_TEST_CASE_BODY(optional_string__get_invalid_type)
+{
+ do_invalid_test(35, store::column_optional_string, "Invalid string");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__ok);
+ATF_TEST_CASE_BODY(test_result_type__ok)
+{
+ do_ok_test(store::bind_test_result_type,
+ model::test_result_passed,
+ store::column_test_result_type);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_type);
+ATF_TEST_CASE_BODY(test_result_type__get_invalid_type)
+{
+ do_invalid_test(12, store::column_test_result_type, "not a string");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_result_type__get_invalid_value);
+ATF_TEST_CASE_BODY(test_result_type__get_invalid_value)
+{
+ do_invalid_test("foo", store::column_test_result_type,
+ "Unknown test result type foo");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(timestamp__ok);
+ATF_TEST_CASE_BODY(timestamp__ok)
+{
+ do_ok_test(store::bind_timestamp,
+ datetime::timestamp::from_microseconds(0),
+ store::column_timestamp);
+ do_ok_test(store::bind_timestamp,
+ datetime::timestamp::from_microseconds(123),
+ store::column_timestamp);
+
+ do_ok_test(store::bind_timestamp,
+ datetime::timestamp::from_values(2012, 2, 9, 23, 15, 51, 987654),
+ store::column_timestamp);
+ do_ok_test(store::bind_timestamp,
+ datetime::timestamp::from_values(1980, 1, 2, 3, 4, 5, 0),
+ store::column_timestamp);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_type);
+ATF_TEST_CASE_BODY(timestamp__get_invalid_type)
+{
+ do_invalid_test(35.6, store::column_timestamp, "not an integer");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(timestamp__get_invalid_value);
+ATF_TEST_CASE_BODY(timestamp__get_invalid_value)
+{
+ do_invalid_test(-1234, store::column_timestamp, "must be positive");
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, bool__ok);
+ ATF_ADD_TEST_CASE(tcs, bool__get_invalid_type);
+ ATF_ADD_TEST_CASE(tcs, bool__get_invalid_value);
+
+ ATF_ADD_TEST_CASE(tcs, delta__ok);
+ ATF_ADD_TEST_CASE(tcs, delta__get_invalid_type);
+
+ ATF_ADD_TEST_CASE(tcs, optional_string__ok);
+ ATF_ADD_TEST_CASE(tcs, optional_string__get_invalid_type);
+
+ ATF_ADD_TEST_CASE(tcs, test_result_type__ok);
+ ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_type);
+ ATF_ADD_TEST_CASE(tcs, test_result_type__get_invalid_value);
+
+ ATF_ADD_TEST_CASE(tcs, timestamp__ok);
+ ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_type);
+ ATF_ADD_TEST_CASE(tcs, timestamp__get_invalid_value);
+}
diff --git a/store/exceptions.cpp b/store/exceptions.cpp
new file mode 100644
index 000000000000..7459f3db75ac
--- /dev/null
+++ b/store/exceptions.cpp
@@ -0,0 +1,88 @@
+// 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/exceptions.hpp"
+
+#include "utils/format/macros.hpp"
+
+
+/// Constructs a new error with a plain-text message.
+///
+/// \param message The plain-text error message.
+store::error::error(const std::string& message) :
+ std::runtime_error(message)
+{
+}
+
+
+/// Destructor for the error.
+store::error::~error(void) throw()
+{
+}
+
+
+/// Constructs a new error with a plain-text message.
+///
+/// \param message The plain-text error message.
+store::integrity_error::integrity_error(const std::string& message) :
+ error(message)
+{
+}
+
+
+/// Destructor for the error.
+store::integrity_error::~integrity_error(void) throw()
+{
+}
+
+
+/// Constructs a new error with a plain-text message.
+///
+/// \param version Version of the current schema.
+store::old_schema_error::old_schema_error(const int version) :
+ error(F("The database contains version %s of the schema, which is "
+ "stale and needs to be upgraded") % version),
+ _old_version(version)
+{
+}
+
+
+/// Destructor for the error.
+store::old_schema_error::~old_schema_error(void) throw()
+{
+}
+
+
+/// Returns the current schema version in the database.
+///
+/// \return A version number.
+int
+store::old_schema_error::old_version(void) const
+{
+ return _old_version;
+}
diff --git a/store/exceptions.hpp b/store/exceptions.hpp
new file mode 100644
index 000000000000..e27c7a02fe3a
--- /dev/null
+++ b/store/exceptions.hpp
@@ -0,0 +1,72 @@
+// 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.
+
+/// \file store/exceptions.hpp
+/// Exception types raised by the store module.
+
+#if !defined(STORE_EXCEPTIONS_HPP)
+#define STORE_EXCEPTIONS_HPP
+
+#include <stdexcept>
+
+namespace store {
+
+
+/// Base exception for store errors.
+class error : public std::runtime_error {
+public:
+ explicit error(const std::string&);
+ virtual ~error(void) throw();
+};
+
+
+/// The data in the database is inconsistent.
+class integrity_error : public error {
+public:
+ explicit integrity_error(const std::string&);
+ virtual ~integrity_error(void) throw();
+};
+
+
+/// The database schema is old and needs a migration.
+class old_schema_error : public error {
+ /// Version in the database that caused this error.
+ int _old_version;
+
+public:
+ explicit old_schema_error(const int);
+ virtual ~old_schema_error(void) throw();
+
+ int old_version(void) const;
+};
+
+
+} // namespace store
+
+
+#endif // !defined(STORE_EXCEPTIONS_HPP)
diff --git a/store/exceptions_test.cpp b/store/exceptions_test.cpp
new file mode 100644
index 000000000000..ce364e26293c
--- /dev/null
+++ b/store/exceptions_test.cpp
@@ -0,0 +1,65 @@
+// 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/exceptions.hpp"
+
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(error);
+ATF_TEST_CASE_BODY(error)
+{
+ const store::error e("Some text");
+ ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(integrity_error);
+ATF_TEST_CASE_BODY(integrity_error)
+{
+ const store::integrity_error e("Some text");
+ ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(old_schema_error);
+ATF_TEST_CASE_BODY(old_schema_error)
+{
+ const store::old_schema_error e(15);
+ ATF_REQUIRE_MATCH("version 15 .*upgraded", e.what());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, error);
+ ATF_ADD_TEST_CASE(tcs, integrity_error);
+ ATF_ADD_TEST_CASE(tcs, old_schema_error);
+}
diff --git a/store/layout.cpp b/store/layout.cpp
new file mode 100644
index 000000000000..f69cd96cb48d
--- /dev/null
+++ b/store/layout.cpp
@@ -0,0 +1,264 @@
+// Copyright 2014 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/layout.hpp"
+
+#include <algorithm>
+#include <cstring>
+
+#include "store/exceptions.hpp"
+#include "utils/datetime.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/directory.hpp"
+#include "utils/fs/exceptions.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/env.hpp"
+#include "utils/optional.ipp"
+#include "utils/sanity.hpp"
+#include "utils/text/exceptions.hpp"
+#include "utils/text/regex.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace layout = store::layout;
+namespace text = utils::text;
+
+using utils::optional;
+
+
+namespace {
+
+
+/// Finds the results file for the latest run of the given test suite.
+///
+/// \param test_suite Identifier of the test suite to query.
+///
+/// \return Path to the located database holding the most recent data for the
+/// given test suite.
+///
+/// \throw store::error If no previous results file can be found.
+static fs::path
+find_latest(const std::string& test_suite)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ try {
+ const text::regex preg = text::regex::compile(
+ F("^results.%s.[0-9]{8}-[0-9]{6}-[0-9]{6}.db$") % test_suite, 0);
+
+ std::string latest;
+
+ const fs::directory dir(store_dir);
+ for (fs::directory::const_iterator iter = dir.begin();
+ iter != dir.end(); ++iter) {
+ const text::regex_matches matches = preg.match(iter->name);
+ if (matches) {
+ if (latest.empty() || iter->name > latest) {
+ latest = iter->name;
+ }
+ } else {
+ // Not a database file; skip.
+ }
+ }
+
+ if (latest.empty())
+ throw store::error(
+ F("No previous results file found for test suite %s")
+ % test_suite);
+
+ return store_dir / latest;
+ } catch (const fs::system_error& e) {
+ LW(F("Failed to open store dir %s: %s") % store_dir % e.what());
+ throw store::error(F("No previous results file found for test suite %s")
+ % test_suite);
+ } catch (const text::regex_error& e) {
+ throw store::error(e.what());
+ }
+}
+
+
+/// Computes the identifier of a new tests results file.
+///
+/// \param test_suite Identifier of the test suite.
+/// \param when Timestamp to attach to the identifier.
+///
+/// \return Identifier of the file to be created.
+static std::string
+new_id(const std::string& test_suite, const datetime::timestamp& when)
+{
+ const std::string when_datetime = when.strftime("%Y%m%d-%H%M%S");
+ const int when_ms = static_cast<int>(when.to_microseconds() % 1000000);
+ return F("%s.%s-%06s") % test_suite % when_datetime % when_ms;
+}
+
+
+} // anonymous namespace
+
+
+/// Value to request the creation of a new results file with an automatic name.
+///
+/// Can be passed to new_db().
+const char* layout::results_auto_create_name = "NEW";
+
+
+/// Value to request the opening of the latest results file.
+///
+/// Can be passed to find_results().
+const char* layout::results_auto_open_name = "LATEST";
+
+
+/// Resolves the results file for the given identifier.
+///
+/// \param id Identifier of the test suite to open.
+///
+/// \return Path to the requested file, if any.
+///
+/// \throw store::error If there is no matching entry.
+fs::path
+layout::find_results(const std::string& id)
+{
+ LI(F("Searching for a results file with id %s") % id);
+
+ if (id == results_auto_open_name) {
+ const std::string test_suite = test_suite_for_path(fs::current_path());
+ return find_latest(test_suite);
+ } else {
+ const fs::path id_as_path(id);
+
+ if (fs::exists(id_as_path) && !fs::is_directory(id_as_path)) {
+ if (id_as_path.is_absolute())
+ return id_as_path;
+ else
+ return id_as_path.to_absolute();
+ } else if (id.find('/') == std::string::npos) {
+ const fs::path candidate =
+ query_store_dir() / (F("results.%s.db") % id);
+ if (fs::exists(candidate)) {
+ return candidate;
+ } else {
+ return find_latest(id);
+ }
+ } else {
+ INV(id.find('/') != std::string::npos);
+ return find_latest(test_suite_for_path(id_as_path));
+ }
+ }
+}
+
+
+/// Computes the path to a new database for the given test suite.
+///
+/// \param id Identifier of the test suite to create.
+/// \param root Path to the root of 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.
+layout::results_id_file_pair
+layout::new_db(const std::string& id, const fs::path& root)
+{
+ std::string generated_id;
+ optional< fs::path > path;
+
+ if (id == results_auto_create_name) {
+ generated_id = new_id(test_suite_for_path(root),
+ datetime::timestamp::now());
+ path = query_store_dir() / (F("results.%s.db") % generated_id);
+ fs::mkdir_p(path.get().branch_path(), 0755);
+ } else {
+ path = fs::path(id);
+ }
+
+ return std::make_pair(generated_id, path.get());
+}
+
+
+/// 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;
+}
+
+
+/// Gets the path to the store directory.
+///
+/// Note that this function does not create the determined directory. It is the
+/// responsibility of the caller to do so.
+///
+/// \return Path to the directory holding all the database files.
+fs::path
+layout::query_store_dir(void)
+{
+ const optional< fs::path > home = utils::get_home();
+ if (home) {
+ const fs::path& home_path = home.get();
+ if (home_path.is_absolute())
+ return home_path / ".kyua/store";
+ else
+ return home_path.to_absolute() / ".kyua/store";
+ } else {
+ LW("HOME not defined; creating store database in current "
+ "directory");
+ return fs::current_path();
+ }
+}
+
+
+/// 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;
+}
diff --git a/store/layout.hpp b/store/layout.hpp
new file mode 100644
index 000000000000..48ab89c45104
--- /dev/null
+++ b/store/layout.hpp
@@ -0,0 +1,84 @@
+// Copyright 2014 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.
+
+/// \file store/layout.hpp
+/// File system layout definition for the Kyua data files.
+///
+/// Tests results files are all stored in a centralized directory by default.
+/// In the general case, we do not want the user to have to worry about files:
+/// we expose an identifier-based interface where each tests results file has a
+/// unique identifier. However, we also want to give full freedom to the user
+/// to store such files wherever he likes so we have to deal with paths as well.
+///
+/// When creating a new results file, the inputs to resolve the path can be:
+/// - NEW: Automatic generation of a new results file, so we want to return its
+/// public identifier and the path for internal consumption.
+/// - A path: The user provided the specific location where he wants the file
+/// stored, so we just obey that. There is no public identifier in this case
+/// because there is no naming scheme imposed on the generated files.
+///
+/// When opening an existing results file, the inputs to resolve the path can
+/// be:
+/// - LATEST: Given the current directory, we derive the corresponding test
+/// suite name and find the latest timestamped file in the centralized
+/// location.
+/// - A path: If the file exists, we just open that. If it doesn't exist or if
+/// it is a directory, we try to resolve that as a test suite name and locate
+/// the latest matching timestamped file.
+/// - Everything else: Treated as a test suite identifier, so we try to locate
+/// the latest matchin timestamped file.
+
+#if !defined(STORE_LAYOUT_HPP)
+#define STORE_LAYOUT_HPP
+
+#include "store/layout_fwd.hpp"
+
+#include <string>
+
+#include "utils/datetime_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+
+namespace store {
+namespace layout {
+
+
+extern const char* results_auto_create_name;
+extern const char* results_auto_open_name;
+
+utils::fs::path find_results(const std::string&);
+results_id_file_pair new_db(const std::string&, const utils::fs::path&);
+utils::fs::path new_db_for_migration(const utils::fs::path&,
+ const utils::datetime::timestamp&);
+utils::fs::path query_store_dir(void);
+std::string test_suite_for_path(const utils::fs::path&);
+
+
+} // namespace layout
+} // namespace store
+
+#endif // !defined(STORE_LAYOUT_HPP)
diff --git a/store/layout_fwd.hpp b/store/layout_fwd.hpp
new file mode 100644
index 000000000000..72d05a27c66a
--- /dev/null
+++ b/store/layout_fwd.hpp
@@ -0,0 +1,54 @@
+// Copyright 2015 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.
+
+/// \file store/layout_fwd.hpp
+/// Forward declarations for store/layout.hpp
+
+#if !defined(STORE_LAYOUT_FWD_HPP)
+#define STORE_LAYOUT_FWD_HPP
+
+#include <string>
+#include <utility>
+
+#include "utils/fs/path_fwd.hpp"
+
+namespace store {
+namespace layout {
+
+
+/// A pair with the user-visible ID of the results file and its path.
+///
+/// It is possible for the ID (first component) to be empty in the cases where
+/// the user explicitly requested to create the database in a specific path.
+typedef std::pair< std::string, utils::fs::path > results_id_file_pair;
+
+
+} // namespace layout
+} // namespace store
+
+#endif // !defined(STORE_LAYOUT_FWD_HPP)
diff --git a/store/layout_test.cpp b/store/layout_test.cpp
new file mode 100644
index 000000000000..8564d3aef93c
--- /dev/null
+++ b/store/layout_test.cpp
@@ -0,0 +1,350 @@
+// Copyright 2014 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/layout.hpp"
+
+extern "C" {
+#include <unistd.h>
+}
+
+#include <iostream>
+
+#include <atf-c++.hpp>
+
+#include "store/exceptions.hpp"
+#include "store/layout.hpp"
+#include "utils/datetime.hpp"
+#include "utils/env.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace layout = store::layout;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__latest);
+ATF_TEST_CASE_BODY(find_results__latest)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+
+ const std::string test_suite = layout::test_suite_for_path(
+ fs::current_path());
+ const std::string base = (store_dir / (
+ "results." + test_suite + ".")).str();
+
+ atf::utils::create_file(base + "20140613-194515-000000.db", "");
+ ATF_REQUIRE_EQ(base + "20140613-194515-000000.db",
+ layout::find_results("LATEST").str());
+
+ atf::utils::create_file(base + "20140614-194515-123456.db", "");
+ ATF_REQUIRE_EQ(base + "20140614-194515-123456.db",
+ layout::find_results("LATEST").str());
+
+ atf::utils::create_file(base + "20130614-194515-999999.db", "");
+ ATF_REQUIRE_EQ(base + "20140614-194515-123456.db",
+ layout::find_results("LATEST").str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__directory);
+ATF_TEST_CASE_BODY(find_results__directory)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+
+ const fs::path dir1("dir1/foo");
+ fs::mkdir_p(dir1, 0755);
+ const fs::path dir2("dir1/bar");
+ fs::mkdir_p(dir2, 0755);
+
+ const std::string base1 = (store_dir / (
+ "results." + layout::test_suite_for_path(dir1) + ".")).str();
+ const std::string base2 = (store_dir / (
+ "results." + layout::test_suite_for_path(dir2) + ".")).str();
+
+ atf::utils::create_file(base1 + "20140613-194515-000000.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db",
+ layout::find_results(dir1.str()).str());
+
+ atf::utils::create_file(base2 + "20140615-111111-000000.db", "");
+ ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db",
+ layout::find_results(dir2.str()).str());
+
+ atf::utils::create_file(base1 + "20140614-194515-123456.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db",
+ layout::find_results(dir1.str()).str());
+
+ atf::utils::create_file(base1 + "20130614-194515-999999.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db",
+ layout::find_results(dir1.str()).str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__file);
+ATF_TEST_CASE_BODY(find_results__file)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+
+ atf::utils::create_file("a-file.db", "");
+ ATF_REQUIRE_EQ(fs::path("a-file.db").to_absolute(),
+ layout::find_results("a-file.db"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__id);
+ATF_TEST_CASE_BODY(find_results__id)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+
+ const fs::path dir1("dir1/foo");
+ fs::mkdir_p(dir1, 0755);
+ const fs::path dir2("dir1/bar");
+ fs::mkdir_p(dir2, 0755);
+
+ const std::string id1 = layout::test_suite_for_path(dir1);
+ const std::string base1 = (store_dir / ("results." + id1 + ".")).str();
+ const std::string id2 = layout::test_suite_for_path(dir2);
+ const std::string base2 = (store_dir / ("results." + id2 + ".")).str();
+
+ atf::utils::create_file(base1 + "20140613-194515-000000.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140613-194515-000000.db",
+ layout::find_results(id1).str());
+
+ atf::utils::create_file(base2 + "20140615-111111-000000.db", "");
+ ATF_REQUIRE_EQ(base2 + "20140615-111111-000000.db",
+ layout::find_results(id2).str());
+
+ atf::utils::create_file(base1 + "20140614-194515-123456.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db",
+ layout::find_results(id1).str());
+
+ atf::utils::create_file(base1 + "20130614-194515-999999.db", "");
+ ATF_REQUIRE_EQ(base1 + "20140614-194515-123456.db",
+ layout::find_results(id1).str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__id_with_timestamp);
+ATF_TEST_CASE_BODY(find_results__id_with_timestamp)
+{
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+
+ const fs::path dir1("dir1/foo");
+ fs::mkdir_p(dir1, 0755);
+ const fs::path dir2("dir1/bar");
+ fs::mkdir_p(dir2, 0755);
+
+ const std::string id1 = layout::test_suite_for_path(dir1);
+ const std::string base1 = (store_dir / ("results." + id1 + ".")).str();
+ const std::string id2 = layout::test_suite_for_path(dir2);
+ const std::string base2 = (store_dir / ("results." + id2 + ".")).str();
+
+ atf::utils::create_file(base1 + "20140613-194515-000000.db", "");
+ atf::utils::create_file(base2 + "20140615-111111-000000.db", "");
+ atf::utils::create_file(base1 + "20140614-194515-123456.db", "");
+ atf::utils::create_file(base1 + "20130614-194515-999999.db", "");
+
+ ATF_REQUIRE_MATCH(
+ "_dir1_foo.20140613-194515-000000.db$",
+ layout::find_results(id1 + ".20140613-194515-000000").str());
+
+ ATF_REQUIRE_MATCH(
+ "_dir1_foo.20140614-194515-123456.db$",
+ layout::find_results(id1 + ".20140614-194515-123456").str());
+
+ ATF_REQUIRE_MATCH(
+ "_dir1_bar.20140615-111111-000000.db$",
+ layout::find_results(id2 + ".20140615-111111-000000").str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find_results__not_found);
+ATF_TEST_CASE_BODY(find_results__not_found)
+{
+ ATF_REQUIRE_THROW_RE(
+ store::error,
+ "No previous results file found for test suite foo_bar",
+ layout::find_results("foo_bar"));
+
+ const fs::path store_dir = layout::query_store_dir();
+ fs::mkdir_p(store_dir, 0755);
+ ATF_REQUIRE_THROW_RE(
+ store::error,
+ "No previous results file found for test suite foo_bar",
+ layout::find_results("foo_bar"));
+
+ const char* candidates[] = {
+ "results.foo.20140613-194515-012345.db", // Bad test suite.
+ "results.foo_bar.20140613-194515-012345", // Missing extension.
+ "foo_bar.20140613-194515-012345.db", // Missing prefix.
+ "results.foo_bar.2010613-194515-012345.db", // Bad date.
+ "results.foo_bar.20140613-19515-012345.db", // Bad time.
+ "results.foo_bar.20140613-194515-01245.db", // Bad microseconds.
+ NULL,
+ };
+ for (const char** candidate = candidates; *candidate != NULL; ++candidate) {
+ std::cout << "Current candidate: " << *candidate << '\n';
+ atf::utils::create_file((store_dir / *candidate).str(), "");
+ ATF_REQUIRE_THROW_RE(
+ store::error,
+ "No previous results file found for test suite foo_bar",
+ layout::find_results("foo_bar"));
+ }
+
+ atf::utils::create_file(
+ (store_dir / "results.foo_bar.20140613-194515-012345.db").str(), "");
+ layout::find_results("foo_bar"); // Expected not to throw.
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(new_db__new);
+ATF_TEST_CASE_BODY(new_db__new)
+{
+ datetime::set_mock_now(2014, 6, 13, 19, 45, 15, 5000);
+ ATF_REQUIRE(!fs::exists(fs::path(".kyua/store")));
+ const layout::results_id_file_pair results = layout::new_db(
+ "NEW", fs::path("/some/path/to/the/suite"));
+ ATF_REQUIRE( fs::exists(fs::path(".kyua/store")));
+ ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store")));
+
+ const std::string id = "some_path_to_the_suite.20140613-194515-005000";
+ ATF_REQUIRE_EQ(id, results.first);
+ ATF_REQUIRE_EQ(layout::query_store_dir() / ("results." + id + ".db"),
+ results.second);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(new_db__explicit);
+ATF_TEST_CASE_BODY(new_db__explicit)
+{
+ ATF_REQUIRE(!fs::exists(fs::path(".kyua/store")));
+ const layout::results_id_file_pair results = layout::new_db(
+ "foo/results-file.db", fs::path("unused"));
+ ATF_REQUIRE(!fs::exists(fs::path(".kyua/store")));
+ ATF_REQUIRE(!fs::exists(fs::path("foo")));
+
+ ATF_REQUIRE(results.first.empty());
+ ATF_REQUIRE_EQ(fs::path("foo/results-file.db"), results.second);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(new_db_for_migration);
+ATF_TEST_CASE_BODY(new_db_for_migration)
+{
+ ATF_REQUIRE(!fs::exists(fs::path(".kyua/store")));
+ const fs::path results_file = layout::new_db_for_migration(
+ fs::path("/some/root"),
+ datetime::timestamp::from_values(2014, 7, 30, 10, 5, 20, 76500));
+ ATF_REQUIRE( fs::exists(fs::path(".kyua/store")));
+ ATF_REQUIRE( fs::is_directory(fs::path(".kyua/store")));
+
+ ATF_REQUIRE_EQ(
+ layout::query_store_dir() /
+ "results.some_root.20140730-100520-076500.db",
+ results_file);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_absolute);
+ATF_TEST_CASE_BODY(query_store_dir__home_absolute)
+{
+ const fs::path home = fs::current_path() / "homedir";
+ utils::setenv("HOME", home.str());
+ const fs::path store_dir = layout::query_store_dir();
+ ATF_REQUIRE(store_dir.is_absolute());
+ ATF_REQUIRE_EQ(home / ".kyua/store", store_dir);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__home_relative);
+ATF_TEST_CASE_BODY(query_store_dir__home_relative)
+{
+ const fs::path home("homedir");
+ utils::setenv("HOME", home.str());
+ const fs::path store_dir = layout::query_store_dir();
+ ATF_REQUIRE(store_dir.is_absolute());
+ ATF_REQUIRE_MATCH((home / ".kyua/store").str(), store_dir.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(query_store_dir__no_home);
+ATF_TEST_CASE_BODY(query_store_dir__no_home)
+{
+ utils::unsetenv("HOME");
+ ATF_REQUIRE_EQ(fs::current_path(), layout::query_store_dir());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__absolute);
+ATF_TEST_CASE_BODY(test_suite_for_path__absolute)
+{
+ ATF_REQUIRE_EQ("dir1_dir2_dir3",
+ layout::test_suite_for_path(fs::path("/dir1/dir2/dir3")));
+ ATF_REQUIRE_EQ("dir1",
+ layout::test_suite_for_path(fs::path("/dir1")));
+ ATF_REQUIRE_EQ("dir1_dir2",
+ layout::test_suite_for_path(fs::path("/dir1///dir2")));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_suite_for_path__relative);
+ATF_TEST_CASE_BODY(test_suite_for_path__relative)
+{
+ const std::string test_suite = layout::test_suite_for_path(
+ fs::path("foo/bar"));
+ ATF_REQUIRE_MATCH("_foo_bar$", test_suite);
+ ATF_REQUIRE_MATCH("^[^_]", test_suite);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, find_results__latest);
+ ATF_ADD_TEST_CASE(tcs, find_results__directory);
+ ATF_ADD_TEST_CASE(tcs, find_results__file);
+ ATF_ADD_TEST_CASE(tcs, find_results__id);
+ ATF_ADD_TEST_CASE(tcs, find_results__id_with_timestamp);
+ ATF_ADD_TEST_CASE(tcs, find_results__not_found);
+
+ ATF_ADD_TEST_CASE(tcs, new_db__new);
+ ATF_ADD_TEST_CASE(tcs, new_db__explicit);
+
+ ATF_ADD_TEST_CASE(tcs, new_db_for_migration);
+
+ ATF_ADD_TEST_CASE(tcs, query_store_dir__home_absolute);
+ ATF_ADD_TEST_CASE(tcs, query_store_dir__home_relative);
+ ATF_ADD_TEST_CASE(tcs, query_store_dir__no_home);
+
+ ATF_ADD_TEST_CASE(tcs, test_suite_for_path__absolute);
+ ATF_ADD_TEST_CASE(tcs, test_suite_for_path__relative);
+}
diff --git a/store/metadata.cpp b/store/metadata.cpp
new file mode 100644
index 000000000000..2d90fe8cb267
--- /dev/null
+++ b/store/metadata.cpp
@@ -0,0 +1,137 @@
+// 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/metadata.hpp"
+
+#include "store/exceptions.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/sanity.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace sqlite = utils::sqlite;
+
+
+namespace {
+
+
+/// Fetches an integer column from a statement of the 'metadata' table.
+///
+/// \param stmt The statement from which to get the column value.
+/// \param column The name of the column to retrieve.
+///
+/// \return The value of the column.
+///
+/// \throw store::integrity_error If there is a problem fetching the value
+/// caused by an invalid schema or data.
+static int64_t
+int64_column(sqlite::statement& stmt, const char* column)
+{
+ int index;
+ try {
+ index = stmt.column_id(column);
+ } catch (const sqlite::invalid_column_error& e) {
+ UNREACHABLE_MSG("Invalid column specification; the SELECT statement "
+ "should have caught this");
+ }
+ if (stmt.column_type(index) != sqlite::type_integer)
+ throw store::integrity_error(F("The '%s' column in 'metadata' table "
+ "has an invalid type") % column);
+ return stmt.column_int64(index);
+}
+
+
+} // anonymous namespace
+
+
+/// Constructs a new metadata object.
+///
+/// \param schema_version_ The schema version.
+/// \param timestamp_ The time at which this version was created.
+store::metadata::metadata(const int schema_version_, const int64_t timestamp_) :
+ _schema_version(schema_version_),
+ _timestamp(timestamp_)
+{
+}
+
+
+/// Returns the timestamp of this entry.
+///
+/// \return The timestamp in this metadata entry.
+int64_t
+store::metadata::timestamp(void) const
+{
+ return _timestamp;
+}
+
+
+/// Returns the schema version.
+///
+/// \return The schema version in this metadata entry.
+int
+store::metadata::schema_version(void) const
+{
+ return _schema_version;
+}
+
+
+/// Reads the latest metadata entry from the database.
+///
+/// \param db The database from which to read the metadata from.
+///
+/// \return The current metadata of the database. It is not OK for the metadata
+/// table to be empty, so this is guaranteed to return a value unless there is
+/// an error.
+///
+/// \throw store::integrity_error If the metadata in the database is empty,
+/// has an invalid schema or its contents are bogus.
+store::metadata
+store::metadata::fetch_latest(sqlite::database& db)
+{
+ try {
+ sqlite::statement stmt = db.create_statement(
+ "SELECT schema_version, timestamp FROM metadata "
+ "ORDER BY schema_version DESC LIMIT 1");
+ if (!stmt.step())
+ throw store::integrity_error("The 'metadata' table is empty");
+
+ const int schema_version_ =
+ static_cast< int >(int64_column(stmt, "schema_version"));
+ const int64_t timestamp_ = int64_column(stmt, "timestamp");
+
+ if (stmt.step())
+ UNREACHABLE_MSG("Got more than one result from a query that "
+ "does not permit this; any pragmas defined?");
+
+ return metadata(schema_version_, timestamp_);
+ } catch (const sqlite::error& e) {
+ throw store::integrity_error(F("Invalid metadata schema: %s") %
+ e.what());
+ }
+}
diff --git a/store/metadata.hpp b/store/metadata.hpp
new file mode 100644
index 000000000000..c155af6d5897
--- /dev/null
+++ b/store/metadata.hpp
@@ -0,0 +1,68 @@
+// 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.
+
+/// \file store/metadata.hpp
+/// Representation of the database metadata.
+
+#if !defined(STORE_METADATA_HPP)
+#define STORE_METADATA_HPP
+
+#include "store/metadata_fwd.hpp"
+
+extern "C" {
+#include <stdint.h>
+}
+
+#include <cstddef>
+
+#include "utils/sqlite/database_fwd.hpp"
+
+namespace store {
+
+
+/// Representation of the database metadata.
+class metadata {
+ /// Current version of the database schema.
+ int _schema_version;
+
+ /// Timestamp of the last metadata entry in the database.
+ int64_t _timestamp;
+
+ metadata(const int, const int64_t);
+
+public:
+ int64_t timestamp(void) const;
+ int schema_version(void) const;
+
+ static metadata fetch_latest(utils::sqlite::database&);
+};
+
+
+} // namespace store
+
+#endif // !defined(STORE_METADATA_HPP)
diff --git a/store/metadata_fwd.hpp b/store/metadata_fwd.hpp
new file mode 100644
index 000000000000..39aa8c2448d4
--- /dev/null
+++ b/store/metadata_fwd.hpp
@@ -0,0 +1,43 @@
+// Copyright 2015 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.
+
+/// \file store/metadata_fwd.hpp
+/// Forward declarations for store/metadata.hpp
+
+#if !defined(STORE_METADATA_FWD_HPP)
+#define STORE_METADATA_FWD_HPP
+
+namespace store {
+
+
+class metadata;
+
+
+} // namespace store
+
+#endif // !defined(STORE_METADATA_FWD_HPP)
diff --git a/store/metadata_test.cpp b/store/metadata_test.cpp
new file mode 100644
index 000000000000..e32f1ae38dfb
--- /dev/null
+++ b/store/metadata_test.cpp
@@ -0,0 +1,154 @@
+// 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/metadata.hpp"
+
+#include <atf-c++.hpp>
+
+#include "store/exceptions.hpp"
+#include "store/write_backend.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/sqlite/database.hpp"
+
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+
+
+namespace {
+
+
+/// Creates a test in-memory database.
+///
+/// When using this function, you must define a 'require.files' property in this
+/// case pointing to store::detail::schema_file().
+///
+/// The database created by this function mimics a real complete database, but
+/// without any predefined values. I.e. for our particular case, the metadata
+/// table is empty.
+///
+/// \return A SQLite database instance.
+static sqlite::database
+create_database(void)
+{
+ sqlite::database db = sqlite::database::in_memory();
+ store::detail::initialize(db);
+ db.exec("DELETE FROM metadata");
+ return db;
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE(fetch_latest__ok);
+ATF_TEST_CASE_HEAD(fetch_latest__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(fetch_latest__ok)
+{
+ sqlite::database db = create_database();
+ db.exec("INSERT INTO metadata (schema_version, timestamp) "
+ "VALUES (512, 5678)");
+ db.exec("INSERT INTO metadata (schema_version, timestamp) "
+ "VALUES (256, 1234)");
+
+ const store::metadata metadata = store::metadata::fetch_latest(db);
+ ATF_REQUIRE_EQ(5678L, metadata.timestamp());
+ ATF_REQUIRE_EQ(512, metadata.schema_version());
+}
+
+
+ATF_TEST_CASE(fetch_latest__empty_metadata);
+ATF_TEST_CASE_HEAD(fetch_latest__empty_metadata)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(fetch_latest__empty_metadata)
+{
+ sqlite::database db = create_database();
+ ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty",
+ store::metadata::fetch_latest(db));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_timestamp);
+ATF_TEST_CASE_BODY(fetch_latest__no_timestamp)
+{
+ sqlite::database db = sqlite::database::in_memory();
+ db.exec("CREATE TABLE metadata (schema_version INTEGER)");
+ db.exec("INSERT INTO metadata VALUES (3)");
+
+ ATF_REQUIRE_THROW_RE(store::integrity_error,
+ "Invalid metadata.*timestamp",
+ store::metadata::fetch_latest(db));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(fetch_latest__no_schema_version);
+ATF_TEST_CASE_BODY(fetch_latest__no_schema_version)
+{
+ sqlite::database db = sqlite::database::in_memory();
+ db.exec("CREATE TABLE metadata (timestamp INTEGER)");
+ db.exec("INSERT INTO metadata VALUES (3)");
+
+ ATF_REQUIRE_THROW_RE(store::integrity_error,
+ "Invalid metadata.*schema_version",
+ store::metadata::fetch_latest(db));
+}
+
+
+ATF_TEST_CASE(fetch_latest__invalid_timestamp);
+ATF_TEST_CASE_HEAD(fetch_latest__invalid_timestamp)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(fetch_latest__invalid_timestamp)
+{
+ sqlite::database db = create_database();
+ db.exec("INSERT INTO metadata (schema_version, timestamp) "
+ "VALUES (3, 'foo')");
+
+ ATF_REQUIRE_THROW_RE(store::integrity_error,
+ "timestamp.*invalid type",
+ store::metadata::fetch_latest(db));
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, fetch_latest__ok);
+ ATF_ADD_TEST_CASE(tcs, fetch_latest__empty_metadata);
+ ATF_ADD_TEST_CASE(tcs, fetch_latest__no_timestamp);
+ ATF_ADD_TEST_CASE(tcs, fetch_latest__no_schema_version);
+ ATF_ADD_TEST_CASE(tcs, fetch_latest__invalid_timestamp);
+}
diff --git a/store/migrate.cpp b/store/migrate.cpp
new file mode 100644
index 000000000000..9ec97c231184
--- /dev/null
+++ b/store/migrate.cpp
@@ -0,0 +1,287 @@
+// 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/migrate.hpp"
+
+#include <stdexcept>
+
+#include "store/dbtypes.hpp"
+#include "store/exceptions.hpp"
+#include "store/layout.hpp"
+#include "store/metadata.hpp"
+#include "store/read_backend.hpp"
+#include "store/write_backend.hpp"
+#include "utils/datetime.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/exceptions.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.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/text/operations.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace sqlite = utils::sqlite;
+namespace text = utils::text;
+
+using utils::none;
+using utils::optional;
+
+
+namespace {
+
+
+/// Schema version at which we switched to results files.
+const int first_chunked_schema_version = 3;
+
+
+/// Queries the schema version of the given database.
+///
+/// \param file The database from which to query the schema version.
+///
+/// \return The schema version number.
+static int
+get_schema_version(const fs::path& file)
+{
+ sqlite::database db = store::detail::open_and_setup(
+ file, sqlite::open_readonly);
+ return store::metadata::fetch_latest(db).schema_version();
+}
+
+
+/// Performs a single migration step.
+///
+/// Both action_id and old_database are little hacks to support the migration
+/// from the historical database to chunked files. We'd use a more generic
+/// "replacements" map, but it's not worth it.
+///
+/// \param file Database on which to apply the migration step.
+/// \param version_from Current schema version in the database.
+/// \param version_to Schema version to migrate to.
+/// \param action_id If not none, replace ACTION_ID in the migration file with
+/// this value.
+/// \param old_database If not none, replace OLD_DATABASE in the migration
+/// file with this value.
+///
+/// \throw error If there is a problem applying the migration.
+static void
+migrate_schema_step(const fs::path& file,
+ const int version_from,
+ const int version_to,
+ const optional< int64_t > action_id = none,
+ const optional< fs::path > old_database = none)
+{
+ LI(F("Migrating schema of %s from version %s to %s") % file % version_from
+ % version_to);
+
+ PRE(version_to == version_from + 1);
+
+ sqlite::database db = store::detail::open_and_setup(
+ file, sqlite::open_readwrite);
+
+ const fs::path migration = store::detail::migration_file(version_from,
+ version_to);
+
+ std::string migration_string;
+ try {
+ migration_string = utils::read_file(migration);
+ } catch (const std::runtime_error& unused_e) {
+ throw store::error(F("Cannot read migration file '%s'") % migration);
+ }
+ if (action_id) {
+ migration_string = text::replace_all(migration_string, "@ACTION_ID@",
+ F("%s") % action_id.get());
+ }
+ if (old_database) {
+ migration_string = text::replace_all(migration_string, "@OLD_DATABASE@",
+ old_database.get().str());
+ }
+ try {
+ db.exec(migration_string);
+ } catch (const sqlite::error& e) {
+ throw store::error(F("Schema migration failed: %s") % e.what());
+ }
+}
+
+
+/// Given a historical database, chunks it up into results files.
+///
+/// The given database is DELETED on success given that it will have been
+/// split up into various different files.
+///
+/// \param old_file Path to the old database.
+static void
+chunk_database(const fs::path& old_file)
+{
+ PRE(get_schema_version(old_file) == first_chunked_schema_version - 1);
+
+ LI(F("Need to split %s into per-action files") % old_file);
+
+ sqlite::database old_db = store::detail::open_and_setup(
+ old_file, sqlite::open_readonly);
+
+ sqlite::statement actions_stmt = old_db.create_statement(
+ "SELECT action_id, cwd FROM actions NATURAL JOIN contexts");
+
+ sqlite::statement start_time_stmt = old_db.create_statement(
+ "SELECT test_results.start_time AS start_time "
+ "FROM test_programs "
+ " JOIN test_cases "
+ " ON test_programs.test_program_id == test_cases.test_program_id"
+ " JOIN test_results "
+ " ON test_cases.test_case_id == test_results.test_case_id "
+ "WHERE test_programs.action_id == :action_id "
+ "ORDER BY start_time LIMIT 1");
+
+ while (actions_stmt.step()) {
+ const int64_t action_id = actions_stmt.safe_column_int64("action_id");
+ const fs::path cwd(actions_stmt.safe_column_text("cwd"));
+
+ LI(F("Extracting action %s") % action_id);
+
+ start_time_stmt.reset();
+ start_time_stmt.bind(":action_id", action_id);
+ if (!start_time_stmt.step()) {
+ LI(F("Skipping empty action %s") % action_id);
+ continue;
+ }
+ const datetime::timestamp start_time = store::column_timestamp(
+ start_time_stmt, "start_time");
+ start_time_stmt.step_without_results();
+
+ const fs::path new_file = store::layout::new_db_for_migration(
+ cwd, start_time);
+ if (fs::exists(new_file)) {
+ LI(F("Skipping action because %s already exists") % new_file);
+ continue;
+ }
+
+ LI(F("Creating %s for previous action %s") % new_file % action_id);
+
+ try {
+ fs::mkdir_p(new_file.branch_path(), 0755);
+ sqlite::database db = store::detail::open_and_setup(
+ new_file, sqlite::open_readwrite | sqlite::open_create);
+ store::detail::initialize(db);
+ db.close();
+ migrate_schema_step(new_file,
+ first_chunked_schema_version - 1,
+ first_chunked_schema_version,
+ utils::make_optional(action_id),
+ utils::make_optional(old_file));
+ } catch (...) {
+ // TODO(jmmv): Handle this better.
+ fs::unlink(new_file);
+ }
+ }
+
+ fs::unlink(old_file);
+}
+
+
+} // anonymous namespace
+
+
+/// Calculates the path to a schema migration file.
+///
+/// \param version_from The version from which the database is being upgraded.
+/// \param version_to The version to which the database is being upgraded.
+///
+/// \return The path to the installed migrate_vX_vY.sql file.
+fs::path
+store::detail::migration_file(const int version_from, const int version_to)
+{
+ return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR))
+ / (F("migrate_v%s_v%s.sql") % version_from % version_to);
+}
+
+
+/// 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);
+ try {
+ fs::copy(source, target);
+ } catch (const fs::error& e) {
+ throw store::error(e.what());
+ }
+}
+
+
+/// Migrates the schema of a database to the current version.
+///
+/// The algorithm implemented here performs a migration step for every
+/// intermediate version between the schema version in the database to the
+/// version implemented in this file. This should permit upgrades from
+/// arbitrary old databases.
+///
+/// \param file The database whose schema to upgrade.
+///
+/// \throw error If there is a problem with the migration.
+void
+store::migrate_schema(const utils::fs::path& file)
+{
+ const int version_from = get_schema_version(file);
+ const int version_to = detail::current_schema_version;
+ if (version_from == version_to) {
+ throw error(F("Database already at schema version %s; migration not "
+ "needed") % version_from);
+ } else if (version_from > version_to) {
+ throw error(F("Database at schema version %s, which is newer than the "
+ "supported version %s") % version_from % version_to);
+ }
+
+ detail::backup_database(file, version_from);
+
+ int i;
+ for (i = version_from; i < first_chunked_schema_version - 1; ++i) {
+ migrate_schema_step(file, i, i + 1);
+ }
+ chunk_database(file);
+ INV(version_to == first_chunked_schema_version);
+}
diff --git a/store/migrate.hpp b/store/migrate.hpp
new file mode 100644
index 000000000000..a2622edc0f87
--- /dev/null
+++ b/store/migrate.hpp
@@ -0,0 +1,55 @@
+// 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.
+
+/// \file store/migrate.hpp
+/// Utilities to upgrade a database with an old schema to the latest one.
+
+#if !defined(STORE_MIGRATE_HPP)
+#define STORE_MIGRATE_HPP
+
+#include "utils/fs/path_fwd.hpp"
+
+namespace store {
+
+
+namespace detail {
+
+
+utils::fs::path migration_file(const int, const int);
+void backup_database(const utils::fs::path&, const int);
+
+
+} // anonymous namespace
+
+
+void migrate_schema(const utils::fs::path&);
+
+
+} // namespace store
+
+#endif // !defined(STORE_MIGRATE_HPP)
diff --git a/store/migrate_test.cpp b/store/migrate_test.cpp
new file mode 100644
index 000000000000..b45cc9e5e39e
--- /dev/null
+++ b/store/migrate_test.cpp
@@ -0,0 +1,132 @@
+// 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/migrate.hpp"
+
+extern "C" {
+#include <sys/stat.h>
+}
+
+#include <atf-c++.hpp>
+
+#include "store/exceptions.hpp"
+#include "utils/env.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+
+namespace fs = utils::fs;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok);
+ATF_TEST_CASE_BODY(detail__backup_database__ok)
+{
+ atf::utils::create_file("test.db", "The DB\n");
+ store::detail::backup_database(fs::path("test.db"), 13);
+ ATF_REQUIRE(fs::exists(fs::path("test.db")));
+ ATF_REQUIRE(fs::exists(fs::path("test.db.v13.backup")));
+ ATF_REQUIRE(atf::utils::compare_file("test.db.v13.backup", "The DB\n"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__ok_overwrite);
+ATF_TEST_CASE_BODY(detail__backup_database__ok_overwrite)
+{
+ atf::utils::create_file("test.db", "Original contents");
+ atf::utils::create_file("test.db.v1.backup", "Overwrite me");
+ store::detail::backup_database(fs::path("test.db"), 1);
+ ATF_REQUIRE(fs::exists(fs::path("test.db")));
+ ATF_REQUIRE(fs::exists(fs::path("test.db.v1.backup")));
+ ATF_REQUIRE(atf::utils::compare_file("test.db.v1.backup",
+ "Original contents"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__backup_database__fail_open);
+ATF_TEST_CASE_BODY(detail__backup_database__fail_open)
+{
+ ATF_REQUIRE_THROW_RE(store::error, "Cannot open.*foo.db",
+ store::detail::backup_database(fs::path("foo.db"), 5));
+}
+
+
+ATF_TEST_CASE_WITH_CLEANUP(detail__backup_database__fail_create);
+ATF_TEST_CASE_HEAD(detail__backup_database__fail_create)
+{
+ set_md_var("require.user", "unprivileged");
+}
+ATF_TEST_CASE_BODY(detail__backup_database__fail_create)
+{
+ ATF_REQUIRE(::mkdir("dir", 0755) != -1);
+ atf::utils::create_file("dir/test.db", "Does not need to be valid");
+ ATF_REQUIRE(::chmod("dir", 0111) != -1);
+ ATF_REQUIRE_THROW_RE(
+ store::error, "Cannot create.*dir/test.db.v13.backup",
+ store::detail::backup_database(fs::path("dir/test.db"), 13));
+}
+ATF_TEST_CASE_CLEANUP(detail__backup_database__fail_create)
+{
+ if (::chmod("dir", 0755) == -1) {
+ // If we cannot restore the original permissions, we cannot do much
+ // more. However, leaving an unwritable directory behind will cause the
+ // runtime engine to report us as broken.
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__migration_file__builtin);
+ATF_TEST_CASE_BODY(detail__migration_file__builtin)
+{
+ utils::unsetenv("KYUA_STOREDIR");
+ ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "migrate_v5_v9.sql",
+ store::detail::migration_file(5, 9));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__migration_file__overriden);
+ATF_TEST_CASE_BODY(detail__migration_file__overriden)
+{
+ utils::setenv("KYUA_STOREDIR", "/tmp/test");
+ ATF_REQUIRE_EQ(fs::path("/tmp/test/migrate_v5_v9.sql"),
+ store::detail::migration_file(5, 9));
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok);
+ ATF_ADD_TEST_CASE(tcs, detail__backup_database__ok_overwrite);
+ ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_open);
+ ATF_ADD_TEST_CASE(tcs, detail__backup_database__fail_create);
+
+ ATF_ADD_TEST_CASE(tcs, detail__migration_file__builtin);
+ ATF_ADD_TEST_CASE(tcs, detail__migration_file__overriden);
+
+ // Tests for migrate_schema are in schema_inttest. This is because, for
+ // such tests to be meaningful, they need to be integration tests and don't
+ // really fit the goal of this unit-test module.
+}
diff --git a/store/migrate_v1_v2.sql b/store/migrate_v1_v2.sql
new file mode 100644
index 000000000000..52d2f6a8e00c
--- /dev/null
+++ b/store/migrate_v1_v2.sql
@@ -0,0 +1,357 @@
+-- Copyright 2013 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.
+
+-- \file store/v1-to-v2.sql
+-- Migration of a database with version 1 of the schema to version 2.
+--
+-- Version 2 appeared in revision 9a73561a1e3975bba4cbfd19aee6b2365a39519e
+-- and its changes were:
+--
+-- * Changed the primary key of the metadata table to be the
+-- schema_version, not the timestamp. Because timestamps only have
+-- second resolution, the old schema made testing of schema migrations
+-- difficult.
+--
+-- * Introduced the metadatas table, which holds the metadata of all test
+-- programs and test cases in an abstract manner regardless of their
+-- interface.
+--
+-- * Added the metadata_id field to the test_programs and test_cases
+-- tables, referencing the new metadatas table.
+--
+-- * Changed the precision of the timeout metadata field to be in seconds
+-- rather than in microseconds. There is no data loss, and the code that
+-- writes the metadata is simplified.
+--
+-- * Removed the atf_* and plain_* tables.
+--
+-- * Added missing indexes to improve the performance of reports.
+--
+-- * Added missing column affinities to the absolute_path and relative_path
+-- columns of the test_programs table.
+
+
+-- TODO(jmmv): Implement addition of missing affinities.
+
+
+--
+-- Change primary key of the metadata table.
+--
+
+
+CREATE TABLE new_metadata (
+ schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1),
+ timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0)
+);
+
+INSERT INTO new_metadata (schema_version, timestamp)
+ SELECT schema_version, timestamp FROM metadata;
+
+DROP TABLE metadata;
+ALTER TABLE new_metadata RENAME TO metadata;
+
+
+--
+-- Add the new tables, columns and indexes.
+--
+
+
+CREATE TABLE metadatas (
+ metadata_id INTEGER NOT NULL,
+ property_name TEXT NOT NULL,
+ property_value TEXT,
+
+ PRIMARY KEY (metadata_id, property_name)
+);
+
+
+-- Upgrade the test_programs table by adding missing column affinities and
+-- the new metadata_id column.
+CREATE TABLE new_test_programs (
+ test_program_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ action_id INTEGER REFERENCES actions,
+
+ absolute_path TEXT NOT NULL,
+ root TEXT NOT NULL,
+ relative_path TEXT NOT NULL,
+ test_suite_name TEXT NOT NULL,
+ metadata_id INTEGER,
+ interface TEXT NOT NULL
+);
+PRAGMA foreign_keys = OFF;
+INSERT INTO new_test_programs (test_program_id, action_id, absolute_path,
+ root, relative_path, test_suite_name,
+ interface)
+ SELECT test_program_id, action_id, absolute_path, root, relative_path,
+ test_suite_name, interface FROM test_programs;
+DROP TABLE test_programs;
+ALTER TABLE new_test_programs RENAME TO test_programs;
+PRAGMA foreign_keys = ON;
+
+
+ALTER TABLE test_cases ADD COLUMN metadata_id INTEGER;
+
+
+CREATE INDEX index_metadatas_by_id
+ ON metadatas (metadata_id);
+CREATE INDEX index_test_programs_by_action_id
+ ON test_programs (action_id);
+CREATE INDEX index_test_cases_by_test_programs_id
+ ON test_cases (test_program_id);
+
+
+--
+-- Data migration
+--
+-- This is, by far, the trickiest part of the migration.
+-- TODO(jmmv): Describe the trickiness in here.
+--
+
+
+-- Auxiliary table to construct the final contents of the metadatas table.
+--
+-- We construct the contents by writing a row for every metadata property of
+-- every test program and test case. Entries corresponding to a test program
+-- will have the test_program_id field set to not NULL and entries corresponding
+-- to test cases will have the test_case_id set to not NULL.
+--
+-- The tricky part, however, is to create the individual identifiers for every
+-- metadata entry. We do this by picking the minimum ROWID of a particular set
+-- of properties that map to a single test_program_id or test_case_id.
+CREATE TABLE tmp_metadatas (
+ test_program_id INTEGER DEFAULT NULL,
+ test_case_id INTEGER DEFAULT NULL,
+ interface TEXT NOT NULL,
+ property_name TEXT NOT NULL,
+ property_value TEXT NOT NULL,
+
+ UNIQUE (test_program_id, test_case_id, property_name)
+);
+CREATE INDEX index_tmp_metadatas_by_test_case_id
+ ON tmp_metadatas (test_case_id);
+CREATE INDEX index_tmp_metadatas_by_test_program_id
+ ON tmp_metadatas (test_program_id);
+
+
+-- Populate default metadata values for all test programs and test cases.
+--
+-- We do this first to ensure that all test programs and test cases have
+-- explicit values for their metadata. Because we want to keep historical data
+-- for the tests, we must record these values unconditionally instead of relying
+-- on the built-in values in the code.
+--
+-- Once this is done, we override any values explicity set by the tests.
+CREATE TABLE tmp_default_metadata (
+ default_name TEXT PRIMARY KEY,
+ default_value TEXT NOT NULL
+);
+INSERT INTO tmp_default_metadata VALUES ('allowed_architectures', '');
+INSERT INTO tmp_default_metadata VALUES ('allowed_platforms', '');
+INSERT INTO tmp_default_metadata VALUES ('description', '');
+INSERT INTO tmp_default_metadata VALUES ('has_cleanup', 'false');
+INSERT INTO tmp_default_metadata VALUES ('required_configs', '');
+INSERT INTO tmp_default_metadata VALUES ('required_files', '');
+INSERT INTO tmp_default_metadata VALUES ('required_memory', '0');
+INSERT INTO tmp_default_metadata VALUES ('required_programs', '');
+INSERT INTO tmp_default_metadata VALUES ('required_user', '');
+INSERT INTO tmp_default_metadata VALUES ('timeout', '300');
+INSERT INTO tmp_metadatas
+ SELECT test_program_id, NULL, interface, default_name, default_value
+ FROM test_programs JOIN tmp_default_metadata;
+INSERT INTO tmp_metadatas
+ SELECT NULL, test_case_id, interface, default_name, default_value
+ FROM test_programs JOIN test_cases
+ ON test_cases.test_program_id = test_programs.test_program_id
+ JOIN tmp_default_metadata;
+DROP TABLE tmp_default_metadata;
+
+
+-- Populate metadata overrides from plain test programs.
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT CAST(timeout / 1000000 AS TEXT) FROM plain_test_programs AS aux
+ WHERE aux.test_program_id = tmp_metadatas.test_program_id)
+ WHERE test_program_id IS NOT NULL AND property_name = 'timeout'
+ AND interface = 'plain';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT DISTINCT CAST(timeout / 1000000 AS TEXT)
+ FROM test_cases AS aux JOIN plain_test_programs
+ ON aux.test_program_id == plain_test_programs.test_program_id
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'timeout'
+ AND interface = 'plain';
+
+
+CREATE INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id
+ ON atf_test_cases_multivalues (test_case_id);
+
+
+-- Populate metadata overrides from ATF test cases.
+UPDATE atf_test_cases SET description = '' WHERE description IS NULL;
+UPDATE atf_test_cases SET required_user = '' WHERE required_user IS NULL;
+
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT description FROM atf_test_cases AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'description'
+ AND interface = 'atf';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT has_cleanup FROM atf_test_cases AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'has_cleanup'
+ AND interface = 'atf';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT CAST(timeout / 1000000 AS TEXT) FROM atf_test_cases AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'timeout'
+ AND interface = 'atf';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT CAST(required_memory AS TEXT) FROM atf_test_cases AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'required_memory'
+ AND interface = 'atf';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT required_user FROM atf_test_cases AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id)
+ WHERE test_case_id IS NOT NULL AND property_name = 'required_user'
+ AND interface = 'atf';
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT GROUP_CONCAT(aux.property_value, ' ')
+ FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id AND
+ aux.property_name = 'require.arch')
+ WHERE test_case_id IS NOT NULL AND property_name = 'allowed_architectures'
+ AND interface = 'atf'
+ AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id
+ AND property_name = 'require.arch');
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT GROUP_CONCAT(aux.property_value, ' ')
+ FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id AND
+ aux.property_name = 'require.machine')
+ WHERE test_case_id IS NOT NULL AND property_name = 'allowed_platforms'
+ AND interface = 'atf'
+ AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id
+ AND property_name = 'require.machine');
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT GROUP_CONCAT(aux.property_value, ' ')
+ FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id AND
+ aux.property_name = 'require.config')
+ WHERE test_case_id IS NOT NULL AND property_name = 'required_configs'
+ AND interface = 'atf'
+ AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id
+ AND property_name = 'require.config');
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT GROUP_CONCAT(aux.property_value, ' ')
+ FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id AND
+ aux.property_name = 'require.files')
+ WHERE test_case_id IS NOT NULL AND property_name = 'required_files'
+ AND interface = 'atf'
+ AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id
+ AND property_name = 'require.files');
+UPDATE tmp_metadatas
+ SET property_value = (
+ SELECT GROUP_CONCAT(aux.property_value, ' ')
+ FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id AND
+ aux.property_name = 'require.progs')
+ WHERE test_case_id IS NOT NULL AND property_name = 'required_programs'
+ AND interface = 'atf'
+ AND EXISTS(SELECT 1 FROM atf_test_cases_multivalues AS aux
+ WHERE aux.test_case_id = tmp_metadatas.test_case_id
+ AND property_name = 'require.progs');
+
+
+-- Fill metadata_id pointers in the test_programs and test_cases tables.
+UPDATE test_programs
+ SET metadata_id = (
+ SELECT MIN(ROWID) FROM tmp_metadatas
+ WHERE tmp_metadatas.test_program_id = test_programs.test_program_id
+ );
+UPDATE test_cases
+ SET metadata_id = (
+ SELECT MIN(ROWID) FROM tmp_metadatas
+ WHERE tmp_metadatas.test_case_id = test_cases.test_case_id
+ );
+
+
+-- Populate the metadatas table based on tmp_metadatas.
+INSERT INTO metadatas (metadata_id, property_name, property_value)
+ SELECT (
+ SELECT MIN(ROWID) FROM tmp_metadatas AS s
+ WHERE s.test_program_id = tmp_metadatas.test_program_id
+ ), property_name, property_value
+ FROM tmp_metadatas WHERE test_program_id IS NOT NULL;
+INSERT INTO metadatas (metadata_id, property_name, property_value)
+ SELECT (
+ SELECT MIN(ROWID) FROM tmp_metadatas AS s
+ WHERE s.test_case_id = tmp_metadatas.test_case_id
+ ), property_name, property_value
+ FROM tmp_metadatas WHERE test_case_id IS NOT NULL;
+
+
+-- Drop temporary entities used during the migration.
+DROP INDEX index_tmp_atf_test_cases_multivalues_by_test_case_id;
+DROP INDEX index_tmp_metadatas_by_test_program_id;
+DROP INDEX index_tmp_metadatas_by_test_case_id;
+DROP TABLE tmp_metadatas;
+
+
+--
+-- Drop obsolete tables.
+--
+
+
+DROP TABLE atf_test_cases;
+DROP TABLE atf_test_cases_multivalues;
+DROP TABLE plain_test_programs;
+
+
+--
+-- Update the metadata version.
+--
+
+
+INSERT INTO metadata (timestamp, schema_version)
+ VALUES (strftime('%s', 'now'), 2);
diff --git a/store/migrate_v2_v3.sql b/store/migrate_v2_v3.sql
new file mode 100644
index 000000000000..7e6061cccf11
--- /dev/null
+++ b/store/migrate_v2_v3.sql
@@ -0,0 +1,120 @@
+-- Copyright 2014 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.
+
+-- \file store/v2-to-v3.sql
+-- Migration of a database with version 2 of the schema to version 3.
+--
+-- Version 3 appeared in revision 084d740b1da635946d153475156e335ddfc4aed6
+-- and its changes were:
+--
+-- * Removal of historical data.
+--
+-- Because from v2 to v3 we went from a unified database to many separate
+-- databases, this file is parameterized on @ACTION_ID@. The file has to
+-- be executed once per action with this string replaced.
+
+
+ATTACH DATABASE "@OLD_DATABASE@" AS old_store;
+
+
+-- New database already contains a record for v3. Just import older entries.
+INSERT INTO metadata SELECT * FROM old_store.metadata;
+
+INSERT INTO contexts
+ SELECT cwd
+ FROM old_store.actions
+ NATURAL JOIN old_store.contexts
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO env_vars
+ SELECT var_name, var_value
+ FROM old_store.actions
+ NATURAL JOIN old_store.contexts
+ NATURAL JOIN old_store.env_vars
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO metadatas
+ SELECT metadata_id, property_name, property_value
+ FROM old_store.metadatas
+ WHERE metadata_id IN (
+ SELECT test_programs.metadata_id
+ FROM old_store.test_programs
+ WHERE action_id == @ACTION_ID@
+ ) OR metadata_id IN (
+ SELECT test_cases.metadata_id
+ FROM old_store.test_programs JOIN old_store.test_cases
+ ON test_programs.test_program_id == test_cases.test_program_id
+ WHERE action_id == @ACTION_ID@
+ );
+
+INSERT INTO test_programs
+ SELECT test_program_id, absolute_path, root, relative_path,
+ test_suite_name, metadata_id, interface
+ FROM old_store.test_programs
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO test_cases
+ SELECT test_cases.test_case_id, test_cases.test_program_id,
+ test_cases.name, test_cases.metadata_id
+ FROM old_store.test_cases JOIN old_store.test_programs
+ ON test_cases.test_program_id == test_programs.test_program_id
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO test_results
+ SELECT test_results.test_case_id, test_results.result_type,
+ test_results.result_reason, test_results.start_time, test_results.end_time
+ FROM old_store.test_results
+ JOIN old_store.test_cases
+ ON test_results.test_case_id == test_cases.test_case_id
+ JOIN old_store.test_programs
+ ON test_cases.test_program_id == test_programs.test_program_id
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO files
+ SELECT files.file_id, files.contents
+ FROM old_store.files
+ JOIN old_store.test_case_files
+ ON files.file_id == test_case_files.file_id
+ JOIN old_store.test_cases
+ ON test_case_files.test_case_id == test_cases.test_case_id
+ JOIN old_store.test_programs
+ ON test_cases.test_program_id == test_programs.test_program_id
+ WHERE action_id == @ACTION_ID@;
+
+INSERT INTO test_case_files
+ SELECT test_case_files.test_case_id, test_case_files.file_name,
+ test_case_files.file_id
+ FROM old_store.test_case_files
+ JOIN old_store.test_cases
+ ON test_case_files.test_case_id == test_cases.test_case_id
+ JOIN old_store.test_programs
+ ON test_cases.test_program_id == test_programs.test_program_id
+ WHERE action_id == @ACTION_ID@;
+
+
+DETACH DATABASE old_store;
diff --git a/store/read_backend.cpp b/store/read_backend.cpp
new file mode 100644
index 000000000000..bc5b860d402c
--- /dev/null
+++ b/store/read_backend.cpp
@@ -0,0 +1,160 @@
+// 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/read_backend.hpp"
+
+#include "store/exceptions.hpp"
+#include "store/metadata.hpp"
+#include "store/read_transaction.hpp"
+#include "store/write_backend.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+
+namespace fs = utils::fs;
+namespace sqlite = utils::sqlite;
+
+
+/// Opens a database and defines session pragmas.
+///
+/// This auxiliary function ensures that, every time we open a SQLite database,
+/// we define the same set of pragmas for it.
+///
+/// \param file The database file to be opened.
+/// \param flags The flags for the open; see sqlite::database::open.
+///
+/// \return The opened database.
+///
+/// \throw store::error If there is a problem opening or creating the database.
+sqlite::database
+store::detail::open_and_setup(const fs::path& file, const int flags)
+{
+ try {
+ sqlite::database database = sqlite::database::open(file, flags);
+ database.exec("PRAGMA foreign_keys = ON");
+ return database;
+ } catch (const sqlite::error& e) {
+ throw store::error(F("Cannot open '%s': %s") % file % e.what());
+ }
+}
+
+
+/// Internal implementation for the backend.
+struct store::read_backend::impl : utils::noncopyable {
+ /// The SQLite database this backend talks to.
+ sqlite::database database;
+
+ /// Constructor.
+ ///
+ /// \param database_ The SQLite database instance.
+ /// \param metadata_ The metadata for the loaded database. This must match
+ /// the schema version we implement in this module; otherwise, a
+ /// migration is necessary.
+ ///
+ /// \throw integrity_error If the schema in the database is too modern,
+ /// which might indicate some form of corruption or an old binary.
+ /// \throw old_schema_error If the schema in the database is older than our
+ /// currently-implemented version and needs an upgrade. The caller can
+ /// use migrate_schema() to fix this problem.
+ impl(sqlite::database& database_, const metadata& metadata_) :
+ database(database_)
+ {
+ const int database_version = metadata_.schema_version();
+
+ if (database_version == detail::current_schema_version) {
+ // OK.
+ } else if (database_version < detail::current_schema_version) {
+ throw old_schema_error(database_version);
+ } else if (database_version > detail::current_schema_version) {
+ throw integrity_error(
+ F("Database at schema version %s, which is newer than the "
+ "supported version %s")
+ % database_version % detail::current_schema_version);
+ }
+ }
+};
+
+
+/// Constructs a new backend.
+///
+/// \param pimpl_ The internal data.
+store::read_backend::read_backend(impl* pimpl_) :
+ _pimpl(pimpl_)
+{
+}
+
+
+/// Destructor.
+store::read_backend::~read_backend(void)
+{
+}
+
+
+/// Opens a database in read-only mode.
+///
+/// \param file The database file to be opened.
+///
+/// \return The backend representation.
+///
+/// \throw store::error If there is any problem opening the database.
+store::read_backend
+store::read_backend::open_ro(const fs::path& file)
+{
+ sqlite::database db = detail::open_and_setup(file, sqlite::open_readonly);
+ return read_backend(new impl(db, metadata::fetch_latest(db)));
+}
+
+
+/// Closes the SQLite database.
+void
+store::read_backend::close(void)
+{
+ _pimpl->database.close();
+}
+
+
+/// Gets the connection to the SQLite database.
+///
+/// \return A database connection.
+sqlite::database&
+store::read_backend::database(void)
+{
+ return _pimpl->database;
+}
+
+
+/// Opens a read-only transaction.
+///
+/// \return A new transaction.
+store::read_transaction
+store::read_backend::start_read(void)
+{
+ return read_transaction(*this);
+}
diff --git a/store/read_backend.hpp b/store/read_backend.hpp
new file mode 100644
index 000000000000..2ddb6e650c86
--- /dev/null
+++ b/store/read_backend.hpp
@@ -0,0 +1,77 @@
+// 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.
+
+/// \file store/read_backend.hpp
+/// Interface to the backend database for read-only operations.
+
+#if !defined(STORE_READ_BACKEND_HPP)
+#define STORE_READ_BACKEND_HPP
+
+#include "store/read_backend_fwd.hpp"
+
+#include <memory>
+
+#include "store/read_transaction_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/sqlite/database_fwd.hpp"
+
+namespace store {
+
+
+namespace detail {
+
+
+utils::sqlite::database open_and_setup(const utils::fs::path&, const int);
+
+
+} // anonymous namespace
+
+
+/// Public interface to the database store for read-only operations.
+class read_backend {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ read_backend(impl*);
+
+public:
+ ~read_backend(void);
+
+ static read_backend open_ro(const utils::fs::path&);
+ void close(void);
+
+ utils::sqlite::database& database(void);
+ read_transaction start_read(void);
+};
+
+
+} // namespace store
+
+#endif // !defined(STORE_READ_BACKEND_HPP)
diff --git a/store/read_backend_fwd.hpp b/store/read_backend_fwd.hpp
new file mode 100644
index 000000000000..4d7f5aa1429b
--- /dev/null
+++ b/store/read_backend_fwd.hpp
@@ -0,0 +1,43 @@
+// Copyright 2015 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.
+
+/// \file store/read_backend_fwd.hpp
+/// Forward declarations for store/read_backend.hpp
+
+#if !defined(STORE_READ_BACKEND_FWD_HPP)
+#define STORE_READ_BACKEND_FWD_HPP
+
+namespace store {
+
+
+class read_backend;
+
+
+} // namespace store
+
+#endif // !defined(STORE_READ_BACKEND_FWD_HPP)
diff --git a/store/read_backend_test.cpp b/store/read_backend_test.cpp
new file mode 100644
index 000000000000..062966cd226d
--- /dev/null
+++ b/store/read_backend_test.cpp
@@ -0,0 +1,152 @@
+// 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/read_backend.hpp"
+
+#include <atf-c++.hpp>
+
+#include "store/exceptions.hpp"
+#include "store/metadata.hpp"
+#include "store/write_backend.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__ok);
+ATF_TEST_CASE_BODY(detail__open_and_setup__ok)
+{
+ {
+ sqlite::database db = sqlite::database::open(
+ fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
+ db.exec("CREATE TABLE one (foo INTEGER PRIMARY KEY AUTOINCREMENT);");
+ db.exec("CREATE TABLE two (foo INTEGER REFERENCES one);");
+ db.close();
+ }
+
+ sqlite::database db = store::detail::open_and_setup(
+ fs::path("test.db"), sqlite::open_readwrite);
+ db.exec("INSERT INTO one (foo) VALUES (12);");
+ // Ensure foreign keys have been enabled.
+ db.exec("INSERT INTO two (foo) VALUES (12);");
+ ATF_REQUIRE_THROW(sqlite::error,
+ db.exec("INSERT INTO two (foo) VALUES (34);"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__open_and_setup__missing_file);
+ATF_TEST_CASE_BODY(detail__open_and_setup__missing_file)
+{
+ ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ",
+ store::detail::open_and_setup(fs::path("missing.db"),
+ sqlite::open_readonly));
+ ATF_REQUIRE(!fs::exists(fs::path("missing.db")));
+}
+
+
+ATF_TEST_CASE(read_backend__open_ro__ok);
+ATF_TEST_CASE_HEAD(read_backend__open_ro__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(read_backend__open_ro__ok)
+{
+ {
+ sqlite::database db = sqlite::database::open(
+ fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
+ store::detail::initialize(db);
+ }
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+ backend.database().exec("SELECT * FROM metadata");
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(read_backend__open_ro__missing_file);
+ATF_TEST_CASE_BODY(read_backend__open_ro__missing_file)
+{
+ ATF_REQUIRE_THROW_RE(store::error, "Cannot open 'missing.db': ",
+ store::read_backend::open_ro(fs::path("missing.db")));
+ ATF_REQUIRE(!fs::exists(fs::path("missing.db")));
+}
+
+
+ATF_TEST_CASE(read_backend__open_ro__integrity_error);
+ATF_TEST_CASE_HEAD(read_backend__open_ro__integrity_error)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(read_backend__open_ro__integrity_error)
+{
+ {
+ sqlite::database db = sqlite::database::open(
+ fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
+ store::detail::initialize(db);
+ db.exec("DELETE FROM metadata");
+ }
+ ATF_REQUIRE_THROW_RE(store::integrity_error, "metadata.*empty",
+ store::read_backend::open_ro(fs::path("test.db")));
+}
+
+
+ATF_TEST_CASE(read_backend__close);
+ATF_TEST_CASE_HEAD(read_backend__close)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(read_backend__close)
+{
+ store::write_backend::open_rw(fs::path("test.db")); // Create database.
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+ backend.database().exec("SELECT * FROM metadata");
+ backend.close();
+ ATF_REQUIRE_THROW(utils::sqlite::error,
+ backend.database().exec("SELECT * FROM metadata"));
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__ok);
+ ATF_ADD_TEST_CASE(tcs, detail__open_and_setup__missing_file);
+
+ ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__ok);
+ ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__missing_file);
+ ATF_ADD_TEST_CASE(tcs, read_backend__open_ro__integrity_error);
+ ATF_ADD_TEST_CASE(tcs, read_backend__close);
+}
diff --git a/store/read_transaction.cpp b/store/read_transaction.cpp
new file mode 100644
index 000000000000..68539c8346e0
--- /dev/null
+++ b/store/read_transaction.cpp
@@ -0,0 +1,532 @@
+// 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/read_transaction.hpp"
+
+extern "C" {
+#include <stdint.h>
+}
+
+#include <map>
+#include <utility>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_case.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/dbtypes.hpp"
+#include "store/exceptions.hpp"
+#include "store/read_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/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::optional;
+
+
+namespace {
+
+
+/// Retrieves the environment variables of the context.
+///
+/// \param db The SQLite database.
+///
+/// \return The environment variables of the specified context.
+///
+/// \throw sqlite::error If there is a problem loading the variables.
+static std::map< std::string, std::string >
+get_env_vars(sqlite::database& db)
+{
+ std::map< std::string, std::string > env;
+
+ sqlite::statement stmt = db.create_statement(
+ "SELECT var_name, var_value FROM env_vars");
+
+ while (stmt.step()) {
+ const std::string name = stmt.safe_column_text("var_name");
+ const std::string value = stmt.safe_column_text("var_value");
+ env[name] = value;
+ }
+
+ return env;
+}
+
+
+/// Retrieves a metadata object.
+///
+/// \param db The SQLite database.
+/// \param metadata_id The identifier of the metadata.
+///
+/// \return A new metadata object.
+static model::metadata
+get_metadata(sqlite::database& db, const int64_t metadata_id)
+{
+ model::metadata_builder builder;
+
+ sqlite::statement stmt = db.create_statement(
+ "SELECT * FROM metadatas WHERE metadata_id == :metadata_id");
+ stmt.bind(":metadata_id", metadata_id);
+ while (stmt.step()) {
+ const std::string name = stmt.safe_column_text("property_name");
+ const std::string value = stmt.safe_column_text("property_value");
+ builder.set_string(name, value);
+ }
+
+ return builder.build();
+}
+
+
+/// Gets a file from the database.
+///
+/// \param db The database to query the file from.
+/// \param file_id The identifier of the file to be queried.
+///
+/// \return A textual representation of the file contents.
+///
+/// \throw integrity_error If there is any problem in the loaded data or if the
+/// file cannot be found.
+static std::string
+get_file(sqlite::database& db, const int64_t file_id)
+{
+ sqlite::statement stmt = db.create_statement(
+ "SELECT contents FROM files WHERE file_id == :file_id");
+ stmt.bind(":file_id", file_id);
+ if (!stmt.step())
+ throw store::integrity_error(F("Cannot find referenced file %s") %
+ file_id);
+
+ try {
+ const sqlite::blob raw_contents = stmt.safe_column_blob("contents");
+ const std::string contents(
+ static_cast< const char *>(raw_contents.memory), raw_contents.size);
+
+ const bool more = stmt.step();
+ INV(!more);
+
+ return contents;
+ } catch (const sqlite::error& e) {
+ throw store::integrity_error(e.what());
+ }
+}
+
+
+/// Gets all the test cases within a particular test program.
+///
+/// \param db The database to query the information from.
+/// \param test_program_id The identifier of the test program whose test cases
+/// to query.
+///
+/// \return The collection of loaded test cases.
+///
+/// \throw integrity_error If there is any problem in the loaded data.
+static model::test_cases_map
+get_test_cases(sqlite::database& db, const int64_t test_program_id)
+{
+ model::test_cases_map_builder test_cases;
+
+ sqlite::statement stmt = db.create_statement(
+ "SELECT name, metadata_id "
+ "FROM test_cases WHERE test_program_id == :test_program_id");
+ stmt.bind(":test_program_id", test_program_id);
+ while (stmt.step()) {
+ const std::string name = stmt.safe_column_text("name");
+ const int64_t metadata_id = stmt.safe_column_int64("metadata_id");
+
+ const model::metadata metadata = get_metadata(db, metadata_id);
+ LD(F("Loaded test case '%s'") % name);
+ test_cases.add(name, metadata);
+ }
+
+ return test_cases.build();
+}
+
+
+/// Retrieves a result from the database.
+///
+/// \param stmt The statement with the data for the result to load.
+/// \param type_column The name of the column containing the type of the result.
+/// \param reason_column The name of the column containing the reason for the
+/// result, if any.
+///
+/// \return The loaded result.
+///
+/// \throw integrity_error If the data in the database is invalid.
+static model::test_result
+parse_result(sqlite::statement& stmt, const char* type_column,
+ const char* reason_column)
+{
+ try {
+ const model::test_result_type type =
+ store::column_test_result_type(stmt, type_column);
+ if (type == model::test_result_passed) {
+ if (stmt.column_type(stmt.column_id(reason_column)) !=
+ sqlite::type_null)
+ throw store::integrity_error("Result of type 'passed' has a "
+ "non-NULL reason");
+ return model::test_result(type);
+ } else {
+ return model::test_result(type,
+ stmt.safe_column_text(reason_column));
+ }
+ } catch (const sqlite::error& e) {
+ throw store::integrity_error(e.what());
+ }
+}
+
+
+} // anonymous namespace
+
+
+/// Loads a specific test program from the database.
+///
+/// \param backend_ The store backend we are dealing with.
+/// \param id The identifier of the test program to load.
+///
+/// \return The instantiated test program.
+///
+/// \throw integrity_error If the data read from the database cannot be properly
+/// interpreted.
+model::test_program_ptr
+store::detail::get_test_program(read_backend& backend_, const int64_t id)
+{
+ sqlite::database& db = backend_.database();
+
+ model::test_program_ptr test_program;
+ sqlite::statement stmt = db.create_statement(
+ "SELECT * FROM test_programs WHERE test_program_id == :id");
+ stmt.bind(":id", id);
+ stmt.step();
+ const std::string interface = stmt.safe_column_text("interface");
+ test_program.reset(new model::test_program(
+ interface,
+ fs::path(stmt.safe_column_text("relative_path")),
+ fs::path(stmt.safe_column_text("root")),
+ stmt.safe_column_text("test_suite_name"),
+ get_metadata(db, stmt.safe_column_int64("metadata_id")),
+ get_test_cases(db, id)));
+ const bool more = stmt.step();
+ INV(!more);
+
+ LD(F("Loaded test program '%s'") % test_program->relative_path());
+ return test_program;
+}
+
+
+/// Internal implementation for a results iterator.
+struct store::results_iterator::impl : utils::noncopyable {
+ /// The store backend we are dealing with.
+ store::read_backend _backend;
+
+ /// The statement to iterate on.
+ sqlite::statement _stmt;
+
+ /// A cache for the last loaded test program.
+ optional< std::pair< int64_t, model::test_program_ptr > >
+ _last_test_program;
+
+ /// Whether the iterator is still valid or not.
+ bool _valid;
+
+ /// Constructor.
+ ///
+ /// \param backend_ The store backend implementation.
+ impl(store::read_backend& backend_) :
+ _backend(backend_),
+ _stmt(backend_.database().create_statement(
+ "SELECT test_programs.test_program_id, "
+ " test_programs.interface, "
+ " test_cases.test_case_id, test_cases.name, "
+ " test_results.result_type, test_results.result_reason, "
+ " test_results.start_time, test_results.end_time "
+ "FROM test_programs "
+ " JOIN test_cases "
+ " ON test_programs.test_program_id = test_cases.test_program_id "
+ " JOIN test_results "
+ " ON test_cases.test_case_id = test_results.test_case_id "
+ "ORDER BY test_programs.absolute_path, test_cases.name"))
+ {
+ _valid = _stmt.step();
+ }
+};
+
+
+/// Constructor.
+///
+/// \param pimpl_ The internal implementation details of the iterator.
+store::results_iterator::results_iterator(
+ std::shared_ptr< impl > pimpl_) :
+ _pimpl(pimpl_)
+{
+}
+
+
+/// Destructor.
+store::results_iterator::~results_iterator(void)
+{
+}
+
+
+/// Moves the iterator forward by one result.
+///
+/// \return The iterator itself.
+store::results_iterator&
+store::results_iterator::operator++(void)
+{
+ _pimpl->_valid = _pimpl->_stmt.step();
+ return *this;
+}
+
+
+/// Checks whether the iterator is still valid.
+///
+/// \return True if there is more elements to iterate on, false otherwise.
+store::results_iterator::operator bool(void) const
+{
+ return _pimpl->_valid;
+}
+
+
+/// Gets the test program this result belongs to.
+///
+/// \return The representation of a test program.
+const model::test_program_ptr
+store::results_iterator::test_program(void) const
+{
+ const int64_t id = _pimpl->_stmt.safe_column_int64("test_program_id");
+ if (!_pimpl->_last_test_program ||
+ _pimpl->_last_test_program.get().first != id)
+ {
+ const model::test_program_ptr tp = detail::get_test_program(
+ _pimpl->_backend, id);
+ _pimpl->_last_test_program = std::make_pair(id, tp);
+ }
+ return _pimpl->_last_test_program.get().second;
+}
+
+
+/// Gets the name of the test case pointed by the iterator.
+///
+/// The caller can look up the test case data by using the find() method on the
+/// test program returned by test_program().
+///
+/// \return A test case name, unique within the test program.
+std::string
+store::results_iterator::test_case_name(void) const
+{
+ return _pimpl->_stmt.safe_column_text("name");
+}
+
+
+/// Gets the result of the test case pointed by the iterator.
+///
+/// \return A test case result.
+model::test_result
+store::results_iterator::result(void) const
+{
+ return parse_result(_pimpl->_stmt, "result_type", "result_reason");
+}
+
+
+/// Gets the start time of the test case execution.
+///
+/// \return The time when the test started execution.
+datetime::timestamp
+store::results_iterator::start_time(void) const
+{
+ return column_timestamp(_pimpl->_stmt, "start_time");
+}
+
+
+/// Gets the end time of the test case execution.
+///
+/// \return The time when the test finished execution.
+datetime::timestamp
+store::results_iterator::end_time(void) const
+{
+ return column_timestamp(_pimpl->_stmt, "end_time");
+}
+
+
+/// Gets a file from a test case.
+///
+/// \param db The database to query the file from.
+/// \param test_case_id The identifier of the test case.
+/// \param filename The name of the file to be retrieved.
+///
+/// \return A textual representation of the file contents.
+///
+/// \throw integrity_error If there is any problem in the loaded data or if the
+/// file cannot be found.
+static std::string
+get_test_case_file(sqlite::database& db, const int64_t test_case_id,
+ const char* filename)
+{
+ sqlite::statement stmt = db.create_statement(
+ "SELECT file_id FROM test_case_files "
+ "WHERE test_case_id == :test_case_id AND file_name == :file_name");
+ stmt.bind(":test_case_id", test_case_id);
+ stmt.bind(":file_name", filename);
+ if (stmt.step())
+ return get_file(db, stmt.safe_column_int64("file_id"));
+ else
+ return "";
+}
+
+
+/// Gets the contents of stdout of a test case.
+///
+/// \return A textual representation of the stdout contents of the test case.
+/// This may of course be empty if the test case didn't print anything.
+std::string
+store::results_iterator::stdout_contents(void) const
+{
+ return get_test_case_file(_pimpl->_backend.database(),
+ _pimpl->_stmt.safe_column_int64("test_case_id"),
+ "__STDOUT__");
+}
+
+
+/// Gets the contents of stderr of a test case.
+///
+/// \return A textual representation of the stderr contents of the test case.
+/// This may of course be empty if the test case didn't print anything.
+std::string
+store::results_iterator::stderr_contents(void) const
+{
+ return get_test_case_file(_pimpl->_backend.database(),
+ _pimpl->_stmt.safe_column_int64("test_case_id"),
+ "__STDERR__");
+}
+
+
+/// Internal implementation for a store read-only transaction.
+struct store::read_transaction::impl : utils::noncopyable {
+ /// The backend instance.
+ store::read_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(read_backend& backend_) :
+ _backend(backend_),
+ _db(backend_.database()),
+ _tx(backend_.database().begin_transaction())
+ {
+ }
+};
+
+
+/// Creates a new read-only transaction.
+///
+/// \param backend_ The backend this transaction belongs to.
+store::read_transaction::read_transaction(read_backend& backend_) :
+ _pimpl(new impl(backend_))
+{
+}
+
+
+/// Destructor.
+store::read_transaction::~read_transaction(void)
+{
+}
+
+
+/// Finishes the transaction.
+///
+/// This actually commits the result of the transaction, but because the
+/// transaction is read-only, we use a different term to denote that there is no
+/// distinction between commit and rollback.
+///
+/// \throw error If there is any problem when talking to the database.
+void
+store::read_transaction::finish(void)
+{
+ try {
+ _pimpl->_tx.commit();
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
+
+
+/// Retrieves an context from the database.
+///
+/// \return The retrieved context.
+///
+/// \throw error If there is a problem loading the context.
+model::context
+store::read_transaction::get_context(void)
+{
+ try {
+ sqlite::statement stmt = _pimpl->_db.create_statement(
+ "SELECT cwd FROM contexts");
+ if (!stmt.step())
+ throw error("Error loading context: no data");
+
+ return model::context(fs::path(stmt.safe_column_text("cwd")),
+ get_env_vars(_pimpl->_db));
+ } catch (const sqlite::error& e) {
+ throw error(F("Error loading context: %s") % e.what());
+ }
+}
+
+
+/// Creates a new iterator to scan tests results.
+///
+/// \return The constructed iterator.
+///
+/// \throw error If there is any problem constructing the iterator.
+store::results_iterator
+store::read_transaction::get_results(void)
+{
+ try {
+ return results_iterator(std::shared_ptr< results_iterator::impl >(
+ new results_iterator::impl(_pimpl->_backend)));
+ } catch (const sqlite::error& e) {
+ throw error(e.what());
+ }
+}
diff --git a/store/read_transaction.hpp b/store/read_transaction.hpp
new file mode 100644
index 000000000000..7dd20db782eb
--- /dev/null
+++ b/store/read_transaction.hpp
@@ -0,0 +1,120 @@
+// 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.
+
+/// \file store/read_transaction.hpp
+/// Implementation of read-only transactions on the backend.
+
+#if !defined(STORE_READ_TRANSACTION_HPP)
+#define STORE_READ_TRANSACTION_HPP
+
+#include "store/read_transaction_fwd.hpp"
+
+extern "C" {
+#include <stdint.h>
+}
+
+#include <memory>
+#include <string>
+
+#include "model/context_fwd.hpp"
+#include "model/test_program_fwd.hpp"
+#include "model/test_result_fwd.hpp"
+#include "store/read_backend_fwd.hpp"
+#include "store/read_transaction_fwd.hpp"
+#include "utils/datetime_fwd.hpp"
+
+namespace store {
+
+
+namespace detail {
+
+
+model::test_program_ptr get_test_program(read_backend&, const int64_t);
+
+
+} // namespace detail
+
+
+/// Iterator for the set of test case results that are part of an action.
+///
+/// \todo Note that this is not a "standard" C++ iterator. I have chosen to
+/// implement a different interface because it makes things easier to represent
+/// an SQL statement state. Rewrite as a proper C++ iterator, inheriting from
+/// std::iterator.
+class results_iterator {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class read_transaction;
+ results_iterator(std::shared_ptr< impl >);
+
+public:
+ ~results_iterator(void);
+
+ results_iterator& operator++(void);
+ operator bool(void) const;
+
+ const model::test_program_ptr test_program(void) const;
+ std::string test_case_name(void) const;
+ model::test_result result(void) const;
+ utils::datetime::timestamp start_time(void) const;
+ utils::datetime::timestamp end_time(void) const;
+
+ std::string stdout_contents(void) const;
+ std::string stderr_contents(void) const;
+};
+
+
+/// Representation of a read-only transaction.
+///
+/// Transactions are the entry place for high-level calls that access the
+/// database.
+class read_transaction {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class read_backend;
+ read_transaction(read_backend&);
+
+public:
+ ~read_transaction(void);
+
+ void finish(void);
+
+ model::context get_context(void);
+ results_iterator get_results(void);
+};
+
+
+} // namespace store
+
+#endif // !defined(STORE_READ_TRANSACTION_HPP)
diff --git a/store/read_transaction_fwd.hpp b/store/read_transaction_fwd.hpp
new file mode 100644
index 000000000000..4aae92d9825c
--- /dev/null
+++ b/store/read_transaction_fwd.hpp
@@ -0,0 +1,44 @@
+// Copyright 2015 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.
+
+/// \file store/read_transaction_fwd.hpp
+/// Forward declarations for store/read_transaction.hpp
+
+#if !defined(STORE_READ_TRANSACTION_FWD_HPP)
+#define STORE_READ_TRANSACTION_FWD_HPP
+
+namespace store {
+
+
+class read_transaction;
+class results_iterator;
+
+
+} // namespace store
+
+#endif // !defined(STORE_READ_TRANSACTION_FWD_HPP)
diff --git a/store/read_transaction_test.cpp b/store/read_transaction_test.cpp
new file mode 100644
index 000000000000..711faa674fbe
--- /dev/null
+++ b/store/read_transaction_test.cpp
@@ -0,0 +1,262 @@
+// 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/read_transaction.hpp"
+
+#include <map>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/exceptions.hpp"
+#include "store/read_backend.hpp"
+#include "store/write_backend.hpp"
+#include "store/write_transaction.hpp"
+#include "utils/datetime.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/optional.ipp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+
+
+ATF_TEST_CASE(get_context__missing);
+ATF_TEST_CASE_HEAD(get_context__missing)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_context__missing)
+{
+ store::write_backend::open_rw(fs::path("test.db")); // Create database.
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+
+ store::read_transaction tx = backend.start_read();
+ ATF_REQUIRE_THROW_RE(store::error, "context: no data", tx.get_context());
+}
+
+
+ATF_TEST_CASE(get_context__invalid_cwd);
+ATF_TEST_CASE_HEAD(get_context__invalid_cwd)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_context__invalid_cwd)
+{
+ {
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "INSERT INTO contexts (cwd) VALUES (:cwd)");
+ const char buffer[10] = "foo bar";
+ stmt.bind(":cwd", sqlite::blob(buffer, sizeof(buffer)));
+ stmt.step_without_results();
+ }
+
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+ store::read_transaction tx = backend.start_read();
+ ATF_REQUIRE_THROW_RE(store::error, "context: .*cwd.*not a string",
+ tx.get_context());
+}
+
+
+ATF_TEST_CASE(get_context__invalid_env_vars);
+ATF_TEST_CASE_HEAD(get_context__invalid_env_vars)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_context__invalid_env_vars)
+{
+ {
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test-bad-name.db"));
+ backend.database().exec("INSERT INTO contexts (cwd) "
+ "VALUES ('/foo/bar')");
+ const char buffer[10] = "foo bar";
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "INSERT INTO env_vars (var_name, var_value) "
+ "VALUES (:var_name, 'abc')");
+ stmt.bind(":var_name", sqlite::blob(buffer, sizeof(buffer)));
+ stmt.step_without_results();
+ }
+ {
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test-bad-name.db"));
+ store::read_transaction tx = backend.start_read();
+ ATF_REQUIRE_THROW_RE(store::error, "context: .*var_name.*not a string",
+ tx.get_context());
+ }
+
+ {
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test-bad-value.db"));
+ backend.database().exec("INSERT INTO contexts (cwd) "
+ "VALUES ('/foo/bar')");
+ const char buffer[10] = "foo bar";
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "INSERT INTO env_vars (var_name, var_value) "
+ "VALUES ('abc', :var_value)");
+ stmt.bind(":var_value", sqlite::blob(buffer, sizeof(buffer)));
+ stmt.step_without_results();
+ }
+ {
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test-bad-value.db"));
+ store::read_transaction tx = backend.start_read();
+ ATF_REQUIRE_THROW_RE(store::error, "context: .*var_value.*not a string",
+ tx.get_context());
+ }
+}
+
+
+ATF_TEST_CASE(get_results__none);
+ATF_TEST_CASE_HEAD(get_results__none)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_results__none)
+{
+ store::write_backend::open_rw(fs::path("test.db")); // Create database.
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+ store::read_transaction tx = backend.start_read();
+ store::results_iterator iter = tx.get_results();
+ ATF_REQUIRE(!iter);
+}
+
+
+ATF_TEST_CASE(get_results__many);
+ATF_TEST_CASE_HEAD(get_results__many)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_results__many)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+
+ store::write_transaction tx = backend.start_write();
+
+ const model::context context(fs::path("/foo/bar"),
+ std::map< std::string, std::string >());
+ tx.put_context(context);
+
+ const datetime::timestamp start_time1 = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 10, 00, 0);
+ const datetime::timestamp end_time1 = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 15, 30, 1234);
+ const datetime::timestamp start_time2 = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 15, 40, 987);
+ const datetime::timestamp end_time2 = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 16, 0, 0);
+
+ atf::utils::create_file("unused.txt", "unused file\n");
+
+ const model::test_program test_program_1 = model::test_program_builder(
+ "plain", fs::path("a/prog1"), fs::path("/the/root"), "suite1")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_1(model::test_result_passed);
+ {
+ const int64_t tp_id = tx.put_test_program(test_program_1);
+ const int64_t tc_id = tx.put_test_case(test_program_1, "main", tp_id);
+ atf::utils::create_file("prog1.out", "stdout of prog1\n");
+ tx.put_test_case_file("__STDOUT__", fs::path("prog1.out"), tc_id);
+ tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id);
+ tx.put_result(result_1, tc_id, start_time1, end_time1);
+ }
+
+ const model::test_program test_program_2 = model::test_program_builder(
+ "plain", fs::path("b/prog2"), fs::path("/the/root"), "suite2")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_2(model::test_result_failed,
+ "Some text");
+ {
+ const int64_t tp_id = tx.put_test_program(test_program_2);
+ const int64_t tc_id = tx.put_test_case(test_program_2, "main", tp_id);
+ atf::utils::create_file("prog2.err", "stderr of prog2\n");
+ tx.put_test_case_file("__STDERR__", fs::path("prog2.err"), tc_id);
+ tx.put_test_case_file("unused.txt", fs::path("unused.txt"), tc_id);
+ tx.put_result(result_2, tc_id, start_time2, end_time2);
+ }
+
+ tx.commit();
+ backend.close();
+
+ store::read_backend backend2 = store::read_backend::open_ro(
+ fs::path("test.db"));
+ store::read_transaction tx2 = backend2.start_read();
+ store::results_iterator iter = tx2.get_results();
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_1, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ("stdout of prog1\n", iter.stdout_contents());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(result_1, iter.result());
+ ATF_REQUIRE_EQ(start_time1, iter.start_time());
+ ATF_REQUIRE_EQ(end_time1, iter.end_time());
+ ATF_REQUIRE(++iter);
+ ATF_REQUIRE_EQ(test_program_2, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE_EQ("stderr of prog2\n", iter.stderr_contents());
+ ATF_REQUIRE_EQ(result_2, iter.result());
+ ATF_REQUIRE_EQ(start_time2, iter.start_time());
+ ATF_REQUIRE_EQ(end_time2, iter.end_time());
+ ATF_REQUIRE(!++iter);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, get_context__missing);
+ ATF_ADD_TEST_CASE(tcs, get_context__invalid_cwd);
+ ATF_ADD_TEST_CASE(tcs, get_context__invalid_env_vars);
+
+ ATF_ADD_TEST_CASE(tcs, get_results__none);
+ ATF_ADD_TEST_CASE(tcs, get_results__many);
+}
diff --git a/store/schema_inttest.cpp b/store/schema_inttest.cpp
new file mode 100644
index 000000000000..cd528b0c48d8
--- /dev/null
+++ b/store/schema_inttest.cpp
@@ -0,0 +1,492 @@
+// Copyright 2013 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 <map>
+
+#include <atf-c++.hpp>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/migrate.hpp"
+#include "store/read_backend.hpp"
+#include "store/read_transaction.hpp"
+#include "store/write_backend.hpp"
+#include "utils/datetime.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/stream.hpp"
+#include "utils/units.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+namespace units = utils::units;
+
+
+namespace {
+
+
+/// Gets a data file from the tests directory.
+///
+/// We cannot use the srcdir property because the required files are not there
+/// when building with an object directory. In those cases, the data files
+/// remainin the source directory while the resulting test program is in the
+/// object directory, thus having the wrong value for its srcdir property.
+///
+/// \param name Basename of the test data file to query.
+///
+/// \return The actual path to the requested data file.
+static fs::path
+testdata_file(const std::string& name)
+{
+ const fs::path testdatadir(utils::getenv_with_default(
+ "KYUA_STORETESTDATADIR", KYUA_STORETESTDATADIR));
+ return testdatadir / name;
+}
+
+
+/// Validates the contents of the action with identifier 1.
+///
+/// \param dbpath Path to the database in which to check the action contents.
+static void
+check_action_1(const fs::path& dbpath)
+{
+ store::read_backend backend = store::read_backend::open_ro(dbpath);
+ store::read_transaction transaction = backend.start_read();
+
+ const fs::path root("/some/root");
+ std::map< std::string, std::string > environment;
+ const model::context context(root, environment);
+
+ ATF_REQUIRE_EQ(context, transaction.get_context());
+
+ store::results_iterator iter = transaction.get_results();
+ ATF_REQUIRE(!iter);
+}
+
+
+/// Validates the contents of the action with identifier 2.
+///
+/// \param dbpath Path to the database in which to check the action contents.
+static void
+check_action_2(const fs::path& dbpath)
+{
+ store::read_backend backend = store::read_backend::open_ro(dbpath);
+ store::read_transaction transaction = backend.start_read();
+
+ const fs::path root("/test/suite/root");
+ std::map< std::string, std::string > environment;
+ environment["HOME"] = "/home/test";
+ environment["PATH"] = "/bin:/usr/bin";
+ const model::context context(root, environment);
+
+ ATF_REQUIRE_EQ(context, transaction.get_context());
+
+ const model::test_program test_program_1 = model::test_program_builder(
+ "plain", fs::path("foo_test"), fs::path("/test/suite/root"),
+ "suite-name")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_1(model::test_result_passed);
+
+ const model::test_program test_program_2 = model::test_program_builder(
+ "plain", fs::path("subdir/another_test"), fs::path("/test/suite/root"),
+ "subsuite-name")
+ .add_test_case("main",
+ model::metadata_builder()
+ .set_timeout(datetime::delta(10, 0))
+ .build())
+ .set_metadata(model::metadata_builder()
+ .set_timeout(datetime::delta(10, 0))
+ .build())
+ .build();
+ const model::test_result result_2(model::test_result_failed,
+ "Exited with code 1");
+
+ const model::test_program test_program_3 = model::test_program_builder(
+ "plain", fs::path("subdir/bar_test"), fs::path("/test/suite/root"),
+ "subsuite-name")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_3(model::test_result_broken,
+ "Received signal 1");
+
+ const model::test_program test_program_4 = model::test_program_builder(
+ "plain", fs::path("top_test"), fs::path("/test/suite/root"),
+ "suite-name")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_4(model::test_result_expected_failure,
+ "Known bug");
+
+ const model::test_program test_program_5 = model::test_program_builder(
+ "plain", fs::path("last_test"), fs::path("/test/suite/root"),
+ "suite-name")
+ .add_test_case("main")
+ .build();
+ const model::test_result result_5(model::test_result_skipped,
+ "Does not apply");
+
+ store::results_iterator iter = transaction.get_results();
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_1, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_1, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357643611000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357643621000500LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_5, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_5, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357643632000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357643638000000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_2, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_2, iter.result());
+ ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents());
+ ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents());
+ ATF_REQUIRE_EQ(1357643622001200LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357643622900021LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_3, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_3, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357643623500000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357643630981932LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_4, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_4, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357643631000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357643631020000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(!iter);
+}
+
+
+/// Validates the contents of the action with identifier 3.
+///
+/// \param dbpath Path to the database in which to check the action contents.
+static void
+check_action_3(const fs::path& dbpath)
+{
+ store::read_backend backend = store::read_backend::open_ro(dbpath);
+ store::read_transaction transaction = backend.start_read();
+
+ const fs::path root("/usr/tests");
+ std::map< std::string, std::string > environment;
+ environment["PATH"] = "/bin:/usr/bin";
+ const model::context context(root, environment);
+
+ ATF_REQUIRE_EQ(context, transaction.get_context());
+
+ const model::test_program test_program_6 = model::test_program_builder(
+ "atf", fs::path("complex_test"), fs::path("/usr/tests"),
+ "suite-name")
+ .add_test_case("this_passes")
+ .add_test_case("this_fails",
+ model::metadata_builder()
+ .set_description("Test description")
+ .set_has_cleanup(true)
+ .set_required_memory(units::bytes(128))
+ .set_required_user("root")
+ .build())
+ .add_test_case("this_skips",
+ model::metadata_builder()
+ .add_allowed_architecture("powerpc")
+ .add_allowed_architecture("x86_64")
+ .add_allowed_platform("amd64")
+ .add_allowed_platform("macppc")
+ .add_required_config("X-foo")
+ .add_required_config("unprivileged_user")
+ .add_required_file(fs::path("/the/data/file"))
+ .add_required_program(fs::path("/bin/ls"))
+ .add_required_program(fs::path("cp"))
+ .set_description("Test explanation")
+ .set_has_cleanup(true)
+ .set_required_memory(units::bytes(512))
+ .set_required_user("unprivileged")
+ .set_timeout(datetime::delta(600, 0))
+ .build())
+ .build();
+ const model::test_result result_6(model::test_result_passed);
+ const model::test_result result_7(model::test_result_failed,
+ "Some reason");
+ const model::test_result result_8(model::test_result_skipped,
+ "Another reason");
+
+ const model::test_program test_program_7 = model::test_program_builder(
+ "atf", fs::path("simple_test"), fs::path("/usr/tests"),
+ "subsuite-name")
+ .add_test_case("main",
+ model::metadata_builder()
+ .set_description("More text")
+ .set_has_cleanup(true)
+ .set_required_memory(units::bytes(128))
+ .set_required_user("unprivileged")
+ .build())
+ .build();
+ const model::test_result result_9(model::test_result_failed,
+ "Exited with code 1");
+
+ store::results_iterator iter = transaction.get_results();
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_6, *iter.test_program());
+ ATF_REQUIRE_EQ("this_fails", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_7, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357648719000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357648720897182LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_6, *iter.test_program());
+ ATF_REQUIRE_EQ("this_passes", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_6, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357648712000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357648718000000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_6, *iter.test_program());
+ ATF_REQUIRE_EQ("this_skips", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_8, iter.result());
+ ATF_REQUIRE_EQ("Another stdout", iter.stdout_contents());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357648729182013LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357648730000000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_7, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_9, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE_EQ("Another stderr", iter.stderr_contents());
+ ATF_REQUIRE_EQ(1357648740120000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357648750081700LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(!iter);
+}
+
+
+/// Validates the contents of the action with identifier 4.
+///
+/// \param dbpath Path to the database in which to check the action contents.
+static void
+check_action_4(const fs::path& dbpath)
+{
+ store::read_backend backend = store::read_backend::open_ro(dbpath);
+ store::read_transaction transaction = backend.start_read();
+
+ const fs::path root("/usr/tests");
+ std::map< std::string, std::string > environment;
+ environment["LANG"] = "C";
+ environment["PATH"] = "/bin:/usr/bin";
+ environment["TERM"] = "xterm";
+ const model::context context(root, environment);
+
+ ATF_REQUIRE_EQ(context, transaction.get_context());
+
+ const model::test_program test_program_8 = model::test_program_builder(
+ "plain", fs::path("subdir/another_test"), fs::path("/usr/tests"),
+ "subsuite-name")
+ .add_test_case("main",
+ model::metadata_builder()
+ .set_timeout(datetime::delta(10, 0))
+ .build())
+ .set_metadata(model::metadata_builder()
+ .set_timeout(datetime::delta(10, 0))
+ .build())
+ .build();
+ const model::test_result result_10(model::test_result_failed,
+ "Exit failure");
+
+ const model::test_program test_program_9 = model::test_program_builder(
+ "atf", fs::path("complex_test"), fs::path("/usr/tests"),
+ "suite-name")
+ .add_test_case("this_passes")
+ .add_test_case("this_fails",
+ model::metadata_builder()
+ .set_description("Test description")
+ .set_required_user("root")
+ .build())
+ .build();
+ const model::test_result result_11(model::test_result_passed);
+ const model::test_result result_12(model::test_result_failed,
+ "Some reason");
+
+ store::results_iterator iter = transaction.get_results();
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_9, *iter.test_program());
+ ATF_REQUIRE_EQ("this_fails", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_12, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357644397100000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357644399005000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_9, *iter.test_program());
+ ATF_REQUIRE_EQ("this_passes", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_11, iter.result());
+ ATF_REQUIRE(iter.stdout_contents().empty());
+ ATF_REQUIRE(iter.stderr_contents().empty());
+ ATF_REQUIRE_EQ(1357644396500000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357644397000000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(iter);
+ ATF_REQUIRE_EQ(test_program_8, *iter.test_program());
+ ATF_REQUIRE_EQ("main", iter.test_case_name());
+ ATF_REQUIRE_EQ(result_10, iter.result());
+ ATF_REQUIRE_EQ("Test stdout", iter.stdout_contents());
+ ATF_REQUIRE_EQ("Test stderr", iter.stderr_contents());
+ ATF_REQUIRE_EQ(1357644395000000LL, iter.start_time().to_microseconds());
+ ATF_REQUIRE_EQ(1357644396000000LL, iter.end_time().to_microseconds());
+
+ ++iter;
+ ATF_REQUIRE(!iter);
+}
+
+
+} // anonymous namespace
+
+
+#define CURRENT_SCHEMA_TEST(dataset) \
+ ATF_TEST_CASE(current_schema_ ##dataset); \
+ ATF_TEST_CASE_HEAD(current_schema_ ##dataset) \
+ { \
+ logging::set_inmemory(); \
+ const std::string required_files = \
+ store::detail::schema_file().str() + " " + \
+ testdata_file("testdata_v3_" #dataset ".sql").str(); \
+ set_md_var("require.files", required_files); \
+ } \
+ ATF_TEST_CASE_BODY(current_schema_ ##dataset) \
+ { \
+ const fs::path testpath("test.db"); \
+ \
+ sqlite::database db = sqlite::database::open( \
+ testpath, sqlite::open_readwrite | sqlite::open_create); \
+ db.exec(utils::read_file(store::detail::schema_file())); \
+ db.exec(utils::read_file(testdata_file(\
+ "testdata_v3_" #dataset ".sql"))); \
+ db.close(); \
+ \
+ check_action_ ## dataset (testpath); \
+ }
+CURRENT_SCHEMA_TEST(1);
+CURRENT_SCHEMA_TEST(2);
+CURRENT_SCHEMA_TEST(3);
+CURRENT_SCHEMA_TEST(4);
+
+
+#define MIGRATE_SCHEMA_TEST(from_version) \
+ ATF_TEST_CASE(migrate_schema__from_v ##from_version); \
+ ATF_TEST_CASE_HEAD(migrate_schema__from_v ##from_version) \
+ { \
+ logging::set_inmemory(); \
+ \
+ const char* schema = "schema_v" #from_version ".sql"; \
+ const char* testdata = "testdata_v" #from_version ".sql"; \
+ \
+ std::string required_files = \
+ testdata_file(schema).str() + " " + testdata_file(testdata).str(); \
+ for (int i = from_version; i < store::detail::current_schema_version; \
+ ++i) \
+ required_files += " " + store::detail::migration_file( \
+ i, i + 1).str(); \
+ \
+ set_md_var("require.files", required_files); \
+ } \
+ ATF_TEST_CASE_BODY(migrate_schema__from_v ##from_version) \
+ { \
+ const char* schema = "schema_v" #from_version ".sql"; \
+ const char* testdata = "testdata_v" #from_version ".sql"; \
+ \
+ const fs::path testpath("test.db"); \
+ \
+ sqlite::database db = sqlite::database::open( \
+ testpath, sqlite::open_readwrite | sqlite::open_create); \
+ db.exec(utils::read_file(testdata_file(schema))); \
+ db.exec(utils::read_file(testdata_file(testdata))); \
+ db.close(); \
+ \
+ store::migrate_schema(fs::path("test.db")); \
+ \
+ check_action_2(fs::path(".kyua/store/" \
+ "results.test_suite_root.20130108-111331-000000.db")); \
+ check_action_3(fs::path(".kyua/store/" \
+ "results.usr_tests.20130108-123832-000000.db")); \
+ check_action_4(fs::path(".kyua/store/" \
+ "results.usr_tests.20130108-112635-000000.db")); \
+ }
+MIGRATE_SCHEMA_TEST(1);
+MIGRATE_SCHEMA_TEST(2);
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, current_schema_1);
+ ATF_ADD_TEST_CASE(tcs, current_schema_2);
+ ATF_ADD_TEST_CASE(tcs, current_schema_3);
+ ATF_ADD_TEST_CASE(tcs, current_schema_4);
+
+ ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v1);
+ ATF_ADD_TEST_CASE(tcs, migrate_schema__from_v2);
+}
diff --git a/store/schema_v1.sql b/store/schema_v1.sql
new file mode 100644
index 000000000000..fbc7355bcd85
--- /dev/null
+++ b/store/schema_v1.sql
@@ -0,0 +1,314 @@
+-- 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.
+
+-- \file store/schema_v1.sql
+-- Definition of the database schema.
+--
+-- The whole contents of this file are wrapped in a transaction. We want
+-- to ensure that the initial contents of the database (the table layout as
+-- well as any predefined values) are written atomically to simplify error
+-- handling in our code.
+
+
+BEGIN TRANSACTION;
+
+
+-- -------------------------------------------------------------------------
+-- Metadata.
+-- -------------------------------------------------------------------------
+
+
+-- Database-wide properties.
+--
+-- Rows in this table are immutable: modifying the metadata implies writing
+-- a new record with a larger timestamp value, and never updating previous
+-- records. When extracting data from this table, the only "valid" row is
+-- the one with the highest timestamp. All the other rows are meaningless.
+--
+-- In other words, this table keeps the history of the database metadata.
+-- The only reason for doing this is for debugging purposes. It may come
+-- in handy to know when a particular database-wide operation happened if
+-- it turns out that the database got corrupted.
+CREATE TABLE metadata (
+ timestamp TIMESTAMP PRIMARY KEY CHECK (timestamp >= 0),
+ schema_version INTEGER NOT NULL CHECK (schema_version >= 1)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Contexts.
+-- -------------------------------------------------------------------------
+
+
+-- Execution contexts.
+--
+-- A context represents the execution environment of a particular action.
+-- Because every action is invoked by the user, the context may have
+-- changed. We record such information for information and debugging
+-- purposes.
+CREATE TABLE contexts (
+ context_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ cwd TEXT NOT NULL
+
+ -- TODO(jmmv): Record the run-time configuration.
+);
+
+
+-- Environment variables of a context.
+CREATE TABLE env_vars (
+ context_id INTEGER REFERENCES contexts,
+ var_name TEXT NOT NULL,
+ var_value TEXT NOT NULL,
+
+ PRIMARY KEY (context_id, var_name)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Actions.
+-- -------------------------------------------------------------------------
+
+
+-- Representation of user-initiated actions.
+--
+-- An action is an operation initiated by the user. At the moment, the
+-- only operation Kyua supports is the "test" operation (in the future we
+-- should be able to store, e.g. build logs). To keep things simple the
+-- database schema is restricted to represent one single action.
+CREATE TABLE actions (
+ action_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ context_id INTEGER REFERENCES contexts
+);
+
+
+-- -------------------------------------------------------------------------
+-- Test suites.
+--
+-- The tables in this section represent all the components that form a test
+-- suite. This includes data about the test suite itself (test programs
+-- and test cases), and also the data about particular runs (test results).
+--
+-- As you will notice, every object belongs to a particular action, has a
+-- unique identifier and there is no attempt to deduplicate data. This
+-- comes from the fact that a test suite is not "stable" over time: i.e. on
+-- each execution of the test suite, test programs and test cases may have
+-- come and gone. This has the interesting result of making the
+-- distinction of a test case and a test result a pure syntactic
+-- difference, because there is always a 1:1 relation.
+--
+-- The code that performs the processing of the actions is the component in
+-- charge of finding correlations between test programs and test cases
+-- across different actions.
+-- -------------------------------------------------------------------------
+
+
+-- Representation of a test program.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_programs (
+ test_program_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ action_id INTEGER REFERENCES actions,
+
+ -- The absolute path to the test program. This should not be necessary
+ -- because it is basically the concatenation of root and relative_path.
+ -- However, this allows us to very easily search for test programs
+ -- regardless of where they were executed from. (I.e. different
+ -- combinations of root + relative_path can map to the same absolute path).
+ absolute_path NOT NULL,
+
+ -- The path to the root of the test suite (where the Kyuafile lives).
+ root TEXT NOT NULL,
+
+ -- The path to the test program, relative to the root.
+ relative_path NOT NULL,
+
+ -- Name of the test suite the test program belongs to.
+ test_suite_name TEXT NOT NULL,
+
+ -- The name of the test program interface.
+ --
+ -- Note that this indicates both the interface for the test program and
+ -- its test cases. See below for the corresponding detail tables.
+ interface TEXT NOT NULL
+);
+
+
+-- Representation of a test case.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_cases (
+ test_case_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ test_program_id INTEGER REFERENCES test_programs,
+ name TEXT NOT NULL
+);
+
+
+-- Representation of test case results.
+--
+-- Note that there is a 1:1 relation between test cases and their results.
+-- This is a result of storing the information of a test case on every
+-- single action.
+CREATE TABLE test_results (
+ test_case_id INTEGER PRIMARY KEY REFERENCES test_cases,
+ result_type TEXT NOT NULL,
+ result_reason TEXT,
+
+ start_time TIMESTAMP NOT NULL,
+ end_time TIMESTAMP NOT NULL
+);
+
+
+-- Collection of output files of the test case.
+CREATE TABLE test_case_files (
+ test_case_id INTEGER NOT NULL REFERENCES test_cases,
+
+ -- The raw name of the file.
+ --
+ -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold
+ -- the stdout and stderr of the test case, respectively. If any of
+ -- these are empty, there will be no corresponding entry in this table
+ -- (hence why we do not allow NULLs in these fields).
+ file_name TEXT NOT NULL,
+
+ -- Pointer to the file itself.
+ file_id INTEGER NOT NULL REFERENCES files,
+
+ PRIMARY KEY (test_case_id, file_name)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Detail tables for the 'atf' test interface.
+-- -------------------------------------------------------------------------
+
+
+-- Properties specific to 'atf' test cases.
+--
+-- This table contains the representation of singly-valued properties such
+-- as 'timeout'. Properties that can have more than one (textual) value
+-- are stored in the atf_test_cases_multivalues table.
+--
+-- Note that all properties can be NULL because test cases are not required
+-- to define them.
+CREATE TABLE atf_test_cases (
+ test_case_id INTEGER PRIMARY KEY REFERENCES test_cases,
+
+ -- Free-form description of the text case.
+ description TEXT,
+
+ -- Either 'true' or 'false', indicating whether the test case has a
+ -- cleanup routine or not.
+ has_cleanup TEXT,
+
+ -- The timeout for the test case in microseconds.
+ timeout INTEGER,
+
+ -- The amount of physical memory required by the test case.
+ required_memory INTEGER,
+
+ -- Either 'root' or 'unprivileged', indicating the privileges required by
+ -- the test case.
+ required_user TEXT
+);
+
+
+-- Representation of test case properties that have more than one value.
+--
+-- While we could store the flattened values of the properties as provided
+-- by the test case itself, we choose to store the processed, split
+-- representation. This allows us to perform queries about the test cases
+-- directly on the database without doing text processing; for example,
+-- "get all test cases that require /bin/ls".
+CREATE TABLE atf_test_cases_multivalues (
+ test_case_id INTEGER REFERENCES test_cases,
+
+ -- The name of the property; for example, 'require.progs'.
+ property_name TEXT NOT NULL,
+
+ -- One of the values of the property.
+ property_value TEXT NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Detail tables for the 'plain' test interface.
+-- -------------------------------------------------------------------------
+
+
+-- Properties specific to 'plain' test programs.
+CREATE TABLE plain_test_programs (
+ test_program_id INTEGER PRIMARY KEY REFERENCES test_programs,
+
+ -- The timeout for the test cases in this test program. While this
+ -- setting has a default value for test programs, we explicitly record
+ -- the information here. The "default value" used when the test
+ -- program was run might change over time, so we want to know what it
+ -- was exactly when this was run.
+ timeout INTEGER NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Verbatim files.
+-- -------------------------------------------------------------------------
+
+
+-- Copies of files or logs generated during testing.
+--
+-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a
+-- hash to the file contents and use that as the primary key instead.
+CREATE TABLE files (
+ file_id INTEGER PRIMARY KEY,
+
+ contents BLOB NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Initialization of values.
+-- -------------------------------------------------------------------------
+
+
+-- Create a new metadata record.
+--
+-- For every new database, we want to ensure that the metadata is valid if
+-- the database creation (i.e. the whole transaction) succeeded.
+--
+-- If you modify the value of the schema version in this statement, you
+-- will also have to modify the version encoded in the backend module.
+INSERT INTO metadata (timestamp, schema_version)
+ VALUES (strftime('%s', 'now'), 1);
+
+
+COMMIT TRANSACTION;
diff --git a/store/schema_v2.sql b/store/schema_v2.sql
new file mode 100644
index 000000000000..48bd1727f91b
--- /dev/null
+++ b/store/schema_v2.sql
@@ -0,0 +1,293 @@
+-- Copyright 2012 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.
+
+-- \file store/schema_v2.sql
+-- Definition of the database schema.
+--
+-- The whole contents of this file are wrapped in a transaction. We want
+-- to ensure that the initial contents of the database (the table layout as
+-- well as any predefined values) are written atomically to simplify error
+-- handling in our code.
+
+
+BEGIN TRANSACTION;
+
+
+-- -------------------------------------------------------------------------
+-- Metadata.
+-- -------------------------------------------------------------------------
+
+
+-- Database-wide properties.
+--
+-- Rows in this table are immutable: modifying the metadata implies writing
+-- a new record with a new schema_version greater than all existing
+-- records, and never updating previous records. When extracting data from
+-- this table, the only "valid" row is the one with the highest
+-- scheam_version. All the other rows are meaningless and only exist for
+-- historical purposes.
+--
+-- In other words, this table keeps the history of the database metadata.
+-- The only reason for doing this is for debugging purposes. It may come
+-- in handy to know when a particular database-wide operation happened if
+-- it turns out that the database got corrupted.
+CREATE TABLE metadata (
+ schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1),
+ timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Contexts.
+-- -------------------------------------------------------------------------
+
+
+-- Execution contexts.
+--
+-- A context represents the execution environment of a particular action.
+-- Because every action is invoked by the user, the context may have
+-- changed. We record such information for information and debugging
+-- purposes.
+CREATE TABLE contexts (
+ context_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ cwd TEXT NOT NULL
+
+ -- TODO(jmmv): Record the run-time configuration.
+);
+
+
+-- Environment variables of a context.
+CREATE TABLE env_vars (
+ context_id INTEGER REFERENCES contexts,
+ var_name TEXT NOT NULL,
+ var_value TEXT NOT NULL,
+
+ PRIMARY KEY (context_id, var_name)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Actions.
+-- -------------------------------------------------------------------------
+
+
+-- Representation of user-initiated actions.
+--
+-- An action is an operation initiated by the user. At the moment, the
+-- only operation Kyua supports is the "test" operation (in the future we
+-- should be able to store, e.g. build logs). To keep things simple the
+-- database schema is restricted to represent one single action.
+CREATE TABLE actions (
+ action_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ context_id INTEGER REFERENCES contexts
+);
+
+
+-- -------------------------------------------------------------------------
+-- Test suites.
+--
+-- The tables in this section represent all the components that form a test
+-- suite. This includes data about the test suite itself (test programs
+-- and test cases), and also the data about particular runs (test results).
+--
+-- As you will notice, every object belongs to a particular action, has a
+-- unique identifier and there is no attempt to deduplicate data. This
+-- comes from the fact that a test suite is not "stable" over time: i.e. on
+-- each execution of the test suite, test programs and test cases may have
+-- come and gone. This has the interesting result of making the
+-- distinction of a test case and a test result a pure syntactic
+-- difference, because there is always a 1:1 relation.
+--
+-- The code that performs the processing of the actions is the component in
+-- charge of finding correlations between test programs and test cases
+-- across different actions.
+-- -------------------------------------------------------------------------
+
+
+-- Representation of the metadata objects.
+--
+-- The way this table works is like this: every time we record a metadata
+-- object, we calculate what its identifier should be as the last rowid of
+-- the table. All properties of that metadata object thus receive the same
+-- identifier.
+CREATE TABLE metadatas (
+ metadata_id INTEGER NOT NULL,
+
+ -- The name of the property.
+ property_name TEXT NOT NULL,
+
+ -- One of the values of the property.
+ property_value TEXT,
+
+ PRIMARY KEY (metadata_id, property_name)
+);
+
+
+-- Optimize the loading of the metadata of any single entity.
+--
+-- The metadata_id column of the metadatas table is not enough to act as a
+-- primary key, yet we need to locate entries in the metadatas table solely by
+-- their identifier.
+--
+-- TODO(jmmv): I think this index is useless given that the primary key in the
+-- metadatas table includes the metadata_id as the first component. Need to
+-- verify this and drop the index or this comment appropriately.
+CREATE INDEX index_metadatas_by_id
+ ON metadatas (metadata_id);
+
+
+-- Representation of a test program.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_programs (
+ test_program_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ action_id INTEGER REFERENCES actions,
+
+ -- The absolute path to the test program. This should not be necessary
+ -- because it is basically the concatenation of root and relative_path.
+ -- However, this allows us to very easily search for test programs
+ -- regardless of where they were executed from. (I.e. different
+ -- combinations of root + relative_path can map to the same absolute path).
+ absolute_path TEXT NOT NULL,
+
+ -- The path to the root of the test suite (where the Kyuafile lives).
+ root TEXT NOT NULL,
+
+ -- The path to the test program, relative to the root.
+ relative_path TEXT NOT NULL,
+
+ -- Name of the test suite the test program belongs to.
+ test_suite_name TEXT NOT NULL,
+
+ -- Reference to the various rows of metadatas.
+ metadata_id INTEGER,
+
+ -- The name of the test program interface.
+ --
+ -- Note that this indicates both the interface for the test program and
+ -- its test cases. See below for the corresponding detail tables.
+ interface TEXT NOT NULL
+);
+
+
+-- Optimize the lookup of test programs by the action they belong to.
+CREATE INDEX index_test_programs_by_action_id
+ ON test_programs (action_id);
+
+
+-- Representation of a test case.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_cases (
+ test_case_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ test_program_id INTEGER REFERENCES test_programs,
+ name TEXT NOT NULL,
+
+ -- Reference to the various rows of metadatas.
+ metadata_id INTEGER
+);
+
+
+-- Optimize the loading of all test cases that are part of a test program.
+CREATE INDEX index_test_cases_by_test_programs_id
+ ON test_cases (test_program_id);
+
+
+-- Representation of test case results.
+--
+-- Note that there is a 1:1 relation between test cases and their results.
+-- This is a result of storing the information of a test case on every
+-- single action.
+CREATE TABLE test_results (
+ test_case_id INTEGER PRIMARY KEY REFERENCES test_cases,
+ result_type TEXT NOT NULL,
+ result_reason TEXT,
+
+ start_time TIMESTAMP NOT NULL,
+ end_time TIMESTAMP NOT NULL
+);
+
+
+-- Collection of output files of the test case.
+CREATE TABLE test_case_files (
+ test_case_id INTEGER NOT NULL REFERENCES test_cases,
+
+ -- The raw name of the file.
+ --
+ -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold
+ -- the stdout and stderr of the test case, respectively. If any of
+ -- these are empty, there will be no corresponding entry in this table
+ -- (hence why we do not allow NULLs in these fields).
+ file_name TEXT NOT NULL,
+
+ -- Pointer to the file itself.
+ file_id INTEGER NOT NULL REFERENCES files,
+
+ PRIMARY KEY (test_case_id, file_name)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Verbatim files.
+-- -------------------------------------------------------------------------
+
+
+-- Copies of files or logs generated during testing.
+--
+-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a
+-- hash to the file contents and use that as the primary key instead.
+CREATE TABLE files (
+ file_id INTEGER PRIMARY KEY,
+
+ contents BLOB NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Initialization of values.
+-- -------------------------------------------------------------------------
+
+
+-- Create a new metadata record.
+--
+-- For every new database, we want to ensure that the metadata is valid if
+-- the database creation (i.e. the whole transaction) succeeded.
+--
+-- If you modify the value of the schema version in this statement, you
+-- will also have to modify the version encoded in the backend module.
+INSERT INTO metadata (timestamp, schema_version)
+ VALUES (strftime('%s', 'now'), 2);
+
+
+COMMIT TRANSACTION;
diff --git a/store/schema_v3.sql b/store/schema_v3.sql
new file mode 100644
index 000000000000..26e8359e1994
--- /dev/null
+++ b/store/schema_v3.sql
@@ -0,0 +1,255 @@
+-- Copyright 2012 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.
+
+-- \file store/schema_v3.sql
+-- Definition of the database schema.
+--
+-- The whole contents of this file are wrapped in a transaction. We want
+-- to ensure that the initial contents of the database (the table layout as
+-- well as any predefined values) are written atomically to simplify error
+-- handling in our code.
+
+
+BEGIN TRANSACTION;
+
+
+-- -------------------------------------------------------------------------
+-- Metadata.
+-- -------------------------------------------------------------------------
+
+
+-- Database-wide properties.
+--
+-- Rows in this table are immutable: modifying the metadata implies writing
+-- a new record with a new schema_version greater than all existing
+-- records, and never updating previous records. When extracting data from
+-- this table, the only "valid" row is the one with the highest
+-- scheam_version. All the other rows are meaningless and only exist for
+-- historical purposes.
+--
+-- In other words, this table keeps the history of the database metadata.
+-- The only reason for doing this is for debugging purposes. It may come
+-- in handy to know when a particular database-wide operation happened if
+-- it turns out that the database got corrupted.
+CREATE TABLE metadata (
+ schema_version INTEGER PRIMARY KEY CHECK (schema_version >= 1),
+ timestamp TIMESTAMP NOT NULL CHECK (timestamp >= 0)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Contexts.
+-- -------------------------------------------------------------------------
+
+
+-- Execution contexts.
+--
+-- A context represents the execution environment of the test run.
+-- We record such information for information and debugging purposes.
+CREATE TABLE contexts (
+ cwd TEXT NOT NULL
+
+ -- TODO(jmmv): Record the run-time configuration.
+);
+
+
+-- Environment variables of a context.
+CREATE TABLE env_vars (
+ var_name TEXT PRIMARY KEY,
+ var_value TEXT NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Test suites.
+--
+-- The tables in this section represent all the components that form a test
+-- suite. This includes data about the test suite itself (test programs
+-- and test cases), and also the data about particular runs (test results).
+--
+-- As you will notice, every object has a unique identifier and there is no
+-- attempt to deduplicate data. This has the interesting result of making
+-- the distinction of a test case and a test result a pure syntactic
+-- difference, because there is always a 1:1 relation.
+-- -------------------------------------------------------------------------
+
+
+-- Representation of the metadata objects.
+--
+-- The way this table works is like this: every time we record a metadata
+-- object, we calculate what its identifier should be as the last rowid of
+-- the table. All properties of that metadata object thus receive the same
+-- identifier.
+CREATE TABLE metadatas (
+ metadata_id INTEGER NOT NULL,
+
+ -- The name of the property.
+ property_name TEXT NOT NULL,
+
+ -- One of the values of the property.
+ property_value TEXT,
+
+ PRIMARY KEY (metadata_id, property_name)
+);
+
+
+-- Optimize the loading of the metadata of any single entity.
+--
+-- The metadata_id column of the metadatas table is not enough to act as a
+-- primary key, yet we need to locate entries in the metadatas table solely by
+-- their identifier.
+--
+-- TODO(jmmv): I think this index is useless given that the primary key in the
+-- metadatas table includes the metadata_id as the first component. Need to
+-- verify this and drop the index or this comment appropriately.
+CREATE INDEX index_metadatas_by_id
+ ON metadatas (metadata_id);
+
+
+-- Representation of a test program.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_programs (
+ test_program_id INTEGER PRIMARY KEY AUTOINCREMENT,
+
+ -- The absolute path to the test program. This should not be necessary
+ -- because it is basically the concatenation of root and relative_path.
+ -- However, this allows us to very easily search for test programs
+ -- regardless of where they were executed from. (I.e. different
+ -- combinations of root + relative_path can map to the same absolute path).
+ absolute_path TEXT NOT NULL,
+
+ -- The path to the root of the test suite (where the Kyuafile lives).
+ root TEXT NOT NULL,
+
+ -- The path to the test program, relative to the root.
+ relative_path TEXT NOT NULL,
+
+ -- Name of the test suite the test program belongs to.
+ test_suite_name TEXT NOT NULL,
+
+ -- Reference to the various rows of metadatas.
+ metadata_id INTEGER,
+
+ -- The name of the test program interface.
+ --
+ -- Note that this indicates both the interface for the test program and
+ -- its test cases. See below for the corresponding detail tables.
+ interface TEXT NOT NULL
+);
+
+
+-- Representation of a test case.
+--
+-- At the moment, there are no substantial differences between the
+-- different interfaces, so we can simplify the design by with having a
+-- single table representing all test caes. We may need to revisit this in
+-- the future.
+CREATE TABLE test_cases (
+ test_case_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ test_program_id INTEGER REFERENCES test_programs,
+ name TEXT NOT NULL,
+
+ -- Reference to the various rows of metadatas.
+ metadata_id INTEGER
+);
+
+
+-- Optimize the loading of all test cases that are part of a test program.
+CREATE INDEX index_test_cases_by_test_programs_id
+ ON test_cases (test_program_id);
+
+
+-- Representation of test case results.
+--
+-- Note that there is a 1:1 relation between test cases and their results.
+CREATE TABLE test_results (
+ test_case_id INTEGER PRIMARY KEY REFERENCES test_cases,
+ result_type TEXT NOT NULL,
+ result_reason TEXT,
+
+ start_time TIMESTAMP NOT NULL,
+ end_time TIMESTAMP NOT NULL
+);
+
+
+-- Collection of output files of the test case.
+CREATE TABLE test_case_files (
+ test_case_id INTEGER NOT NULL REFERENCES test_cases,
+
+ -- The raw name of the file.
+ --
+ -- The special names '__STDOUT__' and '__STDERR__' are reserved to hold
+ -- the stdout and stderr of the test case, respectively. If any of
+ -- these are empty, there will be no corresponding entry in this table
+ -- (hence why we do not allow NULLs in these fields).
+ file_name TEXT NOT NULL,
+
+ -- Pointer to the file itself.
+ file_id INTEGER NOT NULL REFERENCES files,
+
+ PRIMARY KEY (test_case_id, file_name)
+);
+
+
+-- -------------------------------------------------------------------------
+-- Verbatim files.
+-- -------------------------------------------------------------------------
+
+
+-- Copies of files or logs generated during testing.
+--
+-- TODO(jmmv): This will probably grow to unmanageable sizes. We should add a
+-- hash to the file contents and use that as the primary key instead.
+CREATE TABLE files (
+ file_id INTEGER PRIMARY KEY,
+
+ contents BLOB NOT NULL
+);
+
+
+-- -------------------------------------------------------------------------
+-- Initialization of values.
+-- -------------------------------------------------------------------------
+
+
+-- Create a new metadata record.
+--
+-- For every new database, we want to ensure that the metadata is valid if
+-- the database creation (i.e. the whole transaction) succeeded.
+--
+-- If you modify the value of the schema version in this statement, you
+-- will also have to modify the version encoded in the backend module.
+INSERT INTO metadata (timestamp, schema_version)
+ VALUES (strftime('%s', 'now'), 3);
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v1.sql b/store/testdata_v1.sql
new file mode 100644
index 000000000000..75c4d439ac96
--- /dev/null
+++ b/store/testdata_v1.sql
@@ -0,0 +1,330 @@
+-- Copyright 2013 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.
+
+-- \file store/testdata_v1.sql
+-- Populates a v1 database with some test data.
+
+
+BEGIN TRANSACTION;
+
+
+--
+-- Action 1: Empty context and no test programs nor test cases.
+--
+
+
+-- context_id 1
+INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root');
+
+-- action_id 1
+INSERT INTO actions (action_id, context_id) VALUES (1, 1);
+
+
+--
+-- Action 2: Plain test programs only.
+--
+-- This action contains 5 test programs, each with one test case, and each
+-- reporting one of all possible result types.
+--
+
+
+-- context_id 2
+INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (2, 'HOME', '/home/test');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (2, 'PATH', '/bin:/usr/bin');
+
+-- action_id 2
+INSERT INTO actions (action_id, context_id) VALUES (2, 2);
+
+-- test_program_id 1
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root',
+ 'foo_test', 'suite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (1, 300000000);
+
+-- test_case_id 1
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (1, 1, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500);
+
+-- test_program_id 2
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root',
+ 'subdir/another_test', 'subsuite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (2, 10000000);
+
+-- test_case_id 2
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (2, 2, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (2, 'failed', 'Exited with code 1',
+ 1357643622001200, 1357643622900021);
+
+-- file_id 1
+INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDOUT__', 1);
+
+-- file_id 2
+INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDERR__', 2);
+
+-- test_program_id 3
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root',
+ 'subdir/bar_test', 'subsuite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (3, 300000000);
+
+-- test_case_id 3
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (3, 3, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (3, 'broken', 'Received signal 1',
+ 1357643623500000, 1357643630981932);
+
+-- test_program_id 4
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root',
+ 'top_test', 'suite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (4, 300000000);
+
+-- test_case_id 4
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (4, 4, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (4, 'expected_failure', 'Known bug',
+ 1357643631000000, 1357643631020000);
+
+-- test_program_id 5
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root',
+ 'last_test', 'suite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (5, 300000000);
+
+-- test_case_id 5
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (5, 5, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000);
+
+
+--
+-- Action 3: ATF test programs only.
+--
+
+
+-- context_id 3
+INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (3, 'PATH', '/bin:/usr/bin');
+
+-- action_id 3
+INSERT INTO actions (action_id, context_id) VALUES (3, 3);
+
+-- test_program_id 6
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 'atf');
+
+-- test_case_id 6, passed, no optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (6, 6, 'this_passes');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (6, NULL, 'false', 300000000, 0, NULL);
+
+-- test_case_id 7, failed, optional non-multivalue metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (7, 6, 'this_fails');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (7, 'Test description', 'true', 300000000, 128, 'root');
+
+-- test_case_id 8, skipped, all optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (8, 6, 'this_skips');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (8, 'Test explanation', 'true', 600000000, 512, 'unprivileged');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.arch', 'x86_64');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.arch', 'powerpc');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.machine', 'amd64');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.machine', 'macppc');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.config', 'unprivileged_user');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.config', 'X-foo');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.files', '/the/data/file');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.progs', 'cp');
+INSERT INTO atf_test_cases_multivalues (test_case_id, property_name,
+ property_value)
+ VALUES (8, 'require.progs', '/bin/ls');
+
+-- file_id 3
+INSERT INTO files (file_id, contents)
+ VALUES (3, x'416e6f74686572207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (8, '__STDOUT__', 3);
+
+-- test_program_id 7
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests',
+ 'simple_test', 'subsuite-name', 'atf');
+
+-- test_case_id 9
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (9, 7, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (9, 'failed', 'Exited with code 1',
+ 1357648740120000, 1357648750081700);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (9, 'More text', 'true', 300000000, 128, 'unprivileged');
+
+-- file_id 4
+INSERT INTO files (file_id, contents)
+ VALUES (4, x'416e6f7468657220737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (9, '__STDERR__', 4);
+
+
+--
+-- Action 4: Mixture of test programs.
+--
+
+
+-- context_id 4
+INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'LANG', 'C');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'PATH', '/bin:/usr/bin');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'TERM', 'xterm');
+
+-- action_id 4
+INSERT INTO actions (action_id, context_id) VALUES (4, 4);
+
+-- test_program_id 8
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests',
+ 'subdir/another_test', 'subsuite-name', 'plain');
+INSERT INTO plain_test_programs (test_program_id, timeout)
+ VALUES (8, 10000000);
+
+-- test_case_id 10
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (10, 8, 'main');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000);
+
+-- file_id 5
+INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDOUT__', 5);
+
+-- file_id 6
+INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDERR__', 6);
+
+-- test_program_id 9
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, interface)
+ VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 'atf');
+
+-- test_case_id 11
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (11, 9, 'this_passes');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (11, NULL, 'false', 300000000, 0, NULL);
+
+-- test_case_id 12
+INSERT INTO test_cases (test_case_id, test_program_id, name)
+ VALUES (12, 9, 'this_fails');
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000);
+INSERT INTO atf_test_cases (test_case_id, description, has_cleanup, timeout,
+ required_memory, required_user)
+ VALUES (12, 'Test description', 'false', 300000000, 0, 'root');
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v2.sql b/store/testdata_v2.sql
new file mode 100644
index 000000000000..838da4c25956
--- /dev/null
+++ b/store/testdata_v2.sql
@@ -0,0 +1,462 @@
+-- Copyright 2013 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.
+
+-- \file store/testdata_v2.sql
+-- Populates a v2 database with some test data.
+
+
+BEGIN TRANSACTION;
+
+
+--
+-- Action 1: Empty context and no test programs nor test cases.
+--
+
+
+-- context_id 1
+INSERT INTO contexts (context_id, cwd) VALUES (1, '/some/root');
+
+-- action_id 1
+INSERT INTO actions (action_id, context_id) VALUES (1, 1);
+
+
+--
+-- Action 2: Plain test programs only.
+--
+-- This action contains 5 test programs, each with one test case, and each
+-- reporting one of all possible result types.
+--
+
+
+-- context_id 2
+INSERT INTO contexts (context_id, cwd) VALUES (2, '/test/suite/root');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (2, 'HOME', '/home/test');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (2, 'PATH', '/bin:/usr/bin');
+
+-- action_id 2
+INSERT INTO actions (action_id, context_id) VALUES (2, 2);
+
+-- metadata_id 1
+INSERT INTO metadatas VALUES (1, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (1, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (1, 'description', '');
+INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (1, 'required_configs', '');
+INSERT INTO metadatas VALUES (1, 'required_files', '');
+INSERT INTO metadatas VALUES (1, 'required_memory', '0');
+INSERT INTO metadatas VALUES (1, 'required_programs', '');
+INSERT INTO metadatas VALUES (1, 'required_user', '');
+INSERT INTO metadatas VALUES (1, 'timeout', '300');
+
+-- test_program_id 1
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (1, 2, '/test/suite/root/foo_test', '/test/suite/root',
+ 'foo_test', 'suite-name', 1, 'plain');
+
+-- test_case_id 1
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (1, 1, 'main', 1);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500);
+
+-- metadata_id 2
+INSERT INTO metadatas VALUES (2, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (2, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (2, 'description', '');
+INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (2, 'required_configs', '');
+INSERT INTO metadatas VALUES (2, 'required_files', '');
+INSERT INTO metadatas VALUES (2, 'required_memory', '0');
+INSERT INTO metadatas VALUES (2, 'required_programs', '');
+INSERT INTO metadatas VALUES (2, 'required_user', '');
+INSERT INTO metadatas VALUES (2, 'timeout', '10');
+
+-- test_program_id 2
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (2, 2, '/test/suite/root/subdir/another_test', '/test/suite/root',
+ 'subdir/another_test', 'subsuite-name', 2, 'plain');
+
+-- test_case_id 2
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (2, 2, 'main', 2);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (2, 'failed', 'Exited with code 1',
+ 1357643622001200, 1357643622900021);
+
+-- file_id 1
+INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDOUT__', 1);
+
+-- file_id 2
+INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDERR__', 2);
+
+-- metadata_id 3
+INSERT INTO metadatas VALUES (3, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (3, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (3, 'description', '');
+INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (3, 'required_configs', '');
+INSERT INTO metadatas VALUES (3, 'required_files', '');
+INSERT INTO metadatas VALUES (3, 'required_memory', '0');
+INSERT INTO metadatas VALUES (3, 'required_programs', '');
+INSERT INTO metadatas VALUES (3, 'required_user', '');
+INSERT INTO metadatas VALUES (3, 'timeout', '300');
+
+-- test_program_id 3
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (3, 2, '/test/suite/root/subdir/bar_test', '/test/suite/root',
+ 'subdir/bar_test', 'subsuite-name', 3, 'plain');
+
+-- test_case_id 3
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (3, 3, 'main', 3);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (3, 'broken', 'Received signal 1',
+ 1357643623500000, 1357643630981932);
+
+-- metadata_id 4
+INSERT INTO metadatas VALUES (4, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (4, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (4, 'description', '');
+INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (4, 'required_configs', '');
+INSERT INTO metadatas VALUES (4, 'required_files', '');
+INSERT INTO metadatas VALUES (4, 'required_memory', '0');
+INSERT INTO metadatas VALUES (4, 'required_programs', '');
+INSERT INTO metadatas VALUES (4, 'required_user', '');
+INSERT INTO metadatas VALUES (4, 'timeout', '300');
+
+-- test_program_id 4
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (4, 2, '/test/suite/root/top_test', '/test/suite/root',
+ 'top_test', 'suite-name', 4, 'plain');
+
+-- test_case_id 4
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (4, 4, 'main', 4);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (4, 'expected_failure', 'Known bug',
+ 1357643631000000, 1357643631020000);
+
+-- metadata_id 5
+INSERT INTO metadatas VALUES (5, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (5, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (5, 'description', '');
+INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (5, 'required_configs', '');
+INSERT INTO metadatas VALUES (5, 'required_files', '');
+INSERT INTO metadatas VALUES (5, 'required_memory', '0');
+INSERT INTO metadatas VALUES (5, 'required_programs', '');
+INSERT INTO metadatas VALUES (5, 'required_user', '');
+INSERT INTO metadatas VALUES (5, 'timeout', '300');
+
+-- test_program_id 5
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (5, 2, '/test/suite/root/last_test', '/test/suite/root',
+ 'last_test', 'suite-name', 5, 'plain');
+
+-- test_case_id 5
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (5, 5, 'main', 5);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000);
+
+
+--
+-- Action 3: ATF test programs only.
+--
+
+
+-- context_id 3
+INSERT INTO contexts (context_id, cwd) VALUES (3, '/usr/tests');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (3, 'PATH', '/bin:/usr/bin');
+
+-- action_id 3
+INSERT INTO actions (action_id, context_id) VALUES (3, 3);
+
+-- metadata_id 6
+INSERT INTO metadatas VALUES (6, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (6, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (6, 'description', '');
+INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (6, 'required_configs', '');
+INSERT INTO metadatas VALUES (6, 'required_files', '');
+INSERT INTO metadatas VALUES (6, 'required_memory', '0');
+INSERT INTO metadatas VALUES (6, 'required_programs', '');
+INSERT INTO metadatas VALUES (6, 'required_user', '');
+INSERT INTO metadatas VALUES (6, 'timeout', '300');
+
+-- test_program_id 6
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (6, 3, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 6, 'atf');
+
+-- metadata_id 7
+INSERT INTO metadatas VALUES (7, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (7, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (7, 'description', '');
+INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (7, 'required_configs', '');
+INSERT INTO metadatas VALUES (7, 'required_files', '');
+INSERT INTO metadatas VALUES (7, 'required_memory', '0');
+INSERT INTO metadatas VALUES (7, 'required_programs', '');
+INSERT INTO metadatas VALUES (7, 'required_user', '');
+INSERT INTO metadatas VALUES (7, 'timeout', '300');
+
+-- test_case_id 6, passed, no optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (6, 6, 'this_passes', 7);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000);
+
+-- metadata_id 8
+INSERT INTO metadatas VALUES (8, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (8, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (8, 'description', 'Test description');
+INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (8, 'required_configs', '');
+INSERT INTO metadatas VALUES (8, 'required_files', '');
+INSERT INTO metadatas VALUES (8, 'required_memory', '128');
+INSERT INTO metadatas VALUES (8, 'required_programs', '');
+INSERT INTO metadatas VALUES (8, 'required_user', 'root');
+INSERT INTO metadatas VALUES (8, 'timeout', '300');
+
+-- test_case_id 7, failed, optional non-multivalue metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (7, 6, 'this_fails', 8);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182);
+
+-- metadata_id 9
+INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64');
+INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc');
+INSERT INTO metadatas VALUES (9, 'description', 'Test explanation');
+INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo');
+INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file');
+INSERT INTO metadatas VALUES (9, 'required_memory', '512');
+INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls');
+INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged');
+INSERT INTO metadatas VALUES (9, 'timeout', '600');
+
+-- test_case_id 8, skipped, all optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (8, 6, 'this_skips', 9);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000);
+
+-- file_id 3
+INSERT INTO files (file_id, contents)
+ VALUES (3, x'416e6f74686572207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (8, '__STDOUT__', 3);
+
+-- metadata_id 10
+INSERT INTO metadatas VALUES (10, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (10, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (10, 'description', '');
+INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (10, 'required_configs', '');
+INSERT INTO metadatas VALUES (10, 'required_files', '');
+INSERT INTO metadatas VALUES (10, 'required_memory', '0');
+INSERT INTO metadatas VALUES (10, 'required_programs', '');
+INSERT INTO metadatas VALUES (10, 'required_user', '');
+INSERT INTO metadatas VALUES (10, 'timeout', '300');
+
+-- test_program_id 7
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (7, 3, '/usr/tests/simple_test', '/usr/tests',
+ 'simple_test', 'subsuite-name', 10, 'atf');
+
+-- metadata_id 11
+INSERT INTO metadatas VALUES (11, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (11, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (11, 'description', 'More text');
+INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (11, 'required_configs', '');
+INSERT INTO metadatas VALUES (11, 'required_files', '');
+INSERT INTO metadatas VALUES (11, 'required_memory', '128');
+INSERT INTO metadatas VALUES (11, 'required_programs', '');
+INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged');
+INSERT INTO metadatas VALUES (11, 'timeout', '300');
+
+-- test_case_id 9
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (9, 7, 'main', 11);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (9, 'failed', 'Exited with code 1',
+ 1357648740120000, 1357648750081700);
+
+-- file_id 4
+INSERT INTO files (file_id, contents)
+ VALUES (4, x'416e6f7468657220737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (9, '__STDERR__', 4);
+
+
+--
+-- Action 4: Mixture of test programs.
+--
+
+
+-- context_id 4
+INSERT INTO contexts (context_id, cwd) VALUES (4, '/usr/tests');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'LANG', 'C');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'PATH', '/bin:/usr/bin');
+INSERT INTO env_vars (context_id, var_name, var_value)
+ VALUES (4, 'TERM', 'xterm');
+
+-- action_id 4
+INSERT INTO actions (action_id, context_id) VALUES (4, 4);
+
+-- metadata_id 12
+INSERT INTO metadatas VALUES (12, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (12, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (12, 'description', '');
+INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (12, 'required_configs', '');
+INSERT INTO metadatas VALUES (12, 'required_files', '');
+INSERT INTO metadatas VALUES (12, 'required_memory', '0');
+INSERT INTO metadatas VALUES (12, 'required_programs', '');
+INSERT INTO metadatas VALUES (12, 'required_user', '');
+INSERT INTO metadatas VALUES (12, 'timeout', '10');
+
+-- test_program_id 8
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (8, 4, '/usr/tests/subdir/another_test', '/usr/tests',
+ 'subdir/another_test', 'subsuite-name', 12, 'plain');
+
+-- test_case_id 10
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (10, 8, 'main', 12);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000);
+
+-- file_id 5
+INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDOUT__', 5);
+
+-- file_id 6
+INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDERR__', 6);
+
+-- metadata_id 13
+INSERT INTO metadatas VALUES (13, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (13, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (13, 'description', '');
+INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (13, 'required_configs', '');
+INSERT INTO metadatas VALUES (13, 'required_files', '');
+INSERT INTO metadatas VALUES (13, 'required_memory', '0');
+INSERT INTO metadatas VALUES (13, 'required_programs', '');
+INSERT INTO metadatas VALUES (13, 'required_user', '');
+INSERT INTO metadatas VALUES (13, 'timeout', '300');
+
+-- test_program_id 9
+INSERT INTO test_programs (test_program_id, action_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (9, 4, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 14, 'atf');
+
+-- metadata_id 15
+INSERT INTO metadatas VALUES (15, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (15, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (15, 'description', '');
+INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (15, 'required_configs', '');
+INSERT INTO metadatas VALUES (15, 'required_files', '');
+INSERT INTO metadatas VALUES (15, 'required_memory', '0');
+INSERT INTO metadatas VALUES (15, 'required_programs', '');
+INSERT INTO metadatas VALUES (15, 'required_user', '');
+INSERT INTO metadatas VALUES (15, 'timeout', '300');
+
+-- test_case_id 11
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (11, 9, 'this_passes', 15);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000);
+
+-- metadata_id 16
+INSERT INTO metadatas VALUES (16, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (16, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (16, 'description', 'Test description');
+INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (16, 'required_configs', '');
+INSERT INTO metadatas VALUES (16, 'required_files', '');
+INSERT INTO metadatas VALUES (16, 'required_memory', '0');
+INSERT INTO metadatas VALUES (16, 'required_programs', '');
+INSERT INTO metadatas VALUES (16, 'required_user', 'root');
+INSERT INTO metadatas VALUES (16, 'timeout', '300');
+
+-- test_case_id 12
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (12, 9, 'this_fails', 16);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000);
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v3_1.sql b/store/testdata_v3_1.sql
new file mode 100644
index 000000000000..9715db490ba0
--- /dev/null
+++ b/store/testdata_v3_1.sql
@@ -0,0 +1,42 @@
+-- Copyright 2014 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.
+
+-- \file store/testdata_v3.sql
+-- Populates a v3 database with some test data.
+--
+-- Empty context and no test programs nor test cases.
+
+
+BEGIN TRANSACTION;
+
+
+-- context
+INSERT INTO contexts (cwd) VALUES ('/some/root');
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v3_2.sql b/store/testdata_v3_2.sql
new file mode 100644
index 000000000000..0ef42a328c7c
--- /dev/null
+++ b/store/testdata_v3_2.sql
@@ -0,0 +1,190 @@
+-- Copyright 2014 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.
+
+-- \file store/testdata_v3.sql
+-- Populates a v3 database with some test data.
+--
+-- This contains 5 test programs, each with one test case, and each
+-- reporting one of all possible result types.
+
+
+BEGIN TRANSACTION;
+
+
+-- context
+INSERT INTO contexts (cwd) VALUES ('/test/suite/root');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('HOME', '/home/test');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('PATH', '/bin:/usr/bin');
+
+-- metadata_id 1
+INSERT INTO metadatas VALUES (1, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (1, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (1, 'description', '');
+INSERT INTO metadatas VALUES (1, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (1, 'required_configs', '');
+INSERT INTO metadatas VALUES (1, 'required_files', '');
+INSERT INTO metadatas VALUES (1, 'required_memory', '0');
+INSERT INTO metadatas VALUES (1, 'required_programs', '');
+INSERT INTO metadatas VALUES (1, 'required_user', '');
+INSERT INTO metadatas VALUES (1, 'timeout', '300');
+
+-- test_program_id 1
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (1, '/test/suite/root/foo_test', '/test/suite/root',
+ 'foo_test', 'suite-name', 1, 'plain');
+
+-- test_case_id 1
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (1, 1, 'main', 1);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (1, 'passed', NULL, 1357643611000000, 1357643621000500);
+
+-- metadata_id 2
+INSERT INTO metadatas VALUES (2, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (2, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (2, 'description', '');
+INSERT INTO metadatas VALUES (2, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (2, 'required_configs', '');
+INSERT INTO metadatas VALUES (2, 'required_files', '');
+INSERT INTO metadatas VALUES (2, 'required_memory', '0');
+INSERT INTO metadatas VALUES (2, 'required_programs', '');
+INSERT INTO metadatas VALUES (2, 'required_user', '');
+INSERT INTO metadatas VALUES (2, 'timeout', '10');
+
+-- test_program_id 2
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (2, '/test/suite/root/subdir/another_test', '/test/suite/root',
+ 'subdir/another_test', 'subsuite-name', 2, 'plain');
+
+-- test_case_id 2
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (2, 2, 'main', 2);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (2, 'failed', 'Exited with code 1',
+ 1357643622001200, 1357643622900021);
+
+-- file_id 1
+INSERT INTO files (file_id, contents) VALUES (1, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDOUT__', 1);
+
+-- file_id 2
+INSERT INTO files (file_id, contents) VALUES (2, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (2, '__STDERR__', 2);
+
+-- metadata_id 3
+INSERT INTO metadatas VALUES (3, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (3, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (3, 'description', '');
+INSERT INTO metadatas VALUES (3, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (3, 'required_configs', '');
+INSERT INTO metadatas VALUES (3, 'required_files', '');
+INSERT INTO metadatas VALUES (3, 'required_memory', '0');
+INSERT INTO metadatas VALUES (3, 'required_programs', '');
+INSERT INTO metadatas VALUES (3, 'required_user', '');
+INSERT INTO metadatas VALUES (3, 'timeout', '300');
+
+-- test_program_id 3
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (3, '/test/suite/root/subdir/bar_test', '/test/suite/root',
+ 'subdir/bar_test', 'subsuite-name', 3, 'plain');
+
+-- test_case_id 3
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (3, 3, 'main', 3);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (3, 'broken', 'Received signal 1',
+ 1357643623500000, 1357643630981932);
+
+-- metadata_id 4
+INSERT INTO metadatas VALUES (4, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (4, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (4, 'description', '');
+INSERT INTO metadatas VALUES (4, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (4, 'required_configs', '');
+INSERT INTO metadatas VALUES (4, 'required_files', '');
+INSERT INTO metadatas VALUES (4, 'required_memory', '0');
+INSERT INTO metadatas VALUES (4, 'required_programs', '');
+INSERT INTO metadatas VALUES (4, 'required_user', '');
+INSERT INTO metadatas VALUES (4, 'timeout', '300');
+
+-- test_program_id 4
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (4, '/test/suite/root/top_test', '/test/suite/root',
+ 'top_test', 'suite-name', 4, 'plain');
+
+-- test_case_id 4
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (4, 4, 'main', 4);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (4, 'expected_failure', 'Known bug',
+ 1357643631000000, 1357643631020000);
+
+-- metadata_id 5
+INSERT INTO metadatas VALUES (5, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (5, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (5, 'description', '');
+INSERT INTO metadatas VALUES (5, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (5, 'required_configs', '');
+INSERT INTO metadatas VALUES (5, 'required_files', '');
+INSERT INTO metadatas VALUES (5, 'required_memory', '0');
+INSERT INTO metadatas VALUES (5, 'required_programs', '');
+INSERT INTO metadatas VALUES (5, 'required_user', '');
+INSERT INTO metadatas VALUES (5, 'timeout', '300');
+
+-- test_program_id 5
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (5, '/test/suite/root/last_test', '/test/suite/root',
+ 'last_test', 'suite-name', 5, 'plain');
+
+-- test_case_id 5
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (5, 5, 'main', 5);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (5, 'skipped', 'Does not apply', 1357643632000000, 1357643638000000);
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v3_3.sql b/store/testdata_v3_3.sql
new file mode 100644
index 000000000000..80d5a6b9a6e2
--- /dev/null
+++ b/store/testdata_v3_3.sql
@@ -0,0 +1,171 @@
+-- Copyright 2014 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.
+
+-- \file store/testdata_v3.sql
+-- Populates a v3 database with some test data.
+--
+-- ATF test programs only.
+
+
+BEGIN TRANSACTION;
+
+
+-- context
+INSERT INTO contexts (cwd) VALUES ('/usr/tests');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('PATH', '/bin:/usr/bin');
+
+-- metadata_id 6
+INSERT INTO metadatas VALUES (6, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (6, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (6, 'description', '');
+INSERT INTO metadatas VALUES (6, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (6, 'required_configs', '');
+INSERT INTO metadatas VALUES (6, 'required_files', '');
+INSERT INTO metadatas VALUES (6, 'required_memory', '0');
+INSERT INTO metadatas VALUES (6, 'required_programs', '');
+INSERT INTO metadatas VALUES (6, 'required_user', '');
+INSERT INTO metadatas VALUES (6, 'timeout', '300');
+
+-- test_program_id 6
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (6, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 6, 'atf');
+
+-- metadata_id 7
+INSERT INTO metadatas VALUES (7, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (7, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (7, 'description', '');
+INSERT INTO metadatas VALUES (7, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (7, 'required_configs', '');
+INSERT INTO metadatas VALUES (7, 'required_files', '');
+INSERT INTO metadatas VALUES (7, 'required_memory', '0');
+INSERT INTO metadatas VALUES (7, 'required_programs', '');
+INSERT INTO metadatas VALUES (7, 'required_user', '');
+INSERT INTO metadatas VALUES (7, 'timeout', '300');
+
+-- test_case_id 6, passed, no optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (6, 6, 'this_passes', 7);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (6, 'passed', NULL, 1357648712000000, 1357648718000000);
+
+-- metadata_id 8
+INSERT INTO metadatas VALUES (8, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (8, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (8, 'description', 'Test description');
+INSERT INTO metadatas VALUES (8, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (8, 'required_configs', '');
+INSERT INTO metadatas VALUES (8, 'required_files', '');
+INSERT INTO metadatas VALUES (8, 'required_memory', '128');
+INSERT INTO metadatas VALUES (8, 'required_programs', '');
+INSERT INTO metadatas VALUES (8, 'required_user', 'root');
+INSERT INTO metadatas VALUES (8, 'timeout', '300');
+
+-- test_case_id 7, failed, optional non-multivalue metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (7, 6, 'this_fails', 8);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (7, 'failed', 'Some reason', 1357648719000000, 1357648720897182);
+
+-- metadata_id 9
+INSERT INTO metadatas VALUES (9, 'allowed_architectures', 'powerpc x86_64');
+INSERT INTO metadatas VALUES (9, 'allowed_platforms', 'amd64 macppc');
+INSERT INTO metadatas VALUES (9, 'description', 'Test explanation');
+INSERT INTO metadatas VALUES (9, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (9, 'required_configs', 'unprivileged_user X-foo');
+INSERT INTO metadatas VALUES (9, 'required_files', '/the/data/file');
+INSERT INTO metadatas VALUES (9, 'required_memory', '512');
+INSERT INTO metadatas VALUES (9, 'required_programs', 'cp /bin/ls');
+INSERT INTO metadatas VALUES (9, 'required_user', 'unprivileged');
+INSERT INTO metadatas VALUES (9, 'timeout', '600');
+
+-- test_case_id 8, skipped, all optional metadata.
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (8, 6, 'this_skips', 9);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (8, 'skipped', 'Another reason', 1357648729182013, 1357648730000000);
+
+-- file_id 3
+INSERT INTO files (file_id, contents)
+ VALUES (3, x'416e6f74686572207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (8, '__STDOUT__', 3);
+
+-- metadata_id 10
+INSERT INTO metadatas VALUES (10, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (10, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (10, 'description', '');
+INSERT INTO metadatas VALUES (10, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (10, 'required_configs', '');
+INSERT INTO metadatas VALUES (10, 'required_files', '');
+INSERT INTO metadatas VALUES (10, 'required_memory', '0');
+INSERT INTO metadatas VALUES (10, 'required_programs', '');
+INSERT INTO metadatas VALUES (10, 'required_user', '');
+INSERT INTO metadatas VALUES (10, 'timeout', '300');
+
+-- test_program_id 7
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (7, '/usr/tests/simple_test', '/usr/tests',
+ 'simple_test', 'subsuite-name', 10, 'atf');
+
+-- metadata_id 11
+INSERT INTO metadatas VALUES (11, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (11, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (11, 'description', 'More text');
+INSERT INTO metadatas VALUES (11, 'has_cleanup', 'true');
+INSERT INTO metadatas VALUES (11, 'required_configs', '');
+INSERT INTO metadatas VALUES (11, 'required_files', '');
+INSERT INTO metadatas VALUES (11, 'required_memory', '128');
+INSERT INTO metadatas VALUES (11, 'required_programs', '');
+INSERT INTO metadatas VALUES (11, 'required_user', 'unprivileged');
+INSERT INTO metadatas VALUES (11, 'timeout', '300');
+
+-- test_case_id 9
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (9, 7, 'main', 11);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (9, 'failed', 'Exited with code 1',
+ 1357648740120000, 1357648750081700);
+
+-- file_id 4
+INSERT INTO files (file_id, contents)
+ VALUES (4, x'416e6f7468657220737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (9, '__STDERR__', 4);
+
+
+COMMIT TRANSACTION;
diff --git a/store/testdata_v3_4.sql b/store/testdata_v3_4.sql
new file mode 100644
index 000000000000..1007bc7adac4
--- /dev/null
+++ b/store/testdata_v3_4.sql
@@ -0,0 +1,141 @@
+-- Copyright 2014 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.
+
+-- \file store/testdata_v3.sql
+-- Populates a v3 database with some test data.
+--
+-- Mixture of test programs.
+
+
+BEGIN TRANSACTION;
+
+
+-- context
+INSERT INTO contexts (cwd) VALUES ('/usr/tests');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('LANG', 'C');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('PATH', '/bin:/usr/bin');
+INSERT INTO env_vars (var_name, var_value)
+ VALUES ('TERM', 'xterm');
+
+-- metadata_id 12
+INSERT INTO metadatas VALUES (12, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (12, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (12, 'description', '');
+INSERT INTO metadatas VALUES (12, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (12, 'required_configs', '');
+INSERT INTO metadatas VALUES (12, 'required_files', '');
+INSERT INTO metadatas VALUES (12, 'required_memory', '0');
+INSERT INTO metadatas VALUES (12, 'required_programs', '');
+INSERT INTO metadatas VALUES (12, 'required_user', '');
+INSERT INTO metadatas VALUES (12, 'timeout', '10');
+
+-- test_program_id 8
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (8, '/usr/tests/subdir/another_test', '/usr/tests',
+ 'subdir/another_test', 'subsuite-name', 12, 'plain');
+
+-- test_case_id 10
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (10, 8, 'main', 12);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (10, 'failed', 'Exit failure', 1357644395000000, 1357644396000000);
+
+-- file_id 5
+INSERT INTO files (file_id, contents) VALUES (5, x'54657374207374646f7574');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDOUT__', 5);
+
+-- file_id 6
+INSERT INTO files (file_id, contents) VALUES (6, x'5465737420737464657272');
+INSERT INTO test_case_files (test_case_id, file_name, file_id)
+ VALUES (10, '__STDERR__', 6);
+
+-- metadata_id 13
+INSERT INTO metadatas VALUES (13, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (13, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (13, 'description', '');
+INSERT INTO metadatas VALUES (13, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (13, 'required_configs', '');
+INSERT INTO metadatas VALUES (13, 'required_files', '');
+INSERT INTO metadatas VALUES (13, 'required_memory', '0');
+INSERT INTO metadatas VALUES (13, 'required_programs', '');
+INSERT INTO metadatas VALUES (13, 'required_user', '');
+INSERT INTO metadatas VALUES (13, 'timeout', '300');
+
+-- test_program_id 9
+INSERT INTO test_programs (test_program_id, absolute_path, root,
+ relative_path, test_suite_name, metadata_id,
+ interface)
+ VALUES (9, '/usr/tests/complex_test', '/usr/tests',
+ 'complex_test', 'suite-name', 14, 'atf');
+
+-- metadata_id 15
+INSERT INTO metadatas VALUES (15, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (15, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (15, 'description', '');
+INSERT INTO metadatas VALUES (15, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (15, 'required_configs', '');
+INSERT INTO metadatas VALUES (15, 'required_files', '');
+INSERT INTO metadatas VALUES (15, 'required_memory', '0');
+INSERT INTO metadatas VALUES (15, 'required_programs', '');
+INSERT INTO metadatas VALUES (15, 'required_user', '');
+INSERT INTO metadatas VALUES (15, 'timeout', '300');
+
+-- test_case_id 11
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (11, 9, 'this_passes', 15);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (11, 'passed', NULL, 1357644396500000, 1357644397000000);
+
+-- metadata_id 16
+INSERT INTO metadatas VALUES (16, 'allowed_architectures', '');
+INSERT INTO metadatas VALUES (16, 'allowed_platforms', '');
+INSERT INTO metadatas VALUES (16, 'description', 'Test description');
+INSERT INTO metadatas VALUES (16, 'has_cleanup', 'false');
+INSERT INTO metadatas VALUES (16, 'required_configs', '');
+INSERT INTO metadatas VALUES (16, 'required_files', '');
+INSERT INTO metadatas VALUES (16, 'required_memory', '0');
+INSERT INTO metadatas VALUES (16, 'required_programs', '');
+INSERT INTO metadatas VALUES (16, 'required_user', 'root');
+INSERT INTO metadatas VALUES (16, 'timeout', '300');
+
+-- test_case_id 12
+INSERT INTO test_cases (test_case_id, test_program_id, name, metadata_id)
+ VALUES (12, 9, 'this_fails', 16);
+INSERT INTO test_results (test_case_id, result_type, result_reason, start_time,
+ end_time)
+ VALUES (12, 'failed', 'Some reason', 1357644397100000, 1357644399005000);
+
+
+COMMIT TRANSACTION;
diff --git a/store/transaction_test.cpp b/store/transaction_test.cpp
new file mode 100644
index 000000000000..62db8bf1ffbe
--- /dev/null
+++ b/store/transaction_test.cpp
@@ -0,0 +1,170 @@
+// 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 <map>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_program.hpp"
+#include "store/read_backend.hpp"
+#include "store/read_transaction.hpp"
+#include "store/write_backend.hpp"
+#include "store/write_transaction.hpp"
+#include "utils/datetime.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/units.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace units = utils::units;
+
+
+namespace {
+
+
+/// Puts and gets a context and validates the results.
+///
+/// \param exp_context The context to save and restore.
+static void
+check_get_put_context(const model::context& exp_context)
+{
+ const fs::path test_db("test.db");
+
+ if (fs::exists(test_db))
+ fs::unlink(test_db);
+
+ {
+ store::write_backend backend = store::write_backend::open_rw(test_db);
+ store::write_transaction tx = backend.start_write();
+ tx.put_context(exp_context);
+ tx.commit();
+ }
+ {
+ store::read_backend backend = store::read_backend::open_ro(test_db);
+ store::read_transaction tx = backend.start_read();
+ model::context context = tx.get_context();
+ tx.finish();
+
+ ATF_REQUIRE(exp_context == context);
+ }
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE(get_put_context__ok);
+ATF_TEST_CASE_HEAD(get_put_context__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_put_context__ok)
+{
+ std::map< std::string, std::string > env1;
+ env1["A1"] = "foo";
+ env1["A2"] = "bar";
+ std::map< std::string, std::string > env2;
+ check_get_put_context(model::context(fs::path("/foo/bar"), env1));
+ check_get_put_context(model::context(fs::path("/foo/bar"), env1));
+ check_get_put_context(model::context(fs::path("/foo/baz"), env2));
+}
+
+
+ATF_TEST_CASE(get_put_test_case__ok);
+ATF_TEST_CASE_HEAD(get_put_test_case__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(get_put_test_case__ok)
+{
+ const model::metadata md2 = model::metadata_builder()
+ .add_allowed_architecture("powerpc")
+ .add_allowed_architecture("x86_64")
+ .add_allowed_platform("amd64")
+ .add_allowed_platform("macppc")
+ .add_custom("user1", "value1")
+ .add_custom("user2", "value2")
+ .add_required_config("var1")
+ .add_required_config("var2")
+ .add_required_config("var3")
+ .add_required_file(fs::path("/file1/yes"))
+ .add_required_file(fs::path("/file2/foo"))
+ .add_required_program(fs::path("/bin/ls"))
+ .add_required_program(fs::path("cp"))
+ .set_description("The description")
+ .set_has_cleanup(true)
+ .set_required_memory(units::bytes::parse("1k"))
+ .set_required_user("root")
+ .set_timeout(datetime::delta(520, 0))
+ .build();
+
+ const model::test_program test_program = model::test_program_builder(
+ "atf", fs::path("the/binary"), fs::path("/some/root"), "the-suite")
+ .add_test_case("tc1")
+ .add_test_case("tc2", md2)
+ .build();
+
+ int64_t test_program_id;
+ {
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+
+ store::write_transaction tx = backend.start_write();
+ test_program_id = tx.put_test_program(test_program);
+ tx.put_test_case(test_program, "tc1", test_program_id);
+ tx.put_test_case(test_program, "tc2", test_program_id);
+ tx.commit();
+ }
+
+ store::read_backend backend = store::read_backend::open_ro(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+
+ store::read_transaction tx = backend.start_read();
+ const model::test_program_ptr loaded_test_program =
+ store::detail::get_test_program(backend, test_program_id);
+ ATF_REQUIRE(test_program == *loaded_test_program);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, get_put_context__ok);
+
+ ATF_ADD_TEST_CASE(tcs, get_put_test_case__ok);
+}
diff --git a/store/write_backend.cpp b/store/write_backend.cpp
new file mode 100644
index 000000000000..7a3eb167f88f
--- /dev/null
+++ b/store/write_backend.cpp
@@ -0,0 +1,208 @@
+// 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_backend.hpp"
+
+#include <stdexcept>
+
+#include "store/exceptions.hpp"
+#include "store/metadata.hpp"
+#include "store/read_backend.hpp"
+#include "store/write_transaction.hpp"
+#include "utils/env.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/macros.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/sanity.hpp"
+#include "utils/stream.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace fs = utils::fs;
+namespace sqlite = utils::sqlite;
+
+
+/// The current schema version.
+///
+/// Any new database gets this schema version. Existing databases with an older
+/// schema version must be first migrated to the current schema with
+/// migrate_schema() before they can be used.
+///
+/// This must be kept in sync with the value in the corresponding schema_vX.sql
+/// file, where X matches this version number.
+///
+/// This variable is not const to allow tests to modify it. No other code
+/// should change its value.
+int store::detail::current_schema_version = 3;
+
+
+namespace {
+
+
+/// Checks if a database is empty (i.e. if it is new).
+///
+/// \param db The database to check.
+///
+/// \return True if the database is empty.
+static bool
+empty_database(sqlite::database& db)
+{
+ sqlite::statement stmt = db.create_statement("SELECT * FROM sqlite_master");
+ return !stmt.step();
+}
+
+
+} // anonymous namespace
+
+
+/// Calculates the path to the schema file for the database.
+///
+/// \return The path to the installed schema_vX.sql file that matches the
+/// current_schema_version.
+fs::path
+store::detail::schema_file(void)
+{
+ return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR))
+ / (F("schema_v%s.sql") % current_schema_version);
+}
+
+
+/// 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();
+
+ LI(F("Populating new database with schema from %s") % schema);
+ try {
+ db.exec(utils::read_file(schema));
+
+ 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());
+ } catch (const std::runtime_error& e) {
+ throw error(F("Cannot read database schema '%s'") % schema);
+ }
+}
+
+
+/// Internal implementation for the backend.
+struct store::write_backend::impl : utils::noncopyable {
+ /// The SQLite database this backend talks to.
+ sqlite::database database;
+
+ /// Constructor.
+ ///
+ /// \param database_ The SQLite database instance.
+ impl(sqlite::database& database_) : database(database_)
+ {
+ }
+};
+
+
+/// Constructs a new backend.
+///
+/// \param pimpl_ The internal data.
+store::write_backend::write_backend(impl* pimpl_) :
+ _pimpl(pimpl_)
+{
+}
+
+
+/// Destructor.
+store::write_backend::~write_backend(void)
+{
+}
+
+
+/// Opens a database in read-write mode and creates it if necessary.
+///
+/// \param file The database file to be opened.
+///
+/// \return The backend representation.
+///
+/// \throw store::error If there is any problem opening or creating
+/// the database.
+store::write_backend
+store::write_backend::open_rw(const fs::path& file)
+{
+ sqlite::database db = detail::open_and_setup(
+ file, sqlite::open_readwrite | sqlite::open_create);
+ if (!empty_database(db))
+ throw error(F("%s already exists and is not empty; cannot open "
+ "for write") % file);
+ detail::initialize(db);
+ return write_backend(new impl(db));
+}
+
+
+/// Closes the SQLite database.
+void
+store::write_backend::close(void)
+{
+ _pimpl->database.close();
+}
+
+
+/// Gets the connection to the SQLite database.
+///
+/// \return A database connection.
+sqlite::database&
+store::write_backend::database(void)
+{
+ return _pimpl->database;
+}
+
+
+/// Opens a write-only transaction.
+///
+/// \return A new transaction.
+store::write_transaction
+store::write_backend::start_write(void)
+{
+ return write_transaction(*this);
+}
diff --git a/store/write_backend.hpp b/store/write_backend.hpp
new file mode 100644
index 000000000000..a1d46f1450c0
--- /dev/null
+++ b/store/write_backend.hpp
@@ -0,0 +1,81 @@
+// 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.
+
+/// \file store/write_backend.hpp
+/// Interface to the backend database for write-only operations.
+
+#if !defined(STORE_WRITE_BACKEND_HPP)
+#define STORE_WRITE_BACKEND_HPP
+
+#include "store/write_backend_fwd.hpp"
+
+#include <memory>
+
+#include "store/metadata_fwd.hpp"
+#include "store/write_transaction_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/sqlite/database_fwd.hpp"
+
+namespace store {
+
+
+namespace detail {
+
+
+utils::fs::path schema_file(void);
+metadata initialize(utils::sqlite::database&);
+
+
+} // anonymous namespace
+
+
+/// Public interface to the database store for write-only operations.
+class write_backend {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class metadata;
+
+ write_backend(impl*);
+
+public:
+ ~write_backend(void);
+
+ static write_backend open_rw(const utils::fs::path&);
+ void close(void);
+
+ utils::sqlite::database& database(void);
+ write_transaction start_write(void);
+};
+
+
+} // namespace store
+
+#endif // !defined(STORE_WRITE_BACKEND_HPP)
diff --git a/store/write_backend_fwd.hpp b/store/write_backend_fwd.hpp
new file mode 100644
index 000000000000..8f2ea12d25cb
--- /dev/null
+++ b/store/write_backend_fwd.hpp
@@ -0,0 +1,52 @@
+// Copyright 2015 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.
+
+/// \file store/write_backend_fwd.hpp
+/// Forward declarations for store/write_backend.hpp
+
+#if !defined(STORE_WRITE_BACKEND_FWD_HPP)
+#define STORE_WRITE_BACKEND_FWD_HPP
+
+namespace store {
+
+
+namespace detail {
+
+
+extern int current_schema_version;
+
+
+} // namespace detail
+
+
+class write_backend;
+
+
+} // namespace store
+
+#endif // !defined(STORE_WRITE_BACKEND_FWD_HPP)
diff --git a/store/write_backend_test.cpp b/store/write_backend_test.cpp
new file mode 100644
index 000000000000..a1052154aaae
--- /dev/null
+++ b/store/write_backend_test.cpp
@@ -0,0 +1,204 @@
+// 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_backend.hpp"
+
+#include <atf-c++.hpp>
+
+#include "store/exceptions.hpp"
+#include "store/metadata.hpp"
+#include "utils/datetime.hpp"
+#include "utils/env.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+
+
+ATF_TEST_CASE(detail__initialize__ok);
+ATF_TEST_CASE_HEAD(detail__initialize__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(detail__initialize__ok)
+{
+ sqlite::database db = sqlite::database::in_memory();
+ const datetime::timestamp before = datetime::timestamp::now();
+ const store::metadata md = store::detail::initialize(db);
+ const datetime::timestamp after = datetime::timestamp::now();
+
+ ATF_REQUIRE(md.timestamp() >= before.to_seconds());
+ ATF_REQUIRE(md.timestamp() <= after.to_microseconds());
+ ATF_REQUIRE_EQ(store::detail::current_schema_version, md.schema_version());
+
+ // Query some known tables to ensure they were created.
+ db.exec("SELECT * FROM metadata");
+
+ // And now query some know values.
+ sqlite::statement stmt = db.create_statement(
+ "SELECT COUNT(*) FROM metadata");
+ ATF_REQUIRE(stmt.step());
+ ATF_REQUIRE_EQ(1, stmt.column_int(0));
+ ATF_REQUIRE(!stmt.step());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__missing_schema);
+ATF_TEST_CASE_BODY(detail__initialize__missing_schema)
+{
+ utils::setenv("KYUA_STOREDIR", "/non-existent");
+ store::detail::current_schema_version = 712;
+
+ sqlite::database db = sqlite::database::in_memory();
+ ATF_REQUIRE_THROW_RE(store::error,
+ "Cannot read.*'/non-existent/schema_v712.sql'",
+ store::detail::initialize(db));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__initialize__sqlite_error);
+ATF_TEST_CASE_BODY(detail__initialize__sqlite_error)
+{
+ utils::setenv("KYUA_STOREDIR", ".");
+ store::detail::current_schema_version = 712;
+
+ atf::utils::create_file("schema_v712.sql", "foo_bar_baz;\n");
+
+ sqlite::database db = sqlite::database::in_memory();
+ ATF_REQUIRE_THROW_RE(store::error, "Failed to initialize.*:.*foo_bar_baz",
+ store::detail::initialize(db));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__builtin);
+ATF_TEST_CASE_BODY(detail__schema_file__builtin)
+{
+ utils::unsetenv("KYUA_STOREDIR");
+ ATF_REQUIRE_EQ(fs::path(KYUA_STOREDIR) / "schema_v3.sql",
+ store::detail::schema_file());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(detail__schema_file__overriden);
+ATF_TEST_CASE_BODY(detail__schema_file__overriden)
+{
+ utils::setenv("KYUA_STOREDIR", "/tmp/test");
+ store::detail::current_schema_version = 123;
+ ATF_REQUIRE_EQ(fs::path("/tmp/test/schema_v123.sql"),
+ store::detail::schema_file());
+}
+
+
+ATF_TEST_CASE(write_backend__open_rw__ok_if_empty);
+ATF_TEST_CASE_HEAD(write_backend__open_rw__ok_if_empty)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(write_backend__open_rw__ok_if_empty)
+{
+ {
+ sqlite::database db = sqlite::database::open(
+ fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
+ }
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("SELECT * FROM metadata");
+}
+
+
+ATF_TEST_CASE(write_backend__open_rw__error_if_not_empty);
+ATF_TEST_CASE_HEAD(write_backend__open_rw__error_if_not_empty)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(write_backend__open_rw__error_if_not_empty)
+{
+ {
+ sqlite::database db = sqlite::database::open(
+ fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
+ store::detail::initialize(db);
+ }
+ ATF_REQUIRE_THROW_RE(store::error, "test.db already exists",
+ store::write_backend::open_rw(fs::path("test.db")));
+}
+
+
+ATF_TEST_CASE(write_backend__open_rw__create_missing);
+ATF_TEST_CASE_HEAD(write_backend__open_rw__create_missing)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(write_backend__open_rw__create_missing)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("SELECT * FROM metadata");
+}
+
+
+ATF_TEST_CASE(write_backend__close);
+ATF_TEST_CASE_HEAD(write_backend__close)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(write_backend__close)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("SELECT * FROM metadata");
+ backend.close();
+ ATF_REQUIRE_THROW(utils::sqlite::error,
+ backend.database().exec("SELECT * FROM metadata"));
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, detail__initialize__ok);
+ ATF_ADD_TEST_CASE(tcs, detail__initialize__missing_schema);
+ ATF_ADD_TEST_CASE(tcs, detail__initialize__sqlite_error);
+
+ ATF_ADD_TEST_CASE(tcs, detail__schema_file__builtin);
+ ATF_ADD_TEST_CASE(tcs, detail__schema_file__overriden);
+
+ ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__ok_if_empty);
+ ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__error_if_not_empty);
+ ATF_ADD_TEST_CASE(tcs, write_backend__open_rw__create_missing);
+ ATF_ADD_TEST_CASE(tcs, write_backend__close);
+}
diff --git a/store/write_transaction.cpp b/store/write_transaction.cpp
new file mode 100644
index 000000000000..134a13a30494
--- /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());
+ }
+}
diff --git a/store/write_transaction.hpp b/store/write_transaction.hpp
new file mode 100644
index 000000000000..5c73d20af788
--- /dev/null
+++ b/store/write_transaction.hpp
@@ -0,0 +1,89 @@
+// 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.
+
+/// \file store/write_transaction.hpp
+/// Implementation of write-only transactions on the backend.
+
+#if !defined(STORE_WRITE_TRANSACTION_HPP)
+#define STORE_WRITE_TRANSACTION_HPP
+
+#include "store/write_transaction_fwd.hpp"
+
+extern "C" {
+#include <stdint.h>
+}
+
+#include <memory>
+#include <string>
+
+#include "model/context_fwd.hpp"
+#include "model/test_program_fwd.hpp"
+#include "model/test_result_fwd.hpp"
+#include "store/write_backend_fwd.hpp"
+#include "utils/datetime_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/optional_fwd.hpp"
+
+namespace store {
+
+
+/// Representation of a write-only transaction.
+///
+/// Transactions are the entry place for high-level calls that access the
+/// database.
+class write_transaction {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class write_backend;
+ write_transaction(write_backend&);
+
+public:
+ ~write_transaction(void);
+
+ void commit(void);
+ void rollback(void);
+
+ void put_context(const model::context&);
+ int64_t put_test_program(const model::test_program&);
+ int64_t put_test_case(const model::test_program&, const std::string&,
+ const int64_t);
+ utils::optional< int64_t > put_test_case_file(const std::string&,
+ const utils::fs::path&,
+ const int64_t);
+ int64_t put_result(const model::test_result&, const int64_t,
+ const utils::datetime::timestamp&,
+ const utils::datetime::timestamp&);
+};
+
+
+} // namespace store
+
+#endif // !defined(STORE_WRITE_TRANSACTION_HPP)
diff --git a/store/write_transaction_fwd.hpp b/store/write_transaction_fwd.hpp
new file mode 100644
index 000000000000..1d2357a52dbe
--- /dev/null
+++ b/store/write_transaction_fwd.hpp
@@ -0,0 +1,43 @@
+// Copyright 2015 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.
+
+/// \file store/write_transaction_fwd.hpp
+/// Forward declarations for store/write_transaction.hpp
+
+#if !defined(STORE_WRITE_TRANSACTION_FWD_HPP)
+#define STORE_WRITE_TRANSACTION_FWD_HPP
+
+namespace store {
+
+
+class write_transaction;
+
+
+} // namespace store
+
+#endif // !defined(STORE_WRITE_TRANSACTION_FWD_HPP)
diff --git a/store/write_transaction_test.cpp b/store/write_transaction_test.cpp
new file mode 100644
index 000000000000..984e328dcdae
--- /dev/null
+++ b/store/write_transaction_test.cpp
@@ -0,0 +1,416 @@
+// 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"
+
+#include <cstring>
+#include <map>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#include "model/context.hpp"
+#include "model/metadata.hpp"
+#include "model/test_case.hpp"
+#include "model/test_program.hpp"
+#include "model/test_result.hpp"
+#include "store/exceptions.hpp"
+#include "store/write_backend.hpp"
+#include "utils/datetime.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/logging/operations.hpp"
+#include "utils/optional.ipp"
+#include "utils/sqlite/database.hpp"
+#include "utils/sqlite/exceptions.hpp"
+#include "utils/sqlite/statement.ipp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace logging = utils::logging;
+namespace sqlite = utils::sqlite;
+
+using utils::optional;
+
+
+namespace {
+
+
+/// Performs a test for a working put_result
+///
+/// \param result The result object to put.
+/// \param result_type The textual name of the result to expect in the
+/// database.
+/// \param exp_reason The reason to expect in the database. This is separate
+/// from the result parameter so that we can handle passed() here as well.
+/// Just provide NULL in this case.
+static void
+do_put_result_ok_test(const model::test_result& result,
+ const char* result_type, const char* exp_reason)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+ store::write_transaction tx = backend.start_write();
+ const datetime::timestamp start_time = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 10, 00, 0);
+ const datetime::timestamp end_time = datetime::timestamp::from_values(
+ 2012, 01, 30, 22, 15, 30, 123456);
+ tx.put_result(result, 312, start_time, end_time);
+ tx.commit();
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "SELECT test_case_id, result_type, result_reason "
+ "FROM test_results");
+
+ ATF_REQUIRE(stmt.step());
+ ATF_REQUIRE_EQ(312, stmt.column_int64(0));
+ ATF_REQUIRE_EQ(result_type, stmt.column_text(1));
+ if (exp_reason != NULL)
+ ATF_REQUIRE_EQ(exp_reason, stmt.column_text(2));
+ else
+ ATF_REQUIRE(stmt.column_type(2) == sqlite::type_null);
+ ATF_REQUIRE(!stmt.step());
+}
+
+
+} // anonymous namespace
+
+
+ATF_TEST_CASE(commit__ok);
+ATF_TEST_CASE_HEAD(commit__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(commit__ok)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ store::write_transaction tx = backend.start_write();
+ backend.database().exec("CREATE TABLE a (b INTEGER PRIMARY KEY)");
+ backend.database().exec("SELECT * FROM a");
+ tx.commit();
+ backend.database().exec("SELECT * FROM a");
+}
+
+
+ATF_TEST_CASE(commit__fail);
+ATF_TEST_CASE_HEAD(commit__fail)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(commit__fail)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ const model::context context(fs::path("/foo/bar"),
+ std::map< std::string, std::string >());
+ {
+ store::write_transaction tx = backend.start_write();
+ tx.put_context(context);
+ backend.database().exec(
+ "CREATE TABLE foo ("
+ "a REFERENCES env_vars(var_name) DEFERRABLE INITIALLY DEFERRED)");
+ backend.database().exec("INSERT INTO foo VALUES (\"WHAT\")");
+ ATF_REQUIRE_THROW(store::error, tx.commit());
+ }
+ // If the code attempts to maintain any state regarding the already-put
+ // objects and the commit does not clean up correctly, this would fail in
+ // some manner.
+ store::write_transaction tx = backend.start_write();
+ tx.put_context(context);
+ tx.commit();
+}
+
+
+ATF_TEST_CASE(rollback__ok);
+ATF_TEST_CASE_HEAD(rollback__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(rollback__ok)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ store::write_transaction tx = backend.start_write();
+ backend.database().exec("CREATE TABLE a_table (b INTEGER PRIMARY KEY)");
+ backend.database().exec("SELECT * FROM a_table");
+ tx.rollback();
+ ATF_REQUIRE_THROW_RE(sqlite::error, "a_table",
+ backend.database().exec("SELECT * FROM a_table"));
+}
+
+
+ATF_TEST_CASE(put_test_program__ok);
+ATF_TEST_CASE_HEAD(put_test_program__ok)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_test_program__ok)
+{
+ const model::metadata md = model::metadata_builder()
+ .add_custom("var1", "value1")
+ .add_custom("var2", "value2")
+ .build();
+ const model::test_program test_program(
+ "mock", fs::path("the/binary"), fs::path("/some//root"),
+ "the-suite", md, model::test_cases_map());
+
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+ store::write_transaction tx = backend.start_write();
+ const int64_t test_program_id = tx.put_test_program(test_program);
+ tx.commit();
+
+ {
+ sqlite::statement stmt = backend.database().create_statement(
+ "SELECT * FROM test_programs");
+
+ ATF_REQUIRE(stmt.step());
+ ATF_REQUIRE_EQ(test_program_id,
+ stmt.safe_column_int64("test_program_id"));
+ ATF_REQUIRE_EQ("/some/root/the/binary",
+ stmt.safe_column_text("absolute_path"));
+ ATF_REQUIRE_EQ("/some/root", stmt.safe_column_text("root"));
+ ATF_REQUIRE_EQ("the/binary", stmt.safe_column_text("relative_path"));
+ ATF_REQUIRE_EQ("the-suite", stmt.safe_column_text("test_suite_name"));
+ ATF_REQUIRE(!stmt.step());
+ }
+}
+
+
+ATF_TEST_CASE(put_test_case__fail);
+ATF_TEST_CASE_HEAD(put_test_case__fail)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_test_case__fail)
+{
+ const model::test_program test_program = model::test_program_builder(
+ "plain", fs::path("the/binary"), fs::path("/some/root"), "the-suite")
+ .add_test_case("main")
+ .build();
+
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ store::write_transaction tx = backend.start_write();
+ ATF_REQUIRE_THROW(store::error, tx.put_test_case(test_program, "main", -1));
+ tx.commit();
+}
+
+
+ATF_TEST_CASE(put_test_case_file__empty);
+ATF_TEST_CASE_HEAD(put_test_case_file__empty)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_test_case_file__empty)
+{
+ atf::utils::create_file("input.txt", "");
+
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+ store::write_transaction tx = backend.start_write();
+ const optional< int64_t > file_id = tx.put_test_case_file(
+ "my-file", fs::path("input.txt"), 123L);
+ tx.commit();
+ ATF_REQUIRE(!file_id);
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "SELECT * FROM test_case_files NATURAL JOIN files");
+ ATF_REQUIRE(!stmt.step());
+}
+
+
+ATF_TEST_CASE(put_test_case_file__some);
+ATF_TEST_CASE_HEAD(put_test_case_file__some)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_test_case_file__some)
+{
+ const char contents[] = "This is a test!";
+
+ atf::utils::create_file("input.txt", contents);
+
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+ store::write_transaction tx = backend.start_write();
+ const optional< int64_t > file_id = tx.put_test_case_file(
+ "my-file", fs::path("input.txt"), 123L);
+ tx.commit();
+ ATF_REQUIRE(file_id);
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "SELECT * FROM test_case_files NATURAL JOIN files");
+
+ ATF_REQUIRE(stmt.step());
+ ATF_REQUIRE_EQ(123L, stmt.safe_column_int64("test_case_id"));
+ ATF_REQUIRE_EQ("my-file", stmt.safe_column_text("file_name"));
+ const sqlite::blob blob = stmt.safe_column_blob("contents");
+ ATF_REQUIRE(std::strlen(contents) == static_cast< std::size_t >(blob.size));
+ ATF_REQUIRE(std::memcmp(contents, blob.memory, blob.size) == 0);
+ ATF_REQUIRE(!stmt.step());
+}
+
+
+ATF_TEST_CASE(put_test_case_file__fail);
+ATF_TEST_CASE_HEAD(put_test_case_file__fail)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_test_case_file__fail)
+{
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ backend.database().exec("PRAGMA foreign_keys = OFF");
+ store::write_transaction tx = backend.start_write();
+ ATF_REQUIRE_THROW(store::error,
+ tx.put_test_case_file("foo", fs::path("missing"), 1L));
+ tx.commit();
+
+ sqlite::statement stmt = backend.database().create_statement(
+ "SELECT * FROM test_case_files NATURAL JOIN files");
+ ATF_REQUIRE(!stmt.step());
+}
+
+
+ATF_TEST_CASE(put_result__ok__broken);
+ATF_TEST_CASE_HEAD(put_result__ok__broken)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__ok__broken)
+{
+ const model::test_result result(model::test_result_broken, "a b cd");
+ do_put_result_ok_test(result, "broken", "a b cd");
+}
+
+
+ATF_TEST_CASE(put_result__ok__expected_failure);
+ATF_TEST_CASE_HEAD(put_result__ok__expected_failure)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__ok__expected_failure)
+{
+ const model::test_result result(model::test_result_expected_failure,
+ "a b cd");
+ do_put_result_ok_test(result, "expected_failure", "a b cd");
+}
+
+
+ATF_TEST_CASE(put_result__ok__failed);
+ATF_TEST_CASE_HEAD(put_result__ok__failed)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__ok__failed)
+{
+ const model::test_result result(model::test_result_failed, "a b cd");
+ do_put_result_ok_test(result, "failed", "a b cd");
+}
+
+
+ATF_TEST_CASE(put_result__ok__passed);
+ATF_TEST_CASE_HEAD(put_result__ok__passed)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__ok__passed)
+{
+ const model::test_result result(model::test_result_passed);
+ do_put_result_ok_test(result, "passed", NULL);
+}
+
+
+ATF_TEST_CASE(put_result__ok__skipped);
+ATF_TEST_CASE_HEAD(put_result__ok__skipped)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__ok__skipped)
+{
+ const model::test_result result(model::test_result_skipped, "a b cd");
+ do_put_result_ok_test(result, "skipped", "a b cd");
+}
+
+
+ATF_TEST_CASE(put_result__fail);
+ATF_TEST_CASE_HEAD(put_result__fail)
+{
+ logging::set_inmemory();
+ set_md_var("require.files", store::detail::schema_file().c_str());
+}
+ATF_TEST_CASE_BODY(put_result__fail)
+{
+ const model::test_result result(model::test_result_broken, "foo");
+
+ store::write_backend backend = store::write_backend::open_rw(
+ fs::path("test.db"));
+ store::write_transaction tx = backend.start_write();
+ const datetime::timestamp zero = datetime::timestamp::from_microseconds(0);
+ ATF_REQUIRE_THROW(store::error, tx.put_result(result, -1, zero, zero));
+ tx.commit();
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, commit__ok);
+ ATF_ADD_TEST_CASE(tcs, commit__fail);
+ ATF_ADD_TEST_CASE(tcs, rollback__ok);
+
+ ATF_ADD_TEST_CASE(tcs, put_test_program__ok);
+ ATF_ADD_TEST_CASE(tcs, put_test_case__fail);
+ ATF_ADD_TEST_CASE(tcs, put_test_case_file__empty);
+ ATF_ADD_TEST_CASE(tcs, put_test_case_file__some);
+ ATF_ADD_TEST_CASE(tcs, put_test_case_file__fail);
+
+ ATF_ADD_TEST_CASE(tcs, put_result__ok__broken);
+ ATF_ADD_TEST_CASE(tcs, put_result__ok__expected_failure);
+ ATF_ADD_TEST_CASE(tcs, put_result__ok__failed);
+ ATF_ADD_TEST_CASE(tcs, put_result__ok__passed);
+ ATF_ADD_TEST_CASE(tcs, put_result__ok__skipped);
+ ATF_ADD_TEST_CASE(tcs, put_result__fail);
+}