#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include <../expression-test/test_runner_common.hpp> #include "allocation_index.hpp" #include "file_source.hpp" #include "metadata.hpp" #include "parser.hpp" #include "runner.hpp" #include #include #include #include using namespace mbgl; GfxProbe::GfxProbe(const mbgl::gfx::RenderingStats& stats, const GfxProbe& prev) : numBuffers(stats.numBuffers), numDrawCalls(stats.numDrawCalls), numFrameBuffers(stats.numFrameBuffers), numTextures(stats.numActiveTextures), memIndexBuffers(stats.memIndexBuffers, std::max(stats.memIndexBuffers, prev.memIndexBuffers.peak)), memVertexBuffers(stats.memVertexBuffers, std::max(stats.memVertexBuffers, prev.memVertexBuffers.peak)), memTextures(stats.memTextures, std::max(stats.memTextures, prev.memTextures.peak)) {} struct RunContext { RunContext() = default; GfxProbe activeGfxProbe; GfxProbe baselineGfxProbe; bool gfxProbeActive; }; class TestRunnerMapObserver : public MapObserver { public: TestRunnerMapObserver() : mapLoadFailure(false), finishRenderingMap(false), idle(false) {} void onDidFailLoadingMap(MapLoadError, const std::string&) override { mapLoadFailure = true; } void onDidFinishRenderingMap(RenderMode mode) override final { if (!finishRenderingMap) finishRenderingMap = mode == RenderMode::Full; } void onDidBecomeIdle() override final { idle = true; } void reset() { mapLoadFailure = false; finishRenderingMap = false; idle = false; } bool mapLoadFailure; bool finishRenderingMap; bool idle; }; // static gfx::HeadlessBackend::SwapBehaviour swapBehavior(MapMode mode) { return mode == MapMode::Continuous ? gfx::HeadlessBackend::SwapBehaviour::Flush : gfx::HeadlessBackend::SwapBehaviour::NoFlush; } std::string simpleDiff(const Value& result, const Value& expected) { std::vector resultTokens{tokenize(toJSON(result, 2, false))}; std::vector expectedTokens{tokenize(toJSON(expected, 2, false))}; std::size_t maxLength = std::max(resultTokens.size(), expectedTokens.size()); std::ostringstream diff; diff << "
" << std::endl;
    const auto flush =
        [](const std::vector& vec, std::size_t pos, std::ostringstream& out, std::string separator) {
            for (std::size_t j = pos; j < vec.size(); ++j) {
                out << separator << vec[j] << std::endl;
            }
        };

    for (std::size_t i = 0; i < maxLength; ++i) {
        if (resultTokens.size() <= i) {
            flush(expectedTokens, i, diff, "-");
            break;
        }

        if (expectedTokens.size() <= i) {
            flush(resultTokens, i, diff, "+");
            break;
        }

        if (!deepEqual(resultTokens[i], expectedTokens[i])) {
            diff << ""
                 << "-" << expectedTokens[i] << "" << std::endl;
            diff << ""
                 << "+" << resultTokens[i] << "" << std::endl;
        } else {
            diff << resultTokens[i] << std::endl;
        }
    }
    diff << "
" << std::endl; return diff.str(); } TestRunner::TestRunner(Manifest manifest_) : manifest(std::move(manifest_)) {} const Manifest& TestRunner::getManifest() const { return manifest; } void TestRunner::doShuffle(uint32_t seed) { manifest.doShuffle(seed); } bool TestRunner::checkQueryTestResults(mbgl::PremultipliedImage&& actualImage, std::vector&& features, TestMetadata& metadata) { const std::string& base = metadata.paths.defaultExpectations(); const std::vector& expectations = metadata.paths.expectations; metadata.actual = mbgl::encodePNG(actualImage); if (actualImage.size.isEmpty()) { metadata.errorMessage = "Invalid size for actual image"; return false; } metadata.actualJson = toJSON(features, 2, false); if (metadata.actualJson.empty()) { metadata.errorMessage = "Invalid size for actual JSON"; return false; } #if !TEST_READ_ONLY if (getenv("UPDATE_PLATFORM")) { mbgl::filesystem::create_directories(expectations.back()); mbgl::util::write_file(expectations.back().string() + "/expected.json", metadata.actualJson); return true; } else if (getenv("UPDATE_DEFAULT")) { mbgl::util::write_file(base + "/expected.json", metadata.actualJson); return true; } mbgl::util::write_file(base + "/actual.json", metadata.actualJson); #endif std::vector expectedJsonPaths; mbgl::filesystem::path expectedMetricsPath; for (auto rit = expectations.rbegin(); rit != expectations.rend(); ++rit) { if (mbgl::filesystem::exists(*rit)) { expectedJsonPaths = readExpectedJSONEntries(*rit); if (!expectedJsonPaths.empty()) break; } } if (expectedJsonPaths.empty()) { metadata.errorMessage = "Failed to find expectations for: " + metadata.paths.stylePath.string(); return false; } for (const auto& entry : expectedJsonPaths) { auto maybeExpectedJson = readJson(entry); if (maybeExpectedJson.is()) { auto& expected = maybeExpectedJson.get(); mbgl::JSDocument actual; actual.Parse<0>(metadata.actualJson); if (actual.HasParseError()) { metadata.errorMessage = "Error parsing actual JSON for: " + metadata.paths.stylePath.string(); return false; } auto actualVal = mapbox::geojson::convert(actual); auto expectedVal = mapbox::geojson::convert(expected); bool equal = deepEqual(actualVal, expectedVal); metadata.difference = !equal; if (equal) { metadata.diff = "Match"; } else { metadata.diff = simpleDiff(actualVal, expectedVal); } } else { metadata.errorMessage = "Failed to load expected JSON " + entry; return false; } } return true; } bool TestRunner::checkRenderTestResults(mbgl::PremultipliedImage&& actualImage, TestMetadata& metadata) { const std::string& base = metadata.paths.defaultExpectations(); const std::vector& expectations = metadata.paths.expectations; if (metadata.outputsImage) { metadata.actual = mbgl::encodePNG(actualImage); if (actualImage.size.isEmpty()) { metadata.errorMessage = "Invalid size for actual image"; return false; } #if !TEST_READ_ONLY if (getenv("UPDATE_PLATFORM")) { mbgl::filesystem::create_directories(expectations.back()); mbgl::util::write_file(expectations.back().string() + "/expected.png", mbgl::encodePNG(actualImage)); return true; } else if (getenv("UPDATE_DEFAULT")) { mbgl::util::write_file(base + "/expected.png", mbgl::encodePNG(actualImage)); return true; } mbgl::util::write_file(base + "/actual.png", metadata.actual); #endif mbgl::PremultipliedImage expectedImage{actualImage.size}; mbgl::PremultipliedImage imageDiff{actualImage.size}; double pixels = 0.0; std::vector expectedImagesPaths; for (auto rit = expectations.rbegin(); rit != expectations.rend(); ++rit) { if (mbgl::filesystem::exists(*rit)) { expectedImagesPaths = readExpectedImageEntries(*rit); if (!expectedImagesPaths.empty()) break; } } if (expectedImagesPaths.empty()) { metadata.errorMessage = "Failed to find expectations for: " + metadata.paths.stylePath.string(); return false; } for (const auto& entry : expectedImagesPaths) { mbgl::optional maybeExpectedImage = mbgl::util::readFile(entry); if (!maybeExpectedImage) { metadata.errorMessage = "Failed to load expected image " + entry; return false; } metadata.expected = *maybeExpectedImage; expectedImage = mbgl::decodeImage(*maybeExpectedImage); pixels = // implicitly converting from uint64_t mapbox::pixelmatch(actualImage.data.get(), expectedImage.data.get(), expectedImage.size.width, expectedImage.size.height, imageDiff.data.get(), 0.1285); // Defined in GL JS metadata.diff = mbgl::encodePNG(imageDiff); #if !TEST_READ_ONLY mbgl::util::write_file(base + "/diff.png", metadata.diff); #endif metadata.difference = pixels / expectedImage.size.area(); if (metadata.difference <= metadata.allowed) { break; } } } #if !TEST_READ_ONLY if (getenv("UPDATE_METRICS")) { if (!metadata.metrics.isEmpty()) { mbgl::filesystem::create_directories(expectations.back()); mbgl::util::write_file(expectations.back().string() + "/metrics.json", serializeMetrics(metadata.metrics)); return true; } } #endif mbgl::filesystem::path expectedMetricsPath; for (auto rit = expectations.rbegin(); rit != expectations.rend(); ++rit) { if (mbgl::filesystem::exists(*rit)) { if (metadata.expectedMetrics.isEmpty()) { mbgl::filesystem::path maybeExpectedMetricsPath{*rit}; maybeExpectedMetricsPath.replace_filename("metrics.json"); metadata.expectedMetrics = readExpectedMetrics(maybeExpectedMetricsPath); } } } // Check file size metrics. for (const auto& expected : metadata.expectedMetrics.fileSize) { auto actual = metadata.metrics.fileSize.find(expected.first); if (actual == metadata.metrics.fileSize.end()) { metadata.errorMessage = "Failed to find fileSize probe: " + expected.first; return false; } if (actual->second.path != expected.second.path) { std::stringstream ss; ss << "Comparing different files at probe \"" << expected.first << "\": " << actual->second.path << ", expected is " << expected.second.path << "."; metadata.errorMessage = ss.str(); return false; } auto result = checkValue(expected.second.size, actual->second.size, actual->second.tolerance); if (!std::get(result)) { std::stringstream ss; ss << "File size does not match at probe \"" << expected.first << "\": " << actual->second.size << ", expected is " << expected.second.size << "."; metadata.errorMessage = ss.str(); return false; } } #if !defined(SANITIZE) // Check memory metrics. for (const auto& expected : metadata.expectedMetrics.memory) { auto actual = metadata.metrics.memory.find(expected.first); if (actual == metadata.metrics.memory.end()) { metadata.errorMessage = "Failed to find memory probe: " + expected.first; return false; } bool passed{false}; float delta{0.0f}; std::stringstream errorStream; std::tie(passed, delta) = MemoryProbe::checkPeak(expected.second, actual->second); if (!passed) { errorStream << "Allocated memory peak size at probe \"" << expected.first << "\" is " << actual->second.peak << " bytes, expected is " << expected.second.peak << "±" << delta << " bytes."; } std::tie(passed, delta) = MemoryProbe::checkAllocations(expected.second, actual->second); if (!passed) { errorStream << "Number of allocations at probe \"" << expected.first << "\" is " << actual->second.allocations << ", expected is " << expected.second.allocations << "±" << std::round(delta) << " allocations."; } metadata.errorMessage = errorStream.str(); if (!metadata.errorMessage.empty()) return false; } // Check network metrics. for (const auto& expected : metadata.expectedMetrics.network) { auto actual = metadata.metrics.network.find(expected.first); if (actual == metadata.metrics.network.end()) { metadata.errorMessage = "Failed to find network probe: " + expected.first; return false; } bool failed = false; if (actual->second.requests != expected.second.requests) { std::stringstream ss; ss << "Number of requests at probe \"" << expected.first << "\" is " << actual->second.requests << ", expected is " << expected.second.requests << ". "; metadata.errorMessage = ss.str(); failed = true; } if (actual->second.transferred != expected.second.transferred) { std::stringstream ss; ss << "Transferred data at probe \"" << expected.first << "\" is " << actual->second.transferred << " bytes, expected is " << expected.second.transferred << " bytes."; metadata.errorMessage += ss.str(); failed = true; } if (failed) { return false; } } #endif // !defined(SANITIZE) // Check fps metrics for (const auto& expected : metadata.expectedMetrics.fps) { auto actual = metadata.metrics.fps.find(expected.first); if (actual == metadata.metrics.fps.end()) { metadata.errorMessage = "Failed to find fps probe: " + expected.first; return false; } auto result = checkValue(expected.second.average, actual->second.average, expected.second.tolerance); if (!std::get(result)) { std::stringstream ss; ss << "Average fps at probe \"" << expected.first << "\" is " << actual->second.average << ", expected to be " << expected.second.average << " with tolerance of " << expected.second.tolerance; metadata.errorMessage = ss.str(); return false; } result = checkValue(expected.second.minOnePc, actual->second.minOnePc, expected.second.tolerance); if (!std::get(result)) { std::stringstream ss; ss << "Minimum(1%) fps at probe \"" << expected.first << "\" is " << actual->second.minOnePc << ", expected to be " << expected.second.minOnePc << " with tolerance of " << expected.second.tolerance; metadata.errorMessage = ss.str(); return false; } } // Check gfx metrics for (const auto& expected : metadata.expectedMetrics.gfx) { auto actual = metadata.metrics.gfx.find(expected.first); if (actual == metadata.metrics.gfx.end()) { metadata.errorMessage = "Failed to find gfx probe: " + expected.first; return false; } const auto& probeName = expected.first; const auto& expectedValue = expected.second; const auto& actualValue = actual->second; bool failed = false; if (expectedValue.numDrawCalls != actualValue.numDrawCalls) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Number of draw calls at probe\"" << probeName << "\" is " << actualValue.numDrawCalls << ", expected is " << expectedValue.numDrawCalls; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.numTextures != actualValue.numTextures) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Number of textures at probe \"" << probeName << "\" is " << actualValue.numTextures << ", expected is " << expectedValue.numTextures; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.numBuffers != actualValue.numBuffers) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Number of vertex and index buffers at probe \"" << probeName << "\" is " << actualValue.numBuffers << ", expected is " << expectedValue.numBuffers; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.numFrameBuffers != actualValue.numFrameBuffers) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Number of frame buffers at probe \"" << probeName << "\" is " << actualValue.numFrameBuffers << ", expected is " << expectedValue.numFrameBuffers; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.memTextures.peak != actualValue.memTextures.peak) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Allocated texture memory peak size at probe \"" << probeName << "\" is " << actualValue.memTextures.peak << " bytes, expected is " << expectedValue.memTextures.peak << " bytes"; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.memIndexBuffers.peak != actualValue.memIndexBuffers.peak) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Allocated index buffer memory peak size at probe \"" << probeName << "\" is " << actualValue.memIndexBuffers.peak << " bytes, expected is " << expectedValue.memIndexBuffers.peak << " bytes"; metadata.errorMessage += ss.str(); failed = true; } if (expectedValue.memVertexBuffers.peak != actualValue.memVertexBuffers.peak) { std::stringstream ss; if (!metadata.errorMessage.empty()) ss << std::endl; ss << "Allocated vertex buffer memory peak size at probe \"" << probeName << "\" is " << actualValue.memVertexBuffers.peak << " bytes, expected is " << expectedValue.memVertexBuffers.peak << " bytes"; metadata.errorMessage += ss.str(); failed = true; } if (failed) return false; } return true; } bool TestRunner::runOperations(const std::string& key, TestMetadata& metadata, RunContext& ctx) { if (!metadata.document.HasMember("metadata") || !metadata.document["metadata"].HasMember("test") || !metadata.document["metadata"]["test"].HasMember("operations")) { return true; } assert(metadata.document["metadata"]["test"]["operations"].IsArray()); const auto& operationsArray = metadata.document["metadata"]["test"]["operations"].GetArray(); if (operationsArray.Empty()) { return true; } const auto& operationIt = operationsArray.Begin(); assert(operationIt->IsArray()); const auto& operationArray = operationIt->GetArray(); assert(operationArray.Size() >= 1u); auto& frontend = maps[key]->frontend; auto& map = maps[key]->map; auto& observer = maps[key]->observer; static const std::string waitOp("wait"); static const std::string sleepOp("sleep"); static const std::string addImageOp("addImage"); static const std::string updateImageOp("updateImage"); static const std::string removeImageOp("removeImage"); static const std::string setStyleOp("setStyle"); static const std::string setCenterOp("setCenter"); static const std::string setZoomOp("setZoom"); static const std::string setBearingOp("setBearing"); static const std::string setPitchOp("setPitch"); static const std::string setFilterOp("setFilter"); static const std::string setLayerZoomRangeOp("setLayerZoomRange"); static const std::string setLightOp("setLight"); static const std::string addLayerOp("addLayer"); static const std::string removeLayerOp("removeLayer"); static const std::string addSourceOp("addSource"); static const std::string removeSourceOp("removeSource"); static const std::string setPaintPropertyOp("setPaintProperty"); static const std::string setLayoutPropertyOp("setLayoutProperty"); static const std::string fileSizeProbeOp("probeFileSize"); static const std::string memoryProbeOp("probeMemory"); static const std::string memoryProbeStartOp("probeMemoryStart"); static const std::string memoryProbeEndOp("probeMemoryEnd"); static const std::string networkProbeOp("probeNetwork"); static const std::string networkProbeStartOp("probeNetworkStart"); static const std::string networkProbeEndOp("probeNetworkEnd"); static const std::string setFeatureStateOp("setFeatureState"); static const std::string getFeatureStateOp("getFeatureState"); static const std::string removeFeatureStateOp("removeFeatureState"); static const std::string panGestureOp("panGesture"); static const std::string gfxProbeOp("probeGFX"); static const std::string gfxProbeStartOp("probeGFXStart"); static const std::string gfxProbeEndOp("probeGFXEnd"); if (operationArray[0].GetString() == waitOp) { // wait try { frontend.render(map); } catch (const std::exception&) { return false; } } else if (operationArray[0].GetString() == sleepOp) { // sleep mbgl::util::Timer sleepTimer; bool sleeping = true; mbgl::Duration duration = mbgl::Seconds(3); if (operationArray.Size() >= 2u) { duration = mbgl::Milliseconds(operationArray[1].GetUint()); } sleepTimer.start(duration, mbgl::Duration::zero(), [&]() { sleeping = false; }); while (sleeping) { mbgl::util::RunLoop::Get()->runOnce(); } } else if (operationArray[0].GetString() == addImageOp || operationArray[0].GetString() == updateImageOp) { // addImage | updateImage assert(operationArray.Size() >= 3u); float pixelRatio = 1.0f; bool sdf = false; if (operationArray.Size() == 4u) { assert(operationArray[3].IsObject()); const auto& imageOptions = operationArray[3].GetObject(); if (imageOptions.HasMember("pixelRatio")) { pixelRatio = imageOptions["pixelRatio"].GetFloat(); } if (imageOptions.HasMember("sdf")) { sdf = imageOptions["sdf"].GetBool(); } } std::string imageName = operationArray[1].GetString(); imageName.erase(std::remove(imageName.begin(), imageName.end(), '"'), imageName.end()); std::string imagePath = operationArray[2].GetString(); imagePath.erase(std::remove(imagePath.begin(), imagePath.end(), '"'), imagePath.end()); const mbgl::filesystem::path filePath = (mbgl::filesystem::path(manifest.getAssetPath()) / imagePath); mbgl::optional maybeImage = mbgl::util::readFile(filePath.string()); if (!maybeImage) { metadata.errorMessage = std::string("Failed to load expected image ") + filePath.string(); return false; } map.getStyle().addImage(std::make_unique(imageName, mbgl::decodeImage(*maybeImage), pixelRatio, sdf)); } else if (operationArray[0].GetString() == removeImageOp) { // removeImage assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); const std::string imageName { operationArray[1].GetString(), operationArray[1].GetStringLength() }; map.getStyle().removeImage(imageName); } else if (operationArray[0].GetString() == setStyleOp) { // setStyle assert(operationArray.Size() >= 2u); if (operationArray[1].IsString()) { std::string stylePath = manifest.localizeURL(operationArray[1].GetString()); auto maybeStyle = readJson(stylePath); if (maybeStyle.is()) { auto& style = maybeStyle.get(); manifest.localizeStyleURLs((mbgl::JSValue&)style, style); map.getStyle().loadJSON(serializeJsonValue(style)); } } else { manifest.localizeStyleURLs(operationArray[1], metadata.document); map.getStyle().loadJSON(serializeJsonValue(operationArray[1])); } } else if (operationArray[0].GetString() == setCenterOp) { // setCenter assert(operationArray.Size() >= 2u); assert(operationArray[1].IsArray()); const auto& centerArray = operationArray[1].GetArray(); assert(centerArray.Size() == 2u); map.jumpTo(mbgl::CameraOptions().withCenter(mbgl::LatLng(centerArray[1].GetDouble(), centerArray[0].GetDouble()))); } else if (operationArray[0].GetString() == setZoomOp) { // setZoom assert(operationArray.Size() >= 2u); assert(operationArray[1].IsNumber()); map.jumpTo(mbgl::CameraOptions().withZoom(operationArray[1].GetDouble())); } else if (operationArray[0].GetString() == setBearingOp) { // setBearing assert(operationArray.Size() >= 2u); assert(operationArray[1].IsNumber()); map.jumpTo(mbgl::CameraOptions().withBearing(operationArray[1].GetDouble())); } else if (operationArray[0].GetString() == setPitchOp) { // setPitch assert(operationArray.Size() >= 2u); assert(operationArray[1].IsNumber()); map.jumpTo(mbgl::CameraOptions().withPitch(operationArray[1].GetDouble())); } else if (operationArray[0].GetString() == setFilterOp) { // setFilter assert(operationArray.Size() >= 3u); assert(operationArray[1].IsString()); const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() }; mbgl::style::conversion::Error error; auto converted = mbgl::style::conversion::convert(operationArray[2], error); if (!converted) { metadata.errorMessage = std::string("Unable to convert filter: ") + error.message; return false; } else { auto layer = map.getStyle().getLayer(layerName); if (!layer) { metadata.errorMessage = std::string("Layer not found: ") + layerName; return false; } else { layer->setFilter(std::move(*converted)); } } } else if (operationArray[0].GetString() == setLayerZoomRangeOp) { // setLayerZoomRange assert(operationArray.Size() >= 4u); assert(operationArray[1].IsString()); assert(operationArray[2].IsNumber()); assert(operationArray[3].IsNumber()); const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() }; auto layer = map.getStyle().getLayer(layerName); if (!layer) { metadata.errorMessage = std::string("Layer not found: ") + layerName; return false; } else { layer->setMinZoom(operationArray[2].GetFloat()); layer->setMaxZoom(operationArray[3].GetFloat()); } } else if (operationArray[0].GetString() == setLightOp) { // setLight assert(operationArray.Size() >= 2u); assert(operationArray[1].IsObject()); mbgl::style::conversion::Error error; auto converted = mbgl::style::conversion::convert(operationArray[1], error); if (!converted) { metadata.errorMessage = std::string("Unable to convert light: ") + error.message; return false; } else { map.getStyle().setLight(std::make_unique(std::move(*converted))); } } else if (operationArray[0].GetString() == addLayerOp) { // addLayer assert(operationArray.Size() >= 2u); assert(operationArray[1].IsObject()); mbgl::style::conversion::Error error; auto converted = mbgl::style::conversion::convert>(operationArray[1], error); if (!converted) { metadata.errorMessage = std::string("Unable to convert layer: ") + error.message; return false; } else { map.getStyle().addLayer(std::move(*converted)); } } else if (operationArray[0].GetString() == removeLayerOp) { // removeLayer assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); map.getStyle().removeLayer(operationArray[1].GetString()); } else if (operationArray[0].GetString() == addSourceOp) { // addSource assert(operationArray.Size() >= 3u); assert(operationArray[1].IsString()); assert(operationArray[2].IsObject()); manifest.localizeSourceURLs(operationArray[2], metadata.document); mbgl::style::conversion::Error error; auto converted = mbgl::style::conversion::convert>(operationArray[2], error, operationArray[1].GetString()); if (!converted) { metadata.errorMessage = std::string("Unable to convert source: ") + error.message; return false; } else { map.getStyle().addSource(std::move(*converted)); } } else if (operationArray[0].GetString() == removeSourceOp) { // removeSource assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); map.getStyle().removeSource(operationArray[1].GetString()); } else if (operationArray[0].GetString() == setPaintPropertyOp) { // setPaintProperty assert(operationArray.Size() >= 4u); assert(operationArray[1].IsString()); assert(operationArray[2].IsString()); const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() }; const std::string propertyName { operationArray[2].GetString(), operationArray[2].GetStringLength() }; auto layer = map.getStyle().getLayer(layerName); if (!layer) { metadata.errorMessage = std::string("Layer not found: ") + layerName; return false; } else { const mbgl::JSValue* propertyValue = &operationArray[3]; layer->setPaintProperty(propertyName, propertyValue); } } else if (operationArray[0].GetString() == setLayoutPropertyOp) { // setLayoutProperty assert(operationArray.Size() >= 4u); assert(operationArray[1].IsString()); assert(operationArray[2].IsString()); const std::string layerName { operationArray[1].GetString(), operationArray[1].GetStringLength() }; const std::string propertyName { operationArray[2].GetString(), operationArray[2].GetStringLength() }; auto layer = map.getStyle().getLayer(layerName); if (!layer) { metadata.errorMessage = std::string("Layer not found: ") + layerName; return false; } else { const mbgl::JSValue* propertyValue = &operationArray[3]; layer->setLayoutProperty(propertyName, propertyValue); } } else if (operationArray[0].GetString() == fileSizeProbeOp) { // probeFileSize assert(operationArray.Size() >= 4u); assert(operationArray[1].IsString()); assert(operationArray[2].IsString()); assert(operationArray[3].IsNumber()); std::string mark = std::string(operationArray[1].GetString(), operationArray[1].GetStringLength()); std::string path = std::string(operationArray[2].GetString(), operationArray[2].GetStringLength()); assert(!path.empty()); float tolerance = operationArray[3].GetDouble(); mbgl::filesystem::path filePath(path); if (!filePath.is_absolute()) { filePath = metadata.paths.defaultExpectations() / filePath; } if (mbgl::filesystem::exists(filePath)) { auto size = mbgl::filesystem::file_size(filePath); metadata.metrics.fileSize.emplace(std::piecewise_construct, std::forward_as_tuple(std::move(mark)), std::forward_as_tuple(std::move(path), size, tolerance)); } else { metadata.errorMessage = std::string("File not found: ") + path; return false; } } else if (operationArray[0].GetString() == memoryProbeStartOp) { // probeMemoryStart assert(!AllocationIndex::isActive()); AllocationIndex::setActive(true); } else if (operationArray[0].GetString() == memoryProbeOp) { // probeMemory assert(AllocationIndex::isActive()); assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); std::string mark = std::string(operationArray[1].GetString(), operationArray[1].GetStringLength()); auto emplaced = metadata.metrics.memory.emplace( std::piecewise_construct, std::forward_as_tuple(std::move(mark)), std::forward_as_tuple(AllocationIndex::getAllocatedSizePeak(), AllocationIndex::getAllocationsCount())); assert(emplaced.second); if (operationArray.Size() >= 3u) { assert(operationArray[2].IsNumber()); emplaced.first->second.tolerance = float(operationArray[2].GetDouble()); } } else if (operationArray[0].GetString() == memoryProbeEndOp) { // probeMemoryEnd assert(AllocationIndex::isActive()); AllocationIndex::setActive(false); AllocationIndex::reset(); } else if (operationArray[0].GetString() == networkProbeStartOp) { // probeNetworkStart assert(!ProxyFileSource::isTrackingActive()); ProxyFileSource::setTrackingActive(true); } else if (operationArray[0].GetString() == networkProbeOp) { // probeNetwork assert(ProxyFileSource::isTrackingActive()); assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); std::string mark = std::string(operationArray[1].GetString(), operationArray[1].GetStringLength()); metadata.metrics.network.emplace( std::piecewise_construct, std::forward_as_tuple(std::move(mark)), std::forward_as_tuple(ProxyFileSource::getRequestCount(), ProxyFileSource::getTransferredSize())); } else if (operationArray[0].GetString() == networkProbeEndOp) { // probeNetworkEnd assert(ProxyFileSource::isTrackingActive()); ProxyFileSource::setTrackingActive(false); } else if (operationArray[0].GetString() == setFeatureStateOp) { // setFeatureState assert(operationArray.Size() >= 3u); assert(operationArray[1].IsObject()); assert(operationArray[2].IsObject()); using namespace mbgl; using namespace mbgl::style::conversion; std::string sourceID; mbgl::optional sourceLayer; std::string featureID; std::string stateKey; Value stateValue; bool valueParsed = false; FeatureState parsedState; const auto& featureOptions = operationArray[1].GetObject(); if (featureOptions.HasMember("source")) { sourceID = featureOptions["source"].GetString(); } if (featureOptions.HasMember("sourceLayer")) { sourceLayer = {featureOptions["sourceLayer"].GetString()}; } if (featureOptions.HasMember("id")) { if (featureOptions["id"].IsString()) { featureID = featureOptions["id"].GetString(); } else if (featureOptions["id"].IsNumber()) { featureID = mbgl::util::toString(featureOptions["id"].GetUint64()); } } const JSValue* state = &operationArray[2]; const std::function(const std::string&, const Convertible&)> convertFn = [&](const std::string& k, const Convertible& v) -> optional { optional value = toValue(v); if (value) { stateValue = std::move(*value); valueParsed = true; } else if (isArray(v)) { std::vector array; std::size_t length = arrayLength(v); array.reserve(length); for (size_t i = 0; i < length; ++i) { optional arrayVal = toValue(arrayMember(v, i)); if (arrayVal) { array.emplace_back(*arrayVal); } } std::unordered_map result; result[k] = std::move(array); stateValue = std::move(result); valueParsed = true; return nullopt; } else if (isObject(v)) { eachMember(v, convertFn); } if (!valueParsed) { metadata.errorMessage = std::string("Could not get feature state value, state key: ") + k; return nullopt; } stateKey = k; parsedState[stateKey] = stateValue; return nullopt; }; eachMember(state, convertFn); try { frontend.render(map); } catch (const std::exception&) { return false; } frontend.getRenderer()->setFeatureState(sourceID, sourceLayer, featureID, parsedState); } else if (operationArray[0].GetString() == getFeatureStateOp) { // getFeatureState assert(operationArray.Size() >= 2u); assert(operationArray[1].IsObject()); std::string sourceID; mbgl::optional sourceLayer; std::string featureID; const auto& featureOptions = operationArray[1].GetObject(); if (featureOptions.HasMember("source")) { sourceID = featureOptions["source"].GetString(); } if (featureOptions.HasMember("sourceLayer")) { sourceLayer = {featureOptions["sourceLayer"].GetString()}; } if (featureOptions.HasMember("id")) { if (featureOptions["id"].IsString()) { featureID = featureOptions["id"].GetString(); } else if (featureOptions["id"].IsNumber()) { featureID = mbgl::util::toString(featureOptions["id"].GetUint64()); } } try { frontend.render(map); } catch (const std::exception&) { return false; } mbgl::FeatureState state; frontend.getRenderer()->getFeatureState(state, sourceID, sourceLayer, featureID); } else if (operationArray[0].GetString() == removeFeatureStateOp) { // removeFeatureState assert(operationArray.Size() >= 2u); assert(operationArray[1].IsObject()); std::string sourceID; mbgl::optional sourceLayer; std::string featureID; mbgl::optional stateKey; const auto& featureOptions = operationArray[1].GetObject(); if (featureOptions.HasMember("source")) { sourceID = featureOptions["source"].GetString(); } if (featureOptions.HasMember("sourceLayer")) { sourceLayer = {featureOptions["sourceLayer"].GetString()}; } if (featureOptions.HasMember("id")) { if (featureOptions["id"].IsString()) { featureID = featureOptions["id"].GetString(); } else if (featureOptions["id"].IsNumber()) { featureID = mbgl::util::toString(featureOptions["id"].GetUint64()); } } if (operationArray.Size() >= 3u) { assert(operationArray[2].IsString()); stateKey = {operationArray[2].GetString()}; } try { frontend.render(map); } catch (const std::exception&) { return false; } frontend.getRenderer()->removeFeatureState(sourceID, sourceLayer, featureID, stateKey); } else if (operationArray[0].GetString() == panGestureOp) { // benchmarkPanGesture assert(operationArray.Size() >= 4u); assert(operationArray[1].IsString()); // identifier assert(operationArray[2].IsNumber()); // duration assert(operationArray[3].IsArray()); // start [lat, lng, zoom] assert(operationArray[4].IsArray()); // end [lat, lng, zoom] if (metadata.mapMode != mbgl::MapMode::Continuous) { metadata.errorMessage = "Map mode must be Continous for " + panGestureOp + " operation"; return false; } std::string mark = operationArray[1].GetString(); int duration = operationArray[2].GetFloat(); LatLng startPos, endPos; double startZoom, endZoom; std::vector samples; auto parsePosition = [](auto arr) -> std::tuple { assert(arr.Size() >= 3); return {{arr[1].GetDouble(), arr[0].GetDouble()}, arr[2].GetDouble()}; }; std::tie(startPos, startZoom) = parsePosition(operationArray[3].GetArray()); std::tie(endPos, endZoom) = parsePosition(operationArray[4].GetArray()); // Jump to the starting point of the segment and make sure there's something to render map.jumpTo(mbgl::CameraOptions().withCenter(startPos).withZoom(startZoom)); observer->reset(); while (!observer->finishRenderingMap) { frontend.renderOnce(map); } if (observer->mapLoadFailure) return false; size_t frames = 0; float totalTime = 0.0; bool transitionFinished = false; mbgl::AnimationOptions animationOptions(mbgl::Milliseconds(duration * 1000)); animationOptions.minZoom = util::min(startZoom, endZoom); animationOptions.transitionFinishFn = [&]() { transitionFinished = true; }; map.flyTo(mbgl::CameraOptions().withCenter(endPos).withZoom(endZoom), animationOptions); while (!transitionFinished) { frames++; frontend.renderOnce(map); float frameTime = (float)frontend.getFrameTime(); totalTime += frameTime; samples.push_back(frameTime); } float averageFps = totalTime > 0.0 ? frames / totalTime : 0.0; float minFrameTime = 0.0; // Use 1% of the longest frames to compute the minimum fps std::sort(samples.begin(), samples.end()); int sampleCount = util::max(1, (int)samples.size() / 100); for (auto it = samples.rbegin(); it != samples.rbegin() + sampleCount; it++) minFrameTime += *it; float minOnePcFps = sampleCount / minFrameTime; metadata.metrics.fps.insert({std::move(mark), {averageFps, minOnePcFps, 0.0f}}); } else if (operationArray[0].GetString() == gfxProbeStartOp) { // probeGFXStart assert(!ctx.gfxProbeActive); ctx.gfxProbeActive = true; ctx.baselineGfxProbe = ctx.activeGfxProbe; } else if (operationArray[0].GetString() == gfxProbeEndOp) { // probeGFXEnd assert(ctx.gfxProbeActive); ctx.gfxProbeActive = false; } else if (operationArray[0].GetString() == gfxProbeOp) { // probeGFX assert(operationArray.Size() >= 2u); assert(operationArray[1].IsString()); std::string mark = std::string(operationArray[1].GetString(), operationArray[1].GetStringLength()); // Render the map and fetch rendering stats gfx::RenderingStats stats; try { stats = frontend.render(map).stats; } catch (const std::exception&) { return false; } ctx.activeGfxProbe = GfxProbe(stats, ctx.activeGfxProbe); // Compare memory allocations to the baseline probe GfxProbe metricProbe = ctx.activeGfxProbe; metricProbe.memIndexBuffers.peak -= ctx.baselineGfxProbe.memIndexBuffers.peak; metricProbe.memVertexBuffers.peak -= ctx.baselineGfxProbe.memVertexBuffers.peak; metricProbe.memTextures.peak -= ctx.baselineGfxProbe.memTextures.peak; metadata.metrics.gfx.insert({mark, metricProbe}); } else { metadata.errorMessage = std::string("Unsupported operation: ") + operationArray[0].GetString(); return false; } operationsArray.Erase(operationIt); return runOperations(key, metadata, ctx); } TestRunner::Impl::Impl(const TestMetadata& metadata) : observer(std::make_unique()), frontend(metadata.size, metadata.pixelRatio, swapBehavior(metadata.mapMode)), map(frontend, *observer.get(), mbgl::MapOptions() .withMapMode(metadata.mapMode) .withSize(metadata.size) .withPixelRatio(metadata.pixelRatio) .withCrossSourceCollisions(metadata.crossSourceCollisions), mbgl::ResourceOptions().withCacheOnlyRequestsSupport(false)) {} TestRunner::Impl::~Impl() {} bool TestRunner::run(TestMetadata& metadata) { AllocationIndex::setActive(false); AllocationIndex::reset(); ProxyFileSource::setTrackingActive(false); std::string key = mbgl::util::toString(uint32_t(metadata.mapMode)) + "/" + mbgl::util::toString(metadata.pixelRatio) + "/" + mbgl::util::toString(uint32_t(metadata.crossSourceCollisions)); if (maps.find(key) == maps.end()) { maps[key] = std::make_unique(metadata); } auto& frontend = maps[key]->frontend; auto& map = maps[key]->map; frontend.setSize(metadata.size); map.setSize(metadata.size); map.setProjectionMode(mbgl::ProjectionMode().withAxonometric(metadata.axonometric).withXSkew(metadata.xSkew).withYSkew(metadata.ySkew)); map.setDebug(metadata.debug); map.getStyle().loadJSON(serializeJsonValue(metadata.document)); map.jumpTo(map.getStyle().getDefaultCamera()); RunContext ctx{}; if (!runOperations(key, metadata, ctx)) { return false; } mbgl::PremultipliedImage image; try { if (metadata.outputsImage) image = frontend.render(map).image; } catch (const std::exception&) { return false; } if (metadata.renderTest) { return checkRenderTestResults(std::move(image), metadata); } else { std::vector features; assert(metadata.document["metadata"]["test"]["queryGeometry"].IsArray()); if (metadata.document["metadata"]["test"]["queryGeometry"][0].IsNumber() && metadata.document["metadata"]["test"]["queryGeometry"][1].IsNumber()) { features = frontend.getRenderer()->queryRenderedFeatures(metadata.queryGeometry, metadata.queryOptions); } else { features = frontend.getRenderer()->queryRenderedFeatures(metadata.queryGeometryBox, metadata.queryOptions); } return checkQueryTestResults(std::move(image), std::move(features), metadata); } } void TestRunner::reset() { maps.clear(); }