From cdedb66387065680efd318dacb61572c920b81b1 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 26 Jan 2016 12:19:04 -0800 Subject: [PATCH] [core] Reimplement existing caching within an offline-capable database schema --- .gitignore | 4 +- gyp/platform-android.gypi | 4 +- gyp/platform-ios.gypi | 4 +- gyp/platform-linux.gypi | 4 +- gyp/platform-osx.gypi | 4 +- include/mbgl/storage/default_file_source.hpp | 17 +- platform/default/default_file_source.cpp | 136 +++-- .../default/mbgl/storage/offline_database.cpp | 297 +++++++++++ .../default/mbgl/storage/offline_database.hpp | 50 ++ .../mbgl/storage/offline_schema.cpp.include | 51 ++ .../default/mbgl/storage/offline_schema.js | 24 + .../default/mbgl/storage/offline_schema.sql | 62 +++ platform/default/sqlite3.cpp | 34 +- platform/default/sqlite_cache.cpp | 502 ------------------ platform/default/sqlite_cache_impl.hpp | 68 --- src/mbgl/storage/sqlite_cache.hpp | 44 -- test/api/offline.cpp | 50 ++ .../{stale => offline}/0-0-0.vector.pbf | Bin test/fixtures/offline/expected.png | Bin 0 -> 47842 bytes test/fixtures/{stale => offline}/glyph.pbf | Bin test/fixtures/{stale => offline}/sprite.json | 0 test/fixtures/{stale => offline}/sprite.png | Bin test/fixtures/{stale => offline}/streets.json | 2 +- .../style.json} | 10 +- test/fixtures/stale/stale_style/expected.png | Bin 13447 -> 0 bytes .../stale/stale_style_and_glyphs/expected.png | Bin 36120 -> 0 bytes .../stale/stale_style_and_sprite/expected.png | Bin 26859 -> 0 bytes .../stale_style_and_tilejson/expected.png | Bin 13447 -> 0 bytes test/fixtures/stale/style.json | 25 - test/fixtures/stale/style_and_sprite.json | 26 - test/fixtures/stale/style_and_tilejson.json | 25 - test/fixtures/util.cpp | 12 +- test/fixtures/util.hpp | 4 +- test/storage/asset_file_source.cpp | 1 - test/storage/cache_response.cpp | 215 -------- test/storage/cache_shared.cpp | 34 -- test/storage/cache_size.cpp | 241 --------- test/storage/cache_stale.cpp | 131 ----- test/storage/database.cpp | 349 ------------ ...revalidate.cpp => default_file_source.cpp} | 52 +- test/storage/offline_database.cpp | 374 +++++++++++++ test/test.gypi | 9 +- 42 files changed, 1115 insertions(+), 1750 deletions(-) create mode 100644 platform/default/mbgl/storage/offline_database.cpp create mode 100644 platform/default/mbgl/storage/offline_database.hpp create mode 100644 platform/default/mbgl/storage/offline_schema.cpp.include create mode 100644 platform/default/mbgl/storage/offline_schema.js create mode 100644 platform/default/mbgl/storage/offline_schema.sql delete mode 100644 platform/default/sqlite_cache.cpp delete mode 100644 platform/default/sqlite_cache_impl.hpp delete mode 100644 src/mbgl/storage/sqlite_cache.hpp create mode 100644 test/api/offline.cpp rename test/fixtures/{stale => offline}/0-0-0.vector.pbf (100%) create mode 100644 test/fixtures/offline/expected.png rename test/fixtures/{stale => offline}/glyph.pbf (100%) rename test/fixtures/{stale => offline}/sprite.json (100%) rename test/fixtures/{stale => offline}/sprite.png (100%) rename test/fixtures/{stale => offline}/streets.json (77%) rename test/fixtures/{stale/style_and_glyphs.json => offline/style.json} (68%) delete mode 100644 test/fixtures/stale/stale_style/expected.png delete mode 100644 test/fixtures/stale/stale_style_and_glyphs/expected.png delete mode 100644 test/fixtures/stale/stale_style_and_sprite/expected.png delete mode 100644 test/fixtures/stale/stale_style_and_tilejson/expected.png delete mode 100644 test/fixtures/stale/style.json delete mode 100644 test/fixtures/stale/style_and_sprite.json delete mode 100644 test/fixtures/stale/style_and_tilejson.json delete mode 100644 test/storage/cache_response.cpp delete mode 100644 test/storage/cache_shared.cpp delete mode 100644 test/storage/cache_size.cpp delete mode 100644 test/storage/cache_stale.cpp delete mode 100644 test/storage/database.cpp rename test/storage/{cache_revalidate.cpp => default_file_source.cpp} (75%) create mode 100644 test/storage/offline_database.cpp diff --git a/.gitignore b/.gitignore index f9fd16cd2..f750e2f87 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,8 @@ /test/fixtures/api/1.png /test/fixtures/api/2.png /test/fixtures/database/*.db -/test/fixtures/*/*/actual.png -/test/fixtures/*/*/diff.png +/test/fixtures/**/actual.png +/test/fixtures/**/diff.png /test/output /include/mbgl/shader/shaders.hpp /src/shader/shaders_gl.cpp diff --git a/gyp/platform-android.gypi b/gyp/platform-android.gypi index 8bb046a58..2c1e06a71 100644 --- a/gyp/platform-android.gypi +++ b/gyp/platform-android.gypi @@ -21,7 +21,8 @@ '../platform/default/timer.cpp', '../platform/default/default_file_source.cpp', '../platform/default/online_file_source.cpp', - '../platform/default/sqlite_cache.cpp', + '../platform/default/mbgl/storage/offline_database.hpp', + '../platform/default/mbgl/storage/offline_database.cpp', '../platform/default/sqlite3.hpp', '../platform/default/sqlite3.cpp', ], @@ -56,6 +57,7 @@ 'include_dirs': [ '../include', '../src', + '../platform/default', ], 'conditions': [ diff --git a/gyp/platform-ios.gypi b/gyp/platform-ios.gypi index f6d94b0e1..806f103a1 100644 --- a/gyp/platform-ios.gypi +++ b/gyp/platform-ios.gypi @@ -16,7 +16,8 @@ '../platform/default/timer.cpp', '../platform/default/default_file_source.cpp', '../platform/default/online_file_source.cpp', - '../platform/default/sqlite_cache.cpp', + '../platform/default/mbgl/storage/offline_database.hpp', + '../platform/default/mbgl/storage/offline_database.cpp', '../platform/default/sqlite3.hpp', '../platform/default/sqlite3.cpp', '../platform/darwin/log_nslog.mm', @@ -98,6 +99,7 @@ '../include/mbgl/darwin', '../include', '../src', + '../platform/default', ], 'xcode_settings': { diff --git a/gyp/platform-linux.gypi b/gyp/platform-linux.gypi index 151382882..2eafe3e82 100644 --- a/gyp/platform-linux.gypi +++ b/gyp/platform-linux.gypi @@ -23,7 +23,8 @@ '../platform/default/timer.cpp', '../platform/default/default_file_source.cpp', '../platform/default/online_file_source.cpp', - '../platform/default/sqlite_cache.cpp', + '../platform/default/mbgl/storage/offline_database.hpp', + '../platform/default/mbgl/storage/offline_database.cpp', '../platform/default/sqlite3.hpp', '../platform/default/sqlite3.cpp', ], @@ -61,6 +62,7 @@ 'include_dirs': [ '../include', '../src', + '../platform/default', ], 'conditions': [ diff --git a/gyp/platform-osx.gypi b/gyp/platform-osx.gypi index 2aedce853..53e704749 100644 --- a/gyp/platform-osx.gypi +++ b/gyp/platform-osx.gypi @@ -15,7 +15,8 @@ '../platform/default/timer.cpp', '../platform/default/default_file_source.cpp', '../platform/default/online_file_source.cpp', - '../platform/default/sqlite_cache.cpp', + '../platform/default/mbgl/storage/offline_database.hpp', + '../platform/default/mbgl/storage/offline_database.cpp', '../platform/default/sqlite3.hpp', '../platform/default/sqlite3.cpp', '../platform/darwin/log_nslog.mm', @@ -83,6 +84,7 @@ '../include/mbgl/darwin', '../include', '../src', + '../platform/default', ], 'xcode_settings': { diff --git a/include/mbgl/storage/default_file_source.hpp b/include/mbgl/storage/default_file_source.hpp index e7f95aa23..d8fc4b98a 100644 --- a/include/mbgl/storage/default_file_source.hpp +++ b/include/mbgl/storage/default_file_source.hpp @@ -5,7 +5,9 @@ namespace mbgl { -class SQLiteCache; +namespace util { +template class Thread; +} // namespace util class DefaultFileSource : public FileSource { public: @@ -20,14 +22,15 @@ public: std::unique_ptr request(const Resource&, Callback) override; - // For testing purposes only. - SQLiteCache& getCache(); + // For testing only. + void put(const Resource&, const Response&); + void goOffline(); + + class Impl; private: - class Impl; - friend class DefaultFileRequest; - - const std::unique_ptr impl; + const std::unique_ptr> thread; + const std::unique_ptr assetFileSource; }; } // namespace mbgl diff --git a/platform/default/default_file_source.cpp b/platform/default/default_file_source.cpp index 56b47539b..efe893d49 100644 --- a/platform/default/default_file_source.cpp +++ b/platform/default/default_file_source.cpp @@ -1,10 +1,11 @@ #include #include #include -#include +#include #include #include +#include #include #include @@ -23,76 +24,117 @@ namespace mbgl { class DefaultFileSource::Impl { public: - Impl(const std::string& cachePath, const std::string& assetRoot) - : assetFileSource(assetRoot), - cache(SQLiteCache::getShared(cachePath)) { + class Task { + public: + Task(Resource resource, FileSource::Callback callback, DefaultFileSource::Impl* impl) { + auto offlineResponse = impl->offlineDatabase.get(resource); + + Resource revalidation = resource; + + if (offlineResponse) { + revalidation.priorModified = offlineResponse->modified; + revalidation.priorExpires = offlineResponse->expires; + revalidation.priorEtag = offlineResponse->etag; + callback(*offlineResponse); + } + + if (!impl->offline) { + onlineRequest = impl->onlineFileSource.request(revalidation, [=] (Response onlineResponse) { + impl->offlineDatabase.put(revalidation, onlineResponse); + callback(onlineResponse); + }); + } + } + + std::unique_ptr onlineRequest; + }; + + Impl(const std::string& cachePath) + : offlineDatabase(cachePath) { } - AssetFileSource assetFileSource; - std::shared_ptr cache; + void setAccessToken(const std::string& accessToken) { + onlineFileSource.setAccessToken(accessToken); + } + + std::string getAccessToken() const { + return onlineFileSource.getAccessToken(); + } + + void add(FileRequest* req, Resource resource, Callback callback) { + tasks[req] = std::make_unique(resource, callback, this); + } + + void cancel(FileRequest* req) { + tasks.erase(req); + } + + void put(const Resource& resource, const Response& response) { + offlineDatabase.put(resource, response); + } + + void goOffline() { + offline = true; + } + + OfflineDatabase offlineDatabase; OnlineFileSource onlineFileSource; + std::unordered_map> tasks; + bool offline = false; +}; + +class DefaultFileRequest : public FileRequest { +public: + DefaultFileRequest(Resource resource, FileSource::Callback callback, util::Thread& thread_) + : thread(thread_), + workRequest(thread.invokeWithCallback(&DefaultFileSource::Impl::add, callback, this, resource)) { + } + + ~DefaultFileRequest() { + thread.invoke(&DefaultFileSource::Impl::cancel, this); + } + + util::Thread& thread; + std::unique_ptr workRequest; }; DefaultFileSource::DefaultFileSource(const std::string& cachePath, const std::string& assetRoot) - : impl(std::make_unique(cachePath, assetRoot)) { + : thread(std::make_unique>(util::ThreadContext{"DefaultFileSource", util::ThreadType::Unknown, util::ThreadPriority::Low}, cachePath)), + assetFileSource(std::make_unique(assetRoot)) { } DefaultFileSource::~DefaultFileSource() = default; void DefaultFileSource::setAccessToken(const std::string& accessToken) { - impl->onlineFileSource.setAccessToken(accessToken); + thread->invokeSync(&Impl::setAccessToken, accessToken); } std::string DefaultFileSource::getAccessToken() const { - return impl->onlineFileSource.getAccessToken(); + return thread->invokeSync(&Impl::getAccessToken); } -void DefaultFileSource::setMaximumCacheSize(uint64_t size) { - impl->cache->setMaximumCacheSize(size); +void DefaultFileSource::setMaximumCacheSize(uint64_t) { + // TODO } -void DefaultFileSource::setMaximumCacheEntrySize(uint64_t size) { - impl->cache->setMaximumCacheEntrySize(size); +void DefaultFileSource::setMaximumCacheEntrySize(uint64_t) { + // TODO } -SQLiteCache& DefaultFileSource::getCache() { - return *impl->cache; -} - -class DefaultFileRequest : public FileRequest { -public: - DefaultFileRequest(Resource resource, FileSource::Callback callback, DefaultFileSource::Impl* impl) { - cacheRequest = impl->cache->get(resource, [=](std::shared_ptr cacheResponse) mutable { - cacheRequest.reset(); - - if (cacheResponse) { - resource.priorModified = cacheResponse->modified; - resource.priorExpires = cacheResponse->expires; - resource.priorEtag = cacheResponse->etag; - } - - onlineRequest = impl->onlineFileSource.request(resource, [=] (Response onlineResponse) { - impl->cache->put(resource, onlineResponse); - callback(onlineResponse); - }); - - // Do this last because it may result in deleting this DefaultFileRequest. - if (cacheResponse) { - callback(*cacheResponse); - } - }); - } - - std::unique_ptr cacheRequest; - std::unique_ptr onlineRequest; -}; - std::unique_ptr DefaultFileSource::request(const Resource& resource, Callback callback) { if (isAssetURL(resource.url)) { - return impl->assetFileSource.request(resource, callback); + return assetFileSource->request(resource, callback); } else { - return std::make_unique(resource, callback, impl.get()); + return std::make_unique(resource, callback, *thread); } } +void DefaultFileSource::put(const Resource& resource, const Response& response) { + thread->invokeSync(&Impl::put, resource, response); +} + +void DefaultFileSource::goOffline() { + thread->invokeSync(&Impl::goOffline); +} + } // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_database.cpp b/platform/default/mbgl/storage/offline_database.cpp new file mode 100644 index 000000000..cd0f88b7f --- /dev/null +++ b/platform/default/mbgl/storage/offline_database.cpp @@ -0,0 +1,297 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sqlite3.hpp" +#include + +namespace mbgl { + +using namespace mapbox::sqlite; + +// If you change the schema you must write a migration from the previous version. +static const uint32_t schemaVersion = 2; + +OfflineDatabase::OfflineDatabase(const std::string& path_) + : path(path_) { + ensureSchema(); +} + +OfflineDatabase::~OfflineDatabase() { + // Deleting these SQLite objects may result in exceptions, but we're in a destructor, so we + // can't throw anything. + try { + statements.clear(); + db.reset(); + } catch (mapbox::sqlite::Exception& ex) { + Log::Error(Event::Database, ex.code, ex.what()); + } +} + +void OfflineDatabase::ensureSchema() { + if (path != ":memory:") { + try { + db = std::make_unique(path.c_str(), ReadWrite); + + { + Statement userVersionStmt(db->prepare("PRAGMA user_version")); + userVersionStmt.run(); + switch (userVersionStmt.get(0)) { + case 0: break; // cache-only database; ok to delete + case 1: break; // cache-only database; ok to delete + case 2: return; + default: throw std::runtime_error("unknown schema version"); + } + } + + removeExisting(); + db = std::make_unique(path.c_str(), ReadWrite | Create); + } catch (mapbox::sqlite::Exception& ex) { + if (ex.code == SQLITE_CANTOPEN) { + db = std::make_unique(path.c_str(), ReadWrite | Create); + } else if (ex.code == SQLITE_NOTADB) { + removeExisting(); + db = std::make_unique(path.c_str(), ReadWrite | Create); + } + } + } + + #include "offline_schema.cpp.include" + + db = std::make_unique(path.c_str(), ReadWrite | Create); + db->exec(schema); + db->exec("PRAGMA user_version = " + util::toString(schemaVersion)); +} + +void OfflineDatabase::removeExisting() { + Log::Warning(Event::Database, "Removing existing incompatible offline database"); + + db.reset(); + + try { + util::deleteFile(path); + } catch (util::IOException& ex) { + Log::Error(Event::Database, ex.code, ex.what()); + } +} + +mapbox::sqlite::Statement& OfflineDatabase::getStatement(const char * sql) { + auto it = statements.find(sql); + + if (it != statements.end()) { + it->second->reset(); + return *it->second; + } + + return *(statements[sql] = std::make_unique(db->prepare(sql))); +} + +optional OfflineDatabase::get(const Resource& resource) { + if (resource.kind == Resource::Kind::Tile) { + assert(resource.tileData); + return getTile(*resource.tileData); + } else { + return getResource(resource); + } +} + +void OfflineDatabase::put(const Resource& resource, const Response& response) { + // Except for 404s, don't store errors in the cache. + if (response.error && response.error->reason != Response::Error::Reason::NotFound) { + return; + } + + if (resource.kind == Resource::Kind::Tile) { + assert(resource.tileData); + putTile(*resource.tileData, response); + } else { + putResource(resource, response); + } +} + +optional OfflineDatabase::getResource(const Resource& resource) { + mapbox::sqlite::Statement& stmt = getStatement( + // 0 1 2 3 4 + "SELECT etag, expires, modified, data, compressed " + "FROM resources " + "WHERE url = ?"); + + stmt.bind(1, resource.url); + + if (!stmt.run()) { + return {}; + } + + Response response; + + response.etag = stmt.get>(0); + response.expires = stmt.get>(1); + response.modified = stmt.get>(2); + + optional data = stmt.get>(3); + if (!data) { + response.error = std::make_unique(Response::Error::Reason::NotFound); + } else if (stmt.get(4)) { + response.data = std::make_shared(util::decompress(*data)); + } else { + response.data = std::make_shared(*data); + } + + return response; +} + +void OfflineDatabase::putResource(const Resource& resource, const Response& response) { + if (response.notModified) { + mapbox::sqlite::Statement& stmt = getStatement( + // 1 2 3 + "UPDATE resources SET accessed = ?, expires = ? WHERE url = ?"); + + stmt.bind(1, SystemClock::now()); + stmt.bind(2, response.expires); + stmt.bind(3, resource.url); + stmt.run(); + } else { + mapbox::sqlite::Statement& stmt = getStatement( + // 1 2 3 4 5 6 7 8 + "REPLACE INTO resources (url, kind, etag, expires, modified, accessed, data, compressed) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + + stmt.bind(1 /* url */, resource.url); + stmt.bind(2 /* kind */, int(resource.kind)); + stmt.bind(3 /* etag */, response.etag); + stmt.bind(4 /* expires */, response.expires); + stmt.bind(5 /* modified */, response.modified); + stmt.bind(6 /* accessed */, SystemClock::now()); + + std::string data; + + if (response.error) { // Can only be NotFound + stmt.bind(7 /* data */, nullptr); + stmt.bind(8 /* compressed */, false); + } else { + data = util::compress(*response.data); + if (data.size() < response.data->size()) { + stmt.bind(7 /* data */, data, false); // do not retain the string internally. + stmt.bind(8 /* compressed */, true); + } else { + stmt.bind(7 /* data */, *response.data, false); // do not retain the string internally. + stmt.bind(8 /* compressed */, false); + } + } + + stmt.run(); + } +} + +optional OfflineDatabase::getTile(const Resource::TileData& tile) { + mapbox::sqlite::Statement& stmt = getStatement( + // 0 1 2 3 4 + "SELECT etag, expires, modified, data, compressed " + "FROM tilesets, tiles " + "WHERE tilesets.url_template = ? " // 1 + "AND tilesets.pixel_ratio = ? " // 2 + "AND tiles.x = ? " // 3 + "AND tiles.y = ? " // 4 + "AND tiles.z = ? " // 5 + "AND tilesets.id = tiles.tileset_id "); + + stmt.bind(1, tile.urlTemplate); + stmt.bind(2, tile.pixelRatio); + stmt.bind(3, tile.x); + stmt.bind(4, tile.y); + stmt.bind(5, tile.z); + + if (!stmt.run()) { + return {}; + } + + Response response; + + response.etag = stmt.get>(0); + response.expires = stmt.get>(1); + response.modified = stmt.get>(2); + + optional data = stmt.get>(3); + if (!data) { + response.error = std::make_unique(Response::Error::Reason::NotFound); + } else if (stmt.get(4)) { + response.data = std::make_shared(util::decompress(*data)); + } else { + response.data = std::make_shared(*data); + } + + return response; +} + +void OfflineDatabase::putTile(const Resource::TileData& tile, const Response& response) { + if (response.notModified) { + mapbox::sqlite::Statement& stmt = getStatement( + "UPDATE tiles SET accessed = ?1, expires = ?2 " + "WHERE tileset_id = ( " + " SELECT id FROM tilesets " + " WHERE url_template = ?3 " + " AND pixel_ratio = ?4) " + "AND tiles.x = ?5 " + "AND tiles.y = ?6 " + "AND tiles.z = ?7 "); + + stmt.bind(1, SystemClock::now()); + stmt.bind(2, response.expires); + stmt.bind(3, tile.urlTemplate); + stmt.bind(4, tile.pixelRatio); + stmt.bind(5, tile.x); + stmt.bind(6, tile.y); + stmt.bind(7, tile.z); + stmt.run(); + } else { + mapbox::sqlite::Statement& stmt1 = getStatement( + "REPLACE INTO tilesets (url_template, pixel_ratio) " + "VALUES (?1, ?2) "); + + stmt1.bind(1 /* url_template */, tile.urlTemplate); + stmt1.bind(2 /* pixel_ratio */, tile.pixelRatio); + stmt1.run(); + + mapbox::sqlite::Statement& stmt2 = getStatement( + "REPLACE INTO tiles (tileset_id, x, y, z, modified, etag, expires, accessed, data, compressed) " + "SELECT tilesets.id, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11 " + "FROM tilesets " + "WHERE url_template = ?1 " + "AND pixel_ratio = ?2 "); + + stmt2.bind(1 /* url_template */, tile.urlTemplate); + stmt2.bind(2 /* pixel_ratio */, tile.pixelRatio); + stmt2.bind(3 /* x */, tile.x); + stmt2.bind(4 /* y */, tile.y); + stmt2.bind(5 /* z */, tile.z); + stmt2.bind(6 /* modified */, response.modified); + stmt2.bind(7 /* etag */, response.etag); + stmt2.bind(8 /* expires */, response.expires); + stmt2.bind(9 /* accessed */, SystemClock::now()); + + std::string data; + + if (response.error) { // Can only be NotFound + stmt2.bind(10 /* data */, nullptr); + stmt2.bind(11 /* compressed */, false); + } else { + data = util::compress(*response.data); + if (data.size() < response.data->size()) { + stmt2.bind(10 /* data */, data, false); // do not retain the string internally. + stmt2.bind(11 /* compressed */, true); + } else { + stmt2.bind(10 /* data */, *response.data, false); // do not retain the string internally. + stmt2.bind(11 /* compressed */, false); + } + } + + stmt2.run(); + } +} + +} // namespace mbgl diff --git a/platform/default/mbgl/storage/offline_database.hpp b/platform/default/mbgl/storage/offline_database.hpp new file mode 100644 index 000000000..bc6f784d5 --- /dev/null +++ b/platform/default/mbgl/storage/offline_database.hpp @@ -0,0 +1,50 @@ +#ifndef MBGL_OFFLINE_DATABASE +#define MBGL_OFFLINE_DATABASE + +#include +#include +#include + +#include +#include +#include + +namespace mapbox { +namespace sqlite { +class Database; +class Statement; +} +} + +namespace mbgl { + +class Response; +class TileID; + +class OfflineDatabase : private util::noncopyable { +public: + OfflineDatabase(const std::string& path); + ~OfflineDatabase(); + + optional get(const Resource&); + void put(const Resource&, const Response&); + +private: + void ensureSchema(); + void removeExisting(); + mapbox::sqlite::Statement& getStatement(const char *); + + optional getTile(const Resource::TileData&); + void putTile(const Resource::TileData&, const Response&); + + optional getResource(const Resource&); + void putResource(const Resource&, const Response&); + + const std::string path; + std::unique_ptr<::mapbox::sqlite::Database> db; + std::unordered_map> statements; +}; + +} // namespace mbgl + +#endif diff --git a/platform/default/mbgl/storage/offline_schema.cpp.include b/platform/default/mbgl/storage/offline_schema.cpp.include new file mode 100644 index 000000000..31b11d5f1 --- /dev/null +++ b/platform/default/mbgl/storage/offline_schema.cpp.include @@ -0,0 +1,51 @@ +/* THIS IS A GENERATED FILE; EDIT offline_schema.sql INSTEAD */ +static const char * schema = +"CREATE TABLE resources (\n" +" url TEXT NOT NULL PRIMARY KEY,\n" +" kind INTEGER NOT NULL,\n" +" expires INTEGER,\n" +" modified INTEGER,\n" +" accessed INTEGER,\n" +" etag TEXT,\n" +" data BLOB,\n" +" compressed INTEGER NOT NULL DEFAULT 0\n" +");\n" +"CREATE TABLE tilesets (\n" +" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n" +" url_template TEXT NOT NULL,\n" +" pixel_ratio INTEGER,\n" +" UNIQUE (url_template, pixel_ratio)\n" +");\n" +"CREATE TABLE tiles (\n" +" tileset_id INTEGER NOT NULL REFERENCES tilesets(id),\n" +" z INTEGER NOT NULL,\n" +" x INTEGER NOT NULL,\n" +" y INTEGER NOT NULL,\n" +" expires INTEGER,\n" +" modified INTEGER,\n" +" accessed INTEGER,\n" +" etag TEXT,\n" +" data BLOB,\n" +" compressed INTEGER NOT NULL DEFAULT 0,\n" +" PRIMARY KEY (tileset_id, z, x, y)\n" +");\n" +"CREATE TABLE regions (\n" +" id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n" +" definition TEXT NOT NULL,\n" +" description BLOB\n" +");\n" +"CREATE TABLE region_resources (\n" +" region_id INTEGER NOT NULL REFERENCES regions(id),\n" +" resource_url TEXT NOT NULL REFERENCES resources(url),\n" +" PRIMARY KEY (region_id, resource_url)\n" +");\n" +"CREATE TABLE region_tiles (\n" +" region_id INTEGER NOT NULL REFERENCES regions(id),\n" +" tileset_id INTEGER NOT NULL REFERENCES tilesets(id),\n" +" z INTEGER NOT NULL,\n" +" x INTEGER NOT NULL,\n" +" y INTEGER NOT NULL,\n" +" PRIMARY KEY (region_id, tileset_id, z, x, y),\n" +" FOREIGN KEY (tileset_id, z, x, y) REFERENCES tiles (tileset_id, z, x, y)\n" +");\n" +; diff --git a/platform/default/mbgl/storage/offline_schema.js b/platform/default/mbgl/storage/offline_schema.js new file mode 100644 index 000000000..153ba34e3 --- /dev/null +++ b/platform/default/mbgl/storage/offline_schema.js @@ -0,0 +1,24 @@ +// To regenerate: +// (cd platform/default/mbgl/storage && node offline_schema.js) + +var fs = require('fs'); +var readline = require('readline'); + +var lineReader = readline.createInterface({ + input: fs.createReadStream('offline_schema.sql') +}); + +var lines = [ + "/* THIS IS A GENERATED FILE; EDIT offline_schema.sql INSTEAD */", + "static const char * schema = ", +]; + +lineReader + .on('line', function (line) { + line = line.replace(/ *--.*/, ''); + if (line) lines.push('"' + line + '\\n"'); + }) + .on('close', function () { + lines.push(';\n'); + fs.writeFileSync('offline_schema.cpp.include', lines.join('\n')); + }); diff --git a/platform/default/mbgl/storage/offline_schema.sql b/platform/default/mbgl/storage/offline_schema.sql new file mode 100644 index 000000000..e55517c48 --- /dev/null +++ b/platform/default/mbgl/storage/offline_schema.sql @@ -0,0 +1,62 @@ +CREATE TABLE resources ( -- Generic table for style, source, sprite, and glyph resources. + url TEXT NOT NULL PRIMARY KEY, -- Same schema as http_cache table. + kind INTEGER NOT NULL, + expires INTEGER, + modified INTEGER, + accessed INTEGER, + etag TEXT, + data BLOB, -- NULL if the response was a 404 + compressed INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE tilesets ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + url_template TEXT NOT NULL, -- As it would appear in TileJSON (but no support for host sharding). + pixel_ratio INTEGER, -- If NULL, 1 is assumed for raster sources. + UNIQUE (url_template, pixel_ratio) -- Capable of caching the same tileset at multiple resolutions. +); + +CREATE TABLE tiles ( + tileset_id INTEGER NOT NULL REFERENCES tilesets(id), + z INTEGER NOT NULL, -- Fully abandon TMS coordinates in favor of ZXY. + x INTEGER NOT NULL, + y INTEGER NOT NULL, + expires INTEGER, + modified INTEGER, + accessed INTEGER, + etag TEXT, + data BLOB, -- NULL if the response was a 404 + compressed INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (tileset_id, z, x, y) +); + +CREATE TABLE regions ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + definition TEXT NOT NULL, -- JSON formatted definition of region. Regions may be of variant types: + -- e.g. bbox and zoom range, route path, flyTo parameters, etc. Note that + -- the set of tiles required for a region may span multiple sources. + description BLOB -- User provided data in user-defined format +); + +CREATE TABLE region_resources ( + region_id INTEGER NOT NULL REFERENCES regions(id), + resource_url TEXT NOT NULL REFERENCES resources(url), + PRIMARY KEY (region_id, resource_url) +); + +CREATE TABLE region_tiles ( + region_id INTEGER NOT NULL REFERENCES regions(id), + tileset_id INTEGER NOT NULL REFERENCES tilesets(id), + z INTEGER NOT NULL, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + PRIMARY KEY (region_id, tileset_id, z, x, y), + FOREIGN KEY (tileset_id, z, x, y) REFERENCES tiles (tileset_id, z, x, y) +); + +-- `region_resources` and `region_tiles` are used for deduplication and deletion logic. +-- A row in `tiles` exists IFF one or more corresponding rows exist in `region_tiles`. If +-- more than one corresponding row exists, it indicates multiple regions contain the tile, and +-- storage for the tile is being deduplicated. When a region is deleted, corresponding rows in +-- `region_tiles` must also be deleted, and then rows in `tiles` and `tilesets` without a +-- corresponding `region_tiles` row must be deleted. Similarly for `resources` / `region_resources`. diff --git a/platform/default/sqlite3.cpp b/platform/default/sqlite3.cpp index 5122e0101..09301bc4d 100644 --- a/platform/default/sqlite3.cpp +++ b/platform/default/sqlite3.cpp @@ -115,9 +115,19 @@ template <> void Statement::bind(int offset, std::nullptr_t) { check(sqlite3_bind_null(stmt, offset)); } -template <> void Statement::bind(int offset, int value) { +template <> void Statement::bind(int offset, int8_t value) { assert(stmt); - check(sqlite3_bind_int(stmt, offset, value)); + check(sqlite3_bind_int64(stmt, offset, value)); +} + +template <> void Statement::bind(int offset, int16_t value) { + assert(stmt); + check(sqlite3_bind_int64(stmt, offset, value)); +} + +template <> void Statement::bind(int offset, int32_t value) { + assert(stmt); + check(sqlite3_bind_int64(stmt, offset, value)); } template <> void Statement::bind(int offset, int64_t value) { @@ -125,6 +135,26 @@ template <> void Statement::bind(int offset, int64_t value) { check(sqlite3_bind_int64(stmt, offset, value)); } +template <> void Statement::bind(int offset, uint8_t value) { + assert(stmt); + check(sqlite3_bind_int64(stmt, offset, value)); +} + +template <> void Statement::bind(int offset, uint16_t value) { + assert(stmt); + check(sqlite3_bind_int64(stmt, offset, value)); +} + +template <> void Statement::bind(int offset, uint32_t value) { + assert(stmt); + check(sqlite3_bind_int64(stmt, offset, value)); +} + +template <> void Statement::bind(int offset, float value) { + assert(stmt); + check(sqlite3_bind_double(stmt, offset, value)); +} + template <> void Statement::bind(int offset, double value) { assert(stmt); check(sqlite3_bind_double(stmt, offset, value)); diff --git a/platform/default/sqlite_cache.cpp b/platform/default/sqlite_cache.cpp deleted file mode 100644 index b3f528ac9..000000000 --- a/platform/default/sqlite_cache.cpp +++ /dev/null @@ -1,502 +0,0 @@ -#include "sqlite_cache_impl.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "sqlite3.hpp" -#include - -#include -#include - -namespace { - -// The cache won't accept entries larger than this arbitrary size -// and will silently discard request for adding them to the cache. -// Large entries can cause the database to grow in disk size and -// never shrink again. -const uint64_t kMaximumCacheEntrySize = 5 * 1024 * 1024; // 5 MB - -// Number of records we delete when we are close to the maximum -// database size, if set. The current criteria is to prune -// the least used entries based on `accessed` time. -const int kPrunedEntriesLimit = 100; - -} // namespace - -namespace mbgl { - -using namespace mapbox::sqlite; - -SQLiteCache::SQLiteCache(const std::string& path_) - : thread(std::make_unique>(util::ThreadContext{"SQLiteCache", util::ThreadType::Unknown, util::ThreadPriority::Low}, path_)) { -} - -SQLiteCache::~SQLiteCache() = default; - -SQLiteCache::Impl::Impl(const std::string& path_) - : maximumCacheSize(0), // Unlimited - maximumCacheEntrySize(kMaximumCacheEntrySize), - path(path_) { -} - -SQLiteCache::Impl::~Impl() { - // Deleting these SQLite objects may result in exceptions, but we're in a destructor, so we - // can't throw anything. - try { - getStmt.reset(); - putStmt.reset(); - refreshStmt.reset(); - countStmt.reset(); - freeStmt.reset(); - pruneStmt.reset(); - accessedStmt.reset(); - db.reset(); - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } -} - -void SQLiteCache::Impl::createDatabase() { - db = std::make_unique(path.c_str(), ReadWrite | Create); -} - -int SQLiteCache::Impl::schemaVersion() const { - // WARNING: Bump the version when changing the cache - // scheme to force the table to be recreated. - return 1; -} - -void SQLiteCache::Impl::createSchema() { - constexpr const char *const sql = "" - "CREATE TABLE IF NOT EXISTS `http_cache` (" - " `url` TEXT PRIMARY KEY NOT NULL," - " `status` INTEGER NOT NULL," // The response status (Successful or Error). - " `kind` INTEGER NOT NULL," // The kind of file. - " `modified` INTEGER," // Timestamp when the file was last modified. - " `etag` TEXT," - " `expires` INTEGER," // Timestamp when the server says the file expires. - " `accessed` INTEGER," // Timestamp when the database record was last accessed. - " `data` BLOB," - " `compressed` INTEGER NOT NULL DEFAULT 0" // Whether the data is compressed. - ");" - "CREATE INDEX IF NOT EXISTS `http_cache_kind_idx` ON `http_cache` (`kind`);" - "CREATE INDEX IF NOT EXISTS `http_cache_accessed_idx` ON `http_cache` (`accessed`);"; - - ensureSchemaVersion(); - - try { - db->exec(sql); - db->exec("PRAGMA user_version = " + util::toString(schemaVersion())); - 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 = std::make_unique(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 columns. Drop it and try to create a new one. - db->exec("DROP TABLE IF EXISTS `http_cache`"); - db->exec(sql); - db->exec("PRAGMA user_version = " + util::toString(schemaVersion())); - } -} - -void SQLiteCache::Impl::ensureSchemaVersion() { - try { - Statement userVersionStmt(db->prepare("PRAGMA user_version")); - if (userVersionStmt.run() && userVersionStmt.get(0) == schemaVersion()) { - return; - } - } catch (mapbox::sqlite::Exception& ex) { - if (ex.code == SQLITE_NOTADB) { - return; - } - - Log::Error(Event::Database, ex.code, ex.what()); - } - - // Version mismatch, drop the table so it will - // get recreated by `createSchema()`. - try { - db->exec("DROP TABLE IF EXISTS `http_cache`"); - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } -} - -void SQLiteCache::setMaximumCacheSize(uint64_t size) { - thread->invoke(&Impl::setMaximumCacheSize, size); -} - -void SQLiteCache::Impl::setMaximumCacheSize(uint64_t size) { - maximumCacheSize = size; - - // Unlimited. - if (size == 0) { - return; - } - - uint64_t lastSoftSize = cacheSoftSize(); - - // Keep pruning until we fit in the new - // size limit. - while (lastSoftSize > maximumCacheSize) { - pruneEntries(); - - if (lastSoftSize != cacheSoftSize()) { - lastSoftSize = cacheSoftSize(); - } else { - break; - } - } - - if (cacheHardSize() > size) { - Log::Warning(mbgl::Event::Database, - "Current cache hard size is bigger than the defined " - "maximum size. Database won't get truncated."); - } -} - -void SQLiteCache::setMaximumCacheEntrySize(uint64_t size) { - thread->invoke(&Impl::setMaximumCacheEntrySize, size); -} - -void SQLiteCache::Impl::setMaximumCacheEntrySize(uint64_t size) { - maximumCacheEntrySize = size; -} - -void SQLiteCache::Impl::initializeDatabase() { - if (!db) { - createDatabase(); - } - - if (!schema) { - createSchema(); - } -} - -int SQLiteCache::Impl::cachePageSize() { - try { - if (!pageSize) { - Statement pageSizeStmt(db->prepare("PRAGMA page_size")); - if (pageSizeStmt.run()) { - pageSize = pageSizeStmt.get(0); - } - } - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } - - return pageSize; -} - -uint64_t SQLiteCache::Impl::cacheHardSize() { - try { - initializeDatabase(); - - if (!countStmt) { - countStmt = std::make_unique(db->prepare("PRAGMA page_count")); - } else { - countStmt->reset(); - } - - if (countStmt->run()) { - return cachePageSize() * countStmt->get(0); - } - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } - - return 0; -} - -uint64_t SQLiteCache::Impl::cacheSoftSize() { - if (!softSizeDirty) { - return softSize; - } - - try { - initializeDatabase(); - - if (!freeStmt) { - freeStmt = std::make_unique(db->prepare("PRAGMA freelist_count")); - } else { - freeStmt->reset(); - } - - uint64_t hardSize = cacheHardSize(); - if (!hardSize) { - return 0; - } - - if (freeStmt->run()) { - return hardSize - cachePageSize() * freeStmt->get(0); - } - - softSizeDirty = false; - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } - - return 0; -} - -bool SQLiteCache::Impl::needsPruning() { - // SQLite database never shrinks in size unless we call VACCUM. We here - // are monitoring the soft limit (i.e. number of free pages in the file) - // and as it approaches to the hard limit (i.e. the actual file size) we - // delete an arbitrary number of old cache entries. - // - // The free pages approach saves us from calling VACCUM or keeping a - // running total, which can be costly. We need a buffer because pages can - // get fragmented on the database. - if (cacheSoftSize() + maximumCacheEntrySize * 2 < maximumCacheSize) { - return false; - } else { - return true; - } -} - -void SQLiteCache::Impl::pruneEntries() { - if (!maximumCacheSize) { - return; - } - - if (!needsPruning()) { - return; - } - - try { - if (!pruneStmt) { - pruneStmt = std::make_unique(db->prepare( - "DELETE FROM `http_cache` WHERE `rowid` IN (SELECT `rowid` FROM " - // 1 - "`http_cache` ORDER BY `accessed` ASC LIMIT ?)")); - } else { - pruneStmt->reset(); - } - - pruneStmt->bind(1, kPrunedEntriesLimit); - - pruneStmt->run(); - softSizeDirty = true; - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } -} - -std::unique_ptr SQLiteCache::get(const Resource &resource, Callback callback) { - // Can be called from any thread, but most likely from the file source thread. - // Will try to load the URL from the SQLite database and call the callback when done. - // Note that the callback is probably going to invoked from another thread, so the caller - // must make sure that it can run in that thread. - return thread->invokeWithCallback(&Impl::get, callback, resource); -} - -void SQLiteCache::Impl::get(const Resource &resource, Callback callback) { - try { - initializeDatabase(); - - if (!getStmt) { - // Initialize the statement 0 1 - getStmt = std::make_unique(db->prepare("SELECT `status`, `modified`, " - // 2 3 4 5 1 - "`etag`, `expires`, `data`, `compressed` FROM `http_cache` WHERE `url` = ?")); - } else { - getStmt->reset(); - } - - getStmt->bind(1, resource.url); - if (getStmt->run()) { - // There is data. - auto response = std::make_unique(); - const auto status = getStmt->get(0); - if (status > 1) { - // Status codes > 1 indicate an error - response->error = std::make_unique(Response::Error::Reason(status)); - } - response->modified = getStmt->get>(1); - response->etag = getStmt->get>(2); - response->expires = getStmt->get>(3); - response->data = std::make_shared(getStmt->get(4)); - if (getStmt->get(5)) { // == compressed - response->data = std::make_shared(util::decompress(*response->data)); - } - callback(std::move(response)); - } else { - // There is no data. - callback(nullptr); - } - - // We do an extra query for refreshing the last time - // the record was accessed that can be costly and is only - // worth doing if we are monitoring the database size. - if (maximumCacheSize) { - if (!accessedStmt) { - accessedStmt = std::make_unique( - // 1 2 - db->prepare("UPDATE `http_cache` SET `accessed` = ? WHERE `url` = ?")); - } else { - accessedStmt->reset(); - } - - accessedStmt->bind(1, SystemClock::now()); - accessedStmt->bind(2, resource.url); - accessedStmt->run(); - } - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - callback(nullptr); - } catch (std::runtime_error& ex) { - Log::Error(Event::Database, "%s", ex.what()); - callback(nullptr); - } -} - -void SQLiteCache::put(const Resource& resource, const Response& response) { - // Except for 404s, don't store errors in the cache. - if (response.error && response.error->reason != Response::Error::Reason::NotFound) { - return; - } - - if (response.notModified) { - thread->invoke(&Impl::refresh, resource, response.expires); - } else { - thread->invoke(&Impl::put, resource, response); - } -} - -void SQLiteCache::Impl::put(const Resource& resource, const Response& response) { - try { - initializeDatabase(); - pruneEntries(); - - if (response.data) { - auto entrySize = response.data->size(); - - if (entrySize > maximumCacheEntrySize) { - Log::Warning(Event::Database, "Entry too big for caching."); - return; - } - - if (maximumCacheSize && entrySize + cacheSoftSize() > maximumCacheSize) { - Log::Warning(Event::Database, "Unable to make space for new entries."); - return; - } - } - - if (!putStmt) { - putStmt = std::make_unique(db->prepare("REPLACE INTO `http_cache` (" - // 1 2 3 4 5 6 7 8 9 - "`url`, `status`, `kind`, `modified`, `etag`, `expires`, `accessed`, `data`, `compressed`" - ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)")); - } else { - putStmt->reset(); - } - - putStmt->bind(1 /* url */, resource.url); - if (response.error) { - putStmt->bind(2 /* status */, int(response.error->reason)); - } else { - putStmt->bind(2 /* status */, 1 /* success */); - } - putStmt->bind(3 /* kind */, int(resource.kind)); - putStmt->bind(4 /* modified */, response.modified); - putStmt->bind(5 /* etag */, response.etag); - putStmt->bind(6 /* expires */, response.expires); - putStmt->bind(7 /* accessed */, SystemClock::now()); - - std::string data; - if (resource.kind != Resource::SpriteImage && response.data) { - // Do not compress images, since they are typically compressed already. - data = util::compress(*response.data); - } - - if (!data.empty() && data.size() < response.data->size()) { - // Store the compressed data when it is smaller than the original - // uncompressed data. - putStmt->bind(8 /* data */, data, false); // do not retain the string internally. - putStmt->bind(9 /* compressed */, true); - } else if (response.data) { - putStmt->bind(8 /* data */, *response.data, false); // do not retain the string internally. - putStmt->bind(9 /* compressed */, false); - } else { - putStmt->bind(8 /* data */, "", false); - putStmt->bind(9 /* compressed */, false); - } - - putStmt->run(); - softSizeDirty = true; - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } catch (std::runtime_error& ex) { - Log::Error(Event::Database, "%s", ex.what()); - } -} - -void SQLiteCache::Impl::refresh(const Resource& resource, optional expires) { - try { - initializeDatabase(); - - if (!refreshStmt) { - refreshStmt = std::make_unique( - db->prepare("UPDATE `http_cache` SET " - // 1 2 3 - "`accessed` = ?, `expires` = ? WHERE `url` = ?")); - } else { - refreshStmt->reset(); - } - - refreshStmt->bind(1, SystemClock::now()); - refreshStmt->bind(2, expires); - refreshStmt->bind(3, resource.url); - refreshStmt->run(); - } catch (mapbox::sqlite::Exception& ex) { - Log::Error(Event::Database, ex.code, ex.what()); - } -} - -namespace { - -static std::mutex sharedMutex; -static std::unordered_map> shared; - -} // namespace - -std::shared_ptr SQLiteCache::getShared(const std::string &path) { - std::lock_guard lock(sharedMutex); - - std::shared_ptr cache; - - auto it = shared.find(path); - if (it != shared.end()) { - cache = it->second.lock(); - if (!cache) { - cache = std::make_shared(path); - it->second = cache; - } - } else { - cache = std::make_shared(path); - shared.emplace(path, cache); - } - - return cache; -} - -} // namespace mbgl diff --git a/platform/default/sqlite_cache_impl.hpp b/platform/default/sqlite_cache_impl.hpp deleted file mode 100644 index e15653240..000000000 --- a/platform/default/sqlite_cache_impl.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef MBGL_STORAGE_DEFAULT_SQLITE_CACHE_IMPL -#define MBGL_STORAGE_DEFAULT_SQLITE_CACHE_IMPL - -#include -#include -#include - -namespace mapbox { -namespace sqlite { -class Database; -class Statement; -} -} - -namespace mbgl { - -class SQLiteCache::Impl { -public: - explicit Impl(const std::string &path = ":memory:"); - ~Impl(); - - void setMaximumCacheSize(uint64_t size); - void setMaximumCacheEntrySize(uint64_t size); - - void get(const Resource&, Callback); - void put(const Resource&, const Response&); - void refresh(const Resource&, optional expires); - -private: - void initializeDatabase(); - - int cachePageSize(); - - uint64_t cacheHardSize(); - uint64_t cacheSoftSize(); - - uint64_t softSize = 0; - bool softSizeDirty = true; - - bool needsPruning(); - void pruneEntries(); - - void createDatabase(); - void createSchema(); - - int schemaVersion() const; - void ensureSchemaVersion(); - - int pageSize = 0; - - uint64_t maximumCacheSize; - uint64_t maximumCacheEntrySize; - - const std::string path; - std::unique_ptr<::mapbox::sqlite::Database> db; - std::unique_ptr<::mapbox::sqlite::Statement> getStmt; - std::unique_ptr<::mapbox::sqlite::Statement> putStmt; - std::unique_ptr<::mapbox::sqlite::Statement> refreshStmt; - std::unique_ptr<::mapbox::sqlite::Statement> countStmt; - std::unique_ptr<::mapbox::sqlite::Statement> freeStmt; - std::unique_ptr<::mapbox::sqlite::Statement> pruneStmt; - std::unique_ptr<::mapbox::sqlite::Statement> accessedStmt; - bool schema = false; -}; - -} // namespace mbgl - -#endif diff --git a/src/mbgl/storage/sqlite_cache.hpp b/src/mbgl/storage/sqlite_cache.hpp deleted file mode 100644 index b5a7cbcc0..000000000 --- a/src/mbgl/storage/sqlite_cache.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef MBGL_STORAGE_DEFAULT_SQLITE_CACHE -#define MBGL_STORAGE_DEFAULT_SQLITE_CACHE - -#include -#include - -#include -#include -#include - -namespace mbgl { - -class Resource; -class Response; -class WorkRequest; - -namespace util { -template class Thread; -} // namespace util - -class SQLiteCache : private util::noncopyable { -public: - static std::shared_ptr getShared(const std::string &path = ":memory:"); - - SQLiteCache(const std::string &path = ":memory:"); - ~SQLiteCache(); - - void setMaximumCacheSize(uint64_t size); - void setMaximumCacheEntrySize(uint64_t size); - - using Callback = std::function)>; - - std::unique_ptr get(const Resource&, Callback); - void put(const Resource&, const Response&); - - class Impl; - -private: - const std::unique_ptr> thread; -}; - -} // namespace mbgl - -#endif diff --git a/test/api/offline.cpp b/test/api/offline.cpp new file mode 100644 index 000000000..f168ff71f --- /dev/null +++ b/test/api/offline.cpp @@ -0,0 +1,50 @@ +#include "../fixtures/util.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +using namespace mbgl; +using namespace std::literals::chrono_literals; +using namespace std::literals::string_literals; + +namespace { + +Response expiredItem(const std::string& path) { + Response response; + response.data = std::make_shared(util::read_file("test/fixtures/"s + path)); + response.expires = SystemClock::from_time_t(0); + return response; +} + +const std::string prefix = "http://127.0.0.1:3000"; + +} + +auto display = std::make_shared(); + +TEST(API, Offline) { + HeadlessView view(display, 1); + DefaultFileSource fileSource(":memory:", "."); + + fileSource.put(Resource::style(prefix + "/offline/style.json"), expiredItem("offline/style.json")); + fileSource.put(Resource::source(prefix + "/offline/streets.json"), expiredItem("offline/streets.json")); + fileSource.put(Resource::spriteJSON(prefix + "/offline/sprite", 1.0), expiredItem("offline/sprite.json")); + fileSource.put(Resource::spriteImage(prefix + "/offline/sprite", 1.0), expiredItem("offline/sprite.png")); + fileSource.put(Resource::tile(prefix + "/offline/{z}-{x}-{y}.vector.pbf", 1.0, 0, 0, 0), expiredItem("offline/0-0-0.vector.pbf")); + fileSource.put(Resource::glyphs(prefix + "/offline/{fontstack}/{range}.pbf", "Helvetica", {0, 255}), expiredItem("offline/glyph.pbf")); + fileSource.goOffline(); + + Map map(view, fileSource, MapMode::Still); + map.setStyleURL(prefix + "/offline/style.json"); + + test::checkImage("test/fixtures/offline"s, + test::render(map), + 0.0015, + 0.1); +} diff --git a/test/fixtures/stale/0-0-0.vector.pbf b/test/fixtures/offline/0-0-0.vector.pbf similarity index 100% rename from test/fixtures/stale/0-0-0.vector.pbf rename to test/fixtures/offline/0-0-0.vector.pbf diff --git a/test/fixtures/offline/expected.png b/test/fixtures/offline/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..1b1c2be4c6cf585b53bc2ea8603babbef5eb0983 GIT binary patch literal 47842 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 Q0=!@HGAhzFl4ha*2R)9RZU6uP diff --git a/test/fixtures/stale/stale_style_and_glyphs/expected.png b/test/fixtures/stale/stale_style_and_glyphs/expected.png deleted file mode 100644 index d8dbda1092924bb52a3d2d10490fd1dc8e085331..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36120 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 Q0=!@HGAhzFl4ha*2R)9RZU6uP diff --git a/test/fixtures/stale/style.json b/test/fixtures/stale/style.json deleted file mode 100644 index ca86503d4..000000000 --- a/test/fixtures/stale/style.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": 8, - "name": "Water", - "sources": { - "mapbox": { - "type": "vector", - "url": "asset://test/fixtures/stale/streets.json" - } - }, - "layers": [{ - "id": "background", - "type": "background", - "paint": { - "background-color": "red" - } - }, { - "id": "water", - "type": "fill", - "source": "mapbox", - "source-layer": "water", - "paint": { - "fill-color": "blue" - } - }] -} diff --git a/test/fixtures/stale/style_and_sprite.json b/test/fixtures/stale/style_and_sprite.json deleted file mode 100644 index 61e3214a5..000000000 --- a/test/fixtures/stale/style_and_sprite.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": 8, - "name": "Water", - "sources": { - "mapbox": { - "type": "vector", - "url": "asset://test/fixtures/stale/streets.json" - } - }, - "sprite": "http://127.0.0.1:3000/stale/sprite", - "layers": [{ - "id": "background", - "type": "background", - "paint": { - "background-color": "red" - } - }, { - "id": "water", - "type": "fill", - "source": "mapbox", - "source-layer": "water", - "paint": { - "fill-pattern": "noise" - } - }] -} diff --git a/test/fixtures/stale/style_and_tilejson.json b/test/fixtures/stale/style_and_tilejson.json deleted file mode 100644 index f2a4fddd0..000000000 --- a/test/fixtures/stale/style_and_tilejson.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": 8, - "name": "Water", - "sources": { - "mapbox": { - "type": "vector", - "url": "http://127.0.0.1:3000/stale/streets.json" - } - }, - "layers": [{ - "id": "background", - "type": "background", - "paint": { - "background-color": "red" - } - }, { - "id": "water", - "type": "fill", - "source": "mapbox", - "source-layer": "water", - "paint": { - "fill-color": "blue" - } - }] -} diff --git a/test/fixtures/util.cpp b/test/fixtures/util.cpp index fcb6abed4..5617b15e8 100644 --- a/test/fixtures/util.cpp +++ b/test/fixtures/util.cpp @@ -106,20 +106,12 @@ uint64_t crc64(const PremultipliedImage &image) { return crc64(reinterpret_cast(image.data.get()), image.size()); } -PremultipliedImage render(Map& map, Milliseconds timeout) { +PremultipliedImage render(Map& map) { std::promise promise; map.renderStill([&](std::exception_ptr, PremultipliedImage&& image) { promise.set_value(std::move(image)); }); - - // Limit maximum wait time. - auto future = promise.get_future(); - if (future.wait_for(timeout) != std::future_status::ready) { - // Alas, we didn't get the promised future :( - Log::Error(Event::Image, "Failed to generate image within %dms", timeout.count()); - return { 0, 0 }; - } - return future.get(); + return promise.get_future().get(); } void checkImage(const std::string& base, diff --git a/test/fixtures/util.hpp b/test/fixtures/util.hpp index b93d822a8..2e27e5bf0 100644 --- a/test/fixtures/util.hpp +++ b/test/fixtures/util.hpp @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -36,12 +35,13 @@ uint64_t crc64(const char*, size_t); uint64_t crc64(const std::string&); uint64_t crc64(const PremultipliedImage&); -PremultipliedImage render(Map&, Milliseconds timeout = Milliseconds(1000)); +PremultipliedImage render(Map&); void checkImage(const std::string& base, const PremultipliedImage& actual, double imageThreshold = 0, double pixelThreshold = 0); + } } diff --git a/test/storage/asset_file_source.cpp b/test/storage/asset_file_source.cpp index 6734cc693..a9261ee8a 100644 --- a/test/storage/asset_file_source.cpp +++ b/test/storage/asset_file_source.cpp @@ -1,7 +1,6 @@ #include "storage.hpp" #include -#include #include #include #include diff --git a/test/storage/cache_response.cpp b/test/storage/cache_response.cpp deleted file mode 100644 index adeb45727..000000000 --- a/test/storage/cache_response.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include "storage.hpp" - -#include -#include -#include -#include - -TEST_F(Storage, CacheResponse) { - SCOPED_TEST(CacheResponse); - - using namespace mbgl; - - util::RunLoop loop; - DefaultFileSource fs(":memory:", "."); - - const Resource resource { Resource::Unknown, "http://127.0.0.1:3000/cache" }; - Response response; - - std::unique_ptr req1; - std::unique_ptr req2; - - req1 = fs.request(resource, [&](Response res) { - req1.reset(); - EXPECT_EQ(nullptr, res.error); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("Response 1", *res.data); - EXPECT_TRUE(bool(res.expires)); - EXPECT_FALSE(bool(res.modified)); - EXPECT_FALSE(bool(res.etag)); - response = res; - - // Now test that we get the same values as in the previous request. If we'd go to the server - // again, we'd get different values. - req2 = fs.request(resource, [&](Response res2) { - req2.reset(); - EXPECT_EQ(response.error, res2.error); - ASSERT_TRUE(res2.data.get()); - EXPECT_EQ(*response.data, *res2.data); - EXPECT_EQ(response.expires, res2.expires); - EXPECT_EQ(response.modified, res2.modified); - EXPECT_EQ(response.etag, res2.etag); - - loop.stop(); - CacheResponse.finish(); - }); - }); - - loop.run(); -} - -// Make sure we /do/ store 404 Not Found responses into the cache -TEST_F(Storage, CacheNotFound) { - SCOPED_TEST(CacheNotFound); - - using namespace mbgl; - - util::RunLoop loop; - DefaultFileSource fs(":memory:", "."); - - const Resource resource{ Resource::Unknown, "http://127.0.0.1:3000/not-found" }; - - // Insert existing data into the cache that will be marked as stale. - Response response; - response.data = std::make_shared("existing data"); - fs.getCache().put(resource, response); - - std::unique_ptr req1; - std::unique_ptr req2; - - int counter = 0; - - // Then, request the actual URL and check that we're getting the rigged cache response first, - // then the connection error message. - req1 = fs.request(resource, [&](Response res) { - if (counter == 0) { - EXPECT_EQ(nullptr, res.error); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("existing data", *res.data); - EXPECT_FALSE(bool(res.expires)); - EXPECT_FALSE(bool(res.modified)); - EXPECT_FALSE(bool(res.etag)); - } else if (counter == 1) { - EXPECT_NE(nullptr, res.error); - EXPECT_EQ(Response::Error::Reason::NotFound, res.error->reason); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("Not Found!", *res.data); - req1.reset(); - - // Finally, check the cache to make sure we cached the 404 response. - req2 = fs.getCache().get(resource, [&](std::unique_ptr res2) { - EXPECT_NE(nullptr, res2->error); - EXPECT_EQ(Response::Error::Reason::NotFound, res2->error->reason); - ASSERT_TRUE(res2->data.get()); - EXPECT_EQ("Not Found!", *res2->data); - CacheNotFound.finish(); - loop.stop(); - }); - } else { - FAIL() << "Got too many responses"; - } - counter++; - }); - - loop.run(); -} - -// Make sure we don't store a connection error into the cache -TEST_F(Storage, DontCacheConnectionErrors) { - SCOPED_TEST(DontCacheConnectionErrors); - - using namespace mbgl; - - util::RunLoop loop; - DefaultFileSource fs(":memory:", "."); - - const Resource resource{ Resource::Unknown, "http://127.0.0.1:3001" }; - - // Insert existing data into the cache that will be marked as stale. - Response response; - response.data = std::make_shared("existing data"); - fs.getCache().put(resource, response); - - std::unique_ptr req1; - std::unique_ptr req2; - - int counter = 0; - - // Then, request the actual URL and check that we're getting the rigged cache response first, - // then the connection error message. - req1 = fs.request(resource, [&](Response res) { - if (counter == 0) { - EXPECT_EQ(nullptr, res.error); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("existing data", *res.data); - EXPECT_FALSE(bool(res.expires)); - EXPECT_FALSE(bool(res.modified)); - EXPECT_FALSE(bool(res.etag)); - } else if (counter == 1) { - EXPECT_NE(nullptr, res.error); - EXPECT_EQ(Response::Error::Reason::Connection, res.error->reason); - req1.reset(); - - // Finally, check the cache to make sure we still have our original data in there rather - // than the failed connection attempt. - req2 = fs.getCache().get(resource, [&](std::unique_ptr res2) { - EXPECT_EQ(nullptr, res2->error); - ASSERT_TRUE(res2->data.get()); - EXPECT_EQ("existing data", *res2->data); - DontCacheConnectionErrors.finish(); - loop.stop(); - }); - } else { - FAIL() << "Got too many responses"; - } - counter++; - }); - - loop.run(); -} - -// Make sure we don't store a bad server response into the cache -TEST_F(Storage, DontCacheServerErrors) { - SCOPED_TEST(DontCacheServerErrors); - - using namespace mbgl; - - util::RunLoop loop; - DefaultFileSource fs(":memory:", "."); - - const Resource resource{ Resource::Unknown, "http://127.0.0.1:3000/permanent-error" }; - - // Insert existing data into the cache that will be marked as stale. - Response response; - response.data = std::make_shared("existing data"); - fs.getCache().put(resource, response); - - std::unique_ptr req1; - std::unique_ptr req2; - - int counter = 0; - - // Then, request the actual URL and check that we're getting the rigged cache response first, - // then the server error message. - req1 = fs.request(resource, [&](Response res) { - if (counter == 0) { - EXPECT_EQ(nullptr, res.error); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("existing data", *res.data); - EXPECT_FALSE(bool(res.expires)); - EXPECT_FALSE(bool(res.modified)); - EXPECT_FALSE(bool(res.etag)); - } else if (counter == 1) { - EXPECT_NE(nullptr, res.error); - EXPECT_EQ(Response::Error::Reason::Server, res.error->reason); - ASSERT_TRUE(res.data.get()); - EXPECT_EQ("Server Error!", *res.data); - req1.reset(); - - // Finally, check the cache to make sure we still have our original data in there rather - // than the failed connection attempt. - req2 = fs.getCache().get(resource, [&](std::unique_ptr res2) { - EXPECT_EQ(nullptr, res2->error); - ASSERT_TRUE(res2->data.get()); - EXPECT_EQ("existing data", *res2->data); - DontCacheServerErrors.finish(); - loop.stop(); - }); - } else { - FAIL() << "Got too many responses"; - } - counter++; - }); - - loop.run(); -} diff --git a/test/storage/cache_shared.cpp b/test/storage/cache_shared.cpp deleted file mode 100644 index 89ef3bfb8..000000000 --- a/test/storage/cache_shared.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "storage.hpp" - -#include -#include -#include - -TEST_F(Storage, CacheShared) { - SCOPED_TEST(CacheShared) - using namespace mbgl; - - util::RunLoop loop; - - // Check that we're getting two different caches when we request different paths. - auto memory = SQLiteCache::getShared(); - auto file = SQLiteCache::getShared("test/fixtures/database/cache.db"); - EXPECT_NE(memory.get(), file.get()); - EXPECT_EQ(memory.get(), SQLiteCache::getShared().get()); - - // Store a response into the memory file cache, then delete the last reference. - const Resource resource { Resource::Kind::Unknown, "http://example.com" }; - memory->put(resource, Response()); - memory.reset(); - - // Now check that the original memory file cache has been destructed and that it doesn't contain - // the information we put into it. - memory = SQLiteCache::getShared(); - auto req = memory->get(resource, [&](std::unique_ptr res) { - EXPECT_FALSE(res.get()); - CacheShared.finish(); - loop.stop(); - }); - - loop.run(); -} diff --git a/test/storage/cache_size.cpp b/test/storage/cache_size.cpp deleted file mode 100644 index b0d59d593..000000000 --- a/test/storage/cache_size.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include "storage.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -bool tileIsCached(mbgl::SQLiteCache* cache, unsigned id) { - using namespace mbgl; - - auto url = std::string("http://tile") + mbgl::util::toString(id); - bool replied = false; - - std::unique_ptr response; - auto callback = [&] (std::unique_ptr res) { - replied = true; - response = std::move(res); - }; - - Resource resource{ Resource::Kind::Tile, url }; - auto req = cache->get(resource, callback); - - while (!replied) { - util::RunLoop::Get()->runOnce(); - } - - return response != nullptr; -} - -void insertTile(mbgl::SQLiteCache* cache, unsigned id, uint64_t size) { - using namespace mbgl; - - auto url = std::string("http://tile") + mbgl::util::toString(id); - - Response response; - response.modified = SystemClock::now(); - response.expires = SystemClock::now() + Seconds(5000); - response.etag = url; - - auto data = std::make_shared(size, 0); - - // Fill data with garbage so SQLite won't try to - // optimize allocation by reusing pages. - static std::mt19937 generator; - std::generate_n(data->begin(), size, generator); - - response.data = data; - - Resource resource{ Resource::Kind::Tile, url }; - cache->put(resource, response); -} - -void refreshTile(mbgl::SQLiteCache* cache, unsigned id) { - using namespace mbgl; - - auto url = std::string("http://tile") + mbgl::util::toString(id); - - Response response; - response.modified = SystemClock::now(); - response.expires = SystemClock::now() + Seconds(5000); - response.notModified = true; - - Resource resource{ Resource::Kind::Tile, url }; - cache->put(resource, response); -} - -uint64_t cacheSize(mbgl::SQLiteCache* cache, unsigned entryCount, uint64_t entrySize) { - uint64_t total = 0; - - for (unsigned i = 0; i < entryCount; ++i) { - if (tileIsCached(cache, i)) { - total += entrySize; - } - } - - return total; -} - -TEST_F(Storage, CacheEntrySizeLimit) { - using namespace mbgl; - - util::RunLoop loop; - SQLiteCache cache(":memory:"); - - const uint64_t entrySize = 5 * 1024 * 1024; // 5 MB - - insertTile(&cache, 0, entrySize); - EXPECT_TRUE(tileIsCached(&cache, 0)); - - insertTile(&cache, 1, entrySize + 1); - EXPECT_FALSE(tileIsCached(&cache, 1)); - - insertTile(&cache, 2, entrySize - 1); - EXPECT_TRUE(tileIsCached(&cache, 2)); - - // Setting a new size should not delete existing entries. - cache.setMaximumCacheEntrySize(entrySize / 2); - EXPECT_TRUE(tileIsCached(&cache, 2)); - - insertTile(&cache, 3, entrySize / 2 - 1); - EXPECT_TRUE(tileIsCached(&cache, 3)); - - insertTile(&cache, 4, entrySize); - EXPECT_FALSE(tileIsCached(&cache, 4)); - - cache.setMaximumCacheEntrySize(entrySize * 2); - insertTile(&cache, 5, entrySize); - EXPECT_TRUE(tileIsCached(&cache, 5)); -} - -TEST_F(Storage, CacheSizeSetNewLimit) { - using namespace mbgl; - - util::RunLoop loop; - SQLiteCache cache(":memory:"); - - const unsigned entryCount = 800; - const uint64_t entrySize = 10 * 1024; // 10 KB - - cache.setMaximumCacheEntrySize(entrySize + 1); - - // Cache size defaults to unlimited, all these - // inserts should work. - for (unsigned i = 0; i < entryCount; ++i) { - insertTile(&cache, i, entrySize); - } - - for (unsigned i = 0; i < entryCount; ++i) { - EXPECT_TRUE(tileIsCached(&cache, i)); - } - - uint64_t expectedCacheSize = entryCount * entrySize; - EXPECT_EQ(cacheSize(&cache, entryCount, entrySize), expectedCacheSize); - - // Setting a new size should remove records until the new - // size limit is satisfied. - cache.setMaximumCacheSize(expectedCacheSize / 2); - EXPECT_LT(cacheSize(&cache, entryCount, entrySize), expectedCacheSize / 2); - - // Cache size 1 should practically clean the cache and - // prevent adding any record, although it makes no sense - // to use such size limit IRL. - cache.setMaximumCacheSize(1); - EXPECT_EQ(cacheSize(&cache, entryCount, entrySize), 0); - - insertTile(&cache, 1000, entrySize); - EXPECT_FALSE(tileIsCached(&cache, 1000)); - - // Zero should be treated as unlimited. - cache.setMaximumCacheSize(0); - - for (unsigned i = 0; i < entryCount; ++i) { - insertTile(&cache, i, entrySize); - } - - EXPECT_EQ(cacheSize(&cache, entryCount, entrySize), expectedCacheSize); -} - -TEST_F(Storage, CacheSizePruneLeastAccessed) { - using namespace mbgl; - - util::RunLoop loop; - SQLiteCache cache(":memory:"); - - const unsigned entryCount = 400; - const uint64_t entrySize = 10 * 1024; // 10 KB - - cache.setMaximumCacheEntrySize(entrySize + 1); - cache.setMaximumCacheSize(entrySize * 350); - - for (unsigned i = 0; i < entryCount; ++i) { - insertTile(&cache, i, entrySize); - - if (i == entryCount / 2) { - // We need to sleep for 1s here because - // that is the time resolution for the - // `accessed` time. Then we 'ping' the - // entry, that should update the - // `accessed` time, so it won't get - // pruned when we need more space. - bool done = false; - - util::Timer timer; - timer.start(Milliseconds(1300), - Duration::zero(), - [&done] { done = true; }); - - while (!done) { - loop.runOnce(); - } - - EXPECT_TRUE(tileIsCached(&cache, 7)); - - // Refresh should also update the `accessed` - // time of a tile. - refreshTile(&cache, 9); - } - } - - EXPECT_FALSE(tileIsCached(&cache, 6)); - EXPECT_FALSE(tileIsCached(&cache, 8)); - EXPECT_FALSE(tileIsCached(&cache, 10)); - - EXPECT_TRUE(tileIsCached(&cache, 7)); - EXPECT_TRUE(tileIsCached(&cache, 9)); -} - -TEST_F(Storage, CacheSizeStress) { - using namespace mbgl; - - util::RunLoop loop; - SQLiteCache cache(":memory:"); - - const unsigned entryCount = 2000; - const uint64_t entrySize = 10 * 1024; // 10 KB - - cache.setMaximumCacheEntrySize(entrySize + 1); - cache.setMaximumCacheSize(entrySize * 300); - - for (unsigned i = 0; i < entryCount; ++i) { - insertTile(&cache, i, entrySize); - } - - // Should not be in the cache as they were - // first inserted. - EXPECT_FALSE(tileIsCached(&cache, 0)); - EXPECT_FALSE(tileIsCached(&cache, 99)); - EXPECT_FALSE(tileIsCached(&cache, 199)); - EXPECT_FALSE(tileIsCached(&cache, 299)); - EXPECT_FALSE(tileIsCached(&cache, 399)); - - EXPECT_TRUE(tileIsCached(&cache, entryCount - 1)); - - EXPECT_LT(cacheSize(&cache, entryCount, entrySize), entrySize * 300); -} diff --git a/test/storage/cache_stale.cpp b/test/storage/cache_stale.cpp deleted file mode 100644 index 9ad0d1c06..000000000 --- a/test/storage/cache_stale.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "storage.hpp" - -#include -#include -#include -#include - -#include -#include -#include - -using namespace mbgl; -using namespace std::literals::chrono_literals; -using namespace std::literals::string_literals; - -namespace { - -void checkRendering(Map& map, - const char* name, - std::chrono::milliseconds timeout, - double imageThreshold = 0.001, - double pixelThreshold = 0.1) { - test::checkImage("test/fixtures/stale/"s + name, test::render(map, timeout), imageThreshold, - pixelThreshold); -} - -Response expiredItem(const std::string& path) { - Response response; - response.data = std::make_shared(util::read_file("test/fixtures/"s + path)); - response.expires = SystemClock::from_time_t(0); - return response; -} - -const std::string prefix = "http://127.0.0.1:3000"; - -} - -auto display = std::make_shared(); - -TEST_F(Storage, CacheStaleStyle) { - HeadlessView view(display, 1); - - auto cache = SQLiteCache::getShared(":memory:"); - - // Rig the cache with an expired stylesheet. - const std::string stylePath = "stale/style.json"; - const Resource styleResource{ Resource::Kind::Style, prefix + "/" + stylePath }; - cache->put(styleResource, expiredItem(stylePath)); - - DefaultFileSource fileSource(":memory:", "."); - - Map map(view, fileSource, MapMode::Still); - map.setStyleURL(styleResource.url); - - checkRendering(map, "stale_style", 1000ms); -} - -TEST_F(Storage, CacheStaleStyleAndTileJSON) { - HeadlessView view(display, 1); - - auto cache = SQLiteCache::getShared(":memory:"); - - // Rig the cache with an expired stylesheet. - const std::string stylePath = "stale/style_and_tilejson.json"; - const Resource styleResource{ Resource::Kind::Style, prefix + "/" + stylePath }; - cache->put(styleResource, expiredItem(stylePath)); - - // Rig the cache with an expired TileJSON. - const std::string tilejsonPath = "stale/streets.json"; - const Resource tilejsonResource{ Resource::Kind::Source, prefix + "/" + tilejsonPath }; - cache->put(tilejsonResource, expiredItem(tilejsonPath)); - - DefaultFileSource fileSource(":memory:", "."); - - Map map(view, fileSource, MapMode::Still); - map.setStyleURL(styleResource.url); - - checkRendering(map, "stale_style_and_tilejson", 1000ms); -} - -TEST_F(Storage, CacheStaleStyleAndSprite) { - HeadlessView view(display, 1); - - auto cache = SQLiteCache::getShared(":memory:"); - - // Rig the cache with an expired stylesheet. - const std::string stylePath = "stale/style_and_sprite.json"; - const Resource styleResource{ Resource::Kind::Style, prefix + "/" + stylePath }; - cache->put(styleResource, expiredItem(stylePath)); - - // Rig the cache with an expired sprite JSON. - const std::string spritejsonPath = "stale/sprite.json"; - const Resource spritejsonResource{ Resource::Kind::SpriteJSON, prefix + "/" + spritejsonPath }; - cache->put(spritejsonResource, expiredItem(spritejsonPath)); - - // Rig the cache with an expired sprite JSON. - const std::string spriteimagePath = "stale/sprite.png"; - const Resource spriteimageResource{ Resource::Kind::SpriteImage, prefix + "/" + spriteimagePath }; - cache->put(spriteimageResource, expiredItem(spriteimagePath)); - - DefaultFileSource fileSource(":memory:", "."); - - Map map(view, fileSource, MapMode::Still); - map.setStyleURL(styleResource.url); - - checkRendering(map, "stale_style_and_sprite", 1000ms); -} - -TEST_F(Storage, CacheStaleStyleAndGlyphs) { - HeadlessView view(display, 1); - - auto cache = SQLiteCache::getShared(":memory:"); - - // Rig the cache with an expired stylesheet. - const std::string stylePath = "stale/style_and_glyphs.json"; - const Resource styleResource{ Resource::Kind::Style, prefix + "/" + stylePath }; - cache->put(styleResource, expiredItem(stylePath)); - - // Rig the cache with an expired glyph PBF. - const std::string glyphPath = "stale/glyph.pbf"; - const Resource glyphResource{ Resource::Kind::Glyphs, prefix + "/stale/Open%20Sans%20Regular%2c%20Arial%20Unicode%20MS%20Regular/0-255.pbf" }; - cache->put(glyphResource, expiredItem(glyphPath)); - - DefaultFileSource fileSource(":memory:", "."); - - Map map(view, fileSource, MapMode::Still); - map.setStyleURL(styleResource.url); - - // TODO: this shouldn't take > 1 second - checkRendering(map, "stale_style_and_glyphs", 2000ms, 0.0015); -} diff --git a/test/storage/database.cpp b/test/storage/database.cpp deleted file mode 100644 index 19b46763b..000000000 --- a/test/storage/database.cpp +++ /dev/null @@ -1,349 +0,0 @@ -#include "storage.hpp" -#include "../fixtures/fixture_log_observer.hpp" - -#include "sqlite_cache_impl.hpp" -#include -#include -#include - -#include - -TEST_F(Storage, DatabaseDoesNotExist) { - using namespace mbgl; - - Log::setObserver(std::make_unique()); - - SQLiteCache::Impl cache("test/fixtures/404/cache.db"); - - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - auto observer = Log::removeObserver(); - EXPECT_EQ(1ul, dynamic_cast(observer.get())->count({ EventSeverity::Error, Event::Database, 14, "unable to open database file" })); -} - -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(std::make_unique()); - - SQLiteCache::Impl cache("test/fixtures/database/cache.db"); - - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - Log::removeObserver(); -} - -TEST_F(Storage, DatabaseVersion) { - using namespace mbgl; - - createDir("test/fixtures/database"); - deleteFile("test/fixtures/database/cache.db"); - std::string path("test/fixtures/database/cache.db"); - - Log::setObserver(std::make_unique()); - - { - SQLiteCache::Impl cache(path); - cache.put({ Resource::Unknown, "mapbox://test" }, Response()); - } - - sqlite3* db; - sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READWRITE, nullptr); - sqlite3_exec(db, "PRAGMA user_version = 999999", nullptr, nullptr, nullptr); - sqlite3_close_v2(db); - - // Changing the version will force the database to get recreated - // thus removing every pre-existing cache entry. - { - SQLiteCache::Impl cache(path); - - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - } - - Log::removeObserver(); -} - -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"); - - SQLiteCache::Impl cache("test/fixtures/database/locked.db"); - - { - // First request should fail. - Log::setObserver(std::make_unique()); - - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - // Make sure that we got a few "database locked" errors - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(4ul, 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(std::make_unique()); - - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - // Make sure that we got a no errors - Log::removeObserver(); - } -} - - - -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"); - - SQLiteCache::Impl cache("test/fixtures/database/locked.db"); - - { - // Adds a file (which should fail). - Log::setObserver(std::make_unique()); - - cache.put({ Resource::Unknown, "mapbox://test" }, Response()); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(8ul, 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(std::make_unique()); - - Response response; - response.data = std::make_shared("Demo"); - cache.put({ Resource::Unknown, "mapbox://test" }, response); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - ASSERT_NE(nullptr, res.get()); - ASSERT_TRUE(res->data.get()); - EXPECT_EQ("Demo", *res->data); - }); - - // Make sure that we got a no errors - Log::removeObserver(); - } -} - - - - -TEST_F(Storage, DatabaseLockedRefresh) { - using namespace mbgl; - - // Create a locked file. - createDir("test/fixtures/database"); - deleteFile("test/fixtures/database/locked.db"); - - SQLiteCache::Impl cache("test/fixtures/database/locked.db"); - - // Then, lock the file and try again. - FileLock guard("test/fixtures/database/locked.db"); - - { - // Adds a file. - Log::setObserver(std::make_unique()); - - Response response; - response.data = std::make_shared("Demo"); - cache.put({ Resource::Unknown, "mapbox://test" }, response); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(8ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" })); - } - - { - // Then, try to refresh it. - Log::setObserver(std::make_unique()); - - cache.refresh({ Resource::Unknown, "mapbox://test" }, {}); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - EXPECT_EQ(nullptr, res.get()); - }); - - // Make sure that we got the right errors. - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(8ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" })); - } -} - - - -TEST_F(Storage, DatabaseDeleted) { - using namespace mbgl; - - // Create a locked file. - createDir("test/fixtures/database"); - deleteFile("test/fixtures/database/locked.db"); - - SQLiteCache::Impl cache("test/fixtures/database/locked.db"); - - { - // Adds a file. - Log::setObserver(std::make_unique()); - - Response response; - response.data = std::make_shared("Demo"); - cache.put({ Resource::Unknown, "mapbox://test" }, response); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - ASSERT_NE(nullptr, res.get()); - ASSERT_TRUE(res->data.get()); - EXPECT_EQ("Demo", *res->data); - }); - - Log::removeObserver(); - } - - deleteFile("test/fixtures/database/locked.db"); - - { - // Adds a file. - Log::setObserver(std::make_unique()); - - Response response; - response.data = std::make_shared("Demo"); - cache.put({ Resource::Unknown, "mapbox://test" }, response); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - ASSERT_NE(nullptr, res.get()); - ASSERT_TRUE(res->data.get()); - EXPECT_EQ("Demo", *res->data); - }); - - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(1ul, flo->count({ EventSeverity::Error, Event::Database, 8, "attempt to write a readonly database" })); - } -} - - - -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"); - - SQLiteCache::Impl cache("test/fixtures/database/invalid.db"); - - { - // Adds a file. - Log::setObserver(std::make_unique()); - - Response response; - response.data = std::make_shared("Demo"); - cache.put({ Resource::Unknown, "mapbox://test" }, response); - cache.get({ Resource::Unknown, "mapbox://test" }, [] (std::unique_ptr res) { - ASSERT_NE(nullptr, res.get()); - ASSERT_TRUE(res->data.get()); - EXPECT_EQ("Demo", *res->data); - }); - - auto observer = Log::removeObserver(); - auto flo = dynamic_cast(observer.get()); - EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Trashing invalid database" })); - } -} diff --git a/test/storage/cache_revalidate.cpp b/test/storage/default_file_source.cpp similarity index 75% rename from test/storage/cache_revalidate.cpp rename to test/storage/default_file_source.cpp index e769e934d..26fb164d3 100644 --- a/test/storage/cache_revalidate.cpp +++ b/test/storage/default_file_source.cpp @@ -1,10 +1,54 @@ #include "storage.hpp" #include -#include #include -TEST_F(Storage, CacheRevalidateSame) { +class DefaultFileSourceTest : public Storage {}; + +TEST_F(DefaultFileSourceTest, CacheResponse) { + SCOPED_TEST(CacheResponse); + + using namespace mbgl; + + util::RunLoop loop; + DefaultFileSource fs(":memory:", "."); + + const Resource resource { Resource::Unknown, "http://127.0.0.1:3000/cache" }; + Response response; + + std::unique_ptr req1; + std::unique_ptr req2; + + req1 = fs.request(resource, [&](Response res) { + req1.reset(); + EXPECT_EQ(nullptr, res.error); + ASSERT_TRUE(res.data.get()); + EXPECT_EQ("Response 1", *res.data); + EXPECT_TRUE(bool(res.expires)); + EXPECT_FALSE(bool(res.modified)); + EXPECT_FALSE(bool(res.etag)); + response = res; + + // Now test that we get the same values as in the previous request. If we'd go to the server + // again, we'd get different values. + req2 = fs.request(resource, [&](Response res2) { + req2.reset(); + EXPECT_EQ(response.error, res2.error); + ASSERT_TRUE(res2.data.get()); + EXPECT_EQ(*response.data, *res2.data); + EXPECT_EQ(response.expires, res2.expires); + EXPECT_EQ(response.modified, res2.modified); + EXPECT_EQ(response.etag, res2.etag); + + loop.stop(); + CacheResponse.finish(); + }); + }); + + loop.run(); +} + +TEST_F(DefaultFileSourceTest, CacheRevalidateSame) { SCOPED_TEST(CacheRevalidateSame) using namespace mbgl; @@ -53,7 +97,7 @@ TEST_F(Storage, CacheRevalidateSame) { loop.run(); } -TEST_F(Storage, CacheRevalidateModified) { +TEST_F(DefaultFileSourceTest, CacheRevalidateModified) { SCOPED_TEST(CacheRevalidateModified) using namespace mbgl; @@ -102,7 +146,7 @@ TEST_F(Storage, CacheRevalidateModified) { loop.run(); } -TEST_F(Storage, CacheRevalidateEtag) { +TEST_F(DefaultFileSourceTest, CacheRevalidateEtag) { SCOPED_TEST(CacheRevalidateEtag) using namespace mbgl; diff --git a/test/storage/offline_database.cpp b/test/storage/offline_database.cpp new file mode 100644 index 000000000..e2e32ee36 --- /dev/null +++ b/test/storage/offline_database.cpp @@ -0,0 +1,374 @@ +#include "../fixtures/fixture_log_observer.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace { + +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); +} + +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(OfflineDatabase, NonexistentDirectory) { +// using namespace mbgl; +// +// Log::setObserver(std::make_unique()); +// +// OfflineDatabase db("test/fixtures/404/offline.db"); +// +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// EXPECT_FALSE(bool(res)); +// }); +// +// auto observer = Log::removeObserver(); +// EXPECT_EQ(1ul, dynamic_cast(observer.get())->count({ EventSeverity::Error, Event::Database, 14, "unable to open database file" })); +//} + +TEST(OfflineDatabase, Create) { + using namespace mbgl; + + createDir("test/fixtures/database"); + deleteFile("test/fixtures/database/offline.db"); + + Log::setObserver(std::make_unique()); + + OfflineDatabase db("test/fixtures/database/offline.db"); + EXPECT_FALSE(bool(db.get({ Resource::Unknown, "mapbox://test" }))); + + Log::removeObserver(); +} + +TEST(OfflineDatabase, SchemaVersion) { + using namespace mbgl; + + createDir("test/fixtures/database"); + deleteFile("test/fixtures/database/offline.db"); + std::string path("test/fixtures/database/offline.db"); + + { + sqlite3* db; + sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr); + sqlite3_exec(db, "PRAGMA user_version = 1", nullptr, nullptr, nullptr); + sqlite3_close_v2(db); + } + + Log::setObserver(std::make_unique()); + OfflineDatabase db(path); + + auto observer = Log::removeObserver(); + auto flo = dynamic_cast(observer.get()); + EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Removing existing incompatible offline database" })); +} + +TEST(OfflineDatabase, Invalid) { + using namespace mbgl; + + createDir("test/fixtures/database"); + deleteFile("test/fixtures/database/invalid.db"); + writeFile("test/fixtures/database/invalid.db", "this is an invalid file"); + + Log::setObserver(std::make_unique()); + + OfflineDatabase db("test/fixtures/database/invalid.db"); + + auto observer = Log::removeObserver(); + auto flo = dynamic_cast(observer.get()); + EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Removing existing incompatible offline database" })); +} + +//TEST(OfflineDatabase, 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"); +// +// OfflineDatabase db("test/fixtures/database/locked.db"); +// +// { +// // First request should fail. +// Log::setObserver(std::make_unique()); +// +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// EXPECT_FALSE(bool(res)); +// }); +// +// // Make sure that we got a few "database locked" errors +// auto observer = Log::removeObserver(); +// auto flo = dynamic_cast(observer.get()); +// EXPECT_EQ(4ul, 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(std::make_unique()); +// +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// EXPECT_FALSE(bool(res)); +// }); +// +// // Make sure that we got a no errors +// Log::removeObserver(); +// } +//} +// +//TEST(OfflineDatabase, 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"); +// +// OfflineDatabase db("test/fixtures/database/locked.db"); +// +// { +// // Adds a file (which should fail). +// Log::setObserver(std::make_unique()); +// +// db.put({ Resource::Unknown, "mapbox://test" }, Response()); +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// EXPECT_FALSE(bool(res)); +// }); +// +// auto observer = Log::removeObserver(); +// auto flo = dynamic_cast(observer.get()); +// EXPECT_EQ(8ul, 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(std::make_unique()); +// +// Response response; +// response.data = std::make_shared("Demo"); +// db.put({ Resource::Unknown, "mapbox://test" }, response); +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// ASSERT_TRUE(bool(res)); +// ASSERT_TRUE(res->data.get()); +// EXPECT_EQ("Demo", *res->data); +// }); +// +// // Make sure that we got a no errors +// Log::removeObserver(); +// } +//} +// +//TEST(OfflineDatabase, DatabaseDeleted) { +// using namespace mbgl; +// +// // Create a locked file. +// createDir("test/fixtures/database"); +// deleteFile("test/fixtures/database/locked.db"); +// +// OfflineDatabase db("test/fixtures/database/locked.db"); +// +// { +// // Adds a file. +// Log::setObserver(std::make_unique()); +// +// Response response; +// response.data = std::make_shared("Demo"); +// db.put({ Resource::Unknown, "mapbox://test" }, response); +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// ASSERT_TRUE(bool(res)); +// ASSERT_TRUE(res->data.get()); +// EXPECT_EQ("Demo", *res->data); +// }); +// +// Log::removeObserver(); +// } +// +// deleteFile("test/fixtures/database/locked.db"); +// +// { +// // Adds a file. +// Log::setObserver(std::make_unique()); +// +// Response response; +// response.data = std::make_shared("Demo"); +// db.put({ Resource::Unknown, "mapbox://test" }, response); +// db.get({ Resource::Unknown, "mapbox://test" }, [] (optional res) { +// ASSERT_TRUE(bool(res)); +// ASSERT_TRUE(res->data.get()); +// EXPECT_EQ("Demo", *res->data); +// }); +// +// auto observer = Log::removeObserver(); +// auto flo = dynamic_cast(observer.get()); +// EXPECT_EQ(1ul, flo->count({ EventSeverity::Error, Event::Database, 8, "attempt to write a readonly database" })); +// } +//} + +TEST(OfflineDatabase, PutDoesNotStoreConnectionErrors) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Unknown, "http://example.com/" }; + Response response; + response.error = std::make_unique(Response::Error::Reason::Connection); + + db.put(resource, response); + EXPECT_FALSE(bool(db.get(resource))); +} + +TEST(OfflineDatabase, PutDoesNotStoreServerErrors) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Unknown, "http://example.com/" }; + Response response; + response.error = std::make_unique(Response::Error::Reason::Server); + + db.put(resource, response); + EXPECT_FALSE(bool(db.get(resource))); +} + +TEST(OfflineDatabase, PutResource) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Style, "http://example.com/" }; + Response response; + response.data = std::make_shared("data"); + + db.put(resource, response); + auto res = db.get(resource); + EXPECT_EQ(nullptr, res->error.get()); + EXPECT_EQ("data", *res->data); +} + +TEST(OfflineDatabase, PutTile) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Tile, "http://example.com/" }; + resource.tileData = Resource::TileData { + "http://example.com/", + 1, + 0, + 0, + 0 + }; + Response response; + response.data = std::make_shared("data"); + + db.put(resource, response); + auto res = db.get(resource); + EXPECT_EQ(nullptr, res->error.get()); + EXPECT_EQ("data", *res->data); +} + +TEST(OfflineDatabase, PutResourceNotFound) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Style, "http://example.com/" }; + Response response; + response.error = std::make_unique(Response::Error::Reason::NotFound); + + db.put(resource, response); + auto res = db.get(resource); + EXPECT_NE(nullptr, res->error); + EXPECT_EQ(Response::Error::Reason::NotFound, res->error->reason); + EXPECT_FALSE(res->data.get()); +} + +TEST(OfflineDatabase, PutTileNotFound) { + using namespace mbgl; + + OfflineDatabase db(":memory:"); + + Resource resource { Resource::Tile, "http://example.com/" }; + resource.tileData = Resource::TileData { + "http://example.com/", + 1, + 0, + 0, + 0 + }; + Response response; + response.error = std::make_unique(Response::Error::Reason::NotFound); + + db.put(resource, response); + auto res = db.get(resource); + EXPECT_NE(nullptr, res->error); + EXPECT_EQ(Response::Error::Reason::NotFound, res->error->reason); + EXPECT_FALSE(res->data.get()); +} diff --git a/test/test.gypi b/test/test.gypi index 22e3d0184..883b954a3 100644 --- a/test/test.gypi +++ b/test/test.gypi @@ -58,6 +58,7 @@ 'api/render_missing.cpp', 'api/set_style.cpp', 'api/custom_layer.cpp', + 'api/offline.cpp', 'geometry/binpack.cpp', @@ -68,12 +69,8 @@ 'storage/storage.hpp', 'storage/storage.cpp', - 'storage/cache_response.cpp', - 'storage/cache_revalidate.cpp', - 'storage/cache_shared.cpp', - 'storage/cache_size.cpp', - 'storage/cache_stale.cpp', - 'storage/database.cpp', + 'storage/default_file_source.cpp', + 'storage/offline_database.cpp', 'storage/asset_file_source.cpp', 'storage/headers.cpp', 'storage/http_cancel.cpp',