|  | // Copyright 2012 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "base/android/jni_android.h" | 
|  |  | 
|  | #include <optional> | 
|  | #include <string> | 
|  |  | 
|  | #include "base/android/java_exception_reporter.h" | 
|  | #include "base/at_exit.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/logging.h" | 
|  | #include "base/memory/raw_ptr.h" | 
|  | #include "base/test/scoped_feature_list.h" | 
|  | #include "base/threading/thread.h" | 
|  | #include "base/time/time.h" | 
|  | #include "testing/gmock/include/gmock/gmock.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | // Must come after all headers that specialize FromJniType() / ToJniType(). | 
|  | #include "base/base_unittest_support_jni/JniAndroidTestUtils_jni.h" | 
|  |  | 
|  | using ::testing::Eq; | 
|  | using ::testing::Optional; | 
|  | using ::testing::StartsWith; | 
|  |  | 
|  | namespace base { | 
|  | namespace android { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | class JniAndroidExceptionTestContext final { | 
|  | public: | 
|  | JniAndroidExceptionTestContext() { | 
|  | CHECK(instance == nullptr); | 
|  | instance = this; | 
|  | SetJavaExceptionCallback(CapturingExceptionCallback); | 
|  | g_log_fatal_callback_for_testing = CapturingLogFatalCallback; | 
|  | } | 
|  |  | 
|  | ~JniAndroidExceptionTestContext() { | 
|  | g_log_fatal_callback_for_testing = nullptr; | 
|  | SetJavaExceptionCallback(prev_exception_callback_); | 
|  | env->ExceptionClear(); | 
|  | Java_JniAndroidTestUtils_restoreGlobalExceptionHandler(env); | 
|  | instance = nullptr; | 
|  | } | 
|  |  | 
|  | private: | 
|  | static void CapturingLogFatalCallback(const char* message) { | 
|  | auto* self = instance; | 
|  | CHECK(self); | 
|  | // Capture only the first one (can be called multiple times due to | 
|  | // LOG(FATAL) not terminating). | 
|  | if (!self->assertion_message) { | 
|  | self->assertion_message = message; | 
|  | } | 
|  | } | 
|  |  | 
|  | static void CapturingExceptionCallback(const char* message) { | 
|  | auto* self = instance; | 
|  | CHECK(self); | 
|  | if (self->throw_in_exception_callback) { | 
|  | self->throw_in_exception_callback = false; | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(self->env); | 
|  | } else if (self->throw_oom_in_exception_callback) { | 
|  | self->throw_oom_in_exception_callback = false; | 
|  | Java_JniAndroidTestUtils_throwOutOfMemoryError(self->env); | 
|  | } else { | 
|  | self->last_java_exception = message; | 
|  | } | 
|  | } | 
|  |  | 
|  | static JniAndroidExceptionTestContext* instance; | 
|  | const JavaExceptionCallback prev_exception_callback_ = | 
|  | GetJavaExceptionCallback(); | 
|  |  | 
|  | public: | 
|  | const raw_ptr<JNIEnv> env = base::android::AttachCurrentThread(); | 
|  | bool throw_in_exception_callback = false; | 
|  | bool throw_oom_in_exception_callback = false; | 
|  | std::optional<std::string> assertion_message; | 
|  | std::optional<std::string> last_java_exception; | 
|  | }; | 
|  |  | 
|  | JniAndroidExceptionTestContext* JniAndroidExceptionTestContext::instance = | 
|  | nullptr; | 
|  |  | 
|  | std::atomic<jmethodID> g_atomic_id(nullptr); | 
|  | int LazyMethodIDCall(JNIEnv* env, jclass clazz, int p) { | 
|  | jmethodID id = | 
|  | base::android::MethodID::LazyGet<base::android::MethodID::TYPE_STATIC>( | 
|  | env, clazz, "abs", "(I)I", &g_atomic_id); | 
|  |  | 
|  | return env->CallStaticIntMethod(clazz, id, p); | 
|  | } | 
|  |  | 
|  | int MethodIDCall(JNIEnv* env, jclass clazz, jmethodID id, int p) { | 
|  | return env->CallStaticIntMethod(clazz, id, p); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | TEST(JNIAndroidMicrobenchmark, MethodId) { | 
|  | JNIEnv* env = AttachCurrentThread(); | 
|  | ScopedJavaLocalRef<jclass> clazz(GetClass(env, "java/lang/Math")); | 
|  | base::Time start_lazy = base::Time::Now(); | 
|  | int o = 0; | 
|  | for (int i = 0; i < 1024; ++i) { | 
|  | o += LazyMethodIDCall(env, clazz.obj(), i); | 
|  | } | 
|  | base::Time end_lazy = base::Time::Now(); | 
|  |  | 
|  | jmethodID id = g_atomic_id; | 
|  | base::Time start = base::Time::Now(); | 
|  | for (int i = 0; i < 1024; ++i) { | 
|  | o += MethodIDCall(env, clazz.obj(), id, i); | 
|  | } | 
|  | base::Time end = base::Time::Now(); | 
|  |  | 
|  | // On a Galaxy Nexus, results were in the range of: | 
|  | // JNI LazyMethodIDCall (us) 1984 | 
|  | // JNI MethodIDCall (us) 1861 | 
|  | LOG(ERROR) << "JNI LazyMethodIDCall (us) " | 
|  | << base::TimeDelta(end_lazy - start_lazy).InMicroseconds(); | 
|  | LOG(ERROR) << "JNI MethodIDCall (us) " | 
|  | << base::TimeDelta(end - start).InMicroseconds(); | 
|  | LOG(ERROR) << "JNI " << o; | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidTest, GetJavaStackTraceIfPresent_Normal) { | 
|  | // The main thread should always have Java frames in it. | 
|  | EXPECT_THAT(GetJavaStackTraceIfPresent(), StartsWith("\tat")); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidTest, GetJavaStackTraceIfPresent_NoEnv) { | 
|  | class HelperThread : public Thread { | 
|  | public: | 
|  | HelperThread() | 
|  | : Thread("TestThread"), java_stack_1_("X"), java_stack_2_("X") {} | 
|  |  | 
|  | void Init() override { | 
|  | // Test without a JNIEnv. | 
|  | java_stack_1_ = GetJavaStackTraceIfPresent(); | 
|  |  | 
|  | // Test with a JNIEnv but no Java frames. | 
|  | AttachCurrentThread(); | 
|  | java_stack_2_ = GetJavaStackTraceIfPresent(); | 
|  | } | 
|  |  | 
|  | std::string java_stack_1_; | 
|  | std::string java_stack_2_; | 
|  | }; | 
|  |  | 
|  | HelperThread t; | 
|  | t.StartAndWaitForTesting(); | 
|  | EXPECT_EQ(t.java_stack_1_, ""); | 
|  | EXPECT_EQ(t.java_stack_2_, ""); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidTest, GetJavaStackTraceIfPresent_PendingException) { | 
|  | JNIEnv* env = base::android::AttachCurrentThread(); | 
|  | Java_JniAndroidTestUtils_throwRuntimeExceptionUnchecked(env); | 
|  | std::string result = GetJavaStackTraceIfPresent(); | 
|  | env->ExceptionClear(); | 
|  | EXPECT_EQ(result, kUnableToGetStackTraceMessage); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidTest, GetJavaStackTraceIfPresent_OutOfMemoryError) { | 
|  | JNIEnv* env = base::android::AttachCurrentThread(); | 
|  | Java_JniAndroidTestUtils_setSimulateOomInSanitizedStacktrace(env, true); | 
|  | std::string result = GetJavaStackTraceIfPresent(); | 
|  | Java_JniAndroidTestUtils_setSimulateOomInSanitizedStacktrace(env, false); | 
|  | EXPECT_EQ(result, ""); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInNative) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | test::ScopedFeatureList feature_list; | 
|  | feature_list.InitFromCommandLine("", "HandleJniExceptionsInJava"); | 
|  |  | 
|  | // Do not call setGlobalExceptionHandlerAsNoOp(). | 
|  |  | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(StartsWith("java.lang.RuntimeException"))); | 
|  | EXPECT_THAT(ctx.assertion_message, Optional(Eq(kUncaughtExceptionMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_NoOpHandler) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerAsNoOp(ctx.env); | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  |  | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(StartsWith("java.lang.RuntimeException"))); | 
|  | EXPECT_THAT(ctx.assertion_message, | 
|  | Optional(Eq(kUncaughtExceptionHandlerFailedMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_ThrowingHandler) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerToThrow(ctx.env); | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  |  | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(StartsWith("java.lang.IllegalStateException"))); | 
|  | EXPECT_THAT(ctx.assertion_message, | 
|  | Optional(Eq(kUncaughtExceptionHandlerFailedMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_OomThrowingHandler) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerToThrowOom(ctx.env); | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  |  | 
|  | // Should still report the original exception when the global exception | 
|  | // handler throws an OutOfMemoryError. | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(StartsWith("java.lang.RuntimeException"))); | 
|  | EXPECT_THAT(ctx.assertion_message, | 
|  | Optional(Eq(kUncaughtExceptionHandlerFailedMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_OomInGetJavaExceptionInfo) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerToThrowOom(ctx.env); | 
|  | Java_JniAndroidTestUtils_setSimulateOomInSanitizedStacktrace(ctx.env, true); | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  | Java_JniAndroidTestUtils_setSimulateOomInSanitizedStacktrace(ctx.env, false); | 
|  |  | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(Eq(kOomInGetJavaExceptionInfoMessage))); | 
|  | EXPECT_THAT(ctx.assertion_message, | 
|  | Optional(Eq(kUncaughtExceptionHandlerFailedMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_Reentrant) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | // Use the SetJavaException() callback to trigger re-entrancy. | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerToThrow(ctx.env); | 
|  | ctx.throw_in_exception_callback = true; | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  |  | 
|  | EXPECT_THAT(ctx.last_java_exception, Optional(Eq(kReetrantExceptionMessage))); | 
|  | EXPECT_THAT(ctx.assertion_message, Optional(Eq(kReetrantExceptionMessage))); | 
|  | } | 
|  |  | 
|  | TEST(JniAndroidExceptionTest, HandleExceptionInJava_ReentrantOom) { | 
|  | JniAndroidExceptionTestContext ctx; | 
|  | // Use the SetJavaException() callback to trigger re-entrancy. | 
|  | Java_JniAndroidTestUtils_setGlobalExceptionHandlerToThrow(ctx.env); | 
|  | ctx.throw_oom_in_exception_callback = true; | 
|  | Java_JniAndroidTestUtils_throwRuntimeException(ctx.env); | 
|  |  | 
|  | EXPECT_THAT(ctx.last_java_exception, | 
|  | Optional(Eq(kReetrantOutOfMemoryMessage))); | 
|  | EXPECT_THAT(ctx.assertion_message, Optional(Eq(kReetrantOutOfMemoryMessage))); | 
|  | } | 
|  |  | 
|  | }  // namespace android | 
|  | }  // namespace base |