diff options
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); + } + +} |