blob: e97e86a228b87b4d92527806fb699d4506acf516 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2010-2015, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""A script to run unittests on android.
This file searches all tests under --test_bin_dir (the default is the current
directory), and run them on android (emulator or actual device).
You need to run an android emulator or to connect a dvice, before running this
script.
"""
__author__ = "hidehiko"
import errno
import logging
import multiprocessing
import optparse
import os
import subprocess
import time
from xml.etree import cElementTree as ElementTree
from build_tools import android_util
from build_tools.test_tools import gtest_report
def FindTestBinaries(test_dir):
"""Returns the file names of tests."""
logging.info('Gathering binaries in %s.', os.path.abspath(test_dir))
result = []
for f in os.listdir(test_dir):
path = os.path.join(test_dir, f)
# Test binaries are "executable file" and "its name ends with _test."
if (os.access(path, os.R_OK | os.X_OK) and
os.path.isfile(path) and
f.endswith('_test')):
result.append(f)
return result
def ParseArgs():
"""Parses command line options and returns them."""
parser = optparse.OptionParser()
parser.add_option('--run_java_test', dest='run_java_test', default=False,
action='store_true',
help='Runs JUnit tests. [JAVA] options are used.')
android_devices_default = os.environ.get('ANDROID_DEVICES', '')
parser.add_option('--android_devices', dest='android_devices',
default=android_devices_default,
help='[JAVA][NATIVE] Comma separated serial numbers '
'on which devices the tests run. '
'If not specified all the available devices are used.')
parser.add_option('--run_native_test', dest='run_native_test', default=False,
action='store_true',
help='Runs native tests. [NATIVE] options are used.')
parser.add_option('--configuration', dest='configuration',
default='Debug',
help='[JAVA] Build configuration. Used to build correct '
'testee binary.')
parser.add_option('--app_package_name', dest='app_package_name',
default='org.mozc.android.inputmethod.japanese',
help='[JAVA] Testee project\'s package name. '
'This is used to find test reporting XML file.')
parser.add_option('--remote_dir', dest='remote_dir',
default='/sdcard/mozctest',
help='[NATIVE] Working directory on '
'a SD card on a Android device')
parser.add_option('--remote_device', dest='remote_device',
default='/dev/block/vold/179:0',
help='[NATIVE] Path to the device to be (re)mounted.')
parser.add_option('--remote_mount_point', dest='remote_mount_point',
default='/sdcard',
help='[NATIVE] Mount point for the --remote_device.')
parser.add_option('--test_bin_dir', dest='test_bin_dir', default='.',
help='[NATIVE] The directory which contains test binaries')
parser.add_option('--test_case', dest='testcase', default='',
help='[NATIVE] Limits testcases to run.')
parser.add_option('--native_abi', dest='abi', default='armeabi',
help='[JAVA][NATIVE] ABI of built test executables.')
parser.add_option('--mozc_dictionary_data_file',
dest='mozc_dictionary_data_file', default=None,
help='[NATIVE] Path to system.dictionary file.')
parser.add_option('--mozc_connection_data_file',
dest='mozc_connection_data_file', default=None,
help='[NATIVE] Path to connection.data file.')
parser.add_option('--mozc_connection_text_data_file',
dest='mozc_connection_text_data_file', default=None,
help='[NATIVE] Path to connection_single_column.txt file.')
parser.add_option('--mozc_test_connection_data_file',
dest='mozc_test_connection_data_file', default=None,
help='[NATIVE] Path to test_connection.data file.')
parser.add_option('--mozc_test_connection_text_data_file',
dest='mozc_test_connection_text_data_file', default=None,
help='[NATIVE] Path to connection_single_column.txt file.')
parser.add_option('--mozc_data_dir',
dest='mozc_data_dir', default=None,
help='[NATIVE] Path to data directory.')
parser.add_option('--output_report_dir', dest='output_report_dir',
default=None,
help='[JAVA][NATIVE] Path to output gtest '
'reporting XML files')
parser.add_option('--android_home', dest='android_home',
default=None,
help='[JAVA][NATIVE] Path to the the SDK.')
return parser.parse_args()[0]
def AppendPrefixToSuiteName(in_file_name, out_file_name, prefix):
"""Appends prefix to <testsuite> element's name attribute."""
def AppendPrefix(elem):
name = elem.attrib['name']
elem.attrib['name'] = prefix + name
tree = ElementTree.parse(in_file_name)
root = tree.getroot()
# Root element isn't returned by any traversing methods.
if root.tag == 'testsuite':
AppendPrefix(root)
for elem in root.findall('testsuite'):
AppendPrefix(elem)
with open(out_file_name, 'w') as f:
# Note that ElementTree of 2.6 doesn't write XML declaration.
f.write('<?xml version="1.0" encoding="utf-8"?>\n')
f.write(ElementTree.tostring(root, 'utf-8'))
class AndroidDevice(android_util.AndroidDevice):
def __init__(self, serial, android_home):
android_util.AndroidDevice.__init__(self, serial, android_home)
def WaitForMount(self):
"""Wait until SD card is mounted."""
retry = 10
sleep = 30
for _ in xrange(retry):
if self._RunCommand('mount').find('/sdcard') != -1:
self.GetLogger().info('SD card has been mounted.')
return
self.GetLogger().info(
'SD card has not been mounted. Wait and retry. '
'Don\'t worry. This is typically expected behavior.')
time.sleep(sleep)
raise IOError('No mounted SD card found. Something went wrong or the '
'emulator is too slow.')
def CopyFile(self, host_path, remote_path, operation):
"""Copy a file between host and remote.
Args:
host_path: path in host side.
remote_path: path in remote side.
operation: 'push' or 'pull'.
If 'push', 'adb push host_path remote_path' is executed.
If 'pull', 'adb pull remote_path host_path' is executed.
"""
if operation == 'push':
command_args = [os.path.join(self._android_home, 'platform-tools', 'adb'),
'-s', self.serial, 'push', host_path, remote_path]
elif operation == 'pull':
command_args = [os.path.join(self._android_home, 'platform-tools', 'adb'),
'-s', self.serial, 'pull', remote_path, host_path]
else:
raise ValueError('operation parameter must be push or pull but '
'%s is given.' % operation)
self.GetLogger().info('Copying at %s: %s', self.serial, command_args)
process = subprocess.Popen(command_args)
if process.wait() == 0:
return
raise IOError(
'Failed to copy a file: %s to %s' % (host_path, remote_path))
def _VerifyResultXMLFile(self, xml_path, test_name):
test_suites = gtest_report.GetFromXMLFile(xml_path)
if not test_suites:
error_message = ('[FAIL] Result XML file for %s is invalid; %s' %
(test_name, xml_path))
else:
if test_suites.fail_num:
self.GetLogger().warning('[FAIL] %d of test failures for %s are found',
test_suites.fail_num, test_name)
error_message = test_suites.GetErrorSummary()
else:
self.GetLogger().info('[ OK ] All tests for %s succeeded.', test_name)
error_message = None
return error_message
def _CopyAndVerifyResult(
self, test_name, remote_result_path, host_result_path):
try:
self.CopyFile(host_path=host_result_path,
remote_path=remote_result_path,
operation='pull')
self.GetLogger().info('[ OK ] %s result file is successfully pulled.',
test_name)
error_message = self._VerifyResultXMLFile(host_result_path, test_name)
except IOError:
self.GetLogger().warning('[FAIL] %s result file is not pulled.',
test_name)
error_message = ('[FAIL] Result file does not exist. '
'The process might crash: %s' % test_name)
if error_message:
self.GetLogger().warning(error_message)
return error_message
def RunOneTest(self, test_bin_dir, test_name, remote_dir,
output_report_dir):
"""Execute each test."""
try:
prefix = '%s::' % (self._GetAvdName())
host_path = os.path.join(test_bin_dir, test_name)
remote_path = os.path.join(remote_dir, test_name)
remote_report_file_name = '%s.xml' % test_name
output_report_file_name = '%s%s.xml' % (prefix, test_name)
if output_report_dir:
output_report_path = os.path.join(output_report_dir,
output_report_file_name)
else:
output_report_path = os.tmpnam()
# Create default test report file.
CreateDefaultReportFile(output_report_path, test_name, prefix)
# Copy the binary file, run the test, and clean up the executable.
self.CopyFile(host_path=host_path,
remote_path=remote_path,
operation='push')
remote_report_path = os.path.abspath(
os.path.join(remote_dir, remote_report_file_name))
# We should append colored_log=false to suppress escape sequences on
# continuous build.
command = ['cd', remote_dir, ';',
'./' + test_name, '--test_srcdir=.', '--logtostderr',
'--gunit_output=xml:%s' % remote_report_path,
'--colored_log=false']
self._RunCommand(*command)
temporal_report_path = os.tmpnam()
error_message = self._CopyAndVerifyResult(
test_name, remote_report_path, temporal_report_path)
# Successfully the result file is pulled.
if not error_message:
# Append prefix to testsuite name.
# Otherwise duplicate testsuites name will be generated finally.
AppendPrefixToSuiteName(temporal_report_path, output_report_path,
prefix)
return error_message
finally:
if remote_path:
self._RunCommand('rm', remote_path)
if remote_report_path:
self._RunCommand('rm', remote_report_path)
def SetUpTest(self, device, mount_point, remote_dir,
dictionary_data, connection_data, connection_text_data,
test_connection_data, test_connection_text_data,
mozc_data_dir):
"""Set up the android to run tests."""
self.WaitForMount()
# Now, the binary size of unittests are getting bigger and actually,
# some of them exceed the /data/data/... quota.
# So we'll use sdcard, instead. Unfortunately, sdcard has noexec
# attribute
# by default, so we remove it by remounting.
self._RunCommand('mount', '-o', 'remount', device, mount_point)
# Some tests depend on dictionary data. In the product, the
# data is set at jni loading time, but it is necessary to somehow
# set the data in native tests. So, copy the dictionary data to the
# emulator.
self.CopyFile(host_path=dictionary_data,
remote_path=os.path.join(remote_dir,
'embedded_data', 'dictionary_data'),
operation='push')
self.CopyFile(host_path=connection_data,
remote_path=os.path.join(remote_dir,
'embedded_data', 'connection_data'),
operation='push')
self.CopyFile(host_path=connection_text_data,
remote_path=os.path.join(remote_dir,
'data_manager', 'android',
'connection_single_column.txt'),
operation='push')
self.CopyFile(host_path=test_connection_data,
remote_path=os.path.join(remote_dir,
'data_manager', 'testing',
'connection_data.data'),
operation='push')
self.CopyFile(host_path=test_connection_text_data,
remote_path=os.path.join(remote_dir,
'data_manager', 'testing',
'connection_single_column.txt'),
operation='push')
# mozc_data_dir contains both generated .h files and test data.
# We want only test data and they are in mozc_data_dir/data.
# TODO(matsuzakit): Split generated .h files and test data
# into separate directories.
self.CopyFile(host_path=os.path.join(mozc_data_dir, 'data'),
remote_path=os.path.join(remote_dir, 'data'),
operation='push')
def TearDownTest(self, remote_dir):
self._RunCommand('rm', '-r', remote_dir)
def RunNativeTests(self, options, abi, binaries):
self.GetLogger().info('[NATIVE] Testing device=%s', self.serial)
if not self.IsAcceptableAbi(abi):
self.GetLogger().info('ABI %s is not compatible with built executables.',
abi)
return []
if int(self.GetProperty('ro.build.version.sdk')) <= 15:
self.GetLogger().info('The device does not support PIE so skip testing.')
return []
try:
error_messages = []
self.SetUpTest(options.remote_device, options.remote_mount_point,
options.remote_dir,
options.mozc_dictionary_data_file,
options.mozc_connection_data_file,
options.mozc_connection_text_data_file,
options.mozc_test_connection_data_file,
options.mozc_test_connection_text_data_file,
options.mozc_data_dir)
if options.testcase:
test_list = options.testcase.split(',')
else:
test_list = None
self.GetLogger().info('[NATIVE] Testing device=%s, abi=%s',
self.serial, abi)
for test in binaries:
if test_list and test not in test_list:
continue
self.GetLogger().info('[NATIVE] Testing device=%s, abi=%s, test=%s',
self.serial, abi, test)
error_message = self.RunOneTest(options.test_bin_dir, test,
options.remote_dir,
options.output_report_dir)
if error_message:
error_messages.append(error_message)
self.TearDownTest(options.remote_dir)
return error_messages
except Exception as e: # pylint: disable=broad-except
# Catches all the exceptions and returns string instead.
# If an exception is thrown, multiprocessing module
# complains because exceptions might not be able to be pickled.
return [str(e)]
def RunJavaTests(self, output_report_dir, configuration,
app_package_name, abi):
try:
if not self.IsAcceptableAbi(abi):
self.GetLogger().info(
'ABI %s is not compatible with built executables.', abi)
return []
self.GetLogger().info('[JAVA] Testing device=%s', self.serial)
prefix = '%s::' % self._GetAvdName()
report_file_name_remote = 'gtest-report.xml'
report_file_name_host = prefix + 'java_layer.xml'
output_report_path = os.path.join(output_report_dir,
report_file_name_host)
CreateDefaultReportFile(output_report_path, 'JUnit all tests', prefix)
# Run 'run-tests' target on specified device.
# This target does build (testee and tester), install (both) and run.
args = ['ant', 'run-tests',
'-Dadb.device.arg=-s %s' % self.serial,
'-Dgyp.build_type=%s' % configuration]
process = subprocess.Popen(args, cwd='tests')
process.wait()
if process.wait() != 0:
return '[FAIL] [JAVA] run_test target fails'
remote_report_path = os.path.join('data', 'data',
app_package_name,
report_file_name_remote)
try:
temporal_report_path = os.tmpnam()
self.CopyFile(host_path=temporal_report_path,
remote_path=remote_report_path,
operation='pull')
self.GetLogger().info('[ OK ] Java result file is successfully pulled.')
# Append prefix to testsuite name.
# Otherwise duplicate testsuites name will be generated finally.
AppendPrefixToSuiteName(temporal_report_path, output_report_path,
prefix)
# XML file verification is omitted because
# - Java's report XML doesn't fit verifier's expectation.
except IOError:
self.GetLogger().warning('[FAIL] Java result file is not pulled.')
return '[FAIL] Result file does not exist. The process might crash.'
# TODO(matsuzakit): Verify reporting XML file here.
# gtest_report.GetFromXMLFile() is not applicable becuase of
# format difference.
return None
except Exception as e: # pylint: disable=broad-except
# Catches all the exceptions and returns string instead.
# If an exception is thrown, multiprocessing module
# complains because exceptions might not be able to be pickled.
return str(e)
@staticmethod
def GetDevices(android_home):
return [AndroidDevice(super_device.serial, android_home)
for super_device in
android_util.AndroidDevice.GetDevices(android_home)]
def CreateDefaultReportFile(output_report_path, test_name, prefix):
# Create default test report file.
# It will be used as the result of the test if the test crashes.
with open(output_report_path, 'w') as f:
f.write("""<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="%s%s" tests="1" errors="1">
<testcase name="No reporting XML">
<error message="No reporting XML has been generated. Process crash?" />
</testcase>
</testsuite>""" % (prefix, test_name))
def RunTest(device, options, binaries):
error_messages = []
if options.run_native_test:
native_error_messages = device.RunNativeTests(options, options.abi,
binaries)
if native_error_messages:
error_messages.extend(native_error_messages)
if options.run_java_test:
error_message = device.RunJavaTests(options.output_report_dir,
options.configuration,
options.app_package_name,
options.abi)
if error_message:
error_messages.append(error_message)
return '\n'.join(error_messages)
def main():
# Enable logging.info.
logging.getLogger().setLevel(logging.INFO)
options = ParseArgs()
if not options.android_home:
logging.error('--android_home option must be specified.')
os.exit(1)
if options.run_native_test:
binaries = FindTestBinaries(options.test_bin_dir)
logging.info(binaries)
else:
binaries = None
# Prepare reporting directory.
if options.output_report_dir:
options.output_report_dir = os.path.abspath(options.output_report_dir)
try:
os.makedirs(options.output_report_dir)
logging.info('Made directory; %s', options.output_report_dir)
except OSError as e:
if e.errno == errno.EEXIST:
logging.info('%s has existed already.', options.output_report_dir)
else:
raise
android_home = options.android_home
devices = AndroidDevice.GetDevices(android_home)
logging.info('All the activated devices are %s',
[device.serial for device in devices])
if options.android_devices:
using_devices = options.android_devices.split(',')
devices = [device for device in devices if device.serial in using_devices]
logging.info('Filtered to %s',
[device.serial for device in devices])
if not devices:
logging.info('No devices are specified. Do nothing.')
return
# Maximum # of devices is 10 so running them in the same time is possible.
pool = multiprocessing.Pool(len(devices))
results = [pool.apply_async(RunTest, [device, options, binaries])
for device in devices]
pool.close()
# result.get() blocks until the test terminates.
error_messages = [result.get() for result in results if result.get()]
if error_messages:
print '[FAIL] Native tests result : Test failures are found;'
for message in error_messages:
print message
else:
print '[ OK ] Native tests result : Tests scceeded.'
if __name__ == '__main__':
main()