summaryrefslogtreecommitdiffstats
path: root/build
diff options
context:
space:
mode:
authorfrankf@chromium.org <frankf@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-20 18:06:53 +0000
committerfrankf@chromium.org <frankf@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-20 18:06:53 +0000
commitc0a2c9822de587d02bed838e2f96be2cfb210c82 (patch)
treecba0d92cd1e916d8e79b6303c285b9103d11119e /build
parent1c08c84e93f7acbafe2b0990b4c23484c7667638 (diff)
downloadchromium_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-xbuild/android/adb_install_apk.py4
-rw-r--r--build/android/pylib/android_commands.py57
-rw-r--r--build/android/pylib/host_driven/python_test_base.py9
-rw-r--r--build/android/pylib/host_driven/run_python_tests.py7
-rw-r--r--build/android/pylib/instrumentation/dispatch.py34
-rw-r--r--build/android/pylib/instrumentation/test_jar.py163
-rw-r--r--build/android/pylib/instrumentation/test_package.py31
-rw-r--r--build/android/pylib/instrumentation/test_runner.py75
-rw-r--r--build/android/pylib/uiautomator/__init__.py4
-rw-r--r--build/android/pylib/uiautomator/test_package.py27
-rw-r--r--build/android/pylib/utils/apk_helper.py21
-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.py85
-rwxr-xr-xbuild/android/run_instrumentation_tests.py9
-rwxr-xr-xbuild/android/run_uiautomator_tests.py82
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))