summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--android_webview/android_webview_tests.gypi2
-rw-r--r--android_webview/javatests/AndroidManifest.xml4
-rw-r--r--base/BUILD.gn1
-rw-r--r--base/android/java/src/org/chromium/base/ThreadUtils.java1
-rw-r--r--base/base.gyp1
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java6
-rw-r--r--build/android/pylib/instrumentation/instrumentation_test_instance.py93
-rw-r--r--build/android/pylib/local/device/local_device_instrumentation_test_run.py41
-rw-r--r--build/android/pylib/remote/device/remote_device_helper.py3
-rw-r--r--build/android/pylib/remote/device/remote_device_instrumentation_test_run.py31
-rw-r--r--build/android/pylib/remote/device/remote_device_test_run.py14
-rw-r--r--build/apk_test.gypi1
-rw-r--r--build/config/android/rules.gni1
-rw-r--r--chrome/android/BUILD.gn4
-rw-r--r--chrome/android/shell/javatests/AndroidManifest.xml3
-rw-r--r--chrome/chrome_tests.gypi2
-rw-r--r--content/content_tests.gypi2
-rw-r--r--content/shell/android/BUILD.gn2
-rw-r--r--content/shell/android/javatests/AndroidManifest.xml4
-rw-r--r--testing/android/appurify_support.gyp22
-rw-r--r--testing/android/appurify_support/BUILD.gn15
-rw-r--r--testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java30
-rw-r--r--testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java56
-rw-r--r--testing/android/broker/BUILD.gn13
-rw-r--r--testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java64
-rw-r--r--testing/android/driver/BUILD.gn21
-rw-r--r--testing/android/driver/java/AndroidManifest.xml23
-rw-r--r--testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java271
-rw-r--r--testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java67
-rw-r--r--testing/android/on_device_instrumentation.gyp79
-rw-r--r--testing/android/reporter/BUILD.gn19
-rw-r--r--testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java78
-rw-r--r--testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java128
-rw-r--r--testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java83
34 files changed, 1093 insertions, 92 deletions
diff --git a/android_webview/android_webview_tests.gypi b/android_webview/android_webview_tests.gypi
index 577cab4..9e4c3d0 100644
--- a/android_webview/android_webview_tests.gypi
+++ b/android_webview/android_webview_tests.gypi
@@ -94,6 +94,8 @@
'../base/base.gyp:base_java_test_support',
'../content/content_shell_and_tests.gyp:content_java_test_support',
'../net/net.gyp:net_java_test_support',
+ '../testing/android/on_device_instrumentation.gyp:broker_java',
+ '../testing/android/on_device_instrumentation.gyp:require_driver_apk',
'android_webview_apk_java',
],
'variables': {
diff --git a/android_webview/javatests/AndroidManifest.xml b/android_webview/javatests/AndroidManifest.xml
index 63e5e0b..32cc216 100644
--- a/android_webview/javatests/AndroidManifest.xml
+++ b/android_webview/javatests/AndroidManifest.xml
@@ -7,7 +7,7 @@
<!-- TODO(boliu): Change minSdkVersion to 19 when bots no longer try
to install webview apks on < K devices. crbug.com/474374 -->
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
- <instrumentation android:name="android.test.InstrumentationTestRunner"
+ <instrumentation android:name="org.chromium.base.test.BaseInstrumentationTestRunner"
android:targetPackage="org.chromium.android_webview.shell"
android:label="Tests for org.chromium.android_webview"/>
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
@@ -19,5 +19,7 @@
needed when building test cases. -->
<application android:hardwareAccelerated="false">
<uses-library android:name="android.test.runner" />
+ <activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
+ android:exported="true"/>
</application>
</manifest>
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 2db7261..f1b0bb8 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -1448,6 +1448,7 @@ if (is_android) {
android_library("base_java_test_support") {
deps = [
":base_java",
+ "//testing/android/reporter:reporter_java",
]
DEPRECATED_java_in_dir = "test/android/javatests/src"
}
diff --git a/base/android/java/src/org/chromium/base/ThreadUtils.java b/base/android/java/src/org/chromium/base/ThreadUtils.java
index 2a8deeb..c0b9172 100644
--- a/base/android/java/src/org/chromium/base/ThreadUtils.java
+++ b/base/android/java/src/org/chromium/base/ThreadUtils.java
@@ -176,6 +176,7 @@ public class ThreadUtils {
* @param task The Runnable to run
* @param delayMillis The delay in milliseconds until the Runnable will be run
*/
+ @VisibleForTesting
public static void postOnUiThreadDelayed(Runnable task, long delayMillis) {
getUiThreadHandler().postDelayed(task, delayMillis);
}
diff --git a/base/base.gyp b/base/base.gyp
index e9d3e81..35f883cb 100644
--- a/base/base.gyp
+++ b/base/base.gyp
@@ -1476,6 +1476,7 @@
'type': 'none',
'dependencies': [
'base_java',
+ '../testing/android/on_device_instrumentation.gyp:reporter_java',
],
'variables': {
'java_in_dir': '../base/test/android/javatests',
diff --git a/base/test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java b/base/test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java
index a104831..8a3395a 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java
@@ -14,10 +14,12 @@ import junit.framework.TestCase;
import junit.framework.TestResult;
import org.chromium.base.test.util.MinAndroidSdkLevel;
+import org.chromium.test.reporter.TestStatusListener;
import java.util.ArrayList;
import java.util.List;
+// TODO(jbudorick): Add support for on-device handling of timeouts.
/**
* An Instrumentation test runner that checks SDK level for tests with specific requirements.
*/
@@ -88,7 +90,7 @@ public class BaseInstrumentationTestRunner extends InstrumentationTestRunner {
@Override
protected AndroidTestRunner getAndroidTestRunner() {
- return new AndroidTestRunner() {
+ AndroidTestRunner runner = new AndroidTestRunner() {
@Override
protected TestResult createTestResult() {
SkippingTestResult r = new SkippingTestResult();
@@ -96,6 +98,8 @@ public class BaseInstrumentationTestRunner extends InstrumentationTestRunner {
return r;
}
};
+ runner.addTestListener(new TestStatusListener(getContext()));
+ return runner;
}
/**
diff --git a/build/android/pylib/instrumentation/instrumentation_test_instance.py b/build/android/pylib/instrumentation/instrumentation_test_instance.py
index ccc0e72..0633f14 100644
--- a/build/android/pylib/instrumentation/instrumentation_test_instance.py
+++ b/build/android/pylib/instrumentation/instrumentation_test_instance.py
@@ -30,6 +30,17 @@ _ACTIVITY_RESULT_OK = -1
_DEFAULT_ANNOTATIONS = [
'Smoke', 'SmallTest', 'MediumTest', 'LargeTest',
'EnormousTest', 'IntegrationTest']
+_EXTRA_ENABLE_HTTP_SERVER = (
+ 'org.chromium.chrome.test.ChromeInstrumentationTestRunner.'
+ + 'EnableTestHttpServer')
+_EXTRA_DRIVER_TEST_LIST = (
+ 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TestList')
+_EXTRA_DRIVER_TEST_LIST_FILE = (
+ 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TestListFile')
+_EXTRA_DRIVER_TARGET_PACKAGE = (
+ 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage')
+_EXTRA_DRIVER_TARGET_CLASS = (
+ 'org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass')
_NATIVE_CRASH_RE = re.compile('native crash', re.IGNORECASE)
_PICKLE_FORMAT_VERSION = 10
@@ -130,29 +141,35 @@ class InstrumentationTestInstance(test_instance.TestInstance):
self._apk_under_test = None
self._package_info = None
+ self._suite = None
self._test_apk = None
self._test_jar = None
self._test_package = None
self._test_runner = None
self._test_support_apk = None
- self.__initializeApkAttributes(args, error_func)
+ self._initializeApkAttributes(args, error_func)
self._data_deps = None
self._isolate_abs_path = None
self._isolate_delegate = None
self._isolated_abs_path = None
self._test_data = None
- self.__initializeDataDependencyAttributes(args, isolate_delegate)
+ self._initializeDataDependencyAttributes(args, isolate_delegate)
self._annotations = None
self._excluded_annotations = None
self._test_filter = None
- self.__initializeTestFilterAttributes(args)
+ self._initializeTestFilterAttributes(args)
self._flags = None
- self.__initializeFlagAttributes(args)
+ self._initializeFlagAttributes(args)
- def __initializeApkAttributes(self, args, error_func):
+ self._driver_apk = None
+ self._driver_package = None
+ self._driver_name = None
+ self._initializeDriverAttributes()
+
+ def _initializeApkAttributes(self, args, error_func):
if args.apk_under_test.endswith('.apk'):
self._apk_under_test = args.apk_under_test
else:
@@ -164,20 +181,20 @@ class InstrumentationTestInstance(test_instance.TestInstance):
error_func('Unable to find APK under test: %s' % self._apk_under_test)
if args.test_apk.endswith('.apk'):
- test_apk_root = os.path.splitext(os.path.basename(args.test_apk))[0]
+ self._suite = os.path.splitext(os.path.basename(args.test_apk))[0]
self._test_apk = args.test_apk
else:
- test_apk_root = args.test_apk
+ self._suite = args.test_apk
self._test_apk = os.path.join(
constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
'%s.apk' % args.test_apk)
self._test_jar = os.path.join(
constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
- '%s.jar' % test_apk_root)
+ '%s.jar' % self._suite)
self._test_support_apk = os.path.join(
constants.GetOutDirectory(), constants.SDK_BUILD_TEST_JAVALIB_DIR,
- '%sSupport.apk' % test_apk_root)
+ '%sSupport.apk' % self._suite)
if not os.path.exists(self._test_apk):
error_func('Unable to find test APK: %s' % self._test_apk)
@@ -194,7 +211,7 @@ class InstrumentationTestInstance(test_instance.TestInstance):
if not self._package_info:
logging.warning('Unable to find package info for %s', self._test_package)
- def __initializeDataDependencyAttributes(self, args, isolate_delegate):
+ def _initializeDataDependencyAttributes(self, args, isolate_delegate):
self._data_deps = []
if args.isolate_file_path:
self._isolate_abs_path = os.path.abspath(args.isolate_file_path)
@@ -215,7 +232,7 @@ class InstrumentationTestInstance(test_instance.TestInstance):
if not self._isolate_delegate and not self._test_data:
logging.warning('No data dependencies will be pushed.')
- def __initializeTestFilterAttributes(self, args):
+ def _initializeTestFilterAttributes(self, args):
self._test_filter = args.test_filter
def annotation_dict_element(a):
@@ -240,7 +257,7 @@ class InstrumentationTestInstance(test_instance.TestInstance):
else:
self._excluded_annotations = {}
- def __initializeFlagAttributes(self, args):
+ def _initializeFlagAttributes(self, args):
self._flags = ['--disable-fre', '--enable-test-intents']
# TODO(jbudorick): Transition "--device-flags" to "--device-flags-file"
if hasattr(args, 'device_flags') and args.device_flags:
@@ -252,9 +269,17 @@ class InstrumentationTestInstance(test_instance.TestInstance):
stripped_lines = (l.strip() for l in device_flags_file)
self._flags.extend([flag for flag in stripped_lines if flag])
- @property
- def suite(self):
- return 'instrumentation'
+ def _initializeDriverAttributes(self):
+ self._driver_apk = os.path.join(
+ constants.GetOutDirectory(), constants.SDK_BUILD_APKS_DIR,
+ 'OnDeviceInstrumentationDriver.apk')
+ if os.path.exists(self._driver_apk):
+ self._driver_package = apk_helper.GetPackageName(
+ self._driver_apk)
+ self._driver_name = apk_helper.GetInstrumentationName(
+ self._driver_apk)
+ else:
+ self._driver_apk = None
@property
def apk_under_test(self):
@@ -265,10 +290,26 @@ class InstrumentationTestInstance(test_instance.TestInstance):
return self._flags
@property
+ def driver_apk(self):
+ return self._driver_apk
+
+ @property
+ def driver_package(self):
+ return self._driver_package
+
+ @property
+ def driver_name(self):
+ return self._driver_name
+
+ @property
def package_info(self):
return self._package_info
@property
+ def suite(self):
+ return self._suite
+
+ @property
def test_apk(self):
return self._test_apk
@@ -446,6 +487,28 @@ class InstrumentationTestInstance(test_instance.TestInstance):
return inflated_tests
@staticmethod
+ def GetHttpServerEnvironmentVars():
+ return {
+ _EXTRA_ENABLE_HTTP_SERVER: None,
+ }
+
+ def GetDriverEnvironmentVars(
+ self, test_list=None, test_list_file_path=None):
+ env = {
+ _EXTRA_DRIVER_TARGET_PACKAGE: self.test_package,
+ _EXTRA_DRIVER_TARGET_CLASS: self.test_runner,
+ }
+
+ if test_list:
+ env[_EXTRA_DRIVER_TEST_LIST] = ','.join(test_list)
+
+ if test_list_file_path:
+ env[_EXTRA_DRIVER_TEST_LIST_FILE] = (
+ os.path.basename(test_list_file_path))
+
+ return env
+
+ @staticmethod
def ParseAmInstrumentRawOutput(raw_output):
return ParseAmInstrumentRawOutput(raw_output)
diff --git a/build/android/pylib/local/device/local_device_instrumentation_test_run.py b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
index adc1037..e388fce 100644
--- a/build/android/pylib/local/device/local_device_instrumentation_test_run.py
+++ b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
@@ -130,22 +130,41 @@ class LocalDeviceInstrumentationTestRun(
#override
def _RunTest(self, device, test):
- test_name = self._GetTestName(test)
- logging.info('preparing to run %s: %s' % (test_name, test))
+ extras = self._test_instance.GetHttpServerEnvironmentVars()
+
+ if isinstance(test, list):
+ if not self._test_instance.driver_apk:
+ raise Exception('driver_apk does not exist. '
+ 'Please build it and try again.')
+
+ def name_and_timeout(t):
+ n = self._GetTestName(t)
+ i = self._GetTimeoutFromAnnotations(t['annotations'], n)
+ return (n, i)
+
+ test_names, timeouts = zip(*(name_and_timeout(t) for t in test))
+
+ test_name = ','.join(test_names)
+ target = '%s/%s' % (
+ self._test_instance.driver_package,
+ self._test_instance.driver_name)
+ extras.update(
+ self._test_instance.GetDriverEnvironmentVars(
+ test_list=test_names))
+ timeout = sum(timeouts)
+ else:
+ test_name = self._GetTestName(test)
+ target = '%s/%s' % (
+ self._test_instance.test_package, self._test_instance.test_runner)
+ extras['class'] = test_name
+ timeout = self._GetTimeoutFromAnnotations(test['annotations'], test_name)
- extras = {
- 'class': test_name,
- 'org.chromium.chrome.test.ChromeInstrumentationTestRunner'
- '.EnableTestHttpServer': '',
- }
- timeout = self._GetTimeoutFromAnnotations(test['annotations'], test_name)
+ logging.info('preparing to run %s: %s' % (test_name, test))
time_ms = lambda: int(time.time() * 1e3)
start_ms = time_ms()
output = device.StartInstrumentation(
- '%s/%s' % (self._test_instance.test_package,
- self._test_instance.test_runner),
- raw=True, extras=extras, timeout=timeout, retries=0)
+ target, raw=True, extras=extras, timeout=timeout, retries=0)
duration_ms = time_ms() - start_ms
# TODO(jbudorick): Make instrumentation tests output a JSON so this
diff --git a/build/android/pylib/remote/device/remote_device_helper.py b/build/android/pylib/remote/device/remote_device_helper.py
index 5b1411e..896ae99 100644
--- a/build/android/pylib/remote/device/remote_device_helper.py
+++ b/build/android/pylib/remote/device/remote_device_helper.py
@@ -20,4 +20,5 @@ def TestHttpResponse(response, error_msg):
error_msg: Error message to display if bad response is seen.
"""
if response.status_code != 200:
- raise RemoteDeviceError(error_msg)
+ raise RemoteDeviceError(
+ '%s (%d: %s)' % (error_msg, response.status_code, response.reason))
diff --git a/build/android/pylib/remote/device/remote_device_instrumentation_test_run.py b/build/android/pylib/remote/device/remote_device_instrumentation_test_run.py
index fe173a4..bcdb90c 100644
--- a/build/android/pylib/remote/device/remote_device_instrumentation_test_run.py
+++ b/build/android/pylib/remote/device/remote_device_instrumentation_test_run.py
@@ -8,6 +8,7 @@ import logging
import os
import tempfile
+from pylib import constants
from pylib.base import base_test_result
from pylib.remote.device import remote_device_test_run
from pylib.utils import apk_helper
@@ -25,9 +26,33 @@ class RemoteDeviceInstrumentationTestRun(
def _TriggerSetUp(self):
"""Set up the triggering of a test run."""
logging.info('Triggering test run.')
- self._AmInstrumentTestSetup(
- self._test_instance._apk_under_test, self._test_instance.test_apk,
- self._test_instance.test_runner, environment_variables={})
+
+ with tempfile.NamedTemporaryFile(suffix='.txt') as test_list_file:
+ tests = self._test_instance.GetTests()
+ logging.debug('preparing to run %d instrumentation tests remotely:',
+ len(tests))
+ for t in tests:
+ test_name = '%s#%s' % (t['class'], t['method'])
+ logging.debug(' %s', test_name)
+ test_list_file.write('%s\n' % test_name)
+ test_list_file.flush()
+ self._test_instance._data_deps.append(
+ (os.path.abspath(test_list_file.name), None))
+
+ env_vars = self._test_instance.GetDriverEnvironmentVars(
+ test_list_file_path=test_list_file.name)
+ env_vars.update(self._test_instance.GetHttpServerEnvironmentVars())
+
+ logging.debug('extras:')
+ for k, v in env_vars.iteritems():
+ logging.debug(' %s: %s', k, v)
+
+ self._AmInstrumentTestSetup(
+ self._test_instance.apk_under_test,
+ self._test_instance.driver_apk,
+ self._test_instance.driver_name,
+ environment_variables=env_vars,
+ extra_apks=[self._test_instance.test_apk])
#override
def _ParseTestResults(self):
diff --git a/build/android/pylib/remote/device/remote_device_test_run.py b/build/android/pylib/remote/device/remote_device_test_run.py
index 7aa91ae..3808836 100644
--- a/build/android/pylib/remote/device/remote_device_test_run.py
+++ b/build/android/pylib/remote/device/remote_device_test_run.py
@@ -201,7 +201,7 @@ class RemoteDeviceTestRun(test_run.TestRun):
return self._results['status']
def _AmInstrumentTestSetup(self, app_path, test_path, runner_package,
- environment_variables):
+ environment_variables, extra_apks=None):
config = {'runner': runner_package}
if environment_variables:
config['environment_vars'] = ','.join(
@@ -213,6 +213,7 @@ class RemoteDeviceTestRun(test_run.TestRun):
if data_deps:
with tempfile.NamedTemporaryFile(suffix='.zip') as test_with_deps:
sdcard_files = []
+ additional_apks = []
host_test = os.path.basename(test_path)
with zipfile.ZipFile(test_with_deps.name, 'w') as zip_file:
zip_file.write(test_path, host_test, zipfile.ZIP_DEFLATED)
@@ -223,8 +224,14 @@ class RemoteDeviceTestRun(test_run.TestRun):
else:
zip_utils.WriteToZipFile(zip_file, h, os.path.basename(h))
sdcard_files.append(os.path.basename(h))
+ for a in extra_apks or ():
+ zip_utils.WriteToZipFile(zip_file, a, os.path.basename(a));
+ additional_apks.append(os.path.basename(a))
+
config['sdcard_files'] = ','.join(sdcard_files)
config['host_test'] = host_test
+ if additional_apks:
+ config['additional_apks'] = ','.join(additional_apks)
self._test_id = self._UploadTestToDevice(
'robotium', test_with_deps.name, app_id=self._app_id)
else:
@@ -238,7 +245,8 @@ class RemoteDeviceTestRun(test_run.TestRun):
def _UploadAppToDevice(self, app_path):
"""Upload app to device."""
- logging.info('Uploading %s to remote service.', app_path)
+ logging.info('Uploading %s to remote service as %s.', app_path,
+ self._test_instance.suite)
with open(app_path, 'rb') as apk_src:
with appurify_sanitized.SanitizeLogging(self._env.verbose_count,
logging.WARNING):
@@ -297,4 +305,4 @@ class RemoteDeviceTestRun(test_run.TestRun):
config_response = appurify_sanitized.api.config_upload(
self._env.token, config, self._test_id)
remote_device_helper.TestHttpResponse(
- config_response, 'Unable to upload test config.') \ No newline at end of file
+ config_response, 'Unable to upload test config.')
diff --git a/build/apk_test.gypi b/build/apk_test.gypi
index 792d92ca..3a66e3b 100644
--- a/build/apk_test.gypi
+++ b/build/apk_test.gypi
@@ -22,6 +22,7 @@
'<(DEPTH)/base/base.gyp:base_java',
'<(DEPTH)/build/android/pylib/device/commands/commands.gyp:chromium_commands',
'<(DEPTH)/build/android/pylib/remote/device/dummy/dummy.gyp:remote_device_dummy_apk',
+ '<(DEPTH)/testing/android/appurify_support.gyp:appurify_support_java',
'<(DEPTH)/tools/android/android_tools.gyp:android_tools',
],
'conditions': [
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 39b2f39..ddd58ee 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1566,6 +1566,7 @@ template("unittest_apk") {
deps = [
"//base:base_java",
"//build/android/pylib/remote/device/dummy:remote_device_dummy_apk",
+ "//testing/android/appurify_support:appurify_support_java",
]
if (defined(invoker.deps)) {
deps += invoker.deps
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index fb29c01..a3292c8 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -449,8 +449,12 @@ android_apk("chrome_shell_test_apk") {
":chrome_javatests",
":chrome_shell_test_java",
"//sync/android:sync_javatests",
+ "//testing/android/broker:broker_java",
"//ui/android:ui_javatests",
]
+ datadeps = [
+ "//testing/android/driver:driver_apk",
+ ]
apk_name = "ChromeShellTest"
android_manifest = "shell/javatests/AndroidManifest.xml"
}
diff --git a/chrome/android/shell/javatests/AndroidManifest.xml b/chrome/android/shell/javatests/AndroidManifest.xml
index b36520a..8bdb644 100644
--- a/chrome/android/shell/javatests/AndroidManifest.xml
+++ b/chrome/android/shell/javatests/AndroidManifest.xml
@@ -17,6 +17,9 @@
<provider android:name="org.chromium.chrome.test.partnercustomizations.TestPartnerBrowserCustomizationsDelayedProvider"
android:authorities="org.chromium.chrome.test.partnercustomizations.TestPartnerBrowserCustomizationsDelayedProvider" />
+ <activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
+ android:exported="true"/>
+
<activity android:name="org.chromium.sync.test.util.MockGrantCredentialsPermissionActivity"
android:exported="false">
<intent-filter>
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index c24ba9f..8c21609 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -2938,6 +2938,8 @@
'../components/components.gyp:precache_javatests',
'../content/content_shell_and_tests.gyp:content_java_test_support',
'../sync/sync.gyp:sync_javatests',
+ '../testing/android/on_device_instrumentation.gyp:broker_java',
+ '../testing/android/on_device_instrumentation.gyp:require_driver_apk',
'../ui/android/ui_android.gyp:ui_javatests',
],
'variables': {
diff --git a/content/content_tests.gypi b/content/content_tests.gypi
index d8da1a6..89e3078 100644
--- a/content/content_tests.gypi
+++ b/content/content_tests.gypi
@@ -1966,6 +1966,8 @@
'../net/net.gyp:net_java',
'../net/net.gyp:net_javatests',
'../net/net.gyp:net_java_test_support',
+ '../testing/android/on_device_instrumentation.gyp:broker_java',
+ '../testing/android/on_device_instrumentation.gyp:require_driver_apk',
'../third_party/mojo/mojo_public.gyp:mojo_public_test_interfaces',
],
'variables': {
diff --git a/content/shell/android/BUILD.gn b/content/shell/android/BUILD.gn
index 3ad831e..96a8872 100644
--- a/content/shell/android/BUILD.gn
+++ b/content/shell/android/BUILD.gn
@@ -153,9 +153,11 @@ android_apk("content_shell_test_apk") {
"//content/public/android:content_javatests",
"//base:base_javatests",
"//net/android:net_javatests",
+ "//testing/android/broker:broker_java",
]
datadeps = [
":content_shell_apk",
+ "//testing/android/driver:driver_apk",
]
apk_under_test = ":content_shell_apk"
apk_name = "ContentShellTest"
diff --git a/content/shell/android/javatests/AndroidManifest.xml b/content/shell/android/javatests/AndroidManifest.xml
index 5f50ead..a249ba1 100644
--- a/content/shell/android/javatests/AndroidManifest.xml
+++ b/content/shell/android/javatests/AndroidManifest.xml
@@ -11,9 +11,11 @@
needed when building test cases. -->
<application>
<uses-library android:name="android.test.runner" />
+ <activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
+ android:exported="true"/>
</application>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" />
- <instrumentation android:name="android.test.InstrumentationTestRunner"
+ <instrumentation android:name="org.chromium.base.test.BaseInstrumentationTestRunner"
android:targetPackage="org.chromium.content_shell_apk"
android:label="Tests for org.chromium.content_shell_apk"/>
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
diff --git a/testing/android/appurify_support.gyp b/testing/android/appurify_support.gyp
new file mode 100644
index 0000000..2904368
--- /dev/null
+++ b/testing/android/appurify_support.gyp
@@ -0,0 +1,22 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'conditions': [
+ ['OS=="android"', {
+ 'targets': [
+ {
+ 'target_name': 'appurify_support_java',
+ 'type': 'none',
+ 'variables': {
+ 'java_in_dir': '../../testing/android/appurify_support/java',
+ },
+ 'includes': [
+ '../../build/java.gypi',
+ ],
+ },
+ ],
+ }],
+ ],
+}
diff --git a/testing/android/appurify_support/BUILD.gn b/testing/android/appurify_support/BUILD.gn
new file mode 100644
index 0000000..871f9d0
--- /dev/null
+++ b/testing/android/appurify_support/BUILD.gn
@@ -0,0 +1,15 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+# GYP: //testing/android/appurify_support.gyp:appurify_support_java
+android_library("appurify_support_java") {
+ chromium_code = true
+
+ java_files = [
+ "java/src/org/chromium/test/support/ResultsBundleGenerator.java",
+ "java/src/org/chromium/test/support/RobotiumBundleGenerator.java",
+ ]
+}
diff --git a/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java b/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java
new file mode 100644
index 0000000..c9588d9
--- /dev/null
+++ b/testing/android/appurify_support/java/src/org/chromium/test/support/ResultsBundleGenerator.java
@@ -0,0 +1,30 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.support;
+
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * Creates a results Bundle.
+ */
+public interface ResultsBundleGenerator {
+
+ /** Indicates the state of a test.
+ */
+ static enum TestResult {
+ PASSED, FAILED, ERROR, UNKNOWN
+ }
+
+ /** Creates a bundle of test results from the provided raw results.
+
+ Note: actual bundle content and format may vary.
+
+ @param rawResults A map between test names and test results.
+ */
+ Bundle generate(Map<String, TestResult> rawResults);
+}
+
diff --git a/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java b/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java
new file mode 100644
index 0000000..167e7b9
--- /dev/null
+++ b/testing/android/appurify_support/java/src/org/chromium/test/support/RobotiumBundleGenerator.java
@@ -0,0 +1,56 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.support;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.Map;
+
+/**
+ * Creates a results bundle that emulates the one created by Robotium.
+ */
+public class RobotiumBundleGenerator implements ResultsBundleGenerator {
+
+ private static final String TAG = "RobotiumBundleGenerator";
+
+ public Bundle generate(Map<String, ResultsBundleGenerator.TestResult> rawResults) {
+ int testsPassed = 0;
+ int testsFailed = 0;
+
+ for (Map.Entry<String, ResultsBundleGenerator.TestResult> entry : rawResults.entrySet()) {
+ switch (entry.getValue()) {
+ case PASSED:
+ ++testsPassed;
+ break;
+ case FAILED:
+ // TODO(jbudorick): Remove this log message once AMP execution and
+ // results handling has been stabilized.
+ Log.d(TAG, "FAILED: " + entry.getKey());
+ ++testsFailed;
+ break;
+ default:
+ Log.w(TAG, "Unhandled: " + entry.getKey() + ", "
+ + entry.getValue().toString());
+ break;
+ }
+ }
+
+ StringBuilder resultBuilder = new StringBuilder();
+ if (testsFailed > 0) {
+ resultBuilder.append(
+ "\nFAILURES!!! Tests run: " + Integer.toString(rawResults.size())
+ + ", Failures: " + Integer.toString(testsFailed) + ", Errors: 0");
+ } else {
+ resultBuilder.append("\nOK (" + Integer.toString(testsPassed) + " tests)");
+ }
+
+ Bundle resultsBundle = new Bundle();
+ resultsBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+ resultBuilder.toString());
+ return resultsBundle;
+ }
+}
diff --git a/testing/android/broker/BUILD.gn b/testing/android/broker/BUILD.gn
new file mode 100644
index 0000000..8daa040
--- /dev/null
+++ b/testing/android/broker/BUILD.gn
@@ -0,0 +1,13 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+# GYP: //testing/android/on_device_instrumentation.gyp:broker_java
+android_library("broker_java") {
+ chromium_code = true
+
+ java_files =
+ [ "java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java" ]
+}
diff --git a/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java b/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java
new file mode 100644
index 0000000..cd755d0
--- /dev/null
+++ b/testing/android/broker/java/src/org/chromium/test/broker/OnDeviceInstrumentationBroker.java
@@ -0,0 +1,64 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.broker;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * An Activity target for OnDeviceInstrumentationDriver that starts the specified
+ * Instrumentation test.
+ */
+public class OnDeviceInstrumentationBroker extends Activity {
+
+ public static final String EXTRA_INSTRUMENTATION_PACKAGE =
+ "org.chromium.test.broker.OnDeviceInstrumentationBroker."
+ + "InstrumentationPackage";
+ public static final String EXTRA_INSTRUMENTATION_CLASS =
+ "org.chromium.test.broker.OnDeviceInstrumentationBroker."
+ + "InstrumentationClass";
+ public static final String EXTRA_TARGET_ARGS =
+ "org.chromium.test.broker.OnDeviceInstrumentationBroker.TargetArgs";
+ public static final String EXTRA_TEST =
+ "org.chromium.test.broker.OnDeviceInstrumentationBroker.Test";
+
+ private static final String TAG = "OnDeviceInstrumentationBroker";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate()");
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Intent i = getIntent();
+ String instrumentationPackage = i.getStringExtra(EXTRA_INSTRUMENTATION_PACKAGE);
+ String instrumentationClass = i.getStringExtra(EXTRA_INSTRUMENTATION_CLASS);
+ Bundle targetArgs = i.getBundleExtra(EXTRA_TARGET_ARGS);
+ String test = i.getStringExtra(EXTRA_TEST);
+
+ if (instrumentationPackage == null || instrumentationClass == null) {
+ finish();
+ return;
+ }
+
+ ComponentName instrumentationComponent =
+ new ComponentName(instrumentationPackage, instrumentationClass);
+
+ if (test != null) {
+ targetArgs.putString("class", test);
+ }
+
+ startInstrumentation(instrumentationComponent, null, targetArgs);
+ finish();
+ }
+}
+
diff --git a/testing/android/driver/BUILD.gn b/testing/android/driver/BUILD.gn
new file mode 100644
index 0000000..436ac63
--- /dev/null
+++ b/testing/android/driver/BUILD.gn
@@ -0,0 +1,21 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+# GYP: //testing/android/on_device_instrumentation.gyp:driver_apk
+android_apk("driver_apk") {
+ android_manifest = "java/AndroidManifest.xml"
+ apk_name = "OnDeviceInstrumentationDriver"
+ testonly = true
+
+ deps = [
+ "//testing/android/appurify_support:appurify_support_java",
+ "//testing/android/broker:broker_java",
+ "//testing/android/reporter:reporter_java",
+ ]
+
+ java_files =
+ [ "java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java" ]
+}
diff --git a/testing/android/driver/java/AndroidManifest.xml b/testing/android/driver/java/AndroidManifest.xml
new file mode 100644
index 0000000..c7e99ef
--- /dev/null
+++ b/testing/android/driver/java/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.chromium.test.driver"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
+
+ <application android:label="OnDeviceInstrumentationDriver" />
+
+ <instrumentation android:name="org.chromium.test.driver.OnDeviceInstrumentationDriver"
+ android:targetPackage="org.chromium.test.driver"
+ android:label="OnDeviceInstrumentationDriver"/>
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+</manifest>
diff --git a/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java b/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java
new file mode 100644
index 0000000..78c571a
--- /dev/null
+++ b/testing/android/driver/java/src/org/chromium/test/driver/OnDeviceInstrumentationDriver.java
@@ -0,0 +1,271 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.driver;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+
+import org.chromium.test.broker.OnDeviceInstrumentationBroker;
+import org.chromium.test.reporter.TestStatusReceiver;
+import org.chromium.test.reporter.TestStatusReporter;
+import org.chromium.test.support.ResultsBundleGenerator;
+import org.chromium.test.support.RobotiumBundleGenerator;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * An Instrumentation that drives instrumentation tests from outside the app.
+ */
+public class OnDeviceInstrumentationDriver extends Instrumentation {
+
+ private static final String TAG = "OnDeviceInstrumentationDriver";
+
+ private static final String EXTRA_TEST_LIST =
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TestList";
+ private static final String EXTRA_TEST_LIST_FILE =
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TestListFile";
+ private static final String EXTRA_TARGET_PACKAGE =
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetPackage";
+ private static final String EXTRA_TARGET_CLASS =
+ "org.chromium.test.driver.OnDeviceInstrumentationDriver.TargetClass";
+
+ private static final Pattern COMMA = Pattern.compile(",");
+ private static final int TEST_WAIT_TIMEOUT = 5 * TestStatusReporter.HEARTBEAT_INTERVAL_MS;
+
+ private boolean mDriverStarted;
+ private Thread mDriverThread;
+ private Bundle mTargetArgs;
+ private String mTargetClass;
+ private String mTargetPackage;
+ private List<String> mTestClasses;
+
+ /** Parse any arguments and prepare to run tests.
+
+ @param arguments The arguments to parse.
+ */
+ @Override
+ public void onCreate(Bundle arguments) {
+ mTargetArgs = new Bundle(arguments);
+ mTargetPackage = arguments.getString(EXTRA_TARGET_PACKAGE);
+ if (mTargetPackage == null) {
+ fail("No target package.");
+ return;
+ }
+ mTargetArgs.remove(EXTRA_TARGET_PACKAGE);
+
+ mTargetClass = arguments.getString(EXTRA_TARGET_CLASS);
+ if (mTargetClass == null) {
+ fail("No target class.");
+ return;
+ }
+ mTargetArgs.remove(EXTRA_TARGET_CLASS);
+
+ mTestClasses = new ArrayList<String>();
+ String testList = arguments.getString(EXTRA_TEST_LIST);
+ if (testList != null) {
+ mTestClasses.addAll(Arrays.asList(COMMA.split(testList)));
+ mTargetArgs.remove(EXTRA_TEST_LIST);
+ }
+
+ String testListFilePath = arguments.getString(EXTRA_TEST_LIST_FILE);
+ if (testListFilePath != null) {
+ File testListFile = new File(Environment.getExternalStorageDirectory(),
+ testListFilePath);
+ try {
+ BufferedReader testListFileReader =
+ new BufferedReader(new FileReader(testListFile));
+ String test;
+ while ((test = testListFileReader.readLine()) != null) {
+ mTestClasses.add(test);
+ }
+ testListFileReader.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading " + testListFile.getAbsolutePath(), e);
+ }
+ mTargetArgs.remove(EXTRA_TEST_LIST_FILE);
+ }
+
+ if (mTestClasses.isEmpty()) {
+ fail("No tests.");
+ return;
+ }
+
+ mDriverThread = new Thread(
+ new Driver(mTargetPackage, mTargetClass, mTargetArgs, mTestClasses));
+
+ start();
+ }
+
+ /** Start running tests. */
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Start the driver on its own thread s.t. it can block while the main thread's
+ // Looper receives and handles messages.
+ if (!mDriverStarted) {
+ mDriverThread.start();
+ mDriverStarted = true;
+ }
+ }
+
+ /** Clean up the reporting service. */
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ private class Driver implements Runnable {
+
+ private static final String TAG = OnDeviceInstrumentationDriver.TAG + ".Driver";
+
+ private Bundle mTargetArgs;
+ private String mTargetClass;
+ private String mTargetPackage;
+ private List<String> mTestClasses;
+
+ public Driver(String targetPackage, String targetClass, Bundle targetArgs,
+ List<String> testClasses) {
+ mTargetPackage = targetPackage;
+ mTargetClass = targetClass;
+ mTargetArgs = targetArgs;
+ mTestClasses = testClasses;
+ }
+
+ private void sendTestStatus(int status, String testClass, String testMethod) {
+ Bundle statusBundle = new Bundle();
+ statusBundle.putString(InstrumentationTestRunner.REPORT_KEY_NAME_CLASS, testClass);
+ statusBundle.putString(InstrumentationTestRunner.REPORT_KEY_NAME_TEST, testMethod);
+ sendStatus(status, statusBundle);
+ }
+
+ /** Run the tests. */
+ @Override
+ public void run() {
+ final HashMap<String, ResultsBundleGenerator.TestResult> finished =
+ new HashMap<String, ResultsBundleGenerator.TestResult>();
+ final Object statusLock = new Object();
+
+ try {
+ TestStatusReceiver r = new TestStatusReceiver();
+ r.registerCallback(new TestStatusReceiver.StartCallback() {
+ @Override
+ public void testStarted(String testClass, String testMethod) {
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_START,
+ testClass, testMethod);
+ synchronized (statusLock) {
+ statusLock.notify();
+ }
+ }
+ });
+ r.registerCallback(new TestStatusReceiver.PassCallback() {
+ @Override
+ public void testPassed(String testClass, String testMethod) {
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_OK, testClass,
+ testMethod);
+ synchronized (statusLock) {
+ finished.put(testClass + "#" + testMethod,
+ ResultsBundleGenerator.TestResult.PASSED);
+ statusLock.notify();
+ }
+ }
+ });
+ r.registerCallback(new TestStatusReceiver.FailCallback() {
+ @Override
+ public void testFailed(String testClass, String testMethod) {
+ sendTestStatus(InstrumentationTestRunner.REPORT_VALUE_RESULT_ERROR,
+ testClass, testMethod);
+ synchronized (statusLock) {
+ finished.put(testClass + "#" + testMethod,
+ ResultsBundleGenerator.TestResult.FAILED);
+ statusLock.notify();
+ }
+ }
+ });
+ r.registerCallback(new TestStatusReceiver.HeartbeatCallback() {
+ @Override
+ public void heartbeat() {
+ Log.i(TAG, "Heartbeat received.");
+ synchronized (statusLock) {
+ statusLock.notify();
+ }
+ }
+ });
+ r.register(getContext());
+
+ for (String t : mTestClasses) {
+ Intent slaveIntent = new Intent();
+ slaveIntent.setComponent(new ComponentName(
+ mTargetPackage, OnDeviceInstrumentationBroker.class.getName()));
+ slaveIntent.putExtra(
+ OnDeviceInstrumentationBroker.EXTRA_INSTRUMENTATION_PACKAGE,
+ mTargetPackage);
+ slaveIntent.putExtra(
+ OnDeviceInstrumentationBroker.EXTRA_INSTRUMENTATION_CLASS,
+ mTargetClass);
+ slaveIntent.putExtra(OnDeviceInstrumentationBroker.EXTRA_TEST, t);
+ slaveIntent.putExtra(OnDeviceInstrumentationBroker.EXTRA_TARGET_ARGS,
+ mTargetArgs);
+ slaveIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ getContext().startActivity(slaveIntent);
+
+ synchronized (statusLock) {
+ while (!finished.containsKey(t)) {
+ long waitStart = System.currentTimeMillis();
+ statusLock.wait(TEST_WAIT_TIMEOUT);
+ if (System.currentTimeMillis() - waitStart > TEST_WAIT_TIMEOUT) {
+ Log.e(TAG, t + " has gone missing and is assumed to be dead.");
+ finished.put(t, ResultsBundleGenerator.TestResult.FAILED);
+ break;
+ }
+ }
+ }
+ }
+ getContext().unregisterReceiver(r);
+
+ } catch (InterruptedException e) {
+ fail("Interrupted while running tests.", e);
+ return;
+ }
+ pass(new RobotiumBundleGenerator().generate(finished));
+ }
+
+ }
+
+ private void fail(String reason) {
+ Log.e(TAG, reason);
+ failImpl(reason);
+ }
+
+ private void fail(String reason, Exception e) {
+ Log.e(TAG, reason, e);
+ failImpl(reason);
+ }
+
+ private void failImpl(String reason) {
+ Bundle b = new Bundle();
+ b.putString("reason", reason);
+ finish(Activity.RESULT_CANCELED, b);
+ }
+
+ private void pass(Bundle results) {
+ finish(Activity.RESULT_OK, results);
+ }
+}
diff --git a/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java b/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java
index 4e1a067..25b6570 100644
--- a/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java
+++ b/testing/android/native_test/java/src/org/chromium/native_test/ChromeNativeTestInstrumentationTestRunner.java
@@ -12,6 +12,9 @@ import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
+import org.chromium.test.support.ResultsBundleGenerator;
+import org.chromium.test.support.RobotiumBundleGenerator;
+
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
@@ -38,10 +41,6 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation {
private static final int ACCEPT_TIMEOUT_MS = 5000;
private static final Pattern RE_TEST_OUTPUT = Pattern.compile("\\[ *([^ ]*) *\\] ?([^ ]+) .*");
- private static interface ResultsBundleGenerator {
- public Bundle generate(Map<String, TestResult> rawResults);
- }
-
private String mCommandLineFile;
private String mCommandLineFlags;
private File mStdoutFile;
@@ -91,7 +90,7 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation {
}
Log.i(TAG, "Getting results.");
- Map<String, TestResult> results = parseResults(activityUnderTest);
+ Map<String, ResultsBundleGenerator.TestResult> results = parseResults(activityUnderTest);
Log.i(TAG, "Parsing results and generating output.");
return mBundleGenerator.generate(results);
@@ -117,16 +116,14 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation {
return startActivitySync(i);
}
- private static enum TestResult {
- PASSED, FAILED, ERROR, UNKNOWN
- }
-
/**
* Generates a map between test names and test results from the instrumented Activity's
* output.
*/
- private Map<String, TestResult> parseResults(Activity activityUnderTest) {
- Map<String, TestResult> results = new HashMap<String, TestResult>();
+ private Map<String, ResultsBundleGenerator.TestResult> parseResults(
+ Activity activityUnderTest) {
+ Map<String, ResultsBundleGenerator.TestResult> results =
+ new HashMap<String, ResultsBundleGenerator.TestResult>();
BufferedReader r = null;
@@ -145,14 +142,14 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation {
boolean isFailure = false;
if (m.matches()) {
if (m.group(1).equals("RUN")) {
- results.put(m.group(2), TestResult.UNKNOWN);
+ results.put(m.group(2), ResultsBundleGenerator.TestResult.UNKNOWN);
} else if (m.group(1).equals("FAILED")) {
- results.put(m.group(2), TestResult.FAILED);
+ results.put(m.group(2), ResultsBundleGenerator.TestResult.FAILED);
isFailure = true;
mLogBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT, l + "\n");
sendStatus(0, mLogBundle);
} else if (m.group(1).equals("OK")) {
- results.put(m.group(2), TestResult.PASSED);
+ results.put(m.group(2), ResultsBundleGenerator.TestResult.PASSED);
}
}
@@ -185,46 +182,4 @@ public class ChromeNativeTestInstrumentationTestRunner extends Instrumentation {
return results;
}
- /**
- * Creates a results bundle that emulates the one created by Robotium.
- */
- private static class RobotiumBundleGenerator implements ResultsBundleGenerator {
- public Bundle generate(Map<String, TestResult> rawResults) {
- Bundle resultsBundle = new Bundle();
-
- int testsPassed = 0;
- int testsFailed = 0;
-
- for (Map.Entry<String, TestResult> entry : rawResults.entrySet()) {
- switch (entry.getValue()) {
- case PASSED:
- ++testsPassed;
- break;
- case FAILED:
- // TODO(jbudorick): Remove this log message once AMP execution and
- // results handling has been stabilized.
- Log.d(TAG, "FAILED: " + entry.getKey());
- ++testsFailed;
- break;
- default:
- Log.w(TAG, "Unhandled: " + entry.getKey() + ", "
- + entry.getValue().toString());
- break;
- }
- }
-
- StringBuilder resultBuilder = new StringBuilder();
- if (testsFailed > 0) {
- resultBuilder.append(
- "\nFAILURES!!! Tests run: " + Integer.toString(rawResults.size())
- + ", Failures: " + Integer.toString(testsFailed) + ", Errors: 0");
- } else {
- resultBuilder.append("\nOK (" + Integer.toString(testsPassed) + " tests)");
- }
- resultsBundle.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
- resultBuilder.toString());
- return resultsBundle;
- }
- }
-
}
diff --git a/testing/android/on_device_instrumentation.gyp b/testing/android/on_device_instrumentation.gyp
new file mode 100644
index 0000000..12e0f35
--- /dev/null
+++ b/testing/android/on_device_instrumentation.gyp
@@ -0,0 +1,79 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+ 'conditions': [
+ ['OS=="android"', {
+ 'variables' : {
+ 'driver_apk_name': 'OnDeviceInstrumentationDriver',
+ 'driver_apk_path': '<(PRODUCT_DIR)/apks/<(driver_apk_name).apk'
+ },
+ 'targets': [
+ {
+ 'target_name': 'reporter_java',
+ 'type': 'none',
+ 'dependencies': ['../../base/base.gyp:base_java'],
+ 'variables': {
+ 'java_in_dir': '../../testing/android/reporter/java',
+ },
+ 'includes': [
+ '../../build/java.gypi',
+ ],
+ },
+ {
+ 'target_name': 'broker_java',
+ 'type': 'none',
+ 'variables': {
+ 'java_in_dir': '../../testing/android/broker/java',
+ },
+ 'includes': [
+ '../../build/java.gypi',
+ ],
+ },
+ {
+ 'target_name': 'driver_apk',
+ 'type': 'none',
+ 'dependencies': [
+ 'broker_java',
+ 'reporter_java',
+ 'appurify_support.gyp:appurify_support_java',
+ ],
+ 'variables': {
+ 'apk_name': '<(driver_apk_name)',
+ 'final_apk_path': '<(driver_apk_path)',
+ 'java_in_dir': '../../testing/android/driver/java',
+ },
+ 'includes': [
+ '../../build/java_apk.gypi',
+ ],
+ },
+ {
+ # This emulates gn's datadeps fields, allowing other APKs to declare
+ # that they require that this APK be built without including the
+ # driver's code.
+ 'target_name': 'require_driver_apk',
+ 'type': 'none',
+ 'actions': [
+ {
+ 'action_name': 'require_<(driver_apk_name)',
+ 'message': 'Making sure <(driver_apk_path) has been built.',
+ 'variables': {
+ 'required_file': '<(PRODUCT_DIR)/driver_apk/<(driver_apk_name).apk.required',
+ },
+ 'inputs': [
+ '<(driver_apk_path)',
+ ],
+ 'outputs': [
+ '<(required_file)',
+ ],
+ 'action': [
+ 'python', '../../build/android/gyp/touch.py', '<(required_file)',
+ ],
+ },
+ ],
+ },
+ ],
+ }],
+ ],
+}
diff --git a/testing/android/reporter/BUILD.gn b/testing/android/reporter/BUILD.gn
new file mode 100644
index 0000000..7086a79
--- /dev/null
+++ b/testing/android/reporter/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+
+# GYP: //testing/android/on_device_instrumentation.gyp:reporter_java
+android_library("reporter_java") {
+ chromium_code = true
+
+ deps = [
+ "//base:base_java",
+ ]
+ java_files = [
+ "java/src/org/chromium/test/reporter/TestStatusListener.java",
+ "java/src/org/chromium/test/reporter/TestStatusReceiver.java",
+ "java/src/org/chromium/test/reporter/TestStatusReporter.java",
+ ]
+}
diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java
new file mode 100644
index 0000000..6275418
--- /dev/null
+++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusListener.java
@@ -0,0 +1,78 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.reporter;
+
+import android.content.Context;
+import android.util.Log;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestListener;
+
+/**
+ * A TestListener that reports when tests start, pass, or fail.
+ */
+public class TestStatusListener implements TestListener {
+
+ private static final String TAG = "TestStatusListener";
+
+ private boolean mFailed;
+ private final TestStatusReporter mReporter;
+
+ public TestStatusListener(Context context) {
+ mReporter = new TestStatusReporter(context);
+ }
+
+ /** Called when an error has occurred while running a test.
+
+ Note that an error usually means a problem with the test or test harness, not with
+ the code under test.
+
+ @param test The test in which the error occurred.
+ @param t The exception that was raised.
+ */
+ @Override
+ public void addError(Test test, Throwable t) {
+ Log.e(TAG, "Error while running " + test.toString(), t);
+ mFailed = true;
+ }
+
+ /** Called when a test has failed.
+
+ @param test The test in which the failure occurred.
+ @param t The exception that was raised.
+ */
+ public void addFailure(Test test, AssertionFailedError e) {
+ Log.e(TAG, "Failure while running " + test.toString(), e);
+ mFailed = true;
+ }
+
+ /** Called when a test has started.
+ @param test The test that started.
+ */
+ @Override
+ public void startTest(Test test) {
+ mFailed = false;
+ TestCase testCase = (TestCase) test;
+ mReporter.startHeartbeat();
+ mReporter.testStarted(testCase.getClass().getName(), testCase.getName());
+ }
+
+ /** Called when a test has ended.
+ @param test The test that ended.
+ */
+ @Override
+ public void endTest(Test test) {
+ TestCase testCase = (TestCase) test;
+ if (mFailed) {
+ mReporter.testFailed(testCase.getClass().getName(), testCase.getName());
+ } else {
+ mReporter.testPassed(testCase.getClass().getName(), testCase.getName());
+ }
+ mReporter.stopHeartbeat();
+ }
+
+}
diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java
new file mode 100644
index 0000000..e4af9b6
--- /dev/null
+++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReceiver.java
@@ -0,0 +1,128 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.reporter;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Receives test status broadcasts send from
+ {@link org.chromium.test.reporter.TestStatusReporter}.
+ */
+public class TestStatusReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "ResultReceiver";
+
+ private final List<FailCallback> mFailCallbacks = new ArrayList<FailCallback>();
+ private final List<HeartbeatCallback> mHeartbeatCallbacks = new ArrayList<HeartbeatCallback>();
+ private final List<PassCallback> mPassCallbacks = new ArrayList<PassCallback>();
+ private final List<StartCallback> mStartCallbacks = new ArrayList<StartCallback>();
+
+ /** An IntentFilter that matches the intents that this class can receive. */
+ private static final IntentFilter INTENT_FILTER;
+ static {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(TestStatusReporter.ACTION_HEARTBEAT);
+ filter.addAction(TestStatusReporter.ACTION_TEST_FAILED);
+ filter.addAction(TestStatusReporter.ACTION_TEST_PASSED);
+ filter.addAction(TestStatusReporter.ACTION_TEST_STARTED);
+ try {
+ filter.addDataType(TestStatusReporter.DATA_TYPE_HEARTBEAT);
+ filter.addDataType(TestStatusReporter.DATA_TYPE_RESULT);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.wtf(TAG, "Invalid MIME type", e);
+ }
+ INTENT_FILTER = filter;
+ }
+
+ /** A callback used when a test has failed. */
+ public interface FailCallback {
+ void testFailed(String testClass, String testMethod);
+ }
+
+ /** A callback used when a heartbeat is received. */
+ public interface HeartbeatCallback {
+ void heartbeat();
+ }
+
+ /** A callback used when a test has passed. */
+ public interface PassCallback {
+ void testPassed(String testClass, String testMethod);
+ }
+
+ /** A callback used when a test has started. */
+ public interface StartCallback {
+ void testStarted(String testClass, String testMethod);
+ }
+
+ /** Register a callback for when a test has failed. */
+ public void registerCallback(FailCallback c) {
+ mFailCallbacks.add(c);
+ }
+
+ /** Register a callback for when a heartbeat is received. */
+ public void registerCallback(HeartbeatCallback c) {
+ mHeartbeatCallbacks.add(c);
+ }
+
+ /** Register a callback for when a test has passed. */
+ public void registerCallback(PassCallback c) {
+ mPassCallbacks.add(c);
+ }
+
+ /** Register a callback for when a test has started. */
+ public void registerCallback(StartCallback c) {
+ mStartCallbacks.add(c);
+ }
+
+ /** Register this receiver using the provided context. */
+ public void register(Context c) {
+ c.registerReceiver(this, INTENT_FILTER);
+ }
+
+ /** Receive a broadcast intent.
+ *
+ * @param context The Context in which the receiver is running.
+ * @param intent The intent received.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String testClass = intent.getStringExtra(TestStatusReporter.EXTRA_TEST_CLASS);
+ String testMethod = intent.getStringExtra(TestStatusReporter.EXTRA_TEST_METHOD);
+
+ switch (intent.getAction()) {
+ case TestStatusReporter.ACTION_TEST_STARTED:
+ for (StartCallback c : mStartCallbacks) {
+ c.testStarted(testClass, testMethod);
+ }
+ break;
+ case TestStatusReporter.ACTION_TEST_PASSED:
+ for (PassCallback c : mPassCallbacks) {
+ c.testPassed(testClass, testMethod);
+ }
+ break;
+ case TestStatusReporter.ACTION_TEST_FAILED:
+ for (FailCallback c : mFailCallbacks) {
+ c.testFailed(testClass, testMethod);
+ }
+ break;
+ case TestStatusReporter.ACTION_HEARTBEAT:
+ for (HeartbeatCallback c : mHeartbeatCallbacks) {
+ c.heartbeat();
+ }
+ break;
+ default:
+ Log.e(TAG, "Unrecognized intent received: " + intent.toString());
+ break;
+ }
+ }
+
+}
+
diff --git a/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java
new file mode 100644
index 0000000..6ac7312
--- /dev/null
+++ b/testing/android/reporter/java/src/org/chromium/test/reporter/TestStatusReporter.java
@@ -0,0 +1,83 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.test.reporter;
+
+import android.content.Context;
+import android.content.Intent;
+
+import org.chromium.base.ThreadUtils;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Broadcasts test status to any listening {@link org.chromium.test.reporter.TestStatusReceiver}.
+ */
+public class TestStatusReporter {
+
+ public static final String ACTION_HEARTBEAT =
+ "org.chromium.test.reporter.TestStatusReporter.HEARTBEAT";
+ public static final String ACTION_TEST_STARTED =
+ "org.chromium.test.reporter.TestStatusReporter.TEST_STARTED";
+ public static final String ACTION_TEST_PASSED =
+ "org.chromium.test.reporter.TestStatusReporter.TEST_PASSED";
+ public static final String ACTION_TEST_FAILED =
+ "org.chromium.test.reporter.TestStatusReporter.TEST_FAILED";
+ public static final String DATA_TYPE_HEARTBEAT = "org.chromium.test.reporter/heartbeat";
+ public static final String DATA_TYPE_RESULT = "org.chromium.test.reporter/result";
+ public static final String EXTRA_TEST_CLASS =
+ "org.chromium.test.reporter.TestStatusReporter.TEST_CLASS";
+ public static final String EXTRA_TEST_METHOD =
+ "org.chromium.test.reporter.TestStatusReporter.TEST_METHOD";
+
+ public static final int HEARTBEAT_INTERVAL_MS = 5000;
+
+ private final Context mContext;
+ private final AtomicBoolean mKeepBeating = new AtomicBoolean(false);
+
+ public TestStatusReporter(Context c) {
+ mContext = c;
+ }
+
+ public void startHeartbeat() {
+ mKeepBeating.set(true);
+ Runnable heartbeat = new Runnable() {
+ @Override
+ public void run() {
+ Intent i = new Intent(ACTION_HEARTBEAT);
+ i.setType(DATA_TYPE_HEARTBEAT);
+ mContext.sendBroadcast(i);
+ if (mKeepBeating.get()) {
+ ThreadUtils.postOnUiThreadDelayed(this, HEARTBEAT_INTERVAL_MS);
+ }
+ }
+ };
+ ThreadUtils.postOnUiThreadDelayed(heartbeat, HEARTBEAT_INTERVAL_MS);
+ }
+
+ public void testStarted(String testClass, String testMethod) {
+ sendBroadcast(testClass, testMethod, ACTION_TEST_STARTED);
+ }
+
+ public void testPassed(String testClass, String testMethod) {
+ sendBroadcast(testClass, testMethod, ACTION_TEST_PASSED);
+ }
+
+ public void testFailed(String testClass, String testMethod) {
+ sendBroadcast(testClass, testMethod, ACTION_TEST_FAILED);
+ }
+
+ public void stopHeartbeat() {
+ mKeepBeating.set(false);
+ }
+
+ private void sendBroadcast(String testClass, String testMethod, String action) {
+ Intent i = new Intent(action);
+ i.setType(DATA_TYPE_RESULT);
+ i.putExtra(EXTRA_TEST_CLASS, testClass);
+ i.putExtra(EXTRA_TEST_METHOD, testMethod);
+ mContext.sendBroadcast(i);
+ }
+
+}