diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 3bc84c4b1..c925e8b46 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -31,6 +31,36 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + jni-lifetimes: + name: Android HTTP JNI lifetime regressions + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Setup Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: 'temurin' + java-version: '17' + - name: Build production client and regression harness + run: >- + cmake -S tests/android-http-client -B out-http-jni -G Ninja + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" + -DANDROID_ABI=x86_64 -DANDROID_PLATFORM=android-30 + -DANDROID_STL=c++_static -DCMAKE_BUILD_TYPE=Debug + - name: Compile JNI tests + run: cmake --build out-http-jni --target android-http-client-tests --parallel 2 + - name: Enable KVM + run: sudo chmod a+rw /dev/kvm + - name: Run ART reference lifetime tests with CheckJNI + uses: reactivecircus/android-emulator-runner@324029e2f414c084d8b15ba075288885e74aef9c # v2.34.0 + with: + api-level: 30 + arch: x86_64 + target: google_apis + disable-animations: true + script: python3 tests/android-http-client/run-tests.py --sdk "$ANDROID_HOME" --library out-http-jni/libandroid-http-client-tests.so + no-exceptions: name: Android without C++ exceptions (${{ matrix.abi }}, Room=${{ matrix.room }}) runs-on: ubuntu-latest @@ -44,7 +74,8 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Configure full JNI SDK run: >- - cmake -S . -B out-no-exceptions -G Ninja + cmake -S tests/android-storage -B out-no-exceptions -G Ninja + -DEXPECT_ROOM=${{ matrix.room }} -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" -DANDROID_ABI=${{ matrix.abi }} -DANDROID_PLATFORM=android-24 @@ -59,6 +90,8 @@ jobs: -DMATSDK_BUILD_PACKAGE=OFF - name: Compile full SDK run: cmake --build out-no-exceptions --target mat --parallel 2 + - name: Verify storage option compatibility + run: cmake -P tests/android-storage/options-tests.cmake build: runs-on: windows-latest diff --git a/cmake/MatsdkOptions.cmake b/cmake/MatsdkOptions.cmake index 433f035b3..ac8a38c7e 100644 --- a/cmake/MatsdkOptions.cmake +++ b/cmake/MatsdkOptions.cmake @@ -18,6 +18,15 @@ option(MATSDK_BUILD_FUNC_TESTS "Build functional tests" "${MATSDK_PROJECT_IS_TOP_LEVEL}") option(MATSDK_BUILD_JNI_WRAPPER "Build the JNI wrapper" OFF) +if(DEFINED USE_ROOM) + if(NOT DEFINED MATSDK_ANDROID_USE_ROOM) + set(MATSDK_ANDROID_USE_ROOM "${USE_ROOM}" CACHE BOOL + "Use Android Room for offline storage") + endif() + message(DEPRECATION + "USE_ROOM is deprecated; use MATSDK_ANDROID_USE_ROOM instead. " + "MATSDK_ANDROID_USE_ROOM=${MATSDK_ANDROID_USE_ROOM} takes precedence.") +endif() option(MATSDK_ANDROID_USE_ROOM "Use Android Room for offline storage" OFF) option(MATSDK_ENABLE_CAPI_HTTP_CLIENT diff --git a/lib/android_build/maesdk/src/main/cpp/CMakeLists.txt b/lib/android_build/maesdk/src/main/cpp/CMakeLists.txt index 6712289dd..dd0042230 100644 --- a/lib/android_build/maesdk/src/main/cpp/CMakeLists.txt +++ b/lib/android_build/maesdk/src/main/cpp/CMakeLists.txt @@ -6,7 +6,9 @@ get_filename_component(SDK_ROOT set(BUILD_SHARED_LIBS ON CACHE BOOL "") set(MATSDK_BUILD_JNI_WRAPPER ON CACHE BOOL "" FORCE) -set(MATSDK_ANDROID_USE_ROOM ON CACHE BOOL "" FORCE) +if(NOT DEFINED MATSDK_ANDROID_USE_ROOM AND NOT DEFINED USE_ROOM) + set(MATSDK_ANDROID_USE_ROOM ON CACHE BOOL "Use Android Room for offline storage") +endif() set(MATSDK_BUILD_PACKAGE OFF CACHE BOOL "" FORCE) set(MATSDK_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) set(MATSDK_BUILD_FUNC_TESTS OFF CACHE BOOL "" FORCE) diff --git a/lib/http/HttpClient_Android.cpp b/lib/http/HttpClient_Android.cpp index 7b1af48bf..aef92c828 100644 --- a/lib/http/HttpClient_Android.cpp +++ b/lib/http/HttpClient_Android.cpp @@ -10,6 +10,35 @@ #include #include +namespace +{ + class LocalFrame + { + public: + explicit LocalFrame(JNIEnv* env) : + m_env(env), m_pushed(env->PushLocalFrame(16) == JNI_OK) + { + } + + ~LocalFrame() + { + if (m_pushed) + { + m_env->PopLocalFrame(nullptr); + } + } + + bool IsValid() const + { + return m_pushed; + } + + private: + JNIEnv* m_env; + bool m_pushed; + }; +} + namespace MAT_NS_BEGIN { HttpClient_Android::HttpRequest::~HttpRequest() noexcept @@ -22,7 +51,16 @@ namespace MAT_NS_BEGIN { return; } + ReleaseJavaRequest(env); + } + } + + void HttpClient_Android::HttpRequest::ReleaseJavaRequest(JNIEnv* env) + { + if (m_java_request) + { env->DeleteGlobalRef(m_java_request); + m_java_request = nullptr; } } @@ -88,7 +126,7 @@ namespace MAT_NS_BEGIN IHttpResponseCallback* callback) { JNIEnv* env = nullptr; - std::string const& target_id(request->GetId()); + std::string const target_id(request->GetId()); if (s_java_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) { @@ -134,14 +172,22 @@ namespace MAT_NS_BEGIN total_size += u.first.length() + u.second.length(); } - static constexpr int FrameSize = 16; // at most 16 local refs in the frame - auto pushed = env->PushLocalFrame(FrameSize); - if (CheckException(env, r)) + LocalFrame frame(env); + if (CheckException(env, target_id)) { return; } + if (!frame.IsValid()) + { + auto failed = GetAndRemoveRequest(target_id); + if (failed) + { + CallbackForCancel(env, failed); + } + return; + } auto header_buffer = env->NewByteArray(static_cast(total_size)); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } @@ -156,7 +202,7 @@ namespace MAT_NS_BEGIN { env->SetByteArrayRegion(header_buffer, offset, key_len, reinterpret_cast(u.first.data())); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } @@ -168,7 +214,7 @@ namespace MAT_NS_BEGIN { env->SetByteArrayRegion(header_buffer, offset, value_len, reinterpret_cast(u.second.data())); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } @@ -176,39 +222,39 @@ namespace MAT_NS_BEGIN offset += value_len; } auto header_lengths = env->NewIntArray(static_cast(index_buffer.size())); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } env->SetIntArrayRegion(header_lengths, 0, index_buffer.size(), index_buffer.data()); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } auto body = env->NewByteArray(r->m_body.size()); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } env->SetByteArrayRegion(body, 0, r->m_body.size(), reinterpret_cast(r->m_body.data())); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } auto java_id = env->NewStringUTF(request->GetId().c_str()); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } auto url_id = env->NewStringUTF(r->m_url.c_str()); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } auto method_id = env->NewStringUTF(r->m_method.c_str()); - if (CheckException(env, r)) + if (CheckException(env, target_id)) { return; } @@ -231,10 +277,9 @@ namespace MAT_NS_BEGIN header_lengths, header_buffer); } - if (pushed == JNI_OK) + if (CheckException(env, target_id)) { - // pass the return value "result" through the local frame pop - result = env->PopLocalFrame(result); + return; } HttpRequest* cancel_me = nullptr; @@ -263,6 +308,10 @@ namespace MAT_NS_BEGIN } } } + if (CheckException(env, target_id)) + { + return; + } if (cancel_me) { CallbackForCancel(env, cancel_me); @@ -270,6 +319,7 @@ namespace MAT_NS_BEGIN else { env->CallVoidMethod(m_client, m_execute_id, result); + CheckException(env, target_id); } } @@ -278,8 +328,18 @@ namespace MAT_NS_BEGIN if (request->m_java_request && env) { auto request_class = env->GetObjectClass(request->m_java_request); - auto cancel_method = env->GetMethodID(request_class, "cancel", "(Z)Z"); - env->CallBooleanMethod(request->m_java_request, cancel_method, static_cast(JNI_TRUE)); + auto cancel_method = request_class ? env->GetMethodID(request_class, "cancel", "(Z)Z") : nullptr; + if (cancel_method) + { + env->CallBooleanMethod(request->m_java_request, cancel_method, static_cast(JNI_TRUE)); + } + env->DeleteLocalRef(request_class); + request->ReleaseJavaRequest(env); + if (env->ExceptionCheck()) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } } if (request->m_callback) { @@ -379,11 +439,12 @@ namespace MAT_NS_BEGIN env->DeleteGlobalRef(m_client); } m_client = env->NewGlobalRef(client_); - m_client_class = env->GetObjectClass(m_client); - m_create_id = env->GetMethodID(m_client_class, "createTask", + auto client_class = env->GetObjectClass(m_client); + m_create_id = env->GetMethodID(client_class, "createTask", "(Ljava/lang/String;Ljava/lang/String;[BLjava/lang/String;[I[B)Ljava/util/concurrent/FutureTask;"); - m_execute_id = env->GetMethodID(m_client_class, "executeTask", + m_execute_id = env->GetMethodID(client_class, "executeTask", "(Ljava/util/concurrent/FutureTask;)V"); + env->DeleteLocalRef(client_class); env->GetJavaVM(&s_java_vm); } @@ -485,7 +546,7 @@ namespace MAT_NS_BEGIN return s_client; } - bool HttpClient_Android::CheckException(JNIEnv* env, HttpRequest* request) + bool HttpClient_Android::CheckException(JNIEnv* env, std::string const& id) { if (env->ExceptionCheck() == JNI_FALSE) { @@ -493,7 +554,11 @@ namespace MAT_NS_BEGIN } env->ExceptionDescribe(); env->ExceptionClear(); - CallbackForCancel(env, request); + auto request = GetAndRemoveRequest(id); + if (request) + { + CallbackForCancel(env, request); + } return true; } @@ -559,6 +624,7 @@ extern "C" JNIEXPORT void { return; } + request->ReleaseJavaRequest(env); auto callback = request->GetCallback(); auto response = new Microsoft::Applications::Events::HttpClient_Android::HttpResponse(request->GetId()); response->SetResponse(statusCode); diff --git a/lib/http/HttpClient_Android.hpp b/lib/http/HttpClient_Android.hpp index 65d4af7a5..d3779bc5a 100644 --- a/lib/http/HttpClient_Android.hpp +++ b/lib/http/HttpClient_Android.hpp @@ -159,6 +159,8 @@ namespace MAT_NS_BEGIN m_parent.EraseRequest(this); } + void ReleaseJavaRequest(JNIEnv* env); + protected: HttpClient_Android& m_parent; HttpHeaders m_headers; @@ -199,7 +201,6 @@ namespace MAT_NS_BEGIN std::vector m_requests; jobject m_client = nullptr; - jclass m_client_class = nullptr; jmethodID m_send_id = nullptr; jmethodID m_create_id = nullptr; jmethodID m_execute_id = nullptr; @@ -209,7 +210,7 @@ namespace MAT_NS_BEGIN static std::shared_ptr s_client; static std::string s_cache_file_path; - bool CheckException(JNIEnv* env, HttpRequest* request); + bool CheckException(JNIEnv* env, std::string const& id); }; } diff --git a/tests/android-http-client/CMakeLists.txt b/tests/android-http-client/CMakeLists.txt new file mode 100644 index 000000000..c6d28dbb8 --- /dev/null +++ b/tests/android-http-client/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.19...3.31) +project(AndroidHttpClientTests LANGUAGES C CXX) + +if(NOT ANDROID) + message(FATAL_ERROR "These JNI lifetime tests must run on Android ART.") +endif() + +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_JNI_WRAPPER OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_FUNC_TESTS OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_TEST_TOOL OFF CACHE BOOL "" FORCE) +set(MATSDK_BUILD_PACKAGE OFF CACHE BOOL "" FORCE) +add_subdirectory(../.. sdk) + +add_library(android-http-client-tests SHARED android-http-client-tests.cpp) +target_compile_features(android-http-client-tests PRIVATE cxx_std_14) +target_include_directories(android-http-client-tests PRIVATE + ../../lib + ../../lib/include + ../../lib/include/mat + ../../lib/pal + ../../lib/utils) +target_link_libraries(android-http-client-tests PRIVATE + mat matsdk_internal_config matsdk_build_options) diff --git a/tests/android-http-client/HttpClient.java b/tests/android-http-client/HttpClient.java new file mode 100644 index 000000000..e985143b7 --- /dev/null +++ b/tests/android-http-client/HttpClient.java @@ -0,0 +1,112 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +package com.microsoft.applications.events; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +// A deterministic Java peer for the production C++ client, without network or AndroidX dependencies. +public final class HttpClient { + private final ArrayList> tasks = new ArrayList<>(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private int mode; + + private final class FutureShim extends FutureTask { + FutureShim(String id) { + super(() -> { + dispatchCallback(id, mode == 1 ? -1 : 200, new String[0], new byte[0]); + return true; + }); + } + + @Override + public boolean cancel(boolean interrupt) { + if (mode == 9) { + throw new IllegalStateException("Injected cancel failure"); + } + return super.cancel(interrupt); + } + } + + public FutureTask createTask( + String url, String method, byte[] body, String id, int[] lengths, byte[] headers) { + FutureShim task = new FutureShim(id); + tasks.add(new WeakReference<>(task)); + if (mode == 5) { + cancelPreparing(id); + } + if (mode == 6) { + throw new IllegalStateException("Injected create failure"); + } + return mode == 12 ? null : task; + } + + public void executeTask(FutureTask task) { + if (mode == 7) { + throw new IllegalStateException("Injected execute failure"); + } + if (mode == 0 || mode == 1 || mode == 8) { + task.run(); + } else if (mode == 10 || mode == 11) { + executor.execute(task); + } + } + + private native void dispatchCallback(String id, int status, String[] headers, byte[] body); + private native void cancelPreparing(String id); + private native void startBatch(int mode, int count); + private native boolean finishBatch(); + + public static void main(String[] args) throws Exception { + System.load(args[0]); + HttpClient client = new HttpClient(); + String[] cases = {"success", "network-failure", "cancel", "cancel-all", "early-cancel", + "preparing-cancel", "create-exception", "execute-exception", + "callback-deletes-request", "cancel-exception", "async-success", "cancel-completion-race", + "null-task"}; + String selected = args.length > 1 ? args[1] : "all"; + int stressCount = args.length > 2 ? Integer.parseInt(args[2]) : 60000; + boolean ran = false; + try { + for (int mode = 0; mode < cases.length; ++mode) { + if (!selected.equals("all") && !selected.equals(cases[mode])) { + continue; + } + ran = true; + client.mode = mode; + client.tasks.clear(); + client.startBatch(mode, mode == 0 ? stressCount : mode == 6 || mode == 7 || mode == 9 ? 4 : 1024); + int live = 0; + for (int attempt = 0; attempt < 20; ++attempt) { + System.gc(); + System.runFinalization(); + Thread.sleep(50); + live = 0; + for (WeakReference task : client.tasks) { + if (task.get() != null) { + ++live; + } + } + if (live == 0) { + break; + } + } + boolean callbacksPassed = client.finishBatch(); + if (!callbacksPassed || live != 0) { + throw new AssertionError(cases[mode] + ": callbacks=" + callbacksPassed + ", retained=" + live); + } + System.out.println("PASS " + cases[mode] + ": retained FutureShim objects=" + live); + } + } finally { + client.executor.shutdownNow(); + } + if (!ran) { + throw new IllegalArgumentException("Unknown test case: " + selected); + } + } +} diff --git a/tests/android-http-client/android-http-client-tests.cpp b/tests/android-http-client/android-http-client-tests.cpp new file mode 100644 index 000000000..6fcb99568 --- /dev/null +++ b/tests/android-http-client/android-http-client-tests.cpp @@ -0,0 +1,162 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#include "http/HttpClient_Android.hpp" + +#include +#include +#include +#include +#include + +using namespace Microsoft::Applications::Events; + +namespace +{ + JavaVM* vm = nullptr; + std::thread worker; + std::mutex mutex; + std::condition_variable changed; + bool ready = false; + bool finish = false; + bool passed = false; + + class Callback : public IHttpResponseCallback + { + public: + void OnHttpResponse(IHttpResponse* response) override + { + std::unique_ptr owned(response); + result = response->GetResult(); + if (deleteRequest) + { + delete request; + request = nullptr; + } + ++calls; + } + + IHttpRequest* request = nullptr; + bool deleteRequest = false; + std::atomic calls{0}; + HttpResult result = HttpResult_LocalFailure; + }; + + void RunBatch(jint mode, jint count) + { + JNIEnv* attached = nullptr; + vm->AttachCurrentThread(&attached, nullptr); + auto nativeClient = HttpClient_Android::GetClientInstance(); + std::vector> callbacks; + for (int i = 0; i < count; ++i) + { + std::unique_ptr callback(new Callback); + callback->request = nativeClient->CreateRequest(); + callback->request->SetUrl("https://unused.invalid/"); + callback->request->SetMethod("POST"); + callback->request->GetHeaders().set("test", "value"); + std::vector body{1, 2, 3}; + callback->request->SetBody(body); + callback->deleteRequest = mode == 8; + if (mode == 4) + { + nativeClient->CancelRequestAsync(callback->request->GetId()); + } + nativeClient->SendRequestAsync(callback->request, callback.get()); + if (mode == 2 || mode == 9 || mode == 11) + { + nativeClient->CancelRequestAsync(callback->request->GetId()); + } + if (mode == 3) + { + callbacks.push_back(std::move(callback)); + } + else + { + if (mode == 10 || mode == 11) + { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (callback->calls == 0 && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::yield(); + } + } + auto expected = HttpResult_Aborted; + if (mode == 0 || mode == 8 || mode == 10) + { + expected = HttpResult_OK; + } + else if (mode == 1) + { + expected = HttpResult_NetworkFailure; + } + passed = passed && callback->calls == 1 && + (callback->result == expected || (mode == 11 && callback->result == HttpResult_OK)); + callbacks.push_back(std::move(callback)); + } + passed = passed && !attached->ExceptionCheck(); + } + if (mode == 3) + { + nativeClient->CancelAllRequests(); + for (auto const& callback : callbacks) + { + passed = passed && callback->calls == 1 && callback->result == HttpResult_Aborted; + } + } + { + std::unique_lock lock(mutex); + ready = true; + changed.notify_all(); + while (!finish) + { + changed.wait(lock); + } + } + // Keep native requests AND the attached thread alive until Java checks weak references. + for (auto const& callback : callbacks) + { + delete callback->request; + } + nativeClient.reset(); + vm->DetachCurrentThread(); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_com_microsoft_applications_events_HttpClient_cancelPreparing(JNIEnv* env, jobject, jstring id) +{ + const char* text = env->GetStringUTFChars(id, nullptr); + HttpClient_Android::GetClientInstance()->CancelRequestAsync(text); + env->ReleaseStringUTFChars(id, text); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_microsoft_applications_events_HttpClient_startBatch(JNIEnv* env, jobject client, jint mode, jint count) +{ + env->GetJavaVM(&vm); + HttpClient_Android::CreateClientInstance(env, client); + ready = false; + finish = false; + passed = true; + worker = std::thread(RunBatch, mode, count); + std::unique_lock lock(mutex); + while (!ready) + { + changed.wait(lock); + } +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_com_microsoft_applications_events_HttpClient_finishBatch(JNIEnv* env, jobject client) +{ + { + std::lock_guard lock(mutex); + finish = true; + changed.notify_all(); + } + worker.join(); + HttpClient_Android::DeleteClientInstance(env, client); + return passed ? JNI_TRUE : JNI_FALSE; +} diff --git a/tests/android-http-client/run-tests.py b/tests/android-http-client/run-tests.py new file mode 100644 index 000000000..85dcb4105 --- /dev/null +++ b/tests/android-http-client/run-tests.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +"""Run the production Android HttpClient JNI lifetime tests on a booted emulator.""" + +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--sdk", required=True, type=Path) + parser.add_argument("--library", required=True, type=Path) + parser.add_argument("--java-home", type=Path, default=os.environ.get("JAVA_HOME")) + parser.add_argument("--case", default="all") + parser.add_argument("--stress-count", type=int, default=60000) + args = parser.parse_args() + suffix = ".exe" if os.name == "nt" else "" + adb = args.sdk / "platform-tools" / ("adb" + suffix) + javac = args.java_home / "bin" / ("javac" + suffix) if args.java_home else "javac" + build_tools = sorted( + (args.sdk / "build-tools").iterdir(), + key=lambda path: tuple(int(part) for part in path.name.split(".")), + )[-1] + d8 = build_tools / ("d8.bat" if os.name == "nt" else "d8") + + def run(*command, **kwargs): + environment = os.environ.copy() + if args.java_home: + environment["JAVA_HOME"] = str(args.java_home) + return subprocess.run( + [str(arg) for arg in command], check=True, timeout=180, env=environment, **kwargs) + + with tempfile.TemporaryDirectory(prefix="matsdk-jni-") as directory: + output = Path(directory) + run(javac, "--release", "8", "-d", output, + Path(__file__).with_name("HttpClient.java")) + libraries = ["--lib", args.java_home] if args.java_home else [] + run(d8, "--min-api", "24", *libraries, "--output", output, *output.rglob("*.class")) + remote = "/data/local/tmp/" + output.name + run(adb, "shell", "mkdir", "-p", remote) + try: + run(adb, "push", output / "classes.dex", remote + "/classes.dex") + run(adb, "push", args.library.resolve(), remote + "/libandroid-http-client-tests.so") + run(adb, "shell", "CLASSPATH=" + remote + "/classes.dex", + "app_process", "-Xcheck:jni", "/system/bin", + "com.microsoft.applications.events.HttpClient", + remote + "/libandroid-http-client-tests.so", args.case, args.stress_count) + finally: + run(adb, "shell", "rm", "-f", remote + "/classes.dex", + remote + "/libandroid-http-client-tests.so") + run(adb, "shell", "rmdir", remote) + + +if __name__ == "__main__": + main() diff --git a/tests/android-storage/CMakeLists.txt b/tests/android-storage/CMakeLists.txt new file mode 100644 index 000000000..b54cf50ef --- /dev/null +++ b/tests/android-storage/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.19...3.31) +project(AndroidStorageSelectionTests LANGUAGES C CXX) + +if(NOT ANDROID OR NOT DEFINED EXPECT_ROOM) + message(FATAL_ERROR "Configure with the Android toolchain and -DEXPECT_ROOM=ON or OFF.") +endif() + +add_subdirectory(../../lib/android_build/maesdk/src/main/cpp aar) + +if((EXPECT_ROOM AND NOT MATSDK_ANDROID_USE_ROOM) + OR (NOT EXPECT_ROOM AND MATSDK_ANDROID_USE_ROOM)) + message(FATAL_ERROR "The AAR wrapper overrode the selected storage backend.") +endif() +get_target_property(sources mat SOURCES) +get_target_property(definitions matsdk_internal_config INTERFACE_COMPILE_DEFINITIONS) +if(EXPECT_ROOM) + if(NOT "offline/OfflineStorage_Room.cpp" IN_LIST sources + OR "offline/OfflineStorage_SQLite.cpp" IN_LIST sources + OR NOT "USE_ROOM" IN_LIST definitions) + message(FATAL_ERROR "Room storage sources and compile definitions do not agree.") + endif() +else() + if(NOT "offline/OfflineStorage_SQLite.cpp" IN_LIST sources + OR "offline/OfflineStorage_Room.cpp" IN_LIST sources + OR "USE_ROOM" IN_LIST definitions + OR NOT TARGET SQLite3::SQLite3) + message(FATAL_ERROR "SQLite storage sources, dependency, and compile definitions do not agree.") + endif() +endif() +message(STATUS "AAR storage selection passed: Room=${MATSDK_ANDROID_USE_ROOM}") diff --git a/tests/android-storage/options-tests.cmake b/tests/android-storage/options-tests.cmake new file mode 100644 index 000000000..4bd9fdb32 --- /dev/null +++ b/tests/android-storage/options-tests.cmake @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.19) + +function(check_options expected canonical legacy) + unset(MATSDK_ANDROID_USE_ROOM CACHE) + unset(USE_ROOM CACHE) + if(NOT canonical STREQUAL "unset") + set(MATSDK_ANDROID_USE_ROOM "${canonical}" CACHE BOOL "") + endif() + if(NOT legacy STREQUAL "unset") + set(USE_ROOM "${legacy}" CACHE BOOL "") + endif() + set(CMAKE_SYSTEM_NAME Android) + include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/MatsdkOptions.cmake") + if((expected AND NOT MATSDK_ANDROID_USE_ROOM) + OR (NOT expected AND MATSDK_ANDROID_USE_ROOM)) + message(FATAL_ERROR "Unexpected Room selection: canonical=${canonical}, legacy=${legacy}") + endif() + if(expected AND NOT MATSDK_SQLITE_PROVIDER_RESOLVED STREQUAL "NONE") + message(FATAL_ERROR "Room must not depend on SQLite by default.") + elseif(NOT expected AND NOT MATSDK_SQLITE_PROVIDER_RESOLVED STREQUAL "VENDORED") + message(FATAL_ERROR "SQLite storage must resolve its native dependency.") + endif() +endfunction() + +check_options(OFF unset unset) +check_options(ON ON unset) +check_options(OFF OFF unset) +check_options(ON unset ON) +check_options(OFF unset OFF) +check_options(ON ON OFF) +check_options(OFF OFF ON) +message(STATUS "Android storage option compatibility and precedence passed")