[go: nahoru, domu]

blob: f940ef6295ce63ee7d7082d715b07e04d2bf9d0a [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test;
import static org.chromium.android_webview.test.AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS;
import android.os.Looper;
import androidx.test.filters.MediumTest;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwThreadUtils;
import org.chromium.android_webview.common.crash.AwCrashReporterClient;
import org.chromium.base.JniAndroid;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
/**
* Test suite for actions that should cause java exceptions to be propagated to the embedding
* application.
*/
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@DoNotBatch(reason = "uncaught exceptions leave the process in a bad state")
public class AwUncaughtExceptionTest extends AwParameterizedTest {
// Initialization of WebView is delayed until a background thread
// is started. This gives us the chance to process the uncaught
// exception off the UI thread. An uncaught exception on the UI
// thread appears to cause the test to fail to exit.
@Rule public AwActivityTestRule mActivityTestRule;
public AwUncaughtExceptionTest(AwSettingsMutation param) {
mActivityTestRule =
new AwActivityTestRule(param.getMutation()) {
@Override
public boolean needsAwBrowserContextCreated() {
return false;
}
@Override
public boolean needsBrowserProcessStarted() {
return false;
}
@Override
public boolean needsAwContentsCleanup() {
// State of VM might be hosed after throwing and not catching exceptions.
// Do not assume it is safe to destroy AwContents by posting to the UI
// thread.
// Instead explicitly destroy any AwContents created in this test.
return false;
}
};
}
private class BackgroundThread extends Thread {
private Looper mLooper;
BackgroundThread(String name) {
super(name);
}
@Override
public void run() {
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
ThreadUtils.setUiThread(mLooper);
notifyAll();
}
try {
Looper.loop();
} finally {
}
}
public Looper getLooper() {
if (!isAlive()) return null;
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
}
private BackgroundThread mBackgroundThread;
private TestAwContentsClient mContentsClient;
private AwTestContainerView mTestContainerView;
private AwContents mAwContents;
private Thread.UncaughtExceptionHandler mDefaultUncaughtExceptionHandler;
private boolean mCleanupBackgroundThread = true;
// Since this test overrides the UI thread, Android's ActivityLifecycleMonitor assertions fail
// as our UI thread isn't the Main Looper thread, so we have to disable them.
private void disableLifecycleThreadAssertion() throws Exception {
ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance();
Field declawThreadCheck = monitor.getClass().getDeclaredField("declawThreadCheck");
declawThreadCheck.setAccessible(true);
declawThreadCheck.set(monitor, true);
}
@Before
public void setUp() throws Exception {
disableLifecycleThreadAssertion();
ThreadUtils.setThreadAssertsDisabledForTesting(true);
mDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
mBackgroundThread = new BackgroundThread("background");
mBackgroundThread.start();
// Once the background thread looper exists, it has been
// designated as the main thread.
mBackgroundThread.getLooper();
mActivityTestRule.createAwBrowserContext();
mActivityTestRule.startBrowserProcess();
// Clearing the UI thread isn't really supported so we're not left in a state where we can
// cleanly finish the Activity after these tests.
mActivityTestRule.setFinishActivity(false);
}
@After
public void tearDown() throws InterruptedException {
if (mCleanupBackgroundThread) {
Looper backgroundThreadLooper = mBackgroundThread.getLooper();
if (backgroundThreadLooper != null) {
backgroundThreadLooper.quitSafely();
}
mBackgroundThread.join();
}
Thread.setDefaultUncaughtExceptionHandler(mDefaultUncaughtExceptionHandler);
}
private void expectUncaughtException(
Thread onThread,
Class<? extends Exception> exceptionClass,
String message,
boolean reportable,
Runnable onException) {
Thread.setDefaultUncaughtExceptionHandler(
(thread, exception) -> {
if (exception instanceof JniAndroid.UncaughtExceptionException) {
// Unwrap the UncaughtExceptionException.
exception = exception.getCause();
}
if ((onThread == null || onThread.equals(thread))
&& (exceptionClass == null || exceptionClass.isInstance(exception))
&& (message == null || exception.getMessage().equals(message))) {
Assert.assertEquals(
reportable,
AwCrashReporterClient.stackTraceContainsWebViewCode(exception));
onException.run();
} else {
mDefaultUncaughtExceptionHandler.uncaughtException(thread, exception);
}
});
}
private void doTestUncaughtReportedException(boolean postTask) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final String msg = "dies.";
expectUncaughtException(
mBackgroundThread,
RuntimeException.class,
msg,
/* reportable= */ true,
() -> {
mCleanupBackgroundThread = false;
latch.countDown();
// Do not return to native as this will terminate the process.
Looper.loop();
});
Runnable r =
() -> {
RuntimeException exception = new RuntimeException(msg);
exception.setStackTrace(
new StackTraceElement[] {
new StackTraceElement(
"android.webkit.WebView", "loadUrl", "<none>", 0)
});
throw exception;
};
if (postTask) {
PostTask.postTask(TaskTraits.UI_DEFAULT, r);
} else {
AwThreadUtils.postToUiThreadLooper(r);
}
Assert.assertTrue(
latch.await(SCALED_WAIT_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testUncaughtReportedException_MainHandler() throws InterruptedException {
doTestUncaughtReportedException(false);
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testUncaughtReportedException_PostTask() throws InterruptedException {
doTestUncaughtReportedException(true);
}
private void dotestUncaughtUnreportedException(boolean postTask) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final String msg = "dies.";
expectUncaughtException(
mBackgroundThread,
RuntimeException.class,
msg,
/* reportable= */ false,
() -> {
mCleanupBackgroundThread = false;
latch.countDown();
// Do not return to native as this will terminate the process.
Looper.loop();
});
Runnable r =
() -> {
RuntimeException exception = new RuntimeException(msg);
exception.setStackTrace(
new StackTraceElement[] {
new StackTraceElement("java.lang.Object", "equals", "<none>", 0)
});
throw exception;
};
if (postTask) {
PostTask.postTask(TaskTraits.UI_DEFAULT, r);
} else {
AwThreadUtils.postToUiThreadLooper(r);
}
Assert.assertTrue(
latch.await(SCALED_WAIT_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS));
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testUncaughtUnreportedException_MainThread() throws InterruptedException {
dotestUncaughtUnreportedException(false);
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testUncaughtUnreportedException_PostTask() throws InterruptedException {
dotestUncaughtUnreportedException(true);
}
@Test
@MediumTest
@Feature({"AndroidWebView"})
public void testShouldOverrideUrlLoading() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final String msg = "dies.";
expectUncaughtException(
mBackgroundThread,
RuntimeException.class,
msg,
/* reportable= */ true,
() -> {
latch.countDown();
});
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
mContentsClient =
new TestAwContentsClient() {
@Override
public boolean shouldOverrideUrlLoading(
AwWebResourceRequest request) {
mAwContents.destroyNatives();
throw new RuntimeException(msg);
}
};
mTestContainerView =
mActivityTestRule.createDetachedAwTestContainerView(mContentsClient);
mAwContents = mTestContainerView.getAwContents();
mAwContents.getSettings().setJavaScriptEnabled(true);
mAwContents.loadUrl(
"data:text/html,<script>window.location='https://www.google.com';</script>");
});
Assert.assertTrue(
latch.await(SCALED_WAIT_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS));
}
}
;