From 5d5bd84d95f3692fa1bf05931cbf2fd1e222a6ce Mon Sep 17 00:00:00 2001 From: Bart Louwers Date: Thu, 4 Jul 2024 01:16:29 +0300 Subject: [PATCH] Add C++ Unit Tests for Android (#2563) --- benchmark/android/app/build.gradle | 4 +- platform/android/Makefile | 46 +++++++------- platform/android/TESTS.md | 2 +- platform/android/android.cmake | 9 ++- platform/android/gradle/native-build.gradle | 8 +-- platform/android/settings.gradle | 5 ++ platform/android/src/test/test_runner.cpp | 61 +++++++++++++++++-- render-test/android/app/build.gradle | 4 +- test/CMakeLists.txt | 2 +- test/android/app/build.gradle | 17 ++++-- test/android/app/src/main/AndroidManifest.xml | 2 +- test/android/build.gradle | 4 +- test/android/gradle.properties | 1 + test/android/settings.gradle | 2 + 14 files changed, 117 insertions(+), 50 deletions(-) diff --git a/benchmark/android/app/build.gradle b/benchmark/android/app/build.gradle index d2199bd5e..f89a4cb16 100644 --- a/benchmark/android/app/build.gradle +++ b/benchmark/android/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 21 targetSdkVersion 33 def abi = 'all' - if (project.hasProperty('mapbox.abis')) { - abi = project.getProperty('mapbox.abis') + if (project.hasProperty('maplibre.abis')) { + abi = project.getProperty('maplibre.abis') } ndk { if (abi != 'all') { diff --git a/platform/android/Makefile b/platform/android/Makefile index 01b163d70..1f3f0f3d8 100644 --- a/platform/android/Makefile +++ b/platform/android/Makefile @@ -61,21 +61,21 @@ define ANDROID_RULES .PHONY: android-test-lib-$1 android-test-lib-$1: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 -Pmapbox.with_test=true :MapLibreAndroidTestApp:assemble$(BUILDTYPE) + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 -Pmapbox.with_test=true :MapLibreAndroidTestApp:assemble$(BUILDTYPE) .PHONY: android-benchmark-$1 android-benchmark-$1: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 -Pmapbox.with_benchmark=true :MapLibreAndroidTestApp:assemble$(BUILDTYPE) + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 -Pmapbox.with_benchmark=true :MapLibreAndroidTestApp:assemble$(BUILDTYPE) # Build SDK for for specified abi .PHONY: android-lib-$1 android-lib-$1: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroid:assemble$(BUILDTYPE) + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroid:assemble$(BUILDTYPE) # Build test app and SDK for for specified abi .PHONY: android-$1 android-$1: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:assemble$(BUILDTYPE) + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:assemble$(BUILDTYPE) # Build the core test for specified abi .PHONY: android-core-test-$1 @@ -140,23 +140,23 @@ run-android-benchmark-$1-%: android-benchmark-$1 .PHONY: run-android-$1 run-android-$1: -adb uninstall org.maplibre.testapp 2> /dev/null - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:install$(BUILDTYPE) && adb shell am start -n org.maplibre.testapp/.activity.FeatureOverviewActivity + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:install$(BUILDTYPE) && adb shell am start -n org.maplibre.testapp/.activity.FeatureOverviewActivity # Build test app instrumentation tests apk and test app apk for specified abi .PHONY: android-ui-test-$1 android-ui-test-$1: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:assembleDebug :MapLibreAndroidTestApp:assembleAndroidTest + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:assembleDebug :MapLibreAndroidTestApp:assembleAndroidTest # Run test app instrumentation tests on a connected android device or emulator with specified abi .PHONY: run-android-ui-test-$1 run-android-ui-test-$1: -adb uninstall org.maplibre.testapp 2> /dev/null - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest # Run Java Instrumentation tests on a connected android device or emulator with specified abi and test filter run-android-ui-test-$1-%: -adb uninstall org.maplibre.testapp 2> /dev/null - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class="$$*" + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class="$$*" # Symbolicate native stack trace with the specified abi .PHONY: android-ndk-stack-$1 @@ -174,7 +174,7 @@ run-android-render-test-$1: $(BUILD_DEPS) cp -r metrics/integration MapLibreAndroidTestApp/src/main/assets cp platform/node/test/ignores.json MapLibreAndroidTestApp/src/main/assets/integration/ignores.json # run RenderTest.java to generate static map images - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class="org.maplibre.testapp.render.RenderTest" + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=$2 :MapLibreAndroidTestApp:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class="org.maplibre.testapp.render.RenderTest" # pull generated images from the device adb pull "`adb shell 'printenv EXTERNAL_STORAGE' | tr -d '\r'`/maplibre/render" build/render-test # copy expected result and run pixelmatch @@ -211,22 +211,22 @@ run-android-ui-test-%: run-android-ui-test-arm-v7-% # Run Java Unit tests on the JVM of the development machine executing this .PHONY: run-android-unit-test run-android-unit-test: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:testLegacyDebugUnitTest --info + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:testLegacyDebugUnitTest --info run-android-unit-test-%: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:testLegacyDebugUnitTest --info --tests "$*" + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:testLegacyDebugUnitTest --info --tests "$*" # Builds a release package and .tar.gz with debug symbols of the Android SDK .PHONY: apackage apackage: make android-lib-arm-v7 && make android-lib-arm-v8 && make android-lib-x86 && make android-lib-x86-64 - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=all assembleDrawable$(BUILDTYPE) + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=all assembleDrawable$(BUILDTYPE) mkdir -p build tar -czvf build/debug-symbols.tar.gz -C MapLibreAndroid/build/intermediates/library_jni/drawableRelease/copyDrawableReleaseJniLibsProjectOnly/jni . # Build test app instrumentation tests apk and test app apk for all abi's .PHONY: android-ui-test android-ui-test: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=all :MapLibreAndroidTestApp:assembleDebug :MapLibreAndroidTestApp:assembleAndroidTest + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=all :MapLibreAndroidTestApp:assembleDebug :MapLibreAndroidTestApp:assembleAndroidTest #Run instrumentations tests on MicroSoft App Center, ${devices} can be "xiaomi","huawei","OnePlus","htc" .PHONY: run-android-test-app-center @@ -236,7 +236,7 @@ run-android-test-app-center: # Uploads the compiled Android SDK to Maven Central Staging .PHONY: run-android-publish run-android-publish: - $(MLN_ANDROID_GRADLE_SINGLE_JOB)-Pmapbox.abis=all :MapLibreAndroid:publishReleasePublicationToSonatypeRepository closeAndReleaseSonatypeStagingRepository + $(MLN_ANDROID_GRADLE_SINGLE_JOB)-Pmaplibre.abis=all :MapLibreAndroid:publishReleasePublicationToSonatypeRepository closeAndReleaseSonatypeStagingRepository # Dump system graphics information for the test app .PHONY: android-gfxinfo @@ -250,27 +250,27 @@ android-check : android-ktlint android-checkstyle android-lint-sdk android-lint- # Runs checkstyle on the java code .PHONY: android-checkstyle android-checkstyle: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:checkstyle :MapLibreAndroidTestApp:checkstyle + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:checkstyle :MapLibreAndroidTestApp:checkstyle # Runs checkstyle on the kotlin code .PHONY: android-ktlint android-ktlint: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none checkstyle + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none checkstyle # Runs lint on the Android SDK java code .PHONY: android-lint-sdk android-lint-sdk: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:lint + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:lint # Runs lint on the Android test app java code .PHONY: android-lint-test-app android-lint-test-app: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroidTestApp:lint + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroidTestApp:lint # Generates LICENSE.md file based on all Android project dependencies .PHONY: android-license android-license: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:licenseDrawableReleaseReport + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:licenseDrawableReleaseReport python3 scripts/generate-license.py # Symbolicate ndk stack traces for the arm-v7 abi @@ -280,22 +280,22 @@ android-ndk-stack: android-ndk-stack-arm-v7 # Updates Android's vendor submodules .PHONY: android-update-vendor android-update-vendor: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none updateVendorSubmodules + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none updateVendorSubmodules # Run android nitpick script .PHONY: run-android-nitpick run-android-nitpick: android-update-vendor - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none androidNitpick + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none androidNitpick # Creates a dependency graph using Graphviz .PHONY: android-graph android-graph: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=none :MapLibreAndroid:generateDependencyGraphMapboxLibraries + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=none :MapLibreAndroid:generateDependencyGraphMapboxLibraries # Lists tasks .PHONY: list-tasks list-tasks: - $(MLN_ANDROID_GRADLE) -Pmapbox.abis=all tasks + $(MLN_ANDROID_GRADLE) -Pmaplibre.abis=all tasks #### Miscellaneous targets ##################################################### diff --git a/platform/android/TESTS.md b/platform/android/TESTS.md index 0bb35a08b..c94fbc02e 100644 --- a/platform/android/TESTS.md +++ b/platform/android/TESTS.md @@ -40,7 +40,7 @@ You can also have a run configuration: You can also run the tests from the command line with: ``` -$ ./gradlew -Pmapbox.abis=none test -p MapLibreAndroidTestApp +$ ./gradlew -Pmaplibre.abis=none test -p MapLibreAndroidTestApp ``` ### Running the UI/Application Exerciser Monkey diff --git a/platform/android/android.cmake b/platform/android/android.cmake index 89582be86..b28104d3a 100644 --- a/platform/android/android.cmake +++ b/platform/android/android.cmake @@ -126,14 +126,19 @@ add_library( target_include_directories( mbgl-test-runner - PRIVATE ${ANDROID_NDK}/sources/android/native_app_glue ${PROJECT_SOURCE_DIR}/platform/android/src ${PROJECT_SOURCE_DIR}/src + PRIVATE ${ANDROID_NDK}/sources/android/native_app_glue + ${PROJECT_SOURCE_DIR}/platform/android/src + ${PROJECT_SOURCE_DIR}/src ) +find_package(curl CONFIG) + target_link_libraries( mbgl-test-runner PRIVATE Mapbox::Base::jni.hpp mbgl-compiler-options + $<$:curl::curl_static> -Wl,--whole-archive mbgl-test -Wl,--no-whole-archive @@ -142,7 +147,7 @@ target_link_libraries( target_sources( mbgl-test-runner - PRIVATE ${PROJECT_SOURCE_DIR}/platform/android/MapLibreAndroid/src/cpp/http_file_source.cpp + PRIVATE ${PROJECT_SOURCE_DIR}/platform/default/src/mbgl/storage/http_file_source.cpp ) add_custom_command( diff --git a/platform/android/gradle/native-build.gradle b/platform/android/gradle/native-build.gradle index da54c41ae..bb3b669ee 100644 --- a/platform/android/gradle/native-build.gradle +++ b/platform/android/gradle/native-build.gradle @@ -7,15 +7,15 @@ ext.nativeBuild = { nativeTargets -> // added. When another ABI is specified explicitly, we're just going to build that ABI. In all other // cases, all ABIs are built. // - // When invoking from the command line or to override the device default, set `-Pmapbox.abis=...` to + // When invoking from the command line or to override the device default, set `-Pmaplibre.abis=...` to // only build the desired architectures. // // When building from Android Studio, gradle.properties sets `android.buildOnlyTargetAbi=true` so that // only the architecture for the device you're running on gets built. def abi = 'all' - if (!project.hasProperty('android.injected.invoked.from.ide') && project.hasProperty("mapbox.abis")) { - // Errors when the user invokes Gradle from the command line and didn't set mapbox.abis - abi = project.getProperty("mapbox.abis") + if (!project.hasProperty('android.injected.invoked.from.ide') && project.hasProperty("maplibre.abis")) { + // Errors when the user invokes Gradle from the command line and didn't set maplibre.abis + abi = project.getProperty("maplibre.abis") } if (abi != 'none') { diff --git a/platform/android/settings.gradle b/platform/android/settings.gradle index 35dabc60a..afd803053 100644 --- a/platform/android/settings.gradle +++ b/platform/android/settings.gradle @@ -5,4 +5,9 @@ rootProject.name = "MapLibre Native for Android" def renderTestProjectDir = new File(rootDir, '../../render-test/android') includeBuild(renderTestProjectDir) { name ="Render Test App" +} + +def cppTestProjectDir = new File(rootDir, '../../test/android') +includeBuild(cppTestProjectDir) { + name ="C++ Unit Test App" } \ No newline at end of file diff --git a/platform/android/src/test/test_runner.cpp b/platform/android/src/test/test_runner.cpp index 49d5d31f5..c4ea490a5 100644 --- a/platform/android/src/test/test_runner.cpp +++ b/platform/android/src/test/test_runner.cpp @@ -4,17 +4,65 @@ #include #include #include +#include +#include using namespace mbgl; using namespace mbgl::android; +std::string testResultToStr(const ::testing::TestResult* testResult) { + if (testResult->Passed()) return " (PASSED)"; + if (testResult->Skipped()) return " (SKIPPED)"; + if (testResult->Failed()) return " (FAILED)"; + return ""; +} + +class GTestAndroidLogger : public ::testing::EmptyTestEventListener { +public: + void OnTestProgramStart(const ::testing::UnitTest&) override { + __android_log_print(ANDROID_LOG_INFO, tag, "Test program started."); + } + + void OnTestProgramEnd(const ::testing::UnitTest& unit_test) override { + __android_log_print( + ANDROID_LOG_INFO, tag, "Test program ended with %d tests run.", unit_test.total_test_count()); + } + + void OnTestStart(const ::testing::TestInfo& test_info) override { + __android_log_print(ANDROID_LOG_INFO, tag, "Starting: %s.%s", test_info.test_case_name(), test_info.name()); + } + + void OnTestEnd(const ::testing::TestInfo& test_info) override { + __android_log_print(ANDROID_LOG_INFO, + tag, + "Finished: %s.%s%s", + test_info.test_case_name(), + test_info.name(), + testResultToStr(test_info.result()).c_str()); + } + + void OnTestPartResult(const ::testing::TestPartResult& test_part_result) override { + __android_log_print(ANDROID_LOG_INFO, + tag, + "%s: %s", + test_part_result.failed() ? "FAILED" : "PASSED", + test_part_result.summary()); + } + + const char* tag = "GTest"; +}; + std::atomic running{true}; std::atomic success{false}; std::once_flag done; ALooper* looper = NULL; -void runner() { - std::vector arguments = {"mbgl-test-runner", "--gtest_output=xml:/sdcard/test/results/results.xml"}; +void runner(const std::string& gtestOutput) { + ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); + listeners.Append(new GTestAndroidLogger); // takes ownership + + std::vector arguments = { + "mbgl-test-runner", "--gtest_output=xml:" + gtestOutput, "--gtest_filter=-StringIndexer.GetOOBIdentity"}; std::vector argv; for (const auto& arg : arguments) { argv.push_back((char*)arg.data()); @@ -38,14 +86,15 @@ void android_main(struct android_app* app) { std::string storagePath(app->activity->internalDataPath); std::string zipFile = storagePath + "/data.zip"; + const std::string gtestOutput = storagePath + "/results.xml"; if (copyFile(env, app->activity->assetManager, zipFile, storagePath, "data.zip")) { - if (chdir("/sdcard")) { - mbgl::Log::Error(mbgl::Event::General, "Failed to change the directory to /sdcard"); + if (chdir(storagePath.c_str())) { + mbgl::Log::Error(mbgl::Event::General, "Failed to change the directory to storage dir"); changeState(env, app, success); } else { - unZipFile(env, zipFile, "/sdcard/"); - runnerThread = std::thread(runner); + unZipFile(env, zipFile, storagePath); + runnerThread = std::thread(runner, gtestOutput); } } else { mbgl::Log::Error(mbgl::Event::General, "Failed to copy zip file '" + zipFile + "' to external storage"); diff --git a/render-test/android/app/build.gradle b/render-test/android/app/build.gradle index decbde0ba..b42bb0dbd 100644 --- a/render-test/android/app/build.gradle +++ b/render-test/android/app/build.gradle @@ -13,8 +13,8 @@ android { minSdkVersion 21 targetSdkVersion 33 def abi = 'all' - if (project.hasProperty('mapbox.abis')) { - abi = project.getProperty('mapbox.abis') + if (project.hasProperty('maplibre.abis')) { + abi = project.getProperty('maplibre.abis') } ndk { if (abi != 'all' && abi != 'none') { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fa969fada..a15efa01c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -145,7 +145,7 @@ if(MLN_WITH_OPENGL) ) endif() -if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL Android AND ANDROID_NATIVE_API_LEVEL VERSION_LESS 24) +if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL Android) message("Target platform does not support HTTP tests or dependencies not found.") set(MLN_TEST_HAS_TEST_SERVER 0) diff --git a/test/android/app/build.gradle b/test/android/app/build.gradle index 7a873df20..c289a06b8 100644 --- a/test/android/app/build.gradle +++ b/test/android/app/build.gradle @@ -3,18 +3,21 @@ apply plugin: 'com.android.application' android { namespace "android.app" - compileSdkVersion 33 + buildFeatures { + prefab true + } defaultConfig { - applicationId = 'com.mapbox.mapboxsdk.maps.test_runner' + applicationId = 'org.maplibre.cpp_test_runner' minSdkVersion 28 targetSdkVersion 33 + compileSdk 34 def abi = 'all' - if (project.hasProperty('mapbox.abis')) { - abi = project.getProperty('mapbox.abis') + if (project.hasProperty('maplibre.abis')) { + abi = project.getProperty('maplibre.abis') } ndk { - if (abi != 'all') { + if (abi != 'all' && abi != 'none') { abiFilters abi.split(' ') } else { abiFilters 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64' @@ -22,6 +25,7 @@ android { } externalNativeBuild { cmake { + arguments "-DMLN_LEGACY_RENDERER=OFF", "-DMLN_DRAWABLE_RENDERER=ON" arguments '-DANDROID_CCACHE=ccache' arguments '-DANDROID_STL=c++_static' targets 'mbgl-test-runner' @@ -34,7 +38,7 @@ android { } externalNativeBuild { cmake { - version '3.10.2' + version "3.18.1+" path '../../../CMakeLists.txt' } } @@ -51,4 +55,5 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' androidTestImplementation 'androidx.test:rules:1.5.0' + implementation 'io.github.vvb2060.ndk:curl:8.8.0' } diff --git a/test/android/app/src/main/AndroidManifest.xml b/test/android/app/src/main/AndroidManifest.xml index 5311bf050..7a6e7c632 100644 --- a/test/android/app/src/main/AndroidManifest.xml +++ b/test/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ @@ -15,6 +14,7 @@ android:hasCode="true"> diff --git a/test/android/build.gradle b/test/android/build.gradle index 1cf0c0e59..18b17e2b3 100644 --- a/test/android/build.gradle +++ b/test/android/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.4.1' } } @@ -18,4 +18,4 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/test/android/gradle.properties b/test/android/gradle.properties index 6ed0f8f96..5e2afb41a 100644 --- a/test/android/gradle.properties +++ b/test/android/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=true diff --git a/test/android/settings.gradle b/test/android/settings.gradle index 573abcb32..b9725fc93 100644 --- a/test/android/settings.gradle +++ b/test/android/settings.gradle @@ -1,2 +1,4 @@ +rootProject.name = "androidCppUnitTests" + include ':app'