Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[core] Don't allow style mutations to be overwritten by revalidation #5753

Merged
merged 4 commits into from
Aug 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cmake/test-files.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ set(MBGL_TEST_FILES
test/api/custom_layer.cpp
test/api/render_missing.cpp
test/api/repeated_render.cpp
test/api/set_style.cpp

# geometry
test/geometry/binpack.cpp
Expand Down
4 changes: 4 additions & 0 deletions include/mbgl/storage/response.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class Response {
optional<Timestamp> modified;
optional<Timestamp> expires;
optional<std::string> etag;

bool isFresh() const {
return !expires || *expires > util::now();
}
};

class Response::Error {
Expand Down
20 changes: 20 additions & 0 deletions src/mbgl/map/map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Map::Impl : public style::Observer {

std::string styleURL;
std::string styleJSON;
bool styleMutated = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this to the Style object and have it keep track of whether it was mutated itself?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do it that way, but the issue is that we want some style mutations to be excluded from this logic, specifically those that AnnotationManager::updateStyle performs. Adding annotations should not cancel the initial revalidation. (I'll add a test for this.)


std::unique_ptr<AsyncRequest> styleRequest;

Expand Down Expand Up @@ -293,10 +294,21 @@ void Map::setStyleURL(const std::string& url) {
impl->styleRequest = nullptr;
impl->styleURL = url;
impl->styleJSON.clear();
impl->styleMutated = false;

impl->style = std::make_unique<Style>(impl->fileSource, impl->pixelRatio);

impl->styleRequest = impl->fileSource.request(Resource::style(impl->styleURL), [this](Response res) {
// Once we get a fresh style, or the style is mutated, stop revalidating.
if (res.isFresh() || impl->styleMutated) {
impl->styleRequest.reset();
}

// Don't allow a loaded, mutated style to be overwritten with a new version.
if (impl->styleMutated && impl->style->loaded) {
return;
}

if (res.error) {
if (res.error->reason == Response::Error::Reason::NotFound &&
util::mapbox::isMapboxURL(impl->styleURL)) {
Expand All @@ -323,6 +335,8 @@ void Map::setStyleJSON(const std::string& json) {

impl->styleURL.clear();
impl->styleJSON.clear();
impl->styleMutated = false;

impl->style = std::make_unique<Style>(impl->fileSource, impl->pixelRatio);

impl->loadStyleJSON(json);
Expand Down Expand Up @@ -746,22 +760,27 @@ AnnotationIDs Map::queryPointAnnotations(const ScreenBox& box) {
#pragma mark - Style API

style::Source* Map::getSource(const std::string& sourceID) {
impl->styleMutated = true;
return impl->style ? impl->style->getSource(sourceID) : nullptr;
}

void Map::addSource(std::unique_ptr<style::Source> source) {
impl->styleMutated = true;
impl->style->addSource(std::move(source));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that if checks for if the style is !null on the getters but not on the setters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #6121 as a followup issue for handling attempts to mutate an unloaded style.

}

void Map::removeSource(const std::string& sourceID) {
impl->styleMutated = true;
impl->style->removeSource(sourceID);
}

style::Layer* Map::getLayer(const std::string& layerID) {
impl->styleMutated = true;
return impl->style ? impl->style->getLayer(layerID) : nullptr;
}

void Map::addLayer(std::unique_ptr<Layer> layer, const optional<std::string>& before) {
impl->styleMutated = true;
impl->view.activate();

impl->style->addLayer(std::move(layer), before);
Expand All @@ -772,6 +791,7 @@ void Map::addLayer(std::unique_ptr<Layer> layer, const optional<std::string>& be
}

void Map::removeLayer(const std::string& id) {
impl->styleMutated = true;
impl->view.activate();

impl->style->removeLayer(id);
Expand Down
33 changes: 0 additions & 33 deletions test/api/set_style.cpp

This file was deleted.

109 changes: 109 additions & 0 deletions test/map/map.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <mbgl/test/util.hpp>
#include <mbgl/test/stub_file_source.hpp>
#include <mbgl/test/fake_file_source.hpp>
#include <mbgl/test/fixture_log_observer.hpp>

#include <mbgl/map/map.hpp>
#include <mbgl/platform/default/headless_view.hpp>
Expand Down Expand Up @@ -53,6 +55,24 @@ TEST(Map, Offline) {
NetworkStatus::Set(NetworkStatus::Status::Online);
}

TEST(Map, SetStyleInvalidJSON) {
MapTest test;

Log::setObserver(std::make_unique<FixtureLogObserver>());

{
Map map(test.view, test.fileSource, MapMode::Still);
map.setStyleJSON("invalid");
}

auto observer = Log::removeObserver();
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
EXPECT_EQ(1u, flo->count({ EventSeverity::Error, Event::ParseStyle, -1,
"Error parsing style JSON at 0: Invalid value." }));
auto unchecked = flo->unchecked();
EXPECT_TRUE(unchecked.empty()) << unchecked;
}

TEST(Map, DoubleStyleLoad) {
MapTest test;

Expand All @@ -61,6 +81,95 @@ TEST(Map, DoubleStyleLoad) {
map.setStyleJSON("");
}

TEST(Map, StyleFresh) {
// The map should not revalidate fresh styles.

MapTest test;
FakeFileSource fileSource;

Map map(test.view, fileSource, MapMode::Still);
map.setStyleURL("mapbox://styles/test");
EXPECT_EQ(1u, fileSource.requests.size());

Response response;
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/api/empty.json"));
response.expires = Timestamp::max();

fileSource.respond(Resource::Style, response);
EXPECT_EQ(0u, fileSource.requests.size());
}

TEST(Map, StyleExpired) {
// The map should allow expired styles to be revalidated, so long as no mutations are made.

using namespace std::chrono_literals;

MapTest test;
FakeFileSource fileSource;

Map map(test.view, fileSource, MapMode::Still);
map.setStyleURL("mapbox://styles/test");
EXPECT_EQ(1u, fileSource.requests.size());

Response response;
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/api/empty.json"));
response.expires = util::now() - 1h;

fileSource.respond(Resource::Style, response);
EXPECT_EQ(1u, fileSource.requests.size());

map.addLayer(std::make_unique<style::BackgroundLayer>("bg"));
EXPECT_EQ(1u, fileSource.requests.size());

fileSource.respond(Resource::Style, response);
EXPECT_EQ(0u, fileSource.requests.size());
EXPECT_NE(nullptr, map.getLayer("bg"));
}

TEST(Map, StyleExpiredWithAnnotations) {
// Adding an annotation should not prevent revalidation of an expired style.

using namespace std::chrono_literals;

MapTest test;
FakeFileSource fileSource;

Map map(test.view, fileSource, MapMode::Still);
map.setStyleURL("mapbox://styles/test");
EXPECT_EQ(1u, fileSource.requests.size());

Response response;
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/api/empty.json"));
response.expires = util::now() - 1h;

fileSource.respond(Resource::Style, response);
EXPECT_EQ(1u, fileSource.requests.size());

map.addAnnotation(LineAnnotation { LineString<double> {{ { 0, 0 }, { 10, 10 } }} });
EXPECT_EQ(1u, fileSource.requests.size());

fileSource.respond(Resource::Style, response);
EXPECT_EQ(1u, fileSource.requests.size());
}

TEST(Map, StyleEarlyMutation) {
// An early mutation should not prevent the initial style load.

MapTest test;
FakeFileSource fileSource;

Map map(test.view, fileSource, MapMode::Still);
map.setStyleURL("mapbox://styles/test");
map.addLayer(std::make_unique<style::BackgroundLayer>("bg"));

Response response;
response.data = std::make_shared<std::string>(util::read_file("test/fixtures/api/water.json"));
fileSource.respond(Resource::Style, response);

EXPECT_EQ(0u, fileSource.requests.size());
EXPECT_NE(nullptr, map.getLayer("water"));
}

TEST(Map, AddLayer) {
MapTest test;

Expand Down
60 changes: 60 additions & 0 deletions test/src/mbgl/test/fake_file_source.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

#include <mbgl/storage/file_source.hpp>

#include <list>

namespace mbgl {

/*
FakeFileSource is similar to StubFileSource, but it follows a post hoc, "push" model rather than
a pre hoc, "pull" model. You pass it to code that makes requests, it records what requests are
made, then you can examine them, make assertions about them, and respond as desired.

This is particularly useful if you want to simulate multiple responses, e.g. as part of a resource
revalidation flow. StubFileSource allows only a single response.
*/
class FakeFileSource : public FileSource {
public:
class FakeFileRequest : public AsyncRequest {
public:
Resource resource;
Callback callback;

std::list<FakeFileRequest*>& list;
std::list<FakeFileRequest*>::iterator link;

FakeFileRequest(Resource resource_, Callback callback_, std::list<FakeFileRequest*>& list_)
: resource(std::move(resource_)),
callback(std::move(callback_)),
list(list_),
link((list.push_back(this), std::prev(list.end()))) {
}

~FakeFileRequest() override {
list.erase(link);
}
};

std::unique_ptr<AsyncRequest> request(const Resource& resource, Callback callback) override {
return std::make_unique<FakeFileRequest>(resource, callback, requests);
}

bool respond(Resource::Kind kind, const Response& response) {
auto it = std::find_if(requests.begin(), requests.end(), [&] (FakeFileRequest* request) {
return request->resource.kind == kind;
});

if (it != requests.end()) {
// Copy the callback, in case calling it deallocates the AsyncRequest.
Callback callback_ = (*it)->callback;
callback_(response);
}

return it != requests.end();
}

std::list<FakeFileRequest*> requests;
};

} // namespace mbgl
12 changes: 10 additions & 2 deletions test/src/mbgl/test/stub_file_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ StubFileSource::StubFileSource() {
for (auto& pair : pending_) {
optional<Response> res = std::get<1>(pair.second)(std::get<0>(pair.second));
if (res) {
std::get<2>(pair.second)(*res);

// This must be before calling the callback, because it's possible that the callback
// could:
//
// 1. Deallocate the AsyncRequest itself, thus removing it from pending
// 2. Allocate a new AsyncRequest at the same memory location
//
// If remove(pair.first) was called after both those things happened, it would
// remove the newly allocated request rather than the intended request.
if (!res->error) {
remove(pair.first);
}

std::get<2>(pair.second)(*res);
}
}
});
Expand Down