diff options
author | frankf@chromium.org <frankf@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-20 18:06:53 +0000 |
---|---|---|
committer | frankf@chromium.org <frankf@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-20 18:06:53 +0000 |
commit | c0a2c9822de587d02bed838e2f96be2cfb210c82 (patch) | |
tree | cba0d92cd1e916d8e79b6303c285b9103d11119e /build | |
parent | 1c08c84e93f7acbafe2b0990b4c23484c7667638 (diff) | |
download | chromium_src-c0a2c9822de587d02bed838e2f96be2cfb210c82.zip chromium_src-c0a2c9822de587d02bed838e2f96be2cfb210c82.tar.gz chromium_src-c0a2c9822de587d02bed838e2f96be2cfb210c82.tar.bz2 |
[Android] Enable running uiautomator tests.
- uiautomator uses instrumentation runner for results reporting.
- Separate the concept of test jar (for progaurd consumption) from test
apk
- clear the app before every uiautomator test
BUG=162742
NOTRY=True
Review URL: https://chromiumcodereview.appspot.com/12921004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@189343 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build')
-rwxr-xr-x | build/android/adb_install_apk.py | 4 | ||||
-rw-r--r-- | build/android/pylib/android_commands.py | 57 | ||||
-rw-r--r-- | build/android/pylib/host_driven/python_test_base.py | 9 | ||||
-rw-r--r-- | build/android/pylib/host_driven/run_python_tests.py | 7 | ||||
-rw-r--r-- | build/android/pylib/instrumentation/dispatch.py | 34 | ||||
-rw-r--r-- | build/android/pylib/instrumentation/test_jar.py | 163 | ||||
-rw-r--r-- | build/android/pylib/instrumentation/test_package.py | 31 | ||||
-rw-r--r-- | build/android/pylib/instrumentation/test_runner.py | 75 | ||||
-rw-r--r-- | build/android/pylib/uiautomator/__init__.py | 4 | ||||
-rw-r--r-- | build/android/pylib/uiautomator/test_package.py | 27 | ||||
-rw-r--r-- | build/android/pylib/utils/apk_helper.py | 21 | ||||
-rw-r--r-- | build/android/pylib/utils/jar_helper.py (renamed from build/android/pylib/instrumentation/apk_info.py) | 32 | ||||
-rw-r--r-- | build/android/pylib/utils/test_options_parser.py | 85 | ||||
-rwxr-xr-x | build/android/run_instrumentation_tests.py | 9 | ||||
-rwxr-xr-x | build/android/run_uiautomator_tests.py | 82 |
15 files changed, 517 insertions, 123 deletions
diff --git a/build/android/adb_install_apk.py b/build/android/adb_install_apk.py index 8728c08..ad69457 100755 --- a/build/android/adb_install_apk.py +++ b/build/android/adb_install_apk.py @@ -11,7 +11,7 @@ import sys from pylib import android_commands from pylib import constants -from pylib.instrumentation import apk_info +from pylib.utils import apk_helper from pylib.utils import test_options_parser @@ -36,7 +36,7 @@ def main(argv): raise Exception('Error: no connected devices') if not options.apk_package: - options.apk_package = apk_info.GetPackageNameForApk(options.apk) + options.apk_package = apk_helper.GetPackageName(options.apk) pool = multiprocessing.Pool(len(devices)) # Send a tuple (apk_path, apk_package, device) per device. diff --git a/build/android/pylib/android_commands.py b/build/android/pylib/android_commands.py index 41d5551..46bc3e4 100644 --- a/build/android/pylib/android_commands.py +++ b/build/android/pylib/android_commands.py @@ -18,20 +18,19 @@ import sys import tempfile import time +import cmd_helper +import constants import io_stats_parser try: import pexpect except: pexpect = None -CHROME_SRC = os.path.join( - os.path.abspath(os.path.dirname(__file__)), '..', '..', '..') - -sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner')) +sys.path.append(os.path.join( + constants.CHROME_DIR, 'third_party', 'android_testrunner')) import adb_interface - -import cmd_helper -import errors # is under ../../../third_party/android_testrunner/errors.py +import am_instrument_parser +import errors # Pattern to search for the next whole line of pexpect output and capture it @@ -1214,6 +1213,50 @@ class AndroidCommands(object): """ self._util_wrapper = util_wrapper + def RunInstrumentationTest(self, test, test_package, instr_args, timeout): + """Runs a single instrumentation test. + + Args: + test: Test class/method. + test_package: Package name of test apk. + instr_args: Extra key/value to pass to am instrument. + timeout: Timeout time in seconds. + + Returns: + An instance of am_instrument_parser.TestResult object. + """ + instrumentation_path = ('%s/android.test.InstrumentationTestRunner' % + test_package) + args_with_filter = dict(instr_args) + args_with_filter['class'] = test + logging.info(args_with_filter) + (raw_results, _) = self._adb.StartInstrumentation( + instrumentation_path=instrumentation_path, + instrumentation_args=args_with_filter, + timeout_time=timeout) + assert len(raw_results) == 1 + return raw_results[0] + + def RunUIAutomatorTest(self, test, test_package, timeout): + """Runs a single uiautomator test. + + Args: + test: Test class/method. + test_package: Name of the test jar. + timeout: Timeout time in seconds. + + Returns: + An instance of am_instrument_parser.TestResult object. + """ + cmd = 'uiautomator runtest %s -e class %s' % (test_package, test) + logging.info('>>> $' + cmd) + output = self._adb.SendShellCommand(cmd, timeout_time=timeout) + # uiautomator doesn't fully conform to the instrumenation test runner + # convention and doesn't terminate with INSTRUMENTATION_CODE. + # Just assume the first result is valid. + (test_results, _) = am_instrument_parser.ParseAmInstrumentOutput(output) + return test_results[0] + class NewLineNormalizer(object): """A file-like object to normalize EOLs to '\n'. diff --git a/build/android/pylib/host_driven/python_test_base.py b/build/android/pylib/host_driven/python_test_base.py index 6d9f531..390d028 100644 --- a/build/android/pylib/host_driven/python_test_base.py +++ b/build/android/pylib/host_driven/python_test_base.py @@ -25,7 +25,7 @@ import time from pylib import android_commands from pylib.base.test_result import SingleTestResult, TestResults -from pylib.instrumentation import apk_info +from pylib.instrumentation import test_package from pylib.instrumentation import test_runner @@ -75,10 +75,11 @@ class PythonTestBase(object): TestResults object with a single test result. """ test = self._ComposeFullTestName(fname, suite, test) - apks = [apk_info.ApkInfo(self.options.test_apk_path, - self.options.test_apk_jar_path)] + test_pkg = test_package.TestPackage( + self.options.test_apk_path, self.options.test_apk_jar_path) java_test_runner = test_runner.TestRunner(self.options, self.device_id, - self.shard_index, False, apks, + self.shard_index, False, + test_pkg, self.ports_to_forward) try: java_test_runner.SetUp() diff --git a/build/android/pylib/host_driven/run_python_tests.py b/build/android/pylib/host_driven/run_python_tests.py index 8684225..10ef87c 100644 --- a/build/android/pylib/host_driven/run_python_tests.py +++ b/build/android/pylib/host_driven/run_python_tests.py @@ -12,7 +12,7 @@ import types from pylib import android_commands from pylib import constants from pylib.base.test_result import TestResults -from pylib.instrumentation import apk_info +from pylib.instrumentation import test_package from pylib.instrumentation import test_runner import python_test_base @@ -84,9 +84,10 @@ def DispatchPythonTests(options): # Copy files to each device before running any tests. for device_id in attached_devices: logging.debug('Pushing files to device %s', device_id) - apks = [apk_info.ApkInfo(options.test_apk_path, options.test_apk_jar_path)] + test_pkg = test_package.TestPackage(options.test_apk_path, + options.test_apk_jar_path) test_files_copier = test_runner.TestRunner(options, device_id, 0, False, - apks, []) + test_pkg, []) test_files_copier.CopyTestFilesOnce() # Actually run the tests. diff --git a/build/android/pylib/instrumentation/dispatch.py b/build/android/pylib/instrumentation/dispatch.py index 43a3ab5..8c7e779 100644 --- a/build/android/pylib/instrumentation/dispatch.py +++ b/build/android/pylib/instrumentation/dispatch.py @@ -10,12 +10,13 @@ import os from pylib import android_commands from pylib.base import shard from pylib.base import test_result +from pylib.uiautomator import test_package as uiautomator_package -import apk_info +import test_package import test_runner -def Dispatch(options, apks): +def Dispatch(options): """Dispatches instrumentation tests onto connected device(s). If possible, this method will attempt to shard the tests to @@ -23,7 +24,6 @@ def Dispatch(options, apks): Args: options: Command line options. - apks: list of APKs to use. Returns: A TestResults object holding the results of the Java tests. @@ -31,26 +31,33 @@ def Dispatch(options, apks): Raises: Exception: when there are no attached devices. """ - test_apk = apks[0] + is_uiautomator_test = False + if hasattr(options, 'uiautomator_jar'): + test_pkg = uiautomator_package.TestPackage( + options.uiautomator_jar, options.uiautomator_info_jar) + is_uiautomator_test = True + else: + test_pkg = test_package.TestPackage(options.test_apk_path, + options.test_apk_jar_path) # The default annotation for tests which do not have any sizes annotation. default_size_annotation = 'SmallTest' - def _GetTestsMissingAnnotation(test_apk): + def _GetTestsMissingAnnotation(test_pkg): test_size_annotations = frozenset(['Smoke', 'SmallTest', 'MediumTest', 'LargeTest', 'EnormousTest', 'FlakyTest', 'DisabledTest', 'Manual', 'PerfTest']) tests_missing_annotations = [] - for test_method in test_apk.GetTestMethods(): - annotations = frozenset(test_apk.GetTestAnnotations(test_method)) + for test_method in test_pkg.GetTestMethods(): + annotations = frozenset(test_pkg.GetTestAnnotations(test_method)) if (annotations.isdisjoint(test_size_annotations) and - not apk_info.ApkInfo.IsPythonDrivenTest(test_method)): + not test_pkg.IsPythonDrivenTest(test_method)): tests_missing_annotations.append(test_method) return sorted(tests_missing_annotations) if options.annotation: - available_tests = test_apk.GetAnnotatedTests(options.annotation) + available_tests = test_pkg.GetAnnotatedTests(options.annotation) if options.annotation.count(default_size_annotation) > 0: - tests_missing_annotations = _GetTestsMissingAnnotation(test_apk) + tests_missing_annotations = _GetTestsMissingAnnotation(test_pkg) if tests_missing_annotations: logging.warning('The following tests do not contain any annotation. ' 'Assuming "%s":\n%s', @@ -58,8 +65,8 @@ def Dispatch(options, apks): '\n'.join(tests_missing_annotations)) available_tests += tests_missing_annotations else: - available_tests = [m for m in test_apk.GetTestMethods() - if not apk_info.ApkInfo.IsPythonDrivenTest(m)] + available_tests = [m for m in test_pkg.GetTestMethods() + if not test_pkg.IsPythonDrivenTest(m)] coverage = os.environ.get('EMMA_INSTRUMENT') == 'true' tests = [] @@ -92,7 +99,8 @@ def Dispatch(options, apks): attached_devices = attached_devices[:1] def TestRunnerFactory(device, shard_index): - return test_runner.TestRunner(options, device, shard_index, False, apks, []) + return test_runner.TestRunner( + options, device, shard_index, False, test_pkg, [], is_uiautomator_test) return shard.ShardAndRunTests(TestRunnerFactory, attached_devices, tests, options.build_type) diff --git a/build/android/pylib/instrumentation/test_jar.py b/build/android/pylib/instrumentation/test_jar.py new file mode 100644 index 0000000..ef01656 --- /dev/null +++ b/build/android/pylib/instrumentation/test_jar.py @@ -0,0 +1,163 @@ +# Copyright (c) 2013 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. + +"""Helper class for instrumenation test jar.""" + +import collections +import logging +import os +import pickle +import re + +from pylib import cmd_helper +from pylib import constants + + +# If you change the cached output of proguard, increment this number +PICKLE_FORMAT_VERSION = 1 + + +class TestJar(object): + def __init__(self, jar_path): + sdk_root = os.getenv('ANDROID_SDK_ROOT', constants.ANDROID_SDK_ROOT) + self._PROGUARD_PATH = os.path.join(sdk_root, + 'tools/proguard/bin/proguard.sh') + if not os.path.exists(self._PROGUARD_PATH): + self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'], + 'external/proguard/bin/proguard.sh') + self._PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$') + self._PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$') + self._PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$') + self._PROGUARD_ANNOTATION_CONST_RE = ( + re.compile(r'\s*?- Constant element value.*$')) + self._PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$') + + if not os.path.exists(jar_path): + raise Exception('%s not found, please build it' % jar_path) + self._jar_path = jar_path + self._annotation_map = collections.defaultdict(list) + self._pickled_proguard_name = self._jar_path + '-proguard.pickle' + self._test_methods = [] + if not self._GetCachedProguardData(): + self._GetProguardData() + + def _GetCachedProguardData(self): + if (os.path.exists(self._pickled_proguard_name) and + (os.path.getmtime(self._pickled_proguard_name) > + os.path.getmtime(self._jar_path))): + logging.info('Loading cached proguard output from %s', + self._pickled_proguard_name) + try: + with open(self._pickled_proguard_name, 'r') as r: + d = pickle.loads(r.read()) + if d['VERSION'] == PICKLE_FORMAT_VERSION: + self._annotation_map = d['ANNOTATION_MAP'] + self._test_methods = d['TEST_METHODS'] + return True + except: + logging.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache') + return False + + def _GetProguardData(self): + proguard_output = cmd_helper.GetCmdOutput([self._PROGUARD_PATH, + '-injars', self._jar_path, + '-dontshrink', + '-dontoptimize', + '-dontobfuscate', + '-dontpreverify', + '-dump', + ]).split('\n') + clazz = None + method = None + annotation = None + has_value = False + qualified_method = None + for line in proguard_output: + m = self._PROGUARD_CLASS_RE.match(line) + if m: + clazz = m.group(1).replace('/', '.') # Change package delim. + annotation = None + continue + + m = self._PROGUARD_METHOD_RE.match(line) + if m: + method = m.group(1) + annotation = None + qualified_method = clazz + '#' + method + if method.startswith('test') and clazz.endswith('Test'): + self._test_methods += [qualified_method] + continue + + if not qualified_method: + # Ignore non-method annotations. + continue + + m = self._PROGUARD_ANNOTATION_RE.match(line) + if m: + annotation = m.group(1).split('/')[-1] # Ignore the annotation package. + self._annotation_map[qualified_method].append(annotation) + has_value = False + continue + if annotation: + if not has_value: + m = self._PROGUARD_ANNOTATION_CONST_RE.match(line) + if m: + has_value = True + else: + m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line) + if m: + value = m.group(1) + self._annotation_map[qualified_method].append( + annotation + ':' + value) + has_value = False + + logging.info('Storing proguard output to %s', self._pickled_proguard_name) + d = {'VERSION': PICKLE_FORMAT_VERSION, + 'ANNOTATION_MAP': self._annotation_map, + 'TEST_METHODS': self._test_methods} + with open(self._pickled_proguard_name, 'w') as f: + f.write(pickle.dumps(d)) + + def _GetAnnotationMap(self): + return self._annotation_map + + def _IsTestMethod(self, test): + class_name, method = test.split('#') + return class_name.endswith('Test') and method.startswith('test') + + def GetTestAnnotations(self, test): + """Returns a list of all annotations for the given |test|. May be empty.""" + if not self._IsTestMethod(test): + return [] + return self._GetAnnotationMap()[test] + + def _AnnotationsMatchFilters(self, annotation_filter_list, annotations): + """Checks if annotations match any of the filters.""" + if not annotation_filter_list: + return True + for annotation_filter in annotation_filter_list: + filters = annotation_filter.split('=') + if len(filters) == 2: + key = filters[0] + value_list = filters[1].split(',') + for value in value_list: + if key + ':' + value in annotations: + return True + elif annotation_filter in annotations: + return True + return False + + def GetAnnotatedTests(self, annotation_filter_list): + """Returns a list of all tests that match the given annotation filters.""" + return [test for test, annotations in self._GetAnnotationMap().iteritems() + if self._IsTestMethod(test) and self._AnnotationsMatchFilters( + annotation_filter_list, annotations)] + + def GetTestMethods(self): + """Returns a list of all test methods in this apk as Class#testMethod.""" + return self._test_methods + + @staticmethod + def IsPythonDrivenTest(test): + return 'pythonDrivenTests' in test diff --git a/build/android/pylib/instrumentation/test_package.py b/build/android/pylib/instrumentation/test_package.py new file mode 100644 index 0000000..c0b87ee --- /dev/null +++ b/build/android/pylib/instrumentation/test_package.py @@ -0,0 +1,31 @@ +# Copyright (c) 2013 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. + +"""Class representing instrumentation test apk and jar.""" + +import os + +from pylib.utils import apk_helper + +import test_jar + + +class TestPackage(test_jar.TestJar): + def __init__(self, apk_path, jar_path): + test_jar.TestJar.__init__(self, jar_path) + + if not os.path.exists(apk_path): + raise Exception('%s not found, please build it' % apk_path) + self._apk_path = apk_path + + def GetApkPath(self): + return self._apk_path + + def GetPackageName(self): + """Returns the package name of this APK.""" + return apk_helper.GetPackageName(self._apk_path) + + # Override. + def Install(self, adb): + adb.ManagedInstall(self.GetApkPath(), package_name=self.GetPackageName()) diff --git a/build/android/pylib/instrumentation/test_runner.py b/build/android/pylib/instrumentation/test_runner.py index a058c4c..6948acd 100644 --- a/build/android/pylib/instrumentation/test_runner.py +++ b/build/android/pylib/instrumentation/test_runner.py @@ -21,8 +21,6 @@ from pylib import valgrind_tools from pylib.base import base_test_runner from pylib.base import test_result -import apk_info - _PERF_TEST_ANNOTATION = 'PerfTest' @@ -47,8 +45,8 @@ class TestRunner(base_test_runner.BaseTestRunner): '/chrome-profile*') _DEVICE_HAS_TEST_FILES = {} - def __init__(self, options, device, shard_index, coverage, apks, - ports_to_forward): + def __init__(self, options, device, shard_index, coverage, test_pkg, + ports_to_forward, is_uiautomator_test=False): """Create a new TestRunner. Args: @@ -64,34 +62,30 @@ class TestRunner(base_test_runner.BaseTestRunner): device: Attached android device. shard_index: Shard index. coverage: Collects coverage information if opted. - apks: A list of ApkInfo objects need to be installed. The first element - should be the tests apk, the rests could be the apks used in test. - The default is ChromeTest.apk. + test_pkg: A TestPackage object. ports_to_forward: A list of port numbers for which to set up forwarders. Can be optionally requested by a test case. + is_uiautomator_test: Whether this is a uiautomator test. Raises: Exception: if coverage metadata is not available. """ super(TestRunner, self).__init__(device, options.tool, options.build_type) self._lighttp_port = constants.LIGHTTPD_RANDOM_PORT_FIRST + shard_index - if not apks: - apks = [apk_info.ApkInfo(options.test_apk_path, - options.test_apk_jar_path)] - self.build_type = options.build_type - self.install_apk = options.install_apk self.test_data = options.test_data self.save_perf_json = options.save_perf_json self.screenshot_failures = options.screenshot_failures self.wait_for_debugger = options.wait_for_debugger self.disable_assertions = options.disable_assertions - self.coverage = coverage - self.apks = apks - self.test_apk = apks[0] - self.instrumentation_class_path = self.test_apk.GetPackageName() + self.test_pkg = test_pkg self.ports_to_forward = ports_to_forward + self.is_uiautomator_test = is_uiautomator_test + if self.is_uiautomator_test: + self.package_name = options.package_name + else: + self.install_apk = options.install_apk self.forwarder = None @@ -125,10 +119,11 @@ class TestRunner(base_test_runner.BaseTestRunner): self.adb.PushIfNeeded(host_test_files_path, self.adb.GetExternalStorage() + '/' + TestRunner._DEVICE_DATA_DIR + '/' + dst_layer) - if self.install_apk: - for apk in self.apks: - self.adb.ManagedInstall(apk.GetApkPath(), - package_name=apk.GetPackageName()) + if self.is_uiautomator_test: + self.test_pkg.Install(self.adb) + elif self.install_apk: + self.test_pkg.Install(self.adb) + self.tool.CopyFiles() TestRunner._DEVICE_HAS_TEST_FILES[self.device] = True @@ -253,7 +248,7 @@ class TestRunner(base_test_runner.BaseTestRunner): Returns: Whether the test is annotated as a performance test. """ - return _PERF_TEST_ANNOTATION in self.test_apk.GetTestAnnotations(test) + return _PERF_TEST_ANNOTATION in self.test_pkg.GetTestAnnotations(test) def SetupPerfMonitoringIfNeeded(self, test): """Sets up performance monitoring if the specified test requires it. @@ -352,7 +347,7 @@ class TestRunner(base_test_runner.BaseTestRunner): def _GetIndividualTestTimeoutScale(self, test): """Returns the timeout scale for the given |test|.""" - annotations = self.apks[0].GetTestAnnotations(test) + annotations = self.test_pkg.GetTestAnnotations(test) timeout_scale = 1 if 'TimeoutScale' in annotations: for annotation in annotations: @@ -365,7 +360,7 @@ class TestRunner(base_test_runner.BaseTestRunner): def _GetIndividualTestTimeoutSecs(self, test): """Returns the timeout in seconds for the given |test|.""" - annotations = self.apks[0].GetTestAnnotations(test) + annotations = self.test_pkg.GetTestAnnotations(test) if 'Manual' in annotations: return 600 * 60 if 'External' in annotations: @@ -382,29 +377,31 @@ class TestRunner(base_test_runner.BaseTestRunner): Returns: A test_result.TestResults object. """ - instrumentation_path = (self.instrumentation_class_path + - '/android.test.InstrumentationTestRunner') - instrumentation_args = self._GetInstrumentationArgs() raw_result = None start_date_ms = None test_results = test_result.TestResults() + timeout=(self._GetIndividualTestTimeoutSecs(test) * + self._GetIndividualTestTimeoutScale(test) * + self.tool.GetTimeoutScale()) try: self.TestSetup(test) start_date_ms = int(time.time()) * 1000 - args_with_filter = dict(instrumentation_args) - args_with_filter['class'] = test - # |raw_results| is a list that should contain - # a single TestResult object. - logging.warn(args_with_filter) - (raw_results, _) = self.adb.Adb().StartInstrumentation( - instrumentation_path=instrumentation_path, - instrumentation_args=args_with_filter, - timeout_time=(self._GetIndividualTestTimeoutSecs(test) * - self._GetIndividualTestTimeoutScale(test) * - self.tool.GetTimeoutScale())) + + if self.is_uiautomator_test: + self.adb.ClearApplicationState(self.package_name) + # TODO(frankf): Stop-gap solution. Should use annotations. + if 'FirstRun' in test: + self.flags.RemoveFlags(['--disable-fre']) + else: + self.flags.AddFlags(['--disable-fre']) + raw_result = self.adb.RunUIAutomatorTest( + test, self.test_pkg.GetPackageName(), timeout) + else: + raw_result = self.adb.RunInstrumentationTest( + test, self.test_pkg.GetPackageName(), + self._GetInstrumentationArgs(), timeout) + duration_ms = int(time.time()) * 1000 - start_date_ms - assert len(raw_results) == 1 - raw_result = raw_results[0] status_code = raw_result.GetStatusCode() if status_code: log = raw_result.GetFailureReason() diff --git a/build/android/pylib/uiautomator/__init__.py b/build/android/pylib/uiautomator/__init__.py new file mode 100644 index 0000000..cda7672 --- /dev/null +++ b/build/android/pylib/uiautomator/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2013 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. + diff --git a/build/android/pylib/uiautomator/test_package.py b/build/android/pylib/uiautomator/test_package.py new file mode 100644 index 0000000..e4acbe5 --- /dev/null +++ b/build/android/pylib/uiautomator/test_package.py @@ -0,0 +1,27 @@ +# Copyright (c) 2013 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. + +"""Class representing uiautomator test package.""" + +import os + +from pylib import constants +from pylib.instrumentation import test_jar + + +class TestPackage(test_jar.TestJar): + def __init__(self, jar_path, jar_info_path): + test_jar.TestJar.__init__(self, jar_info_path) + + if not os.path.exists(jar_path): + raise Exception('%s not found, please build it' % jar_path) + self._jar_path = jar_path + + def GetPackageName(self): + """Returns the JAR named that is installed on the device.""" + return os.path.basename(self._jar_path) + + # Override. + def Install(self, adb): + adb.PushIfNeeded(self._jar_path, constants.TEST_EXECUTABLE_DIR) diff --git a/build/android/pylib/utils/apk_helper.py b/build/android/pylib/utils/apk_helper.py new file mode 100644 index 0000000..fbcc595 --- /dev/null +++ b/build/android/pylib/utils/apk_helper.py @@ -0,0 +1,21 @@ +# Copyright (c) 2013 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. + +"""Module containing utilities for apk packages.""" + +import re + +from pylib import cmd_helper + + +def GetPackageName(apk_path): + """Returns the package name of the apk.""" + aapt_output = cmd_helper.GetCmdOutput( + ['aapt', 'dump', 'badging', apk_path]).split('\n') + package_name_re = re.compile(r'package: .*name=\'(\S*)\'') + for line in aapt_output: + m = package_name_re.match(line) + if m: + return m.group(1) + raise Exception('Failed to determine package name of %s' % apk_path) diff --git a/build/android/pylib/instrumentation/apk_info.py b/build/android/pylib/utils/jar_helper.py index 5f5e0c8..e3980d8 100644 --- a/build/android/pylib/instrumentation/apk_info.py +++ b/build/android/pylib/utils/jar_helper.py @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Gathers information about APKs.""" +"""Module containing utilities for jar packages.""" import collections import logging @@ -17,22 +17,9 @@ from pylib import constants # If you change the cached output of proguard, increment this number PICKLE_FORMAT_VERSION = 1 -def GetPackageNameForApk(apk_path): - """Returns the package name of the apk file.""" - aapt_output = cmd_helper.GetCmdOutput( - ['aapt', 'dump', 'badging', apk_path]).split('\n') - package_name_re = re.compile(r'package: .*name=\'(\S*)\'') - for line in aapt_output: - m = package_name_re.match(line) - if m: - return m.group(1) - raise Exception('Failed to determine package name of %s' % apk_path) - -class ApkInfo(object): - """Helper class for inspecting APKs.""" - - def __init__(self, apk_path, jar_path): +class TestPackageJar(object): + def __init__(self, jar_path): sdk_root = os.getenv('ANDROID_SDK_ROOT', constants.ANDROID_SDK_ROOT) self._PROGUARD_PATH = os.path.join(sdk_root, 'tools/proguard/bin/proguard.sh') @@ -46,18 +33,12 @@ class ApkInfo(object): re.compile(r'\s*?- Constant element value.*$')) self._PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$') - if not os.path.exists(apk_path): - raise Exception('%s not found, please build it' % apk_path) - self._apk_path = apk_path if not os.path.exists(jar_path): raise Exception('%s not found, please build it' % jar_path) self._jar_path = jar_path self._annotation_map = collections.defaultdict(list) self._pickled_proguard_name = self._jar_path + '-proguard.pickle' self._test_methods = [] - self._Initialize() - - def _Initialize(self): if not self._GetCachedProguardData(): self._GetProguardData() @@ -145,13 +126,6 @@ class ApkInfo(object): class_name, method = test.split('#') return class_name.endswith('Test') and method.startswith('test') - def GetApkPath(self): - return self._apk_path - - def GetPackageName(self): - """Returns the package name of this APK.""" - return GetPackageNameForApk(self._apk_path) - def GetTestAnnotations(self, test): """Returns a list of all annotations for the given |test|. May be empty.""" if not self._IsTestMethod(test): diff --git a/build/android/pylib/utils/test_options_parser.py b/build/android/pylib/utils/test_options_parser.py index 5352753..0352d21 100644 --- a/build/android/pylib/utils/test_options_parser.py +++ b/build/android/pylib/utils/test_options_parser.py @@ -28,6 +28,7 @@ def AddBuildTypeOption(option_parser): help='If set, run test suites under out/Release. ' 'Default is env var BUILDTYPE or Debug.') + def AddInstallAPKOption(option_parser): """Decorates OptionParser with apk option used to install the APK.""" AddBuildTypeOption(option_parser) @@ -127,14 +128,12 @@ def AddGTestOptions(option_parser): 'the APK.') -def AddInstrumentationOptions(option_parser): - """Decorates OptionParser with instrumentation tests options.""" +def AddCommonInstrumentationOptions(option_parser): + """Decorates OptionParser with base instrumentation tests options.""" AddTestRunnerOptions(option_parser) option_parser.add_option('-w', '--wait_debugger', dest='wait_for_debugger', action='store_true', help='Wait for debugger.') - option_parser.add_option('-I', dest='install_apk', help='Install APK.', - action='store_true') option_parser.add_option('-f', '--test_filter', help='Test filter (if not fully qualified, ' 'will run all matches).') @@ -153,11 +152,6 @@ def AddInstrumentationOptions(option_parser): dest='number_of_runs', default=1, help=('How many times to run each test, regardless ' 'of the result. (Default is 1)')) - option_parser.add_option('--test-apk', dest='test_apk', - help=('The name of the apk containing the tests ' - '(without the .apk extension). For SDK ' - 'builds, the apk name without the debug ' - 'suffix(for example, ContentShellTest).')) option_parser.add_option('--screenshot', dest='screenshot_failures', action='store_true', help='Capture screenshots of test failures') @@ -193,17 +187,44 @@ def AddInstrumentationOptions(option_parser): 'directory, and <source> is relative to the ' 'chromium build directory.')) -def ValidateInstrumentationOptions(option_parser, options, args): - """Validate options/arguments and populate options with defaults.""" + +def AddInstrumentationOptions(option_parser): + """Decorates OptionParser with instrumentation tests options.""" + + AddCommonInstrumentationOptions(option_parser) + option_parser.add_option('-I', dest='install_apk', + help='Install APK.', action='store_true') + option_parser.add_option('--test-apk', dest='test_apk', + help=('The name of the apk containing the tests ' + '(without the .apk extension). For SDK ' + 'builds, the apk name without the debug ' + 'suffix(for example, ContentShellTest).')) + + +def AddUIAutomatorOptions(option_parser): + """Decorates OptionParser with uiautomator tests options.""" + + AddCommonInstrumentationOptions(option_parser) + option_parser.add_option( + '--package-name', + help=('The package name used by the apk containing the application.')) + option_parser.add_option( + '--uiautomator-jar', + help=('Path to the uiautomator jar to be installed on the device.')) + option_parser.add_option( + '--uiautomator-info-jar', + help=('Path to the uiautomator jar for use by proguard.')) + + +def ValidateCommonInstrumentationOptions(option_parser, options, args): + """Validate common options/arguments and populate options with defaults.""" if len(args) > 1: option_parser.print_help(sys.stderr) option_parser.error('Unknown arguments: %s' % args[1:]) + if options.java_only and options.python_only: option_parser.error('Options java_only (-j) and python_only (-p) ' 'are mutually exclusive.') - if not options.test_apk: - option_parser.error('--test-apk must be specified.') - options.run_java_tests = True options.run_python_tests = True if options.java_only: @@ -211,6 +232,21 @@ def ValidateInstrumentationOptions(option_parser, options, args): elif options.python_only: options.run_java_tests = False + if options.annotation_str: + options.annotation = options.annotation_str.split() + elif options.test_filter: + options.annotation = [] + else: + options.annotation = ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest'] + + +def ValidateInstrumentationOptions(option_parser, options, args): + """Validate options/arguments and populate options with defaults.""" + ValidateCommonInstrumentationOptions(option_parser, options, args) + + if not options.test_apk: + option_parser.error('--test-apk must be specified.') + if os.path.exists(options.test_apk): # The APK is fully qualified, assume the JAR lives along side. options.test_apk_path = options.test_apk @@ -224,9 +260,18 @@ def ValidateInstrumentationOptions(option_parser, options, args): options.test_apk_jar_path = os.path.join( _SDK_OUT_DIR, options.build_type, constants.SDK_BUILD_TEST_JAVALIB_DIR, '%s.jar' % options.test_apk) - if options.annotation_str: - options.annotation = options.annotation_str.split() - elif options.test_filter: - options.annotation = [] - else: - options.annotation = ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest'] + + +def ValidateUIAutomatorOptions(option_parser, options, args): + """Validate uiautomator options/arguments.""" + ValidateCommonInstrumentationOptions(option_parser, options, args) + + if not options.package_name: + option_parser.error('--package-name must be specified.') + + if not options.uiautomator_jar: + option_parser.error('--uiautomator-jar must be specified.') + + if not options.uiautomator_info_jar: + option_parser.error('--uiautomator-info-jar must be specified.') + diff --git a/build/android/run_instrumentation_tests.py b/build/android/run_instrumentation_tests.py index 12a1510..4522ce8e 100755 --- a/build/android/run_instrumentation_tests.py +++ b/build/android/run_instrumentation_tests.py @@ -4,7 +4,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Runs both the Python and Java tests.""" +"""Runs both the Python and Java instrumentation tests.""" import optparse import os @@ -16,7 +16,6 @@ from pylib import constants from pylib import ports from pylib.base import test_result from pylib.host_driven import run_python_tests -from pylib.instrumentation import apk_info from pylib.instrumentation import dispatch from pylib.utils import run_tests_helper from pylib.utils import test_options_parser @@ -42,14 +41,12 @@ def DispatchInstrumentationTests(options): if not ports.ResetTestServerPortAllocation(): raise Exception('Failed to reset test server port.') - start_date = int(time.time() * 1000) java_results = test_result.TestResults() python_results = test_result.TestResults() if options.run_java_tests: - java_results = dispatch.Dispatch( - options, - [apk_info.ApkInfo(options.test_apk_path, options.test_apk_jar_path)]) + java_results = dispatch.Dispatch(options) + if options.run_python_tests: python_results = run_python_tests.DispatchPythonTests(options) diff --git a/build/android/run_uiautomator_tests.py b/build/android/run_uiautomator_tests.py new file mode 100755 index 0000000..58c8244 --- /dev/null +++ b/build/android/run_uiautomator_tests.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# +# Copyright (c) 2013 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. + +"""Runs both the Python and Java UIAutomator tests.""" + +import optparse +import os +import sys +import time + +from pylib import buildbot_report +from pylib import constants +from pylib import ports +from pylib.base import test_result +from pylib.host_driven import run_python_tests +from pylib.instrumentation import dispatch +from pylib.utils import run_tests_helper +from pylib.utils import test_options_parser + + +def DispatchUIAutomatorTests(options): + """Dispatches the UIAutomator tests, sharding if possible. + + Uses the logging module to print the combined final results and + summary of the Java and Python tests. If the java_only option is set, only + the Java tests run. If the python_only option is set, only the python tests + run. If neither are set, run both Java and Python tests. + + Args: + options: command-line options for running the Java and Python tests. + + Returns: + An integer representing the number of broken tests. + """ + if not options.keep_test_server_ports: + # Reset the test port allocation. It's important to do it before starting + # to dispatch any tests. + if not ports.ResetTestServerPortAllocation(): + raise Exception('Failed to reset test server port.') + + java_results = test_result.TestResults() + python_results = test_result.TestResults() + + if options.run_java_tests: + java_results = dispatch.Dispatch(options) + + if options.run_python_tests: + python_results = run_python_tests.DispatchPythonTests(options) + + all_results = test_result.TestResults.FromTestResults([java_results, + python_results]) + + all_results.LogFull( + test_type='UIAutomator', + test_package=os.path.basename(options.uiautomator_jar), + annotation=options.annotation, + build_type=options.build_type, + flakiness_server=options.flakiness_dashboard_server) + + return len(all_results.GetAllBroken()) + + +def main(argv): + option_parser = optparse.OptionParser() + test_options_parser.AddUIAutomatorOptions(option_parser) + options, args = option_parser.parse_args(argv) + test_options_parser.ValidateUIAutomatorOptions(option_parser, options, args) + + run_tests_helper.SetLogLevel(options.verbose_count) + ret = 1 + try: + ret = DispatchUIAutomatorTests(options) + finally: + buildbot_report.PrintStepResultIfNeeded(options, ret) + return ret + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) |