auto import from //depot/cupcake/@136654
This commit is contained in:
347
testrunner/adb_interface.py
Executable file
347
testrunner/adb_interface.py
Executable file
@@ -0,0 +1,347 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2008, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Provides an interface to communicate with the device via the adb command.
|
||||||
|
|
||||||
|
Assumes adb binary is currently on system path.
|
||||||
|
"""
|
||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
import time
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
import am_instrument_parser
|
||||||
|
import errors
|
||||||
|
import logger
|
||||||
|
import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class AdbInterface:
|
||||||
|
"""Helper class for communicating with Android device via adb."""
|
||||||
|
|
||||||
|
# argument to pass to adb, to direct command to specific device
|
||||||
|
_target_arg = ""
|
||||||
|
|
||||||
|
DEVICE_TRACE_DIR = "/data/test_results/"
|
||||||
|
|
||||||
|
def SetEmulatorTarget(self):
|
||||||
|
"""Direct all future commands to the only running emulator."""
|
||||||
|
self._target_arg = "-e"
|
||||||
|
|
||||||
|
def SetDeviceTarget(self):
|
||||||
|
"""Direct all future commands to the only connected USB device."""
|
||||||
|
self._target_arg = "-d"
|
||||||
|
|
||||||
|
def SetTargetSerial(self, serial):
|
||||||
|
"""Direct all future commands to Android target with the given serial."""
|
||||||
|
self._target_arg = "-s %s" % serial
|
||||||
|
|
||||||
|
def SendCommand(self, command_string, timeout_time=20, retry_count=3):
|
||||||
|
"""Send a command via adb.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command_string: adb command to run
|
||||||
|
timeout_time: number of seconds to wait for command to respond before
|
||||||
|
retrying
|
||||||
|
retry_count: number of times to retry command before raising
|
||||||
|
WaitForResponseTimedOutError
|
||||||
|
Returns:
|
||||||
|
string output of command
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WaitForResponseTimedOutError if device does not respond to command
|
||||||
|
"""
|
||||||
|
adb_cmd = "adb %s %s" % (self._target_arg, command_string)
|
||||||
|
logger.SilentLog("about to run %s" % adb_cmd)
|
||||||
|
return run_command.RunCommand(adb_cmd, timeout_time=timeout_time,
|
||||||
|
retry_count=retry_count)
|
||||||
|
|
||||||
|
def SendShellCommand(self, cmd, timeout_time=20, retry_count=3):
|
||||||
|
"""Send a adb shell command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: adb shell command to run
|
||||||
|
timeout_time: number of seconds to wait for command to respond before
|
||||||
|
retrying
|
||||||
|
retry_count: number of times to retry command before raising
|
||||||
|
WaitForResponseTimedOutError
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string output of command
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WaitForResponseTimedOutError: if device does not respond to command
|
||||||
|
"""
|
||||||
|
return self.SendCommand("shell %s" % cmd, timeout_time=timeout_time,
|
||||||
|
retry_count=retry_count)
|
||||||
|
|
||||||
|
def BugReport(self, path):
|
||||||
|
"""Dumps adb bugreport to the file specified by the path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path of the file where adb bugreport is dumped to.
|
||||||
|
"""
|
||||||
|
bug_output = self.SendShellCommand("bugreport", timeout_time=60)
|
||||||
|
bugreport_file = open(path, "w")
|
||||||
|
bugreport_file.write(bug_output)
|
||||||
|
bugreport_file.close()
|
||||||
|
|
||||||
|
def Push(self, src, dest):
|
||||||
|
"""Pushes the file src onto the device at dest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src: file path of host file to push
|
||||||
|
dest: destination absolute file path on device
|
||||||
|
"""
|
||||||
|
self.SendCommand("push %s %s" % (src, dest), timeout_time=60)
|
||||||
|
|
||||||
|
def Pull(self, src, dest):
|
||||||
|
"""Pulls the file src on the device onto dest on the host.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src: absolute file path of file on device to pull
|
||||||
|
dest: destination file path on host
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if success and False otherwise.
|
||||||
|
"""
|
||||||
|
# Create the base dir if it doesn't exist already
|
||||||
|
if not os.path.exists(os.path.dirname(dest)):
|
||||||
|
os.makedirs(os.path.dirname(dest))
|
||||||
|
|
||||||
|
if self.DoesFileExist(src):
|
||||||
|
self.SendCommand("pull %s %s" % (src, dest), timeout_time=60)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.Log("ADB Pull Failed: Source file %s does not exist." % src)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def DoesFileExist(self, src):
|
||||||
|
"""Checks if the given path exists on device target.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src: file path to be checked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if file exists
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = self.SendShellCommand("ls %s" % src)
|
||||||
|
error = "No such file or directory"
|
||||||
|
|
||||||
|
if error in output:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def StartInstrumentationForPackage(
|
||||||
|
self, package_name, runner_name, timeout_time=60*10,
|
||||||
|
no_window_animation=False, instrumentation_args={}):
|
||||||
|
"""Run instrumentation test for given package and runner.
|
||||||
|
|
||||||
|
Equivalent to StartInstrumentation, except instrumentation path is
|
||||||
|
separated into its package and runner components.
|
||||||
|
"""
|
||||||
|
instrumentation_path = "%s/%s" % (package_name, runner_name)
|
||||||
|
return self.StartInstrumentation(self, instrumentation_path, timeout_time,
|
||||||
|
no_window_animation, instrumentation_args)
|
||||||
|
|
||||||
|
def StartInstrumentation(
|
||||||
|
self, instrumentation_path, timeout_time=60*10, no_window_animation=False,
|
||||||
|
profile=False, instrumentation_args={}):
|
||||||
|
|
||||||
|
"""Runs an instrumentation class on the target.
|
||||||
|
|
||||||
|
Returns a dictionary containing the key value pairs from the
|
||||||
|
instrumentations result bundle and a list of TestResults. Also handles the
|
||||||
|
interpreting of error output from the device and raises the necessary
|
||||||
|
exceptions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instrumentation_path: string. It should be the fully classified package
|
||||||
|
name, and instrumentation test runner, separated by "/"
|
||||||
|
e.g. com.android.globaltimelaunch/.GlobalTimeLaunch
|
||||||
|
timeout_time: Timeout value for the am command.
|
||||||
|
no_window_animation: boolean, Whether you want window animations enabled
|
||||||
|
or disabled
|
||||||
|
profile: If True, profiling will be turned on for the instrumentation.
|
||||||
|
instrumentation_args: Dictionary of key value bundle arguments to pass to
|
||||||
|
instrumentation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(test_results, inst_finished_bundle)
|
||||||
|
|
||||||
|
test_results: a list of TestResults
|
||||||
|
inst_finished_bundle (dict): Key/value pairs contained in the bundle that
|
||||||
|
is passed into ActivityManager.finishInstrumentation(). Included in this
|
||||||
|
bundle is the return code of the Instrumentation process, any error
|
||||||
|
codes reported by the activity manager, and any results explicitly added
|
||||||
|
by the instrumentation code.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WaitForResponseTimedOutError: if timeout occurred while waiting for
|
||||||
|
response to adb instrument command
|
||||||
|
DeviceUnresponsiveError: if device system process is not responding
|
||||||
|
InstrumentationError: if instrumentation failed to run
|
||||||
|
"""
|
||||||
|
|
||||||
|
command_string = self._BuildInstrumentationCommandPath(
|
||||||
|
instrumentation_path, no_window_animation=no_window_animation,
|
||||||
|
profile=profile, raw_mode=True,
|
||||||
|
instrumentation_args=instrumentation_args)
|
||||||
|
|
||||||
|
(test_results, inst_finished_bundle) = (
|
||||||
|
am_instrument_parser.ParseAmInstrumentOutput(
|
||||||
|
self.SendShellCommand(command_string, timeout_time=timeout_time,
|
||||||
|
retry_count=2)))
|
||||||
|
|
||||||
|
if "code" not in inst_finished_bundle:
|
||||||
|
raise errors.InstrumentationError("no test results... device setup "
|
||||||
|
"correctly?")
|
||||||
|
|
||||||
|
if inst_finished_bundle["code"] == "0":
|
||||||
|
short_msg_result = "no error message"
|
||||||
|
if "shortMsg" in inst_finished_bundle:
|
||||||
|
short_msg_result = inst_finished_bundle["shortMsg"]
|
||||||
|
logger.Log(short_msg_result)
|
||||||
|
raise errors.InstrumentationError(short_msg_result)
|
||||||
|
|
||||||
|
if "INSTRUMENTATION_ABORTED" in inst_finished_bundle:
|
||||||
|
logger.Log("INSTRUMENTATION ABORTED!")
|
||||||
|
raise errors.DeviceUnresponsiveError
|
||||||
|
|
||||||
|
return (test_results, inst_finished_bundle)
|
||||||
|
|
||||||
|
def StartInstrumentationNoResults(
|
||||||
|
self, package_name, runner_name, no_window_animation=False,
|
||||||
|
raw_mode=False, instrumentation_args={}):
|
||||||
|
"""Runs instrumentation and dumps output to stdout.
|
||||||
|
|
||||||
|
Equivalent to StartInstrumentation, but will dump instrumentation
|
||||||
|
'normal' output to stdout, instead of parsing return results. Command will
|
||||||
|
never timeout.
|
||||||
|
"""
|
||||||
|
adb_command_string = self.PreviewInstrumentationCommand(
|
||||||
|
package_name, runner_name, no_window_animation=no_window_animation,
|
||||||
|
raw_mode=raw_mode, instrumentation_args=instrumentation_args)
|
||||||
|
logger.Log(adb_command_string)
|
||||||
|
run_command.RunCommand(adb_command_string, return_output=False)
|
||||||
|
|
||||||
|
def PreviewInstrumentationCommand(
|
||||||
|
self, package_name, runner_name, no_window_animation=False,
|
||||||
|
raw_mode=False, instrumentation_args={}):
|
||||||
|
"""Returns a string of adb command that will be executed."""
|
||||||
|
inst_command_string = self._BuildInstrumentationCommand(
|
||||||
|
package_name, runner_name, no_window_animation=no_window_animation,
|
||||||
|
raw_mode=raw_mode, instrumentation_args=instrumentation_args)
|
||||||
|
command_string = "adb %s shell %s" % (self._target_arg, inst_command_string)
|
||||||
|
return command_string
|
||||||
|
|
||||||
|
def _BuildInstrumentationCommand(
|
||||||
|
self, package, runner_name, no_window_animation=False, profile=False,
|
||||||
|
raw_mode=True, instrumentation_args={}):
|
||||||
|
instrumentation_path = "%s/%s" % (package, runner_name)
|
||||||
|
|
||||||
|
return self._BuildInstrumentationCommandPath(
|
||||||
|
instrumentation_path, no_window_animation=no_window_animation,
|
||||||
|
profile=profile, raw_mode=raw_mode,
|
||||||
|
instrumentation_args=instrumentation_args)
|
||||||
|
|
||||||
|
def _BuildInstrumentationCommandPath(
|
||||||
|
self, instrumentation_path, no_window_animation=False, profile=False,
|
||||||
|
raw_mode=True, instrumentation_args={}):
|
||||||
|
command_string = "am instrument"
|
||||||
|
if no_window_animation:
|
||||||
|
command_string += " --no_window_animation"
|
||||||
|
if profile:
|
||||||
|
self._CreateTraceDir()
|
||||||
|
command_string += (
|
||||||
|
" -p %s/%s.dmtrace" %
|
||||||
|
(self.DEVICE_TRACE_DIR, instrumentation_path.split(".")[-1]))
|
||||||
|
|
||||||
|
for key, value in instrumentation_args.items():
|
||||||
|
command_string += " -e %s %s" % (key, value)
|
||||||
|
if raw_mode:
|
||||||
|
command_string += " -r"
|
||||||
|
command_string += " -w %s" % instrumentation_path
|
||||||
|
return command_string
|
||||||
|
|
||||||
|
def _CreateTraceDir(self):
|
||||||
|
ls_response = self.SendShellCommand("ls /data/trace")
|
||||||
|
if ls_response.strip("#").strip(string.whitespace) != "":
|
||||||
|
self.SendShellCommand("create /data/trace", "mkdir /data/trace")
|
||||||
|
self.SendShellCommand("make /data/trace world writeable",
|
||||||
|
"chmod 777 /data/trace")
|
||||||
|
|
||||||
|
def WaitForDevicePm(self, wait_time=120):
|
||||||
|
"""Waits for targeted device's package manager to be up.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wait_time: time in seconds to wait
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WaitForResponseTimedOutError if wait_time elapses and pm still does not
|
||||||
|
respond.
|
||||||
|
"""
|
||||||
|
logger.Log("Waiting for device package manager for %s seconds..."
|
||||||
|
% wait_time)
|
||||||
|
self.SendCommand("wait-for-device")
|
||||||
|
# Now the device is there, but may not be running.
|
||||||
|
# Query the package manager with a basic command
|
||||||
|
pm_found = False
|
||||||
|
attempts = 0
|
||||||
|
wait_period = 5
|
||||||
|
while not pm_found and (attempts*wait_period) < wait_time:
|
||||||
|
# assume the 'adb shell pm path android' command will always
|
||||||
|
# return 'package: something' in the success case
|
||||||
|
output = self.SendShellCommand("pm path android", retry_count=1)
|
||||||
|
if "package:" in output:
|
||||||
|
pm_found = True
|
||||||
|
else:
|
||||||
|
time.sleep(wait_period)
|
||||||
|
attempts += 1
|
||||||
|
if not pm_found:
|
||||||
|
raise errors.WaitForResponseTimedOutError
|
||||||
|
|
||||||
|
def Sync(self, retry_count=3):
|
||||||
|
"""Perform a adb sync.
|
||||||
|
|
||||||
|
Blocks until device package manager is responding.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
retry_count: number of times to retry sync before failing
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WaitForResponseTimedOutError if package manager does not respond
|
||||||
|
"""
|
||||||
|
output = self.SendCommand("sync", retry_count=retry_count)
|
||||||
|
if "Read-only file system" in output:
|
||||||
|
logger.SilentLog(output)
|
||||||
|
logger.Log("adb sync failed due to read only fs, retrying")
|
||||||
|
self.SendCommand("remount")
|
||||||
|
output = self.SendCommand("sync", retry_count=retry_count)
|
||||||
|
if "No space left on device" in output:
|
||||||
|
logger.SilentLog(output)
|
||||||
|
logger.Log("adb sync failed due to no space on device, trying shell" +
|
||||||
|
" start/stop")
|
||||||
|
self.SendShellCommand("stop", retry_count=retry_count)
|
||||||
|
output = self.SendCommand("sync", retry_count=retry_count)
|
||||||
|
self.SendShellCommand("start", retry_count=retry_count)
|
||||||
|
|
||||||
|
logger.SilentLog(output)
|
||||||
|
self.WaitForDevicePm()
|
||||||
|
return output
|
||||||
178
testrunner/am_instrument_parser.py
Executable file
178
testrunner/am_instrument_parser.py
Executable file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2008, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Module that assists in parsing the output of "am instrument" commands run on
|
||||||
|
the device."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
def ParseAmInstrumentOutput(result):
|
||||||
|
"""Given the raw output of an "am instrument" command that targets and
|
||||||
|
InstrumentationTestRunner, return structured data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result (string): Raw output of "am instrument"
|
||||||
|
|
||||||
|
Return
|
||||||
|
(test_results, inst_finished_bundle)
|
||||||
|
|
||||||
|
test_results (list of am_output_parser.TestResult)
|
||||||
|
inst_finished_bundle (dict): Key/value pairs contained in the bundle that is
|
||||||
|
passed into ActivityManager.finishInstrumentation(). Included in this bundle is the return
|
||||||
|
code of the Instrumentation process, any error codes reported by the
|
||||||
|
activity manager, and any results explicity added by the instrumentation
|
||||||
|
code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
re_status_code = re.compile(r'INSTRUMENTATION_STATUS_CODE: (?P<status_code>-?\d)$')
|
||||||
|
test_results = []
|
||||||
|
inst_finished_bundle = {}
|
||||||
|
|
||||||
|
result_block_string = ""
|
||||||
|
for line in result.splitlines():
|
||||||
|
result_block_string += line + '\n'
|
||||||
|
|
||||||
|
if "INSTRUMENTATION_STATUS_CODE:" in line:
|
||||||
|
test_result = TestResult(result_block_string)
|
||||||
|
if test_result.GetStatusCode() == 1: # The test started
|
||||||
|
pass
|
||||||
|
elif test_result.GetStatusCode() in [0, -1, -2]:
|
||||||
|
test_results.append(test_result)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
result_block_string = ""
|
||||||
|
if "INSTRUMENTATION_CODE:" in line:
|
||||||
|
inst_finished_bundle = _ParseInstrumentationFinishedBundle(result_block_string)
|
||||||
|
result_block_string = ""
|
||||||
|
|
||||||
|
return (test_results, inst_finished_bundle)
|
||||||
|
|
||||||
|
|
||||||
|
def _ParseInstrumentationFinishedBundle(result):
|
||||||
|
"""Given the raw output of "am instrument" returns a dictionary of the
|
||||||
|
key/value pairs from the bundle passed into
|
||||||
|
ActivityManager.finishInstrumentation().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result (string): Raw output of "am instrument"
|
||||||
|
|
||||||
|
Return:
|
||||||
|
inst_finished_bundle (dict): Key/value pairs contained in the bundle that is
|
||||||
|
passed into ActivityManager.finishInstrumentation(). Included in this bundle is the return
|
||||||
|
code of the Instrumentation process, any error codes reported by the
|
||||||
|
activity manager, and any results explicity added by the instrumentation
|
||||||
|
code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
re_result = re.compile(r'INSTRUMENTATION_RESULT: ([^=]+)=(.+)$')
|
||||||
|
re_code = re.compile(r'INSTRUMENTATION_CODE: (\-?\d)$')
|
||||||
|
result_dict = {}
|
||||||
|
key = ''
|
||||||
|
val = ''
|
||||||
|
last_tag = ''
|
||||||
|
|
||||||
|
for line in result.split('\n'):
|
||||||
|
line = line.strip(string.whitespace)
|
||||||
|
if re_result.match(line):
|
||||||
|
last_tag = 'INSTRUMENTATION_RESULT'
|
||||||
|
key = re_result.search(line).group(1).strip(string.whitespace)
|
||||||
|
if key.startswith('performance.'):
|
||||||
|
key = key[len('performance.'):]
|
||||||
|
val = re_result.search(line).group(2).strip(string.whitespace)
|
||||||
|
try:
|
||||||
|
result_dict[key] = float(val)
|
||||||
|
except ValueError:
|
||||||
|
result_dict[key] = val
|
||||||
|
except TypeError:
|
||||||
|
result_dict[key] = val
|
||||||
|
elif re_code.match(line):
|
||||||
|
last_tag = 'INSTRUMENTATION_CODE'
|
||||||
|
key = 'code'
|
||||||
|
val = re_code.search(line).group(1).strip(string.whitespace)
|
||||||
|
result_dict[key] = val
|
||||||
|
elif 'INSTRUMENTATION_ABORTED:' in line:
|
||||||
|
last_tag = 'INSTRUMENTATION_ABORTED'
|
||||||
|
key = 'INSTRUMENTATION_ABORTED'
|
||||||
|
val = ''
|
||||||
|
result_dict[key] = val
|
||||||
|
elif last_tag == 'INSTRUMENTATION_RESULT':
|
||||||
|
result_dict[key] += '\n' + line
|
||||||
|
|
||||||
|
if not result_dict.has_key('code'):
|
||||||
|
result_dict['code'] = '0'
|
||||||
|
result_dict['shortMsg'] = "No result returned from instrumentation"
|
||||||
|
|
||||||
|
return result_dict
|
||||||
|
|
||||||
|
|
||||||
|
class TestResult(object):
|
||||||
|
"""A class that contains information about a single test result."""
|
||||||
|
|
||||||
|
def __init__(self, result_block_string):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
result_block_string (string): Is a single "block" of output. A single
|
||||||
|
"block" would be either a "test started" status report, or a "test
|
||||||
|
finished" status report.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._test_name = None
|
||||||
|
self._status_code = None
|
||||||
|
self._failure_reason = None
|
||||||
|
|
||||||
|
re_start_block = re.compile(
|
||||||
|
r'\s*INSTRUMENTATION_STATUS: stream=(?P<stream>.*)'
|
||||||
|
'INSTRUMENTATION_STATUS: test=(?P<test>\w+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: class=(?P<class>[\w\.]+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: current=(?P<current>\d+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: numtests=(?P<numtests>\d+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: id=.*\s+'
|
||||||
|
'INSTRUMENTATION_STATUS_CODE: 1\s*', re.DOTALL)
|
||||||
|
|
||||||
|
re_end_block = re.compile(
|
||||||
|
r'\s*INSTRUMENTATION_STATUS: stream=(?P<stream>.*)'
|
||||||
|
'INSTRUMENTATION_STATUS: test=(?P<test>\w+)\s+'
|
||||||
|
'(INSTRUMENTATION_STATUS: stack=(?P<stack>.*))?'
|
||||||
|
'INSTRUMENTATION_STATUS: class=(?P<class>[\w\.]+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: current=(?P<current>\d+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: numtests=(?P<numtests>\d+)\s+'
|
||||||
|
'INSTRUMENTATION_STATUS: id=.*\s+'
|
||||||
|
'INSTRUMENTATION_STATUS_CODE: (?P<status_code>0|-1|-2)\s*', re.DOTALL)
|
||||||
|
|
||||||
|
start_block_match = re_start_block.match(result_block_string)
|
||||||
|
end_block_match = re_end_block.match(result_block_string)
|
||||||
|
|
||||||
|
if start_block_match:
|
||||||
|
self._test_name = "%s:%s" % (start_block_match.group('class'),
|
||||||
|
start_block_match.group('test'))
|
||||||
|
self._status_code = 1
|
||||||
|
elif end_block_match:
|
||||||
|
self._test_name = "%s:%s" % (end_block_match.group('class'),
|
||||||
|
end_block_match.group('test'))
|
||||||
|
self._status_code = int(end_block_match.group('status_code'))
|
||||||
|
self._failure_reason = end_block_match.group('stack')
|
||||||
|
|
||||||
|
def GetTestName(self):
|
||||||
|
return self._test_name
|
||||||
|
|
||||||
|
def GetStatusCode(self):
|
||||||
|
return self._status_code
|
||||||
|
|
||||||
|
def GetFailureReason(self):
|
||||||
|
return self._failure_reason
|
||||||
312
testrunner/coverage.py
Executable file
312
testrunner/coverage.py
Executable file
@@ -0,0 +1,312 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2008, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Utilities for generating code coverage reports for Android tests."""
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import glob
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
import android_build
|
||||||
|
import coverage_targets
|
||||||
|
import errors
|
||||||
|
import logger
|
||||||
|
import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class CoverageGenerator(object):
|
||||||
|
"""Helper utility for obtaining code coverage results on Android.
|
||||||
|
|
||||||
|
Intended to simplify the process of building,running, and generating code
|
||||||
|
coverage results for a pre-defined set of tests and targets
|
||||||
|
"""
|
||||||
|
|
||||||
|
# environment variable to enable emma builds in Android build system
|
||||||
|
_EMMA_BUILD_FLAG = "EMMA_INSTRUMENT"
|
||||||
|
# build path to Emma target Makefile
|
||||||
|
_EMMA_BUILD_PATH = os.path.join("external", "emma")
|
||||||
|
# path to EMMA host jar, relative to Android build root
|
||||||
|
_EMMA_JAR = os.path.join(_EMMA_BUILD_PATH, "lib", "emma.jar")
|
||||||
|
_TEST_COVERAGE_EXT = "ec"
|
||||||
|
# default device-side path to code coverage results file
|
||||||
|
_DEVICE_COVERAGE_PATH = "/sdcard/coverage.ec"
|
||||||
|
# root path of generated coverage report files, relative to Android build root
|
||||||
|
_COVERAGE_REPORT_PATH = os.path.join("out", "emma")
|
||||||
|
_CORE_TARGET_PATH = os.path.join("development", "testrunner",
|
||||||
|
"coverage_targets.xml")
|
||||||
|
# vendor glob file path patterns to tests, relative to android
|
||||||
|
# build root
|
||||||
|
_VENDOR_TARGET_PATH = os.path.join("vendor", "*", "tests", "testinfo",
|
||||||
|
"coverage_targets.xml")
|
||||||
|
|
||||||
|
# path to root of target build intermediates
|
||||||
|
_TARGET_INTERMEDIATES_BASE_PATH = os.path.join("out", "target", "common",
|
||||||
|
"obj")
|
||||||
|
|
||||||
|
def __init__(self, android_root_path, adb_interface):
|
||||||
|
self._root_path = android_root_path
|
||||||
|
self._output_root_path = os.path.join(self._root_path,
|
||||||
|
self._COVERAGE_REPORT_PATH)
|
||||||
|
self._emma_jar_path = os.path.join(self._root_path, self._EMMA_JAR)
|
||||||
|
self._adb = adb_interface
|
||||||
|
self._targets_manifest = self._ReadTargets()
|
||||||
|
|
||||||
|
def EnableCoverageBuild(self):
|
||||||
|
"""Enable building an Android target with code coverage instrumentation."""
|
||||||
|
os.environ[self._EMMA_BUILD_FLAG] = "true"
|
||||||
|
|
||||||
|
def ExtractReport(self, test_suite,
|
||||||
|
device_coverage_path=_DEVICE_COVERAGE_PATH,
|
||||||
|
output_path=None):
|
||||||
|
"""Extract runtime coverage data and generate code coverage report.
|
||||||
|
|
||||||
|
Assumes test has just been executed.
|
||||||
|
Args:
|
||||||
|
test_suite: TestSuite to generate coverage data for
|
||||||
|
device_coverage_path: location of coverage file on device
|
||||||
|
output_path: path to place output files in. If None will use
|
||||||
|
<android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test>
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
absolute file path string of generated html report file.
|
||||||
|
"""
|
||||||
|
if output_path is None:
|
||||||
|
output_path = os.path.join(self._root_path,
|
||||||
|
self._COVERAGE_REPORT_PATH,
|
||||||
|
test_suite.GetTargetName(),
|
||||||
|
test_suite.GetName())
|
||||||
|
|
||||||
|
coverage_local_name = "%s.%s" % (test_suite.GetName(),
|
||||||
|
self._TEST_COVERAGE_EXT)
|
||||||
|
coverage_local_path = os.path.join(output_path,
|
||||||
|
coverage_local_name)
|
||||||
|
if self._adb.Pull(device_coverage_path, coverage_local_path):
|
||||||
|
|
||||||
|
report_path = os.path.join(output_path,
|
||||||
|
test_suite.GetName())
|
||||||
|
target = self._targets_manifest.GetTarget(test_suite.GetTargetName())
|
||||||
|
return self._GenerateReport(report_path, coverage_local_path, [target],
|
||||||
|
do_src=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _GenerateReport(self, report_path, coverage_file_path, targets,
|
||||||
|
do_src=True):
|
||||||
|
"""Generate the code coverage report.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
report_path: absolute file path of output file, without extension
|
||||||
|
coverage_file_path: absolute file path of code coverage result file
|
||||||
|
targets: list of CoverageTargets to use as base for code coverage
|
||||||
|
measurement.
|
||||||
|
do_src: True if generate coverage report with source linked in.
|
||||||
|
Note this will increase size of generated report.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
absolute file path to generated report file.
|
||||||
|
"""
|
||||||
|
input_metadatas = self._GatherMetadatas(targets)
|
||||||
|
|
||||||
|
if do_src:
|
||||||
|
src_arg = self._GatherSrcs(targets)
|
||||||
|
else:
|
||||||
|
src_arg = ""
|
||||||
|
|
||||||
|
report_file = "%s.html" % report_path
|
||||||
|
cmd1 = ("java -cp %s emma report -r html -in %s %s %s " %
|
||||||
|
(self._emma_jar_path, coverage_file_path, input_metadatas, src_arg))
|
||||||
|
cmd2 = "-Dreport.html.out.file=%s" % report_file
|
||||||
|
self._RunCmd(cmd1 + cmd2)
|
||||||
|
return report_file
|
||||||
|
|
||||||
|
def _GatherMetadatas(self, targets):
|
||||||
|
"""Builds the emma input metadata argument from provided targets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
targets: list of CoverageTargets
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
input metadata argument string
|
||||||
|
"""
|
||||||
|
input_metadatas = ""
|
||||||
|
for target in targets:
|
||||||
|
input_metadata = os.path.join(self._GetBuildIntermediatePath(target),
|
||||||
|
"coverage.em")
|
||||||
|
input_metadatas += " -in %s" % input_metadata
|
||||||
|
return input_metadatas
|
||||||
|
|
||||||
|
def _GetBuildIntermediatePath(self, target):
|
||||||
|
return os.path.join(
|
||||||
|
self._root_path, self._TARGET_INTERMEDIATES_BASE_PATH, target.GetType(),
|
||||||
|
"%s_intermediates" % target.GetName())
|
||||||
|
|
||||||
|
def _GatherSrcs(self, targets):
|
||||||
|
"""Builds the emma input source path arguments from provided targets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
targets: list of CoverageTargets
|
||||||
|
Returns:
|
||||||
|
source path arguments string
|
||||||
|
"""
|
||||||
|
src_list = []
|
||||||
|
for target in targets:
|
||||||
|
target_srcs = target.GetPaths()
|
||||||
|
for path in target_srcs:
|
||||||
|
src_list.append("-sp %s" % os.path.join(self._root_path, path))
|
||||||
|
return " ".join(src_list)
|
||||||
|
|
||||||
|
def _MergeFiles(self, input_paths, dest_path):
|
||||||
|
"""Merges a set of emma coverage files into a consolidated file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_paths: list of string absolute coverage file paths to merge
|
||||||
|
dest_path: absolute file path of destination file
|
||||||
|
"""
|
||||||
|
input_list = []
|
||||||
|
for input_path in input_paths:
|
||||||
|
input_list.append("-in %s" % input_path)
|
||||||
|
input_args = " ".join(input_list)
|
||||||
|
self._RunCmd("java -cp %s emma merge %s -out %s" % (self._emma_jar_path,
|
||||||
|
input_args, dest_path))
|
||||||
|
|
||||||
|
def _RunCmd(self, cmd):
|
||||||
|
"""Runs and logs the given os command."""
|
||||||
|
run_command.RunCommand(cmd, return_output=False)
|
||||||
|
|
||||||
|
def _CombineTargetCoverage(self):
|
||||||
|
"""Combines all target mode code coverage results.
|
||||||
|
|
||||||
|
Will find all code coverage data files in direct sub-directories of
|
||||||
|
self._output_root_path, and combine them into a single coverage report.
|
||||||
|
Generated report is placed at self._output_root_path/android.html
|
||||||
|
"""
|
||||||
|
coverage_files = self._FindCoverageFiles(self._output_root_path)
|
||||||
|
combined_coverage = os.path.join(self._output_root_path,
|
||||||
|
"android.%s" % self._TEST_COVERAGE_EXT)
|
||||||
|
self._MergeFiles(coverage_files, combined_coverage)
|
||||||
|
report_path = os.path.join(self._output_root_path, "android")
|
||||||
|
# don't link to source, to limit file size
|
||||||
|
self._GenerateReport(report_path, combined_coverage,
|
||||||
|
self._targets_manifest.GetTargets(), do_src=False)
|
||||||
|
|
||||||
|
def _CombineTestCoverage(self):
|
||||||
|
"""Consolidates code coverage results for all target result directories."""
|
||||||
|
target_dirs = os.listdir(self._output_root_path)
|
||||||
|
for target_name in target_dirs:
|
||||||
|
output_path = os.path.join(self._output_root_path, target_name)
|
||||||
|
target = self._targets_manifest.GetTarget(target_name)
|
||||||
|
if os.path.isdir(output_path) and target is not None:
|
||||||
|
coverage_files = self._FindCoverageFiles(output_path)
|
||||||
|
combined_coverage = os.path.join(output_path, "%s.%s" %
|
||||||
|
(target_name, self._TEST_COVERAGE_EXT))
|
||||||
|
self._MergeFiles(coverage_files, combined_coverage)
|
||||||
|
report_path = os.path.join(output_path, target_name)
|
||||||
|
self._GenerateReport(report_path, combined_coverage, [target])
|
||||||
|
else:
|
||||||
|
logger.Log("%s is not a valid target directory, skipping" % output_path)
|
||||||
|
|
||||||
|
def _FindCoverageFiles(self, root_path):
|
||||||
|
"""Finds all files in <root_path>/*/*.<_TEST_COVERAGE_EXT>.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root_path: absolute file path string to search from
|
||||||
|
Returns:
|
||||||
|
list of absolute file path strings of coverage files
|
||||||
|
"""
|
||||||
|
file_pattern = os.path.join(root_path, "*", "*.%s" %
|
||||||
|
self._TEST_COVERAGE_EXT)
|
||||||
|
coverage_files = glob.glob(file_pattern)
|
||||||
|
return coverage_files
|
||||||
|
|
||||||
|
def GetEmmaBuildPath(self):
|
||||||
|
return self._EMMA_BUILD_PATH
|
||||||
|
|
||||||
|
def _ReadTargets(self):
|
||||||
|
"""Parses the set of coverage target data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a CoverageTargets object that contains set of parsed targets.
|
||||||
|
Raises:
|
||||||
|
AbortError if a fatal error occurred when parsing the target files.
|
||||||
|
"""
|
||||||
|
core_target_path = os.path.join(self._root_path, self._CORE_TARGET_PATH)
|
||||||
|
try:
|
||||||
|
targets = coverage_targets.CoverageTargets()
|
||||||
|
targets.Parse(core_target_path)
|
||||||
|
vendor_targets_pattern = os.path.join(self._root_path,
|
||||||
|
self._VENDOR_TARGET_PATH)
|
||||||
|
target_file_paths = glob.glob(vendor_targets_pattern)
|
||||||
|
for target_file_path in target_file_paths:
|
||||||
|
targets.Parse(target_file_path)
|
||||||
|
return targets
|
||||||
|
except errors.ParseError:
|
||||||
|
raise errors.AbortError
|
||||||
|
|
||||||
|
def TidyOutput(self):
|
||||||
|
"""Runs tidy on all generated html files.
|
||||||
|
|
||||||
|
This is needed to the html files can be displayed cleanly on a web server.
|
||||||
|
Assumes tidy is on current PATH.
|
||||||
|
"""
|
||||||
|
logger.Log("Tidying output files")
|
||||||
|
self._TidyDir(self._output_root_path)
|
||||||
|
|
||||||
|
def _TidyDir(self, dir_path):
|
||||||
|
"""Recursively tidy all html files in given dir_path."""
|
||||||
|
html_file_pattern = os.path.join(dir_path, "*.html")
|
||||||
|
html_files_iter = glob.glob(html_file_pattern)
|
||||||
|
for html_file_path in html_files_iter:
|
||||||
|
os.system("tidy -m -errors -quiet %s" % html_file_path)
|
||||||
|
sub_dirs = os.listdir(dir_path)
|
||||||
|
for sub_dir_name in sub_dirs:
|
||||||
|
sub_dir_path = os.path.join(dir_path, sub_dir_name)
|
||||||
|
if os.path.isdir(sub_dir_path):
|
||||||
|
self._TidyDir(sub_dir_path)
|
||||||
|
|
||||||
|
def CombineCoverage(self):
|
||||||
|
"""Create combined coverage reports for all targets and tests."""
|
||||||
|
self._CombineTestCoverage()
|
||||||
|
self._CombineTargetCoverage()
|
||||||
|
|
||||||
|
|
||||||
|
def Run():
|
||||||
|
"""Does coverage operations based on command line args."""
|
||||||
|
# TODO: do we want to support combining coverage for a single target
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = optparse.OptionParser(usage="usage: %prog --combine-coverage")
|
||||||
|
parser.add_option(
|
||||||
|
"-c", "--combine-coverage", dest="combine_coverage", default=False,
|
||||||
|
action="store_true", help="Combine coverage results stored given "
|
||||||
|
"android root path")
|
||||||
|
parser.add_option(
|
||||||
|
"-t", "--tidy", dest="tidy", default=False, action="store_true",
|
||||||
|
help="Run tidy on all generated html files")
|
||||||
|
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
|
||||||
|
coverage = CoverageGenerator(android_build.GetTop(), None)
|
||||||
|
if options.combine_coverage:
|
||||||
|
coverage.CombineCoverage()
|
||||||
|
if options.tidy:
|
||||||
|
coverage.TidyOutput()
|
||||||
|
except errors.AbortError:
|
||||||
|
logger.SilentLog("Exiting due to AbortError")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
Run()
|
||||||
40
testrunner/errors.py
Executable file
40
testrunner/errors.py
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2008, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Defines common exception classes for this package."""
|
||||||
|
|
||||||
|
|
||||||
|
class WaitForResponseTimedOutError(Exception):
|
||||||
|
"""We sent a command and had to wait too long for response."""
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceUnresponsiveError(Exception):
|
||||||
|
"""Device is unresponsive to command."""
|
||||||
|
|
||||||
|
|
||||||
|
class InstrumentationError(Exception):
|
||||||
|
"""Failed to run instrumentation."""
|
||||||
|
|
||||||
|
|
||||||
|
class AbortError(Exception):
|
||||||
|
"""Generic exception that indicates a fatal error has occurred and program
|
||||||
|
execution should be aborted."""
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(Exception):
|
||||||
|
"""Raised when xml data to parse has unrecognized format."""
|
||||||
|
|
||||||
85
testrunner/logger.py
Executable file
85
testrunner/logger.py
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2007, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Simple logging utility. Dumps log messages to stdout, and optionally, to a
|
||||||
|
log file.
|
||||||
|
|
||||||
|
Init(path) must be called to enable logging to a file
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
_LOG_FILE = None
|
||||||
|
_verbose = False
|
||||||
|
|
||||||
|
def Init(log_file_path):
|
||||||
|
"""Set the path to the log file"""
|
||||||
|
global _LOG_FILE
|
||||||
|
_LOG_FILE = log_file_path
|
||||||
|
print "Using log file: %s" % _LOG_FILE
|
||||||
|
|
||||||
|
def GetLogFilePath():
|
||||||
|
"""Returns the path and name of the Log file"""
|
||||||
|
global _LOG_FILE
|
||||||
|
return _LOG_FILE
|
||||||
|
|
||||||
|
def Log(new_str):
|
||||||
|
"""Appends new_str to the end of _LOG_FILE and prints it to stdout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
# new_str is a string.
|
||||||
|
new_str: 'some message to log'
|
||||||
|
"""
|
||||||
|
msg = _PrependTimeStamp(new_str)
|
||||||
|
print msg
|
||||||
|
_WriteLog(msg)
|
||||||
|
|
||||||
|
def _WriteLog(msg):
|
||||||
|
global _LOG_FILE
|
||||||
|
if _LOG_FILE is not None:
|
||||||
|
file_handle = file(_LOG_FILE, 'a')
|
||||||
|
file_handle.write('\n' + str(msg))
|
||||||
|
file_handle.close()
|
||||||
|
|
||||||
|
def _PrependTimeStamp(log_string):
|
||||||
|
"""Returns the log_string prepended with current timestamp """
|
||||||
|
return "# %s: %s" % (datetime.datetime.now().strftime("%m/%d/%y %H:%M:%S"),
|
||||||
|
log_string)
|
||||||
|
|
||||||
|
def SilentLog(new_str):
|
||||||
|
"""Silently log new_str. Unless verbose mode is enabled, will log new_str
|
||||||
|
only to the log file
|
||||||
|
Args:
|
||||||
|
# new_str is a string.
|
||||||
|
new_str: 'some message to log'
|
||||||
|
"""
|
||||||
|
global _verbose
|
||||||
|
msg = _PrependTimeStamp(new_str)
|
||||||
|
if _verbose:
|
||||||
|
print msg
|
||||||
|
_WriteLog(msg)
|
||||||
|
|
||||||
|
def SetVerbose(new_verbose=True):
|
||||||
|
""" Enable or disable verbose logging"""
|
||||||
|
global _verbose
|
||||||
|
_verbose = new_verbose
|
||||||
|
|
||||||
|
def main():
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
117
testrunner/run_command.py
Executable file
117
testrunner/run_command.py
Executable file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/python2.4
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Copyright 2007, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# System imports
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# local imports
|
||||||
|
import logger
|
||||||
|
import errors
|
||||||
|
|
||||||
|
_abort_on_error = False
|
||||||
|
|
||||||
|
def SetAbortOnError(abort=True):
|
||||||
|
"""Sets behavior of RunCommand to throw AbortError if command process returns
|
||||||
|
a negative error code"""
|
||||||
|
global _abort_on_error
|
||||||
|
_abort_on_error = abort
|
||||||
|
|
||||||
|
def RunCommand(cmd, timeout_time=None, retry_count=3, return_output=True):
|
||||||
|
"""Spawns a subprocess to run the given shell command, and checks for
|
||||||
|
timeout_time. If return_output is True, the output of the command is returned
|
||||||
|
as a string. Otherwise, output of command directed to stdout """
|
||||||
|
|
||||||
|
result = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
result = RunOnce(cmd, timeout_time=timeout_time,
|
||||||
|
return_output=return_output)
|
||||||
|
except errors.WaitForResponseTimedOutError:
|
||||||
|
if retry_count == 0:
|
||||||
|
raise
|
||||||
|
retry_count -= 1
|
||||||
|
logger.Log("No response for %s, retrying" % cmd)
|
||||||
|
else:
|
||||||
|
# Success
|
||||||
|
return result
|
||||||
|
|
||||||
|
def RunOnce(cmd, timeout_time=None, return_output=True):
|
||||||
|
start_time = time.time()
|
||||||
|
so = []
|
||||||
|
pid = []
|
||||||
|
global _abort_on_error
|
||||||
|
error_occurred = False
|
||||||
|
|
||||||
|
def Run():
|
||||||
|
if return_output:
|
||||||
|
output_dest = subprocess.PIPE
|
||||||
|
else:
|
||||||
|
# None means direct to stdout
|
||||||
|
output_dest = None
|
||||||
|
pipe = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
executable='/bin/bash',
|
||||||
|
stdout=output_dest,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
shell=True)
|
||||||
|
pid.append(pipe.pid)
|
||||||
|
try:
|
||||||
|
output = pipe.communicate()[0]
|
||||||
|
if output is not None and len(output) > 0:
|
||||||
|
so.append(output)
|
||||||
|
except OSError, e:
|
||||||
|
logger.SilentLog("failed to retrieve stdout from: %s" % cmd)
|
||||||
|
logger.Log(e)
|
||||||
|
so.append("ERROR")
|
||||||
|
error_occurred = True
|
||||||
|
if pipe.returncode < 0:
|
||||||
|
logger.SilentLog("Error: %s was terminated by signal %d" %(cmd,
|
||||||
|
pipe.returncode))
|
||||||
|
error_occurred = True
|
||||||
|
|
||||||
|
t = threading.Thread(target=Run)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
break_loop = False
|
||||||
|
while not break_loop:
|
||||||
|
if not t.isAlive():
|
||||||
|
break_loop = True
|
||||||
|
|
||||||
|
# Check the timeout
|
||||||
|
if (not break_loop and timeout_time is not None
|
||||||
|
and time.time() > start_time + timeout_time):
|
||||||
|
try:
|
||||||
|
os.kill(pid[0], signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
# process already dead. No action required.
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.SilentLog("about to raise a timeout for: %s" % cmd)
|
||||||
|
raise errors.WaitForResponseTimedOutError
|
||||||
|
if not break_loop:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
if _abort_on_error and error_occurred:
|
||||||
|
raise errors.AbortError
|
||||||
|
|
||||||
|
return "".join(so)
|
||||||
@@ -15,17 +15,20 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Parser for test definition xml files."""
|
||||||
|
|
||||||
# Python imports
|
# Python imports
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
import xml.parsers
|
import xml.parsers
|
||||||
from sets import Set
|
|
||||||
|
|
||||||
# local imports
|
# local imports
|
||||||
import logger
|
|
||||||
import errors
|
import errors
|
||||||
|
import logger
|
||||||
|
|
||||||
|
|
||||||
class TestDefinitions(object):
|
class TestDefinitions(object):
|
||||||
"""Accessor for a test definitions xml file
|
"""Accessor for a test definitions xml file data.
|
||||||
|
|
||||||
Expected format is:
|
Expected format is:
|
||||||
<test-definitions>
|
<test-definitions>
|
||||||
<test
|
<test
|
||||||
@@ -40,13 +43,13 @@ class TestDefinitions(object):
|
|||||||
<test ...
|
<test ...
|
||||||
</test-definitions>
|
</test-definitions>
|
||||||
|
|
||||||
TODO: add format checking
|
TODO: add format checking.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# tag/attribute constants
|
# tag/attribute constants
|
||||||
_TEST_TAG_NAME = 'test'
|
_TEST_TAG_NAME = "test"
|
||||||
|
|
||||||
def __init__(self, ):
|
def __init__(self):
|
||||||
# dictionary of test name to tests
|
# dictionary of test name to tests
|
||||||
self._testname_map = {}
|
self._testname_map = {}
|
||||||
|
|
||||||
@@ -54,27 +57,25 @@ class TestDefinitions(object):
|
|||||||
return iter(self._testname_map.values())
|
return iter(self._testname_map.values())
|
||||||
|
|
||||||
def Parse(self, file_path):
|
def Parse(self, file_path):
|
||||||
"""Parse the test suite data from from given file path, and add it to the
|
"""Parse the test suite data from from given file path.
|
||||||
current object
|
|
||||||
Args:
|
Args:
|
||||||
file_path: absolute file path to parse
|
file_path: absolute file path to parse
|
||||||
Raises:
|
Raises:
|
||||||
errors.ParseError if file_path cannot be parsed
|
ParseError if file_path cannot be parsed
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
doc = xml.dom.minidom.parse(file_path)
|
doc = xml.dom.minidom.parse(file_path)
|
||||||
except IOError:
|
except IOError:
|
||||||
logger.Log('test file %s does not exist' % file_path)
|
logger.Log("test file %s does not exist" % file_path)
|
||||||
raise errors.ParseError
|
raise errors.ParseError
|
||||||
except xml.parsers.expat.ExpatError:
|
except xml.parsers.expat.ExpatError:
|
||||||
logger.Log('Error Parsing xml file: %s ' % file_path)
|
logger.Log("Error Parsing xml file: %s " % file_path)
|
||||||
raise errors.ParseError
|
raise errors.ParseError
|
||||||
return self._ParseDoc(doc)
|
self._ParseDoc(doc)
|
||||||
|
|
||||||
def ParseString(self, xml_string):
|
def ParseString(self, xml_string):
|
||||||
"""Alternate parse method that accepts a string of the xml data instead of a
|
"""Alternate parse method that accepts a string of the xml data."""
|
||||||
file
|
|
||||||
"""
|
|
||||||
doc = xml.dom.minidom.parseString(xml_string)
|
doc = xml.dom.minidom.parseString(xml_string)
|
||||||
# TODO: catch exceptions and raise ParseError
|
# TODO: catch exceptions and raise ParseError
|
||||||
return self._ParseDoc(doc)
|
return self._ParseDoc(doc)
|
||||||
@@ -87,15 +88,22 @@ class TestDefinitions(object):
|
|||||||
self._AddTest(test)
|
self._AddTest(test)
|
||||||
|
|
||||||
def _ParseTestSuite(self, suite_element):
|
def _ParseTestSuite(self, suite_element):
|
||||||
"""Parse the suite element
|
"""Parse the suite element.
|
||||||
Returns a TestSuite object, populated with parsed data
|
|
||||||
|
Returns:
|
||||||
|
a TestSuite object, populated with parsed data
|
||||||
"""
|
"""
|
||||||
test = TestSuite(suite_element)
|
test = TestSuite(suite_element)
|
||||||
return test
|
return test
|
||||||
|
|
||||||
def _AddTest(self, test):
|
def _AddTest(self, test):
|
||||||
""" Adds a test to this TestManifest. If a test already exists with the
|
"""Adds a test to this TestManifest.
|
||||||
same name, it overrides it"""
|
|
||||||
|
If a test already exists with the same name, it overrides it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
test: TestSuite to add
|
||||||
|
"""
|
||||||
self._testname_map[test.GetName()] = test
|
self._testname_map[test.GetName()] = test
|
||||||
|
|
||||||
def GetTests(self):
|
def GetTests(self):
|
||||||
@@ -109,26 +117,23 @@ class TestDefinitions(object):
|
|||||||
return con_tests
|
return con_tests
|
||||||
|
|
||||||
def GetTest(self, name):
|
def GetTest(self, name):
|
||||||
try:
|
return self._testname_map.get(name, None)
|
||||||
return self._testname_map[name]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
class TestSuite:
|
class TestSuite(object):
|
||||||
""" Represents one test suite definition parsed from xml """
|
"""Represents one test suite definition parsed from xml."""
|
||||||
|
|
||||||
_NAME_ATTR = 'name'
|
_NAME_ATTR = "name"
|
||||||
_PKG_ATTR = 'package'
|
_PKG_ATTR = "package"
|
||||||
_RUNNER_ATTR = 'runner'
|
_RUNNER_ATTR = "runner"
|
||||||
_CLASS_ATTR = 'class'
|
_CLASS_ATTR = "class"
|
||||||
_TARGET_ATTR = 'coverage_target'
|
_TARGET_ATTR = "coverage_target"
|
||||||
_BUILD_ATTR = 'build_path'
|
_BUILD_ATTR = "build_path"
|
||||||
_CONTINUOUS_ATTR = 'continuous'
|
_CONTINUOUS_ATTR = "continuous"
|
||||||
|
|
||||||
_DEFAULT_RUNNER = 'android.test.InstrumentationTestRunner'
|
_DEFAULT_RUNNER = "android.test.InstrumentationTestRunner"
|
||||||
|
|
||||||
def __init__(self, suite_element):
|
def __init__(self, suite_element):
|
||||||
""" Populates this instance's data from given suite xml element"""
|
"""Populates this instance's data from given suite xml element."""
|
||||||
self._name = suite_element.getAttribute(self._NAME_ATTR)
|
self._name = suite_element.getAttribute(self._NAME_ATTR)
|
||||||
self._package = suite_element.getAttribute(self._PKG_ATTR)
|
self._package = suite_element.getAttribute(self._PKG_ATTR)
|
||||||
if suite_element.hasAttribute(self._RUNNER_ATTR):
|
if suite_element.hasAttribute(self._RUNNER_ATTR):
|
||||||
@@ -165,23 +170,27 @@ class TestSuite:
|
|||||||
return self._class
|
return self._class
|
||||||
|
|
||||||
def GetTargetName(self):
|
def GetTargetName(self):
|
||||||
""" Retrieve module that this test is targeting - used to show code coverage
|
"""Retrieve module that this test is targeting.
|
||||||
|
|
||||||
|
Used for generating code coverage metrics.
|
||||||
"""
|
"""
|
||||||
return self._target_name
|
return self._target_name
|
||||||
|
|
||||||
def GetBuildPath(self):
|
def GetBuildPath(self):
|
||||||
""" Return the path, relative to device root, of this test's Android.mk file
|
"""Returns the build path of this test, relative to source tree root."""
|
||||||
"""
|
|
||||||
return self._build_path
|
return self._build_path
|
||||||
|
|
||||||
def IsContinuous(self):
|
def IsContinuous(self):
|
||||||
"""Returns true if test is flagged as continuous worthy"""
|
"""Returns true if test is flagged as being part of the continuous tests"""
|
||||||
return self._continuous
|
return self._continuous
|
||||||
|
|
||||||
def Parse(file_path):
|
def Parse(file_path):
|
||||||
"""parses out a TestDefinitions from given path to xml file
|
"""Parses out a TestDefinitions from given path to xml file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path: string absolute file path
|
file_path: string absolute file path
|
||||||
|
Returns:
|
||||||
|
a TestDefinitions object containing data parsed from file_path
|
||||||
Raises:
|
Raises:
|
||||||
ParseError if xml format is not recognized
|
ParseError if xml format is not recognized
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -215,6 +215,11 @@ public final class ExportWizard extends Wizard implements IExportWizard {
|
|||||||
final boolean[] result = new boolean[1];
|
final boolean[] result = new boolean[1];
|
||||||
try {
|
try {
|
||||||
workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
|
workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
|
||||||
|
/**
|
||||||
|
* Run the export.
|
||||||
|
* @throws InvocationTargetException
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
public void run(IProgressMonitor monitor) throws InvocationTargetException,
|
public void run(IProgressMonitor monitor) throws InvocationTargetException,
|
||||||
InterruptedException {
|
InterruptedException {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -397,8 +397,10 @@ final class KeyCheckPage extends ExportWizardPage {
|
|||||||
/**
|
/**
|
||||||
* Creates the list of destination filenames based on the content of the destination field
|
* Creates the list of destination filenames based on the content of the destination field
|
||||||
* and the list of APK configurations for the project.
|
* and the list of APK configurations for the project.
|
||||||
* @param file
|
*
|
||||||
* @return
|
* @param file File name from the destination field
|
||||||
|
* @return A list of destination filenames based <code>file</code> and the list of APK
|
||||||
|
* configurations for the project.
|
||||||
*/
|
*/
|
||||||
private Map<String, String[]> getApkFileMap(File file) {
|
private Map<String, String[]> getApkFileMap(File file) {
|
||||||
String filename = file.getName();
|
String filename = file.getName();
|
||||||
|
|||||||
Reference in New Issue
Block a user