auto import from //depot/cupcake/@136654

This commit is contained in:
The Android Open Source Project
2009-03-05 17:04:45 -08:00
parent edd86fdaa9
commit 2b83cbdb14
9 changed files with 1181 additions and 86 deletions

347
testrunner/adb_interface.py Executable file
View 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

View 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
View 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
View 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
View 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
View 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)

View File

@@ -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
""" """

View File

@@ -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 {

View File

@@ -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();