#include #include #include #include #include #include #include #include namespace { std::string toAbsoluteURL(const std::string &fileName) { auto path = std::filesystem::current_path() / "test/fixtures/storage/pmtiles" / fileName; return std::string(mbgl::util::PMTILES_PROTOCOL) + std::string(mbgl::util::FILE_PROTOCOL) + path.string(); } } // namespace using namespace mbgl; TEST(PMTilesFileSource, AcceptsURL) { PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); EXPECT_TRUE(pmtiles.canRequest(Resource::style("pmtiles:///test"))); EXPECT_FALSE(pmtiles.canRequest(Resource::style("pmtile://test"))); EXPECT_FALSE(pmtiles.canRequest(Resource::style("pmtiles:"))); EXPECT_FALSE(pmtiles.canRequest(Resource::style(""))); } // Nonexistent pmtiles file raises error TEST(PMTilesFileSource, NonExistentFile) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( {Resource::Unknown, toAbsoluteURL("does_not_exist")}, [&](Response res) { req.reset(); ASSERT_NE(nullptr, res.error); EXPECT_EQ(Response::Error::Reason::NotFound, res.error->reason); EXPECT_NE((res.error->message).find("path not found"), std::string::npos); ASSERT_FALSE(res.data.get()); loop.stop(); }); loop.run(); } // Existing pmtiles file default request returns TileJSON TEST(PMTilesFileSource, TileJSON) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( {Resource::Unknown, toAbsoluteURL("geography-class-png.pmtiles")}, [&](Response res) { req.reset(); EXPECT_EQ(nullptr, res.error); ASSERT_TRUE(res.data.get()); // basic test that TileJSON included a tile URL EXPECT_NE((*res.data).find("geography-class-png.pmtiles"), std::string::npos); loop.stop(); }); loop.run(); } // Existing tiles return tile data TEST(PMTilesFileSource, Tile) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), [&](Response res) { req.reset(); EXPECT_EQ(nullptr, res.error); ASSERT_TRUE(res.data.get()); ASSERT_EQ(res.noContent, false); loop.stop(); }); loop.run(); } // Nonexistent tiles do not raise errors, they simply return no content TEST(PMTilesFileSource, NonExistentTile) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 4, Tileset::Scheme::XYZ), [&](Response res) { req.reset(); EXPECT_EQ(nullptr, res.error); ASSERT_FALSE(res.data.get()); ASSERT_EQ(res.noContent, true); loop.stop(); }); loop.run(); } // A tile whose bytes start with gzip magic but are otherwise corrupt must yield an error // response — the decompression failure must not propagate as an exception. TEST(PMTilesFileSource, CorruptGzipTile) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( Resource::tile(toAbsoluteURL("corrupt-gzip-tile.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), [&](Response res) { req.reset(); ASSERT_NE(nullptr, res.error); EXPECT_EQ(Response::Error::Reason::Other, res.error->reason); EXPECT_NE(res.error->message.find("Error decompressing PMTiles tile:"), std::string::npos); loop.stop(); }); loop.run(); } // Metadata whose bytes start with gzip magic but are otherwise corrupt must yield an error // response — the decompression failure must not propagate as an exception. Unlike the tile path, // this one runs on the PMTiles file source thread with no handler above it, so an escaping // exception calls std::terminate and aborts the process. TEST(PMTilesFileSource, CorruptGzipMetadata) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( {Resource::Unknown, toAbsoluteURL("corrupt-gzip-metadata.pmtiles")}, [&](Response res) { req.reset(); ASSERT_NE(nullptr, res.error); EXPECT_EQ(Response::Error::Reason::Other, res.error->reason); EXPECT_NE(res.error->message.find("Error decompressing PMTiles metadata:"), std::string::npos); loop.stop(); }); loop.run(); } // Regression test for https://github.com/maplibre/maplibre-native/issues/3690 — the loading // method for PMTiles sub-requests changed from `Network` to `All` so that http(s)-hosted // archives flow through the ambient cache. The file:// path bypasses the cache by design, // so this test can only confirm that issuing the same tile twice still succeeds; a true // cache-hit assertion requires an http(s) PMTiles fixture and a mock server. TEST(PMTilesFileSource, RepeatedTileRequest) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req1; std::unique_ptr req2; req1 = pmtiles.request( Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), [&](Response res1) { req1.reset(); ASSERT_EQ(nullptr, res1.error); ASSERT_TRUE(res1.data.get()); const auto firstSize = res1.data->size(); req2 = pmtiles.request( Resource::tile(toAbsoluteURL("geography-class-png.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), [&, firstSize](Response res2) { req2.reset(); EXPECT_EQ(nullptr, res2.error); ASSERT_TRUE(res2.data.get()); EXPECT_EQ(firstSize, res2.data->size()); loop.stop(); }); }); loop.run(); } // An archive whose header flags tile_compression=GZIP but whose individual tiles are stored // without compression must be served without error (uncompressed bytes passed through as-is). TEST(PMTilesFileSource, UncompressedTile) { util::RunLoop loop; PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions()); std::unique_ptr req = pmtiles.request( Resource::tile(toAbsoluteURL("uncompressed-tiles.pmtiles"), 1.0, 0, 0, 0, Tileset::Scheme::XYZ), [&](Response res) { req.reset(); EXPECT_EQ(nullptr, res.error); ASSERT_TRUE(res.data.get()); ASSERT_EQ(res.noContent, false); loop.stop(); }); loop.run(); }