summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Käfer <mail@kkaefer.com>2015-04-08 16:48:58 +0200
committerKonstantin Käfer <mail@kkaefer.com>2015-04-08 16:48:58 +0200
commitb47ebba2b902ee77f510b493ff281b609d65bd35 (patch)
treec85aca061d98932f5e8c7d442afad0f016936143
parent7811acef217ebaf603512d705babe461ed3a43fb (diff)
parent59bd300b4356cb8cee396032e8eb8324376fb630 (diff)
downloadqtlocation-mapboxgl-b47ebba2b902ee77f510b493ff281b609d65bd35.tar.gz
Merge pull request #1071 from mapbox/1071-sqlite-avoid-crash
Gracefully handle SQLite errors
-rw-r--r--include/mbgl/platform/log.hpp4
-rw-r--r--include/mbgl/storage/default/sqlite_cache.hpp2
-rw-r--r--platform/default/sqlite3.cpp33
-rw-r--r--platform/default/sqlite_cache.cpp221
-rw-r--r--src/mbgl/platform/log.cpp6
-rw-r--r--src/mbgl/util/io.cpp10
-rw-r--r--src/mbgl/util/io.hpp9
-rw-r--r--test/fixtures/fixture_log_observer.cpp3
-rw-r--r--test/fixtures/fixture_log_observer.hpp4
-rw-r--r--test/storage/database.cpp391
-rw-r--r--test/test.gyp1
11 files changed, 580 insertions, 104 deletions
diff --git a/include/mbgl/platform/log.hpp b/include/mbgl/platform/log.hpp
index 5d287eb572..d6f3cd1ab4 100644
--- a/include/mbgl/platform/log.hpp
+++ b/include/mbgl/platform/log.hpp
@@ -4,6 +4,7 @@
#include <mbgl/platform/event.hpp>
#include <mbgl/util/std.hpp>
+#include <mbgl/util/noncopyable.hpp>
#include <memory>
#include <string>
@@ -12,7 +13,7 @@ namespace mbgl {
class Log {
public:
- class Observer {
+ class Observer : private util::noncopyable {
public:
virtual ~Observer() = default;
@@ -22,6 +23,7 @@ public:
};
static void setObserver(std::unique_ptr<Observer> Observer);
+ static std::unique_ptr<Observer> removeObserver();
private:
template <typename T, size_t N>
diff --git a/include/mbgl/storage/default/sqlite_cache.hpp b/include/mbgl/storage/default/sqlite_cache.hpp
index 8f2746561c..fe80a41b52 100644
--- a/include/mbgl/storage/default/sqlite_cache.hpp
+++ b/include/mbgl/storage/default/sqlite_cache.hpp
@@ -38,6 +38,7 @@ private:
void process(StopAction &action);
void createDatabase();
+ void createSchema();
const std::string path;
uv_loop_t *loop = nullptr;
@@ -45,6 +46,7 @@ private:
std::thread thread;
std::unique_ptr<::mapbox::sqlite::Database> db;
std::unique_ptr<::mapbox::sqlite::Statement> getStmt, putStmt, refreshStmt;
+ bool schema = false;
};
}
diff --git a/platform/default/sqlite3.cpp b/platform/default/sqlite3.cpp
index 19ecaba0bf..b2ae4e65bc 100644
--- a/platform/default/sqlite3.cpp
+++ b/platform/default/sqlite3.cpp
@@ -76,11 +76,16 @@ Statement::Statement(sqlite3 *db, const char *sql) {
}
}
-#define CHECK_SQLITE_OK(err) \
+#define CHECK_SQLITE_OK_STMT(err, stmt) \
if (err != SQLITE_OK) { \
throw Exception { err, sqlite3_errmsg(sqlite3_db_handle(stmt)) }; \
}
+#define CHECK_SQLITE_OK(err) \
+ if (err != SQLITE_OK) { \
+ throw Exception { err, sqlite3_errstr(err) }; \
+ }
+
Statement::Statement(Statement &&other) {
*this = std::move(other);
}
@@ -92,8 +97,7 @@ Statement &Statement::operator=(Statement &&other) {
Statement::~Statement() {
if (stmt) {
- const int err = sqlite3_finalize(stmt);
- CHECK_SQLITE_OK(err)
+ sqlite3_finalize(stmt);
}
}
@@ -101,38 +105,38 @@ Statement::operator bool() const {
return stmt != nullptr;
}
-#define BIND_3(type, value) \
+#define BIND_3(type, value, stmt) \
assert(stmt); \
const int err = sqlite3_bind_##type(stmt, offset, value); \
- CHECK_SQLITE_OK(err)
+ CHECK_SQLITE_OK_STMT(err, stmt)
-#define BIND_5(type, value, length, param) \
+#define BIND_5(type, value, length, param, stmt) \
assert(stmt); \
const int err = sqlite3_bind_##type(stmt, offset, value, length, param); \
- CHECK_SQLITE_OK(err)
+ CHECK_SQLITE_OK_STMT(err, stmt)
template <> void Statement::bind(int offset, int value) {
- BIND_3(int, value)
+ BIND_3(int, value, stmt)
}
template <> void Statement::bind(int offset, int64_t value) {
- BIND_3(int64, value)
+ BIND_3(int64, value, stmt)
}
template <> void Statement::bind(int offset, double value) {
- BIND_3(double, value)
+ BIND_3(double, value, stmt)
}
template <> void Statement::bind(int offset, bool value) {
- BIND_3(int, value)
+ BIND_3(int, value, stmt)
}
template <> void Statement::bind(int offset, const char *value) {
- BIND_5(text, value, -1, nullptr)
+ BIND_5(text, value, -1, nullptr, stmt)
}
void Statement::bind(int offset, const std::string &value, bool retain) {
- BIND_5(blob, value.data(), int(value.size()), retain ? SQLITE_TRANSIENT : SQLITE_STATIC)
+ BIND_5(blob, value.data(), int(value.size()), retain ? SQLITE_TRANSIENT : SQLITE_STATIC, stmt)
}
bool Statement::run() {
@@ -143,7 +147,8 @@ bool Statement::run() {
} else if (err == SQLITE_ROW) {
return true;
} else {
- throw std::runtime_error("failed to run statement");
+ CHECK_SQLITE_OK_STMT(err, stmt)
+ return false;
}
}
diff --git a/platform/default/sqlite_cache.cpp b/platform/default/sqlite_cache.cpp
index b8d47159ce..a3114098c8 100644
--- a/platform/default/sqlite_cache.cpp
+++ b/platform/default/sqlite_cache.cpp
@@ -6,9 +6,11 @@
#include <mbgl/util/async_queue.hpp>
#include <mbgl/util/variant.hpp>
#include <mbgl/util/compression.hpp>
+#include <mbgl/util/io.hpp>
#include <mbgl/platform/log.hpp>
#include "sqlite3.hpp"
+#include <sqlite3.h>
#include <uv.h>
@@ -88,10 +90,10 @@ struct SQLiteCache::ActionDispatcher {
template <typename T> void operator()(T &t) { cache.process(t); }
};
-SQLiteCache::SQLiteCache(const std::string &path_)
+SQLiteCache::SQLiteCache(const std::string& path_)
: path(path_),
loop(uv_loop_new()),
- queue(new Queue(loop, [this](Action &action) {
+ queue(new Queue(loop, [this](Action& action) {
mapbox::util::apply_visitor(ActionDispatcher{ *this }, action);
})),
thread([this]() {
@@ -99,8 +101,16 @@ SQLiteCache::SQLiteCache(const std::string &path_)
pthread_setname_np("SQLite Cache");
#endif
uv_run(loop, UV_RUN_DEFAULT);
- })
-{
+
+ try {
+ getStmt.reset();
+ putStmt.reset();
+ refreshStmt.reset();
+ db.reset();
+ } catch (mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
+ }) {
}
SQLiteCache::~SQLiteCache() {
@@ -140,6 +150,10 @@ void SQLiteCache::put(const Resource &resource, std::shared_ptr<const Response>
void SQLiteCache::createDatabase() {
db = util::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ createSchema();
+}
+
+void SQLiteCache::createSchema() {
constexpr const char *const sql = ""
"CREATE TABLE IF NOT EXISTS `http_cache` ("
" `url` TEXT PRIMARY KEY NOT NULL,"
@@ -155,111 +169,146 @@ void SQLiteCache::createDatabase() {
try {
db->exec(sql);
- } catch(mapbox::sqlite::Exception &) {
- // Creating the database table + index failed. That means there may already be one, likely
- // with different columsn. Drop it and try to create a new one.
- try {
- db->exec("DROP TABLE IF EXISTS `http_cache`");
- db->exec(sql);
- } catch (mapbox::sqlite::Exception &ex) {
- Log::Error(Event::Database, "Failed to create database: %s", ex.what());
+ schema = true;
+ } catch (mapbox::sqlite::Exception &ex) {
+
+ if (ex.code == SQLITE_NOTADB) {
+ Log::Warning(Event::Database, "Trashing invalid database");
db.reset();
+ try {
+ util::deleteFile(path);
+ } catch (util::IOException& ioEx) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
+ db = util::make_unique<Database>(path.c_str(), ReadWrite | Create);
+ } else {
+ Log::Error(Event::Database, ex.code, ex.what());
}
+
+ // Creating the database table + index failed. That means there may already be one, likely
+ // with different columsn. Drop it and try to create a new one.
+ db->exec("DROP TABLE IF EXISTS `http_cache`");
+ db->exec(sql);
}
}
void SQLiteCache::process(GetAction &action) {
- // This is called in the SQLite event loop.
- if (!db) {
- createDatabase();
- }
+ try {
+ // This is called in the SQLite event loop.
+ if (!db) {
+ createDatabase();
+ }
- if (!getStmt) {
- // Initialize the statement 0 1
- getStmt = util::make_unique<Statement>(db->prepare("SELECT `status`, `modified`, "
- // 2 3 4 5 1
- "`etag`, `expires`, `data`, `compressed` FROM `http_cache` WHERE `url` = ?"));
- } else {
- getStmt->reset();
- }
+ if (!schema) {
+ createSchema();
+ }
- const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
- getStmt->bind(1, unifiedURL.c_str());
- if (getStmt->run()) {
- // There is data.
- auto response = util::make_unique<Response>();
- response->status = Response::Status(getStmt->get<int>(0));
- response->modified = getStmt->get<int64_t>(1);
- response->etag = getStmt->get<std::string>(2);
- response->expires = getStmt->get<int64_t>(3);
- response->data = getStmt->get<std::string>(4);
- if (getStmt->get<int>(5)) { // == compressed
- response->data = util::decompress(response->data);
+ if (!getStmt) {
+ // Initialize the statement 0 1
+ getStmt = util::make_unique<Statement>(db->prepare("SELECT `status`, `modified`, "
+ // 2 3 4 5 1
+ "`etag`, `expires`, `data`, `compressed` FROM `http_cache` WHERE `url` = ?"));
+ } else {
+ getStmt->reset();
}
- action.callback(std::move(response));
- } else {
- // There is no data.
+
+ const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
+ getStmt->bind(1, unifiedURL.c_str());
+ if (getStmt->run()) {
+ // There is data.
+ auto response = util::make_unique<Response>();
+ response->status = Response::Status(getStmt->get<int>(0));
+ response->modified = getStmt->get<int64_t>(1);
+ response->etag = getStmt->get<std::string>(2);
+ response->expires = getStmt->get<int64_t>(3);
+ response->data = getStmt->get<std::string>(4);
+ if (getStmt->get<int>(5)) { // == compressed
+ response->data = util::decompress(response->data);
+ }
+ action.callback(std::move(response));
+ } else {
+ // There is no data.
+ action.callback(nullptr);
+ }
+ } catch (mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
action.callback(nullptr);
}
}
void SQLiteCache::process(PutAction &action) {
- if (!db) {
- createDatabase();
- }
+ try {
+ if (!db) {
+ createDatabase();
+ }
- if (!putStmt) {
- putStmt = util::make_unique<Statement>(db->prepare("REPLACE INTO `http_cache` ("
- // 1 2 3 4 5 6 7 8
- "`url`, `status`, `kind`, `modified`, `etag`, `expires`, `data`, `compressed`"
- ") VALUES(?, ?, ?, ?, ?, ?, ?, ?)"));
- } else {
- putStmt->reset();
- }
+ if (!schema) {
+ createSchema();
+ }
- const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
- putStmt->bind(1 /* url */, unifiedURL.c_str());
- putStmt->bind(2 /* status */, int(action.response->status));
- putStmt->bind(3 /* kind */, int(action.resource.kind));
- putStmt->bind(4 /* modified */, action.response->modified);
- putStmt->bind(5 /* etag */, action.response->etag.c_str());
- putStmt->bind(6 /* expires */, action.response->expires);
-
- std::string data;
- if (action.resource.kind != Resource::Image) {
- // Do not compress images, since they are typically compressed already.
- data = util::compress(action.response->data);
- }
+ if (!putStmt) {
+ putStmt = util::make_unique<Statement>(db->prepare("REPLACE INTO `http_cache` ("
+ // 1 2 3 4 5 6 7 8
+ "`url`, `status`, `kind`, `modified`, `etag`, `expires`, `data`, `compressed`"
+ ") VALUES(?, ?, ?, ?, ?, ?, ?, ?)"));
+ } else {
+ putStmt->reset();
+ }
- if (!data.empty() && data.size() < action.response->data.size()) {
- // Store the compressed data when it is smaller than the original
- // uncompressed data.
- putStmt->bind(7 /* data */, data, false); // do not retain the string internally.
- putStmt->bind(8 /* compressed */, true);
- } else {
- putStmt->bind(7 /* data */, action.response->data, false); // do not retain the string internally.
- putStmt->bind(8 /* compressed */, false);
- }
+ const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
+ putStmt->bind(1 /* url */, unifiedURL.c_str());
+ putStmt->bind(2 /* status */, int(action.response->status));
+ putStmt->bind(3 /* kind */, int(action.resource.kind));
+ putStmt->bind(4 /* modified */, action.response->modified);
+ putStmt->bind(5 /* etag */, action.response->etag.c_str());
+ putStmt->bind(6 /* expires */, action.response->expires);
+
+ std::string data;
+ if (action.resource.kind != Resource::Image) {
+ // Do not compress images, since they are typically compressed already.
+ data = util::compress(action.response->data);
+ }
+
+ if (!data.empty() && data.size() < action.response->data.size()) {
+ // Store the compressed data when it is smaller than the original
+ // uncompressed data.
+ putStmt->bind(7 /* data */, data, false); // do not retain the string internally.
+ putStmt->bind(8 /* compressed */, true);
+ } else {
+ putStmt->bind(7 /* data */, action.response->data, false); // do not retain the string internally.
+ putStmt->bind(8 /* compressed */, false);
+ }
- putStmt->run();
+ putStmt->run();
+ } catch (mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
}
void SQLiteCache::process(RefreshAction &action) {
- if (!db) {
- createDatabase();
- }
+ try {
+ if (!db) {
+ createDatabase();
+ }
- if (!refreshStmt) {
- refreshStmt = util::make_unique<Statement>( // 1 2
- db->prepare("UPDATE `http_cache` SET `expires` = ? WHERE `url` = ?"));
- } else {
- refreshStmt->reset();
- }
+ if (!schema) {
+ createSchema();
+ }
- const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
- refreshStmt->bind(1, int64_t(action.expires));
- refreshStmt->bind(2, unifiedURL.c_str());
- refreshStmt->run();
+ if (!refreshStmt) {
+ refreshStmt = util::make_unique<Statement>( // 1 2
+ db->prepare("UPDATE `http_cache` SET `expires` = ? WHERE `url` = ?"));
+ } else {
+ refreshStmt->reset();
+ }
+
+ const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
+ refreshStmt->bind(1, int64_t(action.expires));
+ refreshStmt->bind(2, unifiedURL.c_str());
+ refreshStmt->run();
+ } catch (mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, ex.code, ex.what());
+ }
}
void SQLiteCache::process(StopAction &) {
diff --git a/src/mbgl/platform/log.cpp b/src/mbgl/platform/log.cpp
index d6cbc4fd11..2a761c6260 100644
--- a/src/mbgl/platform/log.cpp
+++ b/src/mbgl/platform/log.cpp
@@ -17,6 +17,12 @@ void Log::setObserver(std::unique_ptr<Observer> observer) {
currentObserver = std::move(observer);
}
+std::unique_ptr<Log::Observer> Log::removeObserver() {
+ std::unique_ptr<Observer> observer;
+ std::swap(observer, currentObserver);
+ return observer;
+}
+
void Log::record(EventSeverity severity, Event event, const std::string &msg) {
record(severity, event, -1, msg);
}
diff --git a/src/mbgl/util/io.cpp b/src/mbgl/util/io.cpp
index 76f7c35ade..bb4c3595c3 100644
--- a/src/mbgl/util/io.cpp
+++ b/src/mbgl/util/io.cpp
@@ -4,7 +4,8 @@
#include <iostream>
#include <sstream>
#include <fstream>
-#include <stdexcept>
+
+#include <unistd.h>
namespace mbgl {
namespace util {
@@ -30,5 +31,12 @@ std::string read_file(const std::string &filename) {
}
}
+void deleteFile(const std::string& filename) {
+ const int ret = unlink(filename.c_str());
+ if (ret == -1) {
+ throw IOException(errno, "failed to unlink file");
+ }
+}
+
}
}
diff --git a/src/mbgl/util/io.hpp b/src/mbgl/util/io.hpp
index e95fc16d9d..bf15253ee4 100644
--- a/src/mbgl/util/io.hpp
+++ b/src/mbgl/util/io.hpp
@@ -2,13 +2,22 @@
#define MBGL_UTIL_IO
#include <string>
+#include <stdexcept>
namespace mbgl {
namespace util {
+struct IOException : std::runtime_error {
+ inline IOException(int err, const char* msg) : std::runtime_error(msg), code(err) {
+ }
+ const int code = 0;
+};
+
void write_file(const std::string &filename, const std::string &data);
std::string read_file(const std::string &filename);
+void deleteFile(const std::string& filename);
+
}
}
diff --git a/test/fixtures/fixture_log_observer.cpp b/test/fixtures/fixture_log_observer.cpp
index f66d185439..b3b8e56fdb 100644
--- a/test/fixtures/fixture_log_observer.cpp
+++ b/test/fixtures/fixture_log_observer.cpp
@@ -8,6 +8,9 @@ FixtureLogObserver::LogMessage::LogMessage(EventSeverity severity_, Event event_
: severity(severity_), event(event_), code(code_), msg(msg_) {
}
+FixtureLogObserver::LogMessage::LogMessage() : severity(), event(), code(), msg() {
+}
+
bool FixtureLogObserver::LogMessage::operator==(const LogMessage &rhs) const {
return (!severity || !rhs.severity || severity.get() == rhs.severity.get()) &&
(!event || !rhs.event || event.get() == rhs.event.get()) &&
diff --git a/test/fixtures/fixture_log_observer.hpp b/test/fixtures/fixture_log_observer.hpp
index bdb7ea2ca3..7e419a617f 100644
--- a/test/fixtures/fixture_log_observer.hpp
+++ b/test/fixtures/fixture_log_observer.hpp
@@ -2,7 +2,6 @@
#define MBGL_TEST_FIXTURE_LOG_OBSERVER
#include <mbgl/platform/log.hpp>
-#include <mbgl/util/noncopyable.hpp>
#include <mbgl/util/optional.hpp>
#include <vector>
@@ -11,10 +10,11 @@
namespace mbgl {
-class FixtureLogObserver : public Log::Observer, private util::noncopyable {
+class FixtureLogObserver : public Log::Observer {
public:
struct LogMessage {
LogMessage(EventSeverity severity_, Event event_, int64_t code_, const std::string &msg_);
+ LogMessage();
bool operator==(const LogMessage &rhs) const;
diff --git a/test/storage/database.cpp b/test/storage/database.cpp
new file mode 100644
index 0000000000..1a2b618a57
--- /dev/null
+++ b/test/storage/database.cpp
@@ -0,0 +1,391 @@
+#include "storage.hpp"
+#include "../fixtures/fixture_log_observer.hpp"
+
+#include <future>
+
+#include <mbgl/storage/default/sqlite_cache.hpp>
+#include <mbgl/storage/resource.hpp>
+#include <mbgl/storage/response.hpp>
+#include <mbgl/util/io.hpp>
+
+#include <sqlite3.h>
+
+class ScopedTest {
+public:
+ ScopedTest(std::function<void()> destructor_) : destructor(destructor_) {}
+ ScopedTest() {}
+
+ void finish() {
+ promise.set_value();
+ }
+
+ ~ScopedTest() {
+ promise.get_future().get();
+ if (destructor) {
+ destructor();
+ }
+ }
+
+private:
+ const std::function<void()> destructor;
+ std::promise<void> promise;
+};
+
+TEST_F(Storage, DatabaseDoesNotExist) {
+ using namespace mbgl;
+
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+
+ ScopedTest test([&] {
+ auto observer = Log::removeObserver();
+ EXPECT_EQ(1ul, dynamic_cast<FixtureLogObserver*>(observer.get())->count({ EventSeverity::Error, Event::Database, 14, "unable to open database file" }));
+ });
+
+ SQLiteCache cache("test/fixtures/404/cache.db");
+
+ cache.get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ test.finish();
+ });
+}
+
+void createDir(const char* name) {
+ const int ret = mkdir(name, 0755);
+ if (ret == -1) {
+ ASSERT_EQ(EEXIST, errno);
+ } else {
+ ASSERT_EQ(0, ret);
+ }
+}
+
+void deleteFile(const char* name) {
+ const int ret = unlink(name);
+ if (ret == -1) {
+ ASSERT_EQ(ENOENT, errno);
+ } else {
+ ASSERT_EQ(0, ret);
+ }
+}
+
+
+void writeFile(const char* name, const std::string& data) {
+ mbgl::util::write_file(name, data);
+}
+
+
+TEST_F(Storage, DatabaseCreate) {
+ using namespace mbgl;
+
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/cache.db");
+
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+
+ ScopedTest test([&] {
+ Log::removeObserver();
+ });
+
+ SQLiteCache cache("test/fixtures/database/cache.db");
+
+ cache.get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ test.finish();
+ });
+}
+
+class FileLock {
+public:
+ FileLock(const std::string& path) {
+ const int err = sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr);
+ if (err != SQLITE_OK) {
+ throw std::runtime_error("Could not open db");
+ }
+ lock();
+ }
+
+ void lock() {
+ assert(!locked);
+ const int err = sqlite3_exec(db, "begin exclusive transaction", nullptr, nullptr, nullptr);
+ if (err != SQLITE_OK) {
+ throw std::runtime_error("Could not lock db");
+ }
+ locked = true;
+ }
+
+ void unlock() {
+ assert(locked);
+ const int err = sqlite3_exec(db, "commit", nullptr, nullptr, nullptr);
+ if (err != SQLITE_OK) {
+ throw std::runtime_error("Could not unlock db");
+ }
+ locked = false;
+ }
+
+ ~FileLock() {
+ if (locked) {
+ unlock();
+ }
+ }
+
+private:
+ sqlite3* db;
+ bool locked = false;
+};
+
+TEST_F(Storage, DatabaseLockedRead) {
+ using namespace mbgl;
+
+ // Create a locked file.
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/locked.db");
+ FileLock guard("test/fixtures/database/locked.db");
+
+ auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
+
+ std::promise<void> promise;
+
+ {
+ // First request should fail.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ // Make sure that we got a few "database locked" errors
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(2ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
+ }
+
+ // Then, unlock the file and try again.
+ guard.unlock();
+
+ {
+ // First, try getting a file (the cache value should not exist).
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ // Make sure that we got a no errors
+ Log::removeObserver();
+ }
+
+ // Explicitly delete the Cache now.
+ cache.reset();
+}
+
+
+
+TEST_F(Storage, DatabaseLockedWrite) {
+ using namespace mbgl;
+
+ // Create a locked file.
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/locked.db");
+ FileLock guard("test/fixtures/database/locked.db");
+
+ auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
+
+ std::promise<void> promise;
+
+ {
+ // Adds a file (which should fail).
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ auto response = std::make_shared<Response>();
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
+ }
+
+ // Then, unlock the file and try again.
+ guard.unlock();
+
+ {
+ // Then, set a file and obtain it again.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_NE(nullptr, res.get());
+ EXPECT_EQ("Demo", res->data);
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ // Make sure that we got a no errors
+ Log::removeObserver();
+ }
+
+ // Explicitly delete the Cache now.
+ cache.reset();
+}
+
+
+
+
+TEST_F(Storage, DatabaseLockedRefresh) {
+ using namespace mbgl;
+
+ // Create a locked file.
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/locked.db");
+
+ auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
+
+ // Then, lock the file and try again.
+ FileLock guard("test/fixtures/database/locked.db");
+
+ std::promise<void> promise;
+
+ {
+ // Adds a file.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
+ }
+
+ {
+ // Then, try to refresh it.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Refresh);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_EQ(nullptr, res.get());
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ // Make sure that we got the right errors.
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
+ }
+
+ // Explicitly delete the Cache now.
+ cache.reset();
+}
+
+
+
+TEST_F(Storage, DatabaseDeleted) {
+ using namespace mbgl;
+
+ // Create a locked file.
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/locked.db");
+
+ auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
+
+ std::promise<void> promise;
+
+ {
+ // Adds a file.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_NE(nullptr, res.get());
+ EXPECT_EQ("Demo", res->data);
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ Log::removeObserver();
+ }
+
+ deleteFile("test/fixtures/database/locked.db");
+
+ {
+ // Adds a file.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_NE(nullptr, res.get());
+ EXPECT_EQ("Demo", res->data);
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(1ul, flo->count({ EventSeverity::Error, Event::Database, 8, "attempt to write a readonly database" }));
+ }
+
+ // Explicitly delete the Cache now.
+ cache.reset();
+}
+
+
+
+TEST_F(Storage, DatabaseInvalid) {
+ using namespace mbgl;
+
+ // Create a locked file.
+ createDir("test/fixtures/database");
+ deleteFile("test/fixtures/database/invalid.db");
+ writeFile("test/fixtures/database/invalid.db", "this is an invalid file");
+
+ auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/invalid.db");
+
+ std::promise<void> promise;
+
+ {
+ // Adds a file.
+ Log::setObserver(util::make_unique<FixtureLogObserver>());
+ promise = {};
+ auto response = std::make_shared<Response>();
+ response->data = "Demo";
+ cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
+ cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
+ EXPECT_NE(nullptr, res.get());
+ EXPECT_EQ("Demo", res->data);
+ promise.set_value();
+ });
+ promise.get_future().get();
+
+ auto observer = Log::removeObserver();
+ auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
+ EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Trashing invalid database" }));
+ }
+
+ // Explicitly delete the Cache now.
+ cache.reset();
+}
diff --git a/test/test.gyp b/test/test.gyp
index c333a40064..d9e234fe09 100644
--- a/test/test.gyp
+++ b/test/test.gyp
@@ -54,6 +54,7 @@
'storage/storage.cpp',
'storage/cache_response.cpp',
'storage/cache_revalidate.cpp',
+ 'storage/database.cpp',
'storage/directory_reading.cpp',
'storage/file_reading.cpp',
'storage/http_cancel.cpp',