Merge pull request #1071 from mapbox/1071-sqlite-avoid-crash

Gracefully handle SQLite errors
This commit is contained in:
Konstantin Käfer
2015-04-08 16:48:58 +02:00
11 changed files with 581 additions and 105 deletions

View File

@@ -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>

View File

@@ -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;
};
}

View File

@@ -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;
}
}

View File

@@ -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 &) {
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.
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());
db.reset();
}
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();
}
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();
}
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);
try {
// This is called in the SQLite event loop.
if (!db) {
createDatabase();
}
action.callback(std::move(response));
} else {
// There is no data.
if (!schema) {
createSchema();
}
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();
}
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);
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();
}
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);
}
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);
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);
}
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);
}
putStmt->run();
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();
} 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 &) {

View File

@@ -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);
}

View File

@@ -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");
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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()) &&

View File

@@ -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;

391
test/storage/database.cpp Normal file
View File

@@ -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();
}

View File

@@ -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',