diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util')
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); + } + } + } +} |