summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util')
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Environment.java93
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java175
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/TestServer.kt167
-rw-r--r--mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java167
4 files changed, 602 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Environment.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Environment.java
new file mode 100644
index 0000000000..b2ed9df4d5
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Environment.java
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test.util;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.mozilla.geckoview.BuildConfig;
+
+public class Environment {
+ public static final long DEFAULT_TIMEOUT_MILLIS = 30000;
+ public static final long DEFAULT_ARM_DEVICE_TIMEOUT_MILLIS = 30000;
+ public static final long DEFAULT_ARM_EMULATOR_TIMEOUT_MILLIS = 120000;
+ public static final long DEFAULT_X86_DEVICE_TIMEOUT_MILLIS = 30000;
+ public static final long DEFAULT_X86_EMULATOR_TIMEOUT_MILLIS = 30000;
+ public static final long DEFAULT_IDE_DEBUG_TIMEOUT_MILLIS = 86400000;
+
+ private String getEnvVar(final String name) {
+ final int nameLen = name.length();
+ final Bundle args = InstrumentationRegistry.getArguments();
+ String env = args.getString("env0", null);
+ for (int i = 1; env != null; i++) {
+ if (env.length() >= nameLen + 1 && env.startsWith(name) && env.charAt(nameLen) == '=') {
+ return env.substring(nameLen + 1);
+ }
+ env = args.getString("env" + i, null);
+ }
+ return "";
+ }
+
+ public boolean isAutomation() {
+ return !getEnvVar("MOZ_IN_AUTOMATION").isEmpty();
+ }
+
+ public boolean shouldShutdownOnCrash() {
+ return !getEnvVar("MOZ_CRASHREPORTER_SHUTDOWN").isEmpty();
+ }
+
+ public boolean isDebugging() {
+ return Debug.isDebuggerConnected();
+ }
+
+ public boolean isEmulator() {
+ return "generic".equals(Build.DEVICE) || Build.DEVICE.startsWith("generic_");
+ }
+
+ public boolean isDebugBuild() {
+ return BuildConfig.DEBUG_BUILD;
+ }
+
+ public boolean isX86() {
+ final String abi;
+ if (Build.VERSION.SDK_INT >= 21) {
+ abi = Build.SUPPORTED_ABIS[0];
+ } else {
+ abi = Build.CPU_ABI;
+ }
+
+ return abi.startsWith("x86");
+ }
+
+ public boolean isFission() {
+ // NOTE: This isn't accurate, as it doesn't take into account the default
+ // value of the pref or environment variables like
+ // `MOZ_FORCE_DISABLE_FISSION`.
+ return getEnvVar("MOZ_FORCE_ENABLE_FISSION").equals("1");
+ }
+
+ public boolean isWebrender() {
+ return getEnvVar("MOZ_WEBRENDER").equals("1");
+ }
+
+ public boolean isIsolatedProcess() {
+ return BuildConfig.MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS;
+ }
+
+ public long getScaledTimeoutMillis() {
+ if (isX86()) {
+ return isEmulator() ? DEFAULT_X86_EMULATOR_TIMEOUT_MILLIS : DEFAULT_X86_DEVICE_TIMEOUT_MILLIS;
+ }
+ return isEmulator() ? DEFAULT_ARM_EMULATOR_TIMEOUT_MILLIS : DEFAULT_ARM_DEVICE_TIMEOUT_MILLIS;
+ }
+
+ public long getDefaultTimeoutMillis() {
+ return isDebugging() ? DEFAULT_IDE_DEBUG_TIMEOUT_MILLIS : getScaledTimeoutMillis();
+ }
+
+ public boolean isNightly() {
+ return BuildConfig.NIGHTLY_BUILD;
+ }
+}
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java
new file mode 100644
index 0000000000..5431719bc9
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/RuntimeCreator.java
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test.util;
+
+import static org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider;
+
+import android.os.Process;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.test.platform.app.InstrumentationRegistry;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.mozilla.geckoview.ContentBlocking;
+import org.mozilla.geckoview.GeckoRuntime;
+import org.mozilla.geckoview.GeckoRuntimeSettings;
+import org.mozilla.geckoview.RuntimeTelemetry;
+import org.mozilla.geckoview.WebExtension;
+import org.mozilla.geckoview.test.TestCrashHandler;
+
+public class RuntimeCreator {
+ public static final int TEST_SUPPORT_INITIAL = 0;
+ public static final int TEST_SUPPORT_OK = 1;
+ public static final int TEST_SUPPORT_ERROR = 2;
+ public static final String TEST_SUPPORT_EXTENSION_ID = "test-support@tests.mozilla.org";
+ private static final String LOGTAG = "RuntimeCreator";
+
+ private static final Environment env = new Environment();
+ private static GeckoRuntime sRuntime;
+ public static AtomicInteger sTestSupport = new AtomicInteger(0);
+ public static WebExtension sTestSupportExtension;
+
+ // The RuntimeTelemetry.Delegate can only be set when creating the RuntimeCreator, to
+ // let tests set their own Delegate we need to create a proxy here.
+ public static class RuntimeTelemetryDelegate implements RuntimeTelemetry.Delegate {
+ public RuntimeTelemetry.Delegate delegate = null;
+
+ @Override
+ public void onHistogram(@NonNull final RuntimeTelemetry.Histogram metric) {
+ if (delegate != null) {
+ delegate.onHistogram(metric);
+ }
+ }
+
+ @Override
+ public void onBooleanScalar(@NonNull final RuntimeTelemetry.Metric<Boolean> metric) {
+ if (delegate != null) {
+ delegate.onBooleanScalar(metric);
+ }
+ }
+
+ @Override
+ public void onStringScalar(@NonNull final RuntimeTelemetry.Metric<String> metric) {
+ if (delegate != null) {
+ delegate.onStringScalar(metric);
+ }
+ }
+
+ @Override
+ public void onLongScalar(@NonNull final RuntimeTelemetry.Metric<Long> metric) {
+ if (delegate != null) {
+ delegate.onLongScalar(metric);
+ }
+ }
+ }
+
+ public static final RuntimeTelemetryDelegate sRuntimeTelemetryProxy =
+ new RuntimeTelemetryDelegate();
+
+ private static WebExtension.Port sBackgroundPort;
+
+ private static WebExtension.PortDelegate sPortDelegate;
+
+ private static WebExtension.MessageDelegate sMessageDelegate =
+ new WebExtension.MessageDelegate() {
+ @Nullable
+ @Override
+ public void onConnect(@NonNull final WebExtension.Port port) {
+ sBackgroundPort = port;
+ port.setDelegate(sWrapperPortDelegate);
+ }
+ };
+
+ private static WebExtension.PortDelegate sWrapperPortDelegate =
+ new WebExtension.PortDelegate() {
+ @Override
+ public void onPortMessage(
+ @NonNull final Object message, @NonNull final WebExtension.Port port) {
+ if (sPortDelegate != null) {
+ sPortDelegate.onPortMessage(message, port);
+ }
+ }
+ };
+
+ public static WebExtension.Port backgroundPort() {
+ return sBackgroundPort;
+ }
+
+ public static void registerTestSupport() {
+ sTestSupport.set(0);
+
+ sRuntime
+ .getWebExtensionController()
+ .installBuiltIn("resource://android/assets/web_extensions/test-support/")
+ .accept(
+ extension -> {
+ extension.setMessageDelegate(sMessageDelegate, "browser");
+ sTestSupportExtension = extension;
+ sTestSupport.set(TEST_SUPPORT_OK);
+ },
+ exception -> {
+ Log.e(LOGTAG, "Could not register TestSupport", exception);
+ sTestSupport.set(TEST_SUPPORT_ERROR);
+ });
+ }
+
+ /**
+ * Set the {@link RuntimeTelemetry.Delegate} instance for this test. Application code can only
+ * register this delegate when the {@link GeckoRuntime} is created, so we need to proxy it for
+ * test code.
+ *
+ * @param delegate the {@link RuntimeTelemetry.Delegate} for this test run.
+ */
+ public static void setTelemetryDelegate(final RuntimeTelemetry.Delegate delegate) {
+ sRuntimeTelemetryProxy.delegate = delegate;
+ }
+
+ public static void setPortDelegate(final WebExtension.PortDelegate portDelegate) {
+ sPortDelegate = portDelegate;
+ }
+
+ @UiThread
+ public static GeckoRuntime getRuntime() {
+ if (sRuntime != null) {
+ return sRuntime;
+ }
+
+ final SafeBrowsingProvider googleLegacy =
+ SafeBrowsingProvider.from(ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
+ .getHashUrl("http://mochi.test:8888/safebrowsing-dummy/gethash")
+ .updateUrl("http://mochi.test:8888/safebrowsing-dummy/update")
+ .build();
+
+ final SafeBrowsingProvider google =
+ SafeBrowsingProvider.from(ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER)
+ .getHashUrl("http://mochi.test:8888/safebrowsing4-dummy/gethash")
+ .updateUrl("http://mochi.test:8888/safebrowsing4-dummy/update")
+ .build();
+
+ final GeckoRuntimeSettings runtimeSettings =
+ new GeckoRuntimeSettings.Builder()
+ .contentBlocking(
+ new ContentBlocking.Settings.Builder()
+ .safeBrowsingProviders(googleLegacy, google)
+ .build())
+ .arguments(new String[] {"-purgecaches"})
+ .extras(InstrumentationRegistry.getArguments())
+ .remoteDebuggingEnabled(true)
+ .consoleOutput(true)
+ .crashHandler(TestCrashHandler.class)
+ .telemetryDelegate(sRuntimeTelemetryProxy)
+ .build();
+
+ sRuntime =
+ GeckoRuntime.create(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(), runtimeSettings);
+
+ registerTestSupport();
+
+ sRuntime.setDelegate(() -> Process.killProcess(Process.myPid()));
+
+ return sRuntime;
+ }
+}
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/TestServer.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/TestServer.kt
new file mode 100644
index 0000000000..b842a58c2f
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/TestServer.kt
@@ -0,0 +1,167 @@
+package org.mozilla.geckoview.test.util
+
+import android.content.Context
+import android.content.res.AssetManager
+import android.os.SystemClock
+import android.webkit.MimeTypeMap
+import com.koushikdutta.async.ByteBufferList
+import com.koushikdutta.async.http.server.AsyncHttpServer
+import com.koushikdutta.async.http.server.AsyncHttpServerRequest
+import com.koushikdutta.async.http.server.AsyncHttpServerResponse
+import com.koushikdutta.async.util.TaggedList
+import org.json.JSONObject
+import java.io.FileNotFoundException
+import java.math.BigInteger
+import java.security.MessageDigest
+import java.util.* // ktlint-disable no-wildcard-imports
+
+class TestServer {
+ private val server = AsyncHttpServer()
+ private val assets: AssetManager
+ private val stallingResponses = Vector<AsyncHttpServerResponse>()
+
+ constructor(context: Context) {
+ assets = context.resources.assets
+
+ val anything = { request: AsyncHttpServerRequest, response: AsyncHttpServerResponse ->
+ val obj = JSONObject()
+
+ obj.put("method", request.method)
+
+ val headers = JSONObject()
+ for (key in request.headers.multiMap.keys) {
+ val values = request.headers.multiMap.get(key) as TaggedList<String>
+ headers.put(values.tag(), values.joinToString(", "))
+ }
+
+ obj.put("headers", headers)
+
+ if (request.method == "POST") {
+ obj.put("data", request.getBody())
+ }
+
+ response.send(obj)
+ }
+
+ server.post("/anything", anything)
+ server.get("/anything", anything)
+
+ server.get("/assets/.*") { request, response ->
+ try {
+ val mimeType = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(request.path))
+ val name = request.path.substring("/assets/".count())
+ val asset = assets.open(name).readBytes()
+
+ response.send(mimeType, asset)
+ } catch (e: FileNotFoundException) {
+ response.code(404)
+ response.end()
+ }
+ }
+
+ server.get("/status/.*") { request, response ->
+ val statusCode = request.path.substring("/status/".count()).toInt()
+ response.code(statusCode)
+ response.end()
+ }
+
+ server.get("/redirect-to.*") { request, response ->
+ response.redirect(request.query.getString("url"))
+ }
+
+ server.get("/redirect/.*") { request, response ->
+ val count = request.path.split('/').last().toInt() - 1
+ if (count > 0) {
+ response.redirect("/redirect/$count")
+ }
+
+ response.end()
+ }
+
+ server.get("/basic-auth/.*") { _, response ->
+ response.code(401)
+ response.headers.set("WWW-Authenticate", "Basic realm=\"Fake Realm\"")
+ response.end()
+ }
+
+ server.get("/cookies") { request, response ->
+ val cookiesObj = JSONObject()
+
+ request.headers.get("cookie")?.split(";")?.forEach {
+ val parts = it.trim().split('=')
+ cookiesObj.put(parts[0], parts[1])
+ }
+
+ val obj = JSONObject()
+ obj.put("cookies", cookiesObj)
+ response.send(obj)
+ }
+
+ server.get("/cookies/set/.*") { request, response ->
+ val parts = request.path.substring("/cookies/set/".count()).split('/')
+
+ response.headers.set("Set-Cookie", "${parts[0]}=${parts[1]}; Path=/")
+ response.headers.set("Location", "/cookies")
+ response.code(302)
+ response.end()
+ }
+
+ server.get("/bytes/.*") { request, response ->
+ val count = request.path.split("/").last().toInt()
+ val random = Random(System.currentTimeMillis())
+ val payload = ByteArray(count)
+ random.nextBytes(payload)
+
+ val digest = MessageDigest.getInstance("SHA-256").digest(payload)
+ response.headers.set("X-SHA-256", String.format("%064x", BigInteger(1, digest)))
+ response.send("application/octet-stream", payload)
+ }
+
+ server.get("/trickle/.*") { request, response ->
+ val count = request.path.split("/").last().toInt()
+
+ response.setContentType("application/octet-stream")
+ response.headers.set("Content-Length", "$count")
+ response.writeHead()
+
+ val payload = byteArrayOf(1)
+ for (i in 1..count) {
+ response.write(ByteBufferList(payload))
+ SystemClock.sleep(250)
+ }
+
+ response.end()
+ }
+
+ server.get("/stall/.*") { _, response ->
+ // keep trickling data for a long time (until we are stopped)
+ stallingResponses.add(response)
+
+ val count = 100
+ response.setContentType("InstallException")
+ response.headers.set("Content-Length", "$count")
+ response.writeHead()
+
+ val payload = byteArrayOf(1)
+ for (i in 1..count - 1) {
+ response.write(ByteBufferList(payload))
+ SystemClock.sleep(250)
+ }
+
+ stallingResponses.remove(response)
+ response.end()
+ }
+ }
+
+ fun start(port: Int) {
+ server.listen(port)
+ }
+
+ fun stop() {
+ for (response in stallingResponses) {
+ response.end()
+ }
+ server.stop()
+ }
+}
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
new file mode 100644
index 0000000000..f5aee4db3b
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/UiThreadUtils.java
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.geckoview.test.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import androidx.annotation.NonNull;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.mozilla.geckoview.GeckoResult;
+
+public class UiThreadUtils {
+ private static Method sGetNextMessage = null;
+
+ static {
+ try {
+ sGetNextMessage = MessageQueue.class.getDeclaredMethod("next");
+ sGetNextMessage.setAccessible(true);
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static class TimeoutException extends RuntimeException {
+ public TimeoutException(final String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private static final class TimeoutRunnable implements Runnable {
+ private long timeout;
+
+ public void set(final long timeout) {
+ this.timeout = timeout;
+ cancel();
+ HANDLER.postDelayed(this, timeout);
+ }
+
+ public void cancel() {
+ HANDLER.removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ throw new TimeoutException("Timed out after " + timeout + "ms");
+ }
+ }
+
+ public static final Handler HANDLER = new Handler(Looper.getMainLooper());
+ private static final TimeoutRunnable TIMEOUT_RUNNABLE = new TimeoutRunnable();
+
+ private static RuntimeException unwrapRuntimeException(final Throwable e) {
+ final Throwable cause = e.getCause();
+ if (cause != null && cause instanceof RuntimeException) {
+ return (RuntimeException) cause;
+ } else if (e instanceof RuntimeException) {
+ return (RuntimeException) e;
+ }
+
+ return new RuntimeException(cause != null ? cause : e);
+ }
+
+ /**
+ * This waits for the given result and returns it's value. If the result failed with an exception,
+ * it is rethrown.
+ *
+ * @param result A {@link GeckoResult} instance.
+ * @param <T> The type of the value held by the {@link GeckoResult}
+ * @return The value of the completed {@link GeckoResult}.
+ */
+ public static <T> T waitForResult(@NonNull final GeckoResult<T> result, final long timeout)
+ throws Throwable {
+ final ResultHolder<T> holder = new ResultHolder<>(result);
+
+ waitForCondition(() -> holder.isComplete, timeout);
+
+ if (holder.error != null) {
+ throw holder.error;
+ }
+
+ return holder.value;
+ }
+
+ private static class ResultHolder<T> {
+ public T value;
+ public Throwable error;
+ public boolean isComplete;
+
+ public ResultHolder(final GeckoResult<T> result) {
+ result.accept(
+ value -> {
+ ResultHolder.this.value = value;
+ isComplete = true;
+ },
+ error -> {
+ ResultHolder.this.error = error;
+ isComplete = true;
+ });
+ }
+ }
+
+ public interface Condition {
+ boolean test();
+ }
+
+ public static void loopUntilIdle(final long timeout) {
+ final AtomicBoolean idle = new AtomicBoolean(false);
+
+ MessageQueue.IdleHandler handler = null;
+ try {
+ handler =
+ () -> {
+ idle.set(true);
+ // Remove handler
+ return false;
+ };
+
+ HANDLER.getLooper().getQueue().addIdleHandler(handler);
+
+ waitForCondition(() -> idle.get(), timeout);
+ } finally {
+ if (handler != null) {
+ HANDLER.getLooper().getQueue().removeIdleHandler(handler);
+ }
+ }
+ }
+
+ public static void waitForCondition(final Condition condition, final long timeout) {
+ // Adapted from GeckoThread.pumpMessageLoop.
+ final MessageQueue queue = HANDLER.getLooper().getQueue();
+
+ TIMEOUT_RUNNABLE.set(timeout);
+
+ MessageQueue.IdleHandler handler = null;
+ try {
+ handler =
+ () -> {
+ HANDLER.postDelayed(() -> {}, 100);
+ return true;
+ };
+
+ HANDLER.getLooper().getQueue().addIdleHandler(handler);
+ while (!condition.test()) {
+ final Message msg;
+ try {
+ msg = (Message) sGetNextMessage.invoke(queue);
+ } catch (final IllegalAccessException | InvocationTargetException e) {
+ throw unwrapRuntimeException(e);
+ }
+ if (msg.getTarget() == null) {
+ HANDLER.getLooper().quit();
+ return;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+ } finally {
+ TIMEOUT_RUNNABLE.cancel();
+ if (handler != null) {
+ HANDLER.getLooper().getQueue().removeIdleHandler(handler);
+ }
+ }
+ }
+}