Files
MapLibreParticleLab/test/util/action_journal.test.cpp
Adrian Cojocaru 08eb760acd Add action journal (#3409)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bart Louwers <bart@emeel.net>
2025-05-27 16:15:56 +03:00

432 lines
16 KiB
C++

#include <mbgl/util/action_journal.hpp>
#include <mbgl/util/action_journal_impl.hpp>
#include <mbgl/util/run_loop.hpp>
#include <mbgl/util/io.hpp>
#include <mbgl/util/rapidjson.hpp>
#include <mbgl/util/chrono.hpp>
#include <mbgl/style/style.hpp>
#include <mbgl/style/sources/geojson_source.hpp>
#include <mbgl/gfx/headless_frontend.hpp>
#include <mbgl/gfx/shader_registry.hpp>
#include <mbgl/map/map_impl.hpp>
#include <mbgl/test/util.hpp>
#include <mbgl/test/stub_file_source.hpp>
#include <mbgl/test/stub_map_observer.hpp>
#include <gmock/gmock-matchers.h>
#include <gmock/gmock-more-matchers.h>
#include <regex>
using namespace mbgl;
using namespace mbgl::util;
using namespace ::testing;
class ActionJournalTest {
public:
class MapAdapter : public Map {
public:
MapAdapter(RendererFrontend& frontend,
MapObserver& observer,
std::shared_ptr<FileSource> fileSource,
const MapOptions& options,
const util::ActionJournalOptions& actionJournalOptions)
: Map(std::make_unique<Map::Impl>(frontend, observer, std::move(fileSource), options),
actionJournalOptions) {}
Map::Impl& getImpl() { return *impl; }
};
ActionJournalOptions options;
StubMapObserver observer;
std::shared_ptr<StubFileSource> fileSource;
std::unique_ptr<HeadlessFrontend> frontend;
std::unique_ptr<MapAdapter> map;
util::RunLoop runLoop;
ActionJournalTest(const ActionJournalOptions& options_, MapMode mode = MapMode::Static)
: options(options_) {
clear();
createMap(mode);
}
~ActionJournalTest() {
map.reset();
frontend.reset();
fileSource.reset();
clear();
}
void createMap(MapMode mode = MapMode::Static) {
fileSource = std::make_shared<StubFileSource>(ResourceOptions::Default(),
ClientOptions().withName("ActionJournalTest").withVersion("1.0"));
frontend = std::make_unique<HeadlessFrontend>(1.0f);
map = std::make_unique<MapAdapter>(
*frontend, observer, fileSource, MapOptions().withMapMode(mode).withSize(frontend->getSize()), options);
}
std::filesystem::path getDirectoryPath() {
return std::filesystem::canonical(options.path()) / ActionJournal::Impl::getDirectoryName();
}
void clear() { std::filesystem::remove_all(getDirectoryPath()); }
};
TEST(ActionJournal, Create) {
ActionJournalTest test(ActionJournalOptions().enable().withPath("."));
test.map->getActionJournal()->impl->flush();
// map creation logs `onMapCreate`
EXPECT_EQ(test.map->getActionJournal()->getLogDirectory(), test.getDirectoryPath().generic_string());
EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory()));
EXPECT_EQ(test.map->getActionJournal()->getLogFiles().size(), 1u);
const auto log = test.map->getActionJournal()->getLog();
EXPECT_EQ(log.size(), 1u);
EXPECT_THAT(log.front(), HasSubstr("onMapCreate"));
}
TEST(ActionJournal, Clear) {
ActionJournalTest test(ActionJournalOptions().enable().withPath("."));
test.map->getActionJournal()->impl->flush();
test.map->getActionJournal()->clearLog();
EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory()));
EXPECT_EQ(test.map->getActionJournal()->getLogFiles().size(), 1u);
EXPECT_THAT(test.map->getActionJournal()->getLog(), IsEmpty());
}
TEST(ActionJournal, ReadPreviousSession) {
// generate `onMapCreate` event
ActionJournalTest test(ActionJournalOptions().enable().withPath("."));
// generate `onMapDestroy` event
test.map.reset();
// generate `onMapCreate` event
test.createMap();
test.map->getActionJournal()->impl->flush();
const auto log = test.map->getActionJournal()->getLog();
EXPECT_EQ(log.size(), 3u);
EXPECT_THAT(log, ElementsAre(HasSubstr("onMapCreate"), HasSubstr("onMapDestroy"), HasSubstr("onMapCreate")));
}
TEST(ActionJournal, RollFiles) {
// limit the number of files and the size of the file
ActionJournalTest test(ActionJournalOptions().enable().withPath(".").withLogFileSize(1).withLogFileCount(2));
// generate multiple log events by loading a style and rendering
test.map->getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.runLoop.runOnce();
test.frontend->render(*test.map);
test.map->getActionJournal()->impl->flush();
// check if the files roll and respect the limits
const auto files = test.map->getActionJournal()->getLogFiles();
EXPECT_TRUE(std::filesystem::exists(test.map->getActionJournal()->getLogDirectory()));
EXPECT_EQ(files.size(), 2u);
EXPECT_EQ(test.map->getActionJournal()->getLog().size(), 2u);
// the newest event should be `onMapDestroy`
test.map.reset();
EXPECT_THAT(util::read_file(files.back()), HasSubstr("onMapDestroy"));
}
namespace {
template <typename EventMethod, typename... Args>
void validateEventList(ActionJournalTest& test,
std::vector<std::pair<const char*, std::function<void(const mbgl::JSValue&)>>>&& generatedEvents,
EventMethod&& eventGenerator,
Args&&... args) {
test.map->getActionJournal()->clearLog();
(test.map->getImpl().*eventGenerator)(std::forward<Args>(args)...);
test.map->getActionJournal()->impl->flush();
const auto& log = test.map->getActionJournal()->getLog();
EXPECT_EQ(log.size(), generatedEvents.size());
auto logIt = log.begin();
for (const auto& generatedEvent : generatedEvents) {
mbgl::JSDocument document;
document.Parse<rapidjson::kParseDefaultFlags>(*logIt++);
EXPECT_FALSE(document.HasParseError());
EXPECT_TRUE(document.IsObject());
const auto& json = document.GetObject();
EXPECT_TRUE(json.HasMember("name"));
EXPECT_TRUE(json["name"].IsString());
EXPECT_STREQ(json["name"].GetString(), generatedEvent.first);
EXPECT_TRUE(json.HasMember("time"));
EXPECT_TRUE(json["time"].IsString());
EXPECT_TRUE(json.HasMember("clientName"));
EXPECT_TRUE(json["clientName"].IsString());
EXPECT_STREQ(json["clientName"].GetString(), test.fileSource->getClientOptions().name().c_str());
EXPECT_TRUE(json.HasMember("clientVersion"));
EXPECT_TRUE(json["clientVersion"].IsString());
EXPECT_STREQ(json["clientVersion"].GetString(), test.fileSource->getClientOptions().version().c_str());
// `MatchesRegex(R"(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ)")` fails on linux gcc
// https://github.com/google/googletest/issues/3084
// `MatchesRegex(R"([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9][0-9][0-9]Z)"))`
// fails on windows CI
// using std::regex for now
std::cmatch match;
const std::regex timeRegex(R"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)");
EXPECT_TRUE(std::regex_match(json["time"].GetString(), match, timeRegex));
if (generatedEvent.second) {
EXPECT_TRUE(json.HasMember("event"));
EXPECT_TRUE(json["event"].IsObject());
generatedEvent.second(json["event"].GetObject());
}
}
}
template <typename EventMethod, typename... Args>
void validateEvent(ActionJournalTest& test,
const char* generatedEvent,
std::function<void(const mbgl::JSValue&)>&& checkOutput,
EventMethod&& eventGenerator,
Args&&... args) {
validateEventList(test, {{generatedEvent, checkOutput}}, eventGenerator, std::forward<Args>(args)...);
}
} // namespace
TEST(ActionJournal, ValidateEvents) {
ActionJournalTest test(
ActionJournalOptions().enable().withPath(".").withLogFileCount(2).withLogFileSize(1024 * 1024 * 5),
MapMode::Continuous);
test.map->getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.map->getActionJournal()->impl->flush();
const auto testShaderRegistry = std::make_unique<gfx::ShaderRegistry>();
const auto testSource = std::make_unique<style::GeoJSONSource>("testSource");
const auto testSprite = style::Sprite("testID", "testURL");
const auto testExceptionMessage = "test error";
const auto testException = std::make_exception_ptr(std::runtime_error(testExceptionMessage));
const auto testFontStack = FontStack{"Roboto Condensed Italic", "Noto Sans Italic"};
const auto testGlyphRange = GlyphRange(0, 255);
validateEvent(
test,
"onCameraWillChange",
[&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("cameraMode"));
EXPECT_TRUE(json["cameraMode"].IsInt());
EXPECT_EQ(json["cameraMode"].GetInt(), static_cast<int>(MapObserver::CameraChangeMode::Immediate));
},
&TransformObserver::onCameraWillChange,
MapObserver::CameraChangeMode::Immediate);
validateEvent(
test,
"onCameraDidChange",
[&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("cameraMode"));
EXPECT_TRUE(json["cameraMode"].IsInt());
EXPECT_EQ(json["cameraMode"].GetInt(), static_cast<int>(MapObserver::CameraChangeMode::Immediate));
},
&TransformObserver::onCameraDidChange,
MapObserver::CameraChangeMode::Immediate);
validateEvent(test, "onWillStartLoadingMap", nullptr, &style::Observer::onStyleLoading);
validateEvent(test, "onDidFinishLoadingStyle", nullptr, &style::Observer::onStyleLoaded);
validateEvent(
test,
"onSourceChanged",
[&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("type"));
EXPECT_TRUE(json["type"].IsString());
EXPECT_EQ(Enum<style::SourceType>::toEnum(json["type"].GetString()), style::SourceType::GeoJSON);
EXPECT_TRUE(json.HasMember("id"));
EXPECT_TRUE(json["id"].IsString());
EXPECT_STREQ(json["id"].GetString(), testSource->getID().c_str());
},
&style::Observer::onSourceChanged,
*testSource);
validateEvent(test, "onRegisterShaders", nullptr, &RendererObserver::onRegisterShaders, *testShaderRegistry);
validateEvent(
test,
"onDidFailLoadingMap",
[&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("error"));
EXPECT_TRUE(json["error"].IsString());
EXPECT_STREQ(json["error"].GetString(), testExceptionMessage);
EXPECT_TRUE(json.HasMember("code"));
EXPECT_TRUE(json["code"].IsInt());
EXPECT_EQ(json["code"].GetInt(), static_cast<int>(MapLoadError::UnknownError));
},
&style::Observer::onStyleError,
testException);
const auto checkSprite = [&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("id"));
EXPECT_TRUE(json["id"].IsString());
EXPECT_EQ(json["id"].GetString(), testSprite.id);
EXPECT_TRUE(json.HasMember("url"));
EXPECT_TRUE(json["url"].IsString());
EXPECT_EQ(json["url"].GetString(), testSprite.spriteURL);
};
validateEvent(test, "onSpriteLoaded", checkSprite, &style::Observer::onSpriteLoaded, testSprite);
validateEvent(test, "onSpriteRequested", checkSprite, &style::Observer::onSpriteRequested, testSprite);
validateEvent(
test,
"onSpriteError",
[&](const mbgl::JSValue& json) {
checkSprite(json);
EXPECT_TRUE(json.HasMember("error"));
EXPECT_TRUE(json["error"].IsString());
EXPECT_STREQ(json["error"].GetString(), testExceptionMessage);
},
&style::Observer::onSpriteError,
testSprite,
testException);
validateEvent(test, "onWillStartRenderingMap", nullptr, &RendererObserver::onWillStartRenderingMap);
// `onDidFinishRenderingMap` and `onDidFinishLoadingMap` are generated by the same callback
validateEventList(test,
{{"onDidFinishRenderingMap", nullptr}, {"onDidFinishLoadingMap", nullptr}},
&RendererObserver::onDidFinishRenderingMap);
validateEvent(
test,
"onStyleImageMissing",
[&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("id"));
EXPECT_TRUE(json["id"].IsString());
EXPECT_STREQ(json["id"].GetString(), "image");
},
&RendererObserver::onStyleImageMissing,
"image",
[]() {});
const auto checkShader = [&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("shader"));
EXPECT_TRUE(json["shader"].IsString());
EXPECT_EQ(Enum<shaders::BuiltIn>::toEnum(json["shader"].GetString()), shaders::BuiltIn::None);
EXPECT_TRUE(json.HasMember("backend"));
EXPECT_TRUE(json["backend"].IsInt());
EXPECT_EQ(json["backend"].GetInt(), static_cast<int>(gfx::Backend::Type::OpenGL));
};
validateEvent(test,
"onPreCompileShader",
checkShader,
&RendererObserver::onPreCompileShader,
shaders::BuiltIn::None,
gfx::Backend::Type::OpenGL,
"defines");
validateEvent(test,
"onPostCompileShader",
checkShader,
&RendererObserver::onPostCompileShader,
shaders::BuiltIn::None,
gfx::Backend::Type::OpenGL,
"defines");
validateEvent(
test,
"onShaderCompileFailed",
[&](const mbgl::JSValue& json) {
checkShader(json);
EXPECT_TRUE(json.HasMember("defines"));
EXPECT_TRUE(json["defines"].IsString());
EXPECT_STREQ(json["defines"].GetString(), "defines");
},
&RendererObserver::onShaderCompileFailed,
shaders::BuiltIn::None,
gfx::Backend::Type::OpenGL,
"defines");
const auto checkGlyphs = [&](const mbgl::JSValue& json) {
EXPECT_TRUE(json.HasMember("fonts"));
EXPECT_TRUE(json["fonts"].IsArray());
EXPECT_EQ(json["fonts"].GetArray().Size(), testFontStack.size());
auto fontIt = testFontStack.begin();
for (const auto& font : json["fonts"].GetArray()) {
EXPECT_TRUE(font.IsString());
EXPECT_EQ(font.GetString(), *fontIt++);
}
EXPECT_TRUE(json.HasMember("rangeStart"));
EXPECT_TRUE(json["rangeStart"].IsInt());
EXPECT_EQ(json["rangeStart"].GetInt(), testGlyphRange.first);
EXPECT_TRUE(json.HasMember("rangeEnd"));
EXPECT_TRUE(json["rangeEnd"].IsInt());
EXPECT_EQ(json["rangeEnd"].GetInt(), testGlyphRange.second);
};
validateEvent(
test, "onGlyphsLoaded", checkGlyphs, &RendererObserver::onGlyphsLoaded, testFontStack, testGlyphRange);
validateEvent(
test, "onGlyphsRequested", checkGlyphs, &RendererObserver::onGlyphsRequested, testFontStack, testGlyphRange);
validateEvent(
test,
"onGlyphsError",
[&](const mbgl::JSValue& json) {
checkGlyphs(json);
EXPECT_TRUE(json.HasMember("error"));
EXPECT_TRUE(json["error"].IsString());
EXPECT_STREQ(json["error"].GetString(), testExceptionMessage);
},
&RendererObserver::onGlyphsError,
testFontStack,
testGlyphRange,
testException);
validateEvent(test,
"onTileAction",
nullptr,
&RendererObserver::onTileAction,
TileOperation::StartParse,
OverscaledTileID(0, 0, 0),
"");
const auto onDidFinishRenderingFrame =
static_cast<void (RendererObserver::*)(RendererObserver::RenderMode, bool, bool, double, double)>(
&RendererObserver::onDidFinishRenderingFrame);
validateEvent(test,
"onDidBecomeIdle",
nullptr,
onDidFinishRenderingFrame,
RendererObserver::RenderMode::Full,
false,
false,
0.0,
0.0);
}