auto import from //depot/cupcake/@136654
diff --git a/testrunner/adb_interface.py b/testrunner/adb_interface.py
new file mode 100755
index 0000000..fb304df
--- /dev/null
+++ b/testrunner/adb_interface.py
@@ -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
diff --git a/testrunner/am_instrument_parser.py b/testrunner/am_instrument_parser.py
new file mode 100755
index 0000000..cad87c0
--- /dev/null
+++ b/testrunner/am_instrument_parser.py
@@ -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
diff --git a/testrunner/coverage.py b/testrunner/coverage.py
new file mode 100755
index 0000000..507c5c7
--- /dev/null
+++ b/testrunner/coverage.py
@@ -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()
diff --git a/testrunner/errors.py b/testrunner/errors.py
new file mode 100755
index 0000000..6d606ec
--- /dev/null
+++ b/testrunner/errors.py
@@ -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."""
+
diff --git a/testrunner/logger.py b/testrunner/logger.py
new file mode 100755
index 0000000..762c893
--- /dev/null
+++ b/testrunner/logger.py
@@ -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()
diff --git a/testrunner/run_command.py b/testrunner/run_command.py
new file mode 100755
index 0000000..6b72b77
--- /dev/null
+++ b/testrunner/run_command.py
@@ -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)
diff --git a/testrunner/test_defs.py b/testrunner/test_defs.py
index 6039e25..949ad6e 100644
--- a/testrunner/test_defs.py
+++ b/testrunner/test_defs.py
@@ -15,173 +15,182 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+"""Parser for test definition xml files."""
+
# Python imports
import xml.dom.minidom
import xml.parsers
-from sets import Set
# local imports
-import logger
import errors
+import logger
+
class TestDefinitions(object):
- """Accessor for a test definitions xml file
- Expected format is:
- <test-definitions>
- <test
- name=""
- package=""
- [runner=""]
- [class=""]
- [coverage_target=""]
- [build_path=""]
- [continuous]
- />
- <test ...
- </test-definitions>
-
- TODO: add format checking
+ """Accessor for a test definitions xml file data.
+
+ Expected format is:
+ <test-definitions>
+ <test
+ name=""
+ package=""
+ [runner=""]
+ [class=""]
+ [coverage_target=""]
+ [build_path=""]
+ [continuous]
+ />
+ <test ...
+ </test-definitions>
+
+ TODO: add format checking.
"""
# tag/attribute constants
- _TEST_TAG_NAME = 'test'
+ _TEST_TAG_NAME = "test"
- def __init__(self, ):
+ def __init__(self):
# dictionary of test name to tests
self._testname_map = {}
-
+
def __iter__(self):
return iter(self._testname_map.values())
-
+
def Parse(self, file_path):
- """Parse the test suite data from from given file path, and add it to the
- current object
- Args:
- file_path: absolute file path to parse
- Raises:
- errors.ParseError if file_path cannot be parsed
- """
+ """Parse the test suite data from from given file path.
+
+ Args:
+ file_path: absolute file path to parse
+ Raises:
+ ParseError if file_path cannot be parsed
+ """
try:
doc = xml.dom.minidom.parse(file_path)
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
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
- return self._ParseDoc(doc)
-
+ self._ParseDoc(doc)
+
def ParseString(self, xml_string):
- """Alternate parse method that accepts a string of the xml data instead of a
- file
- """
+ """Alternate parse method that accepts a string of the xml data."""
doc = xml.dom.minidom.parseString(xml_string)
# TODO: catch exceptions and raise ParseError
- return self._ParseDoc(doc)
+ return self._ParseDoc(doc)
- def _ParseDoc(self, doc):
+ def _ParseDoc(self, doc):
suite_elements = doc.getElementsByTagName(self._TEST_TAG_NAME)
for suite_element in suite_elements:
test = self._ParseTestSuite(suite_element)
self._AddTest(test)
-
+
def _ParseTestSuite(self, suite_element):
- """Parse the suite element
- Returns a TestSuite object, populated with parsed data
- """
+ """Parse the suite element.
+
+ Returns:
+ a TestSuite object, populated with parsed data
+ """
test = TestSuite(suite_element)
- return test
-
+ return test
+
def _AddTest(self, test):
- """ Adds a test to this TestManifest. If a test already exists with the
- same name, it overrides it"""
- self._testname_map[test.GetName()] = test
+ """Adds a test to this TestManifest.
+ If a test already exists with the same name, it overrides it.
+
+ Args:
+ test: TestSuite to add
+ """
+ self._testname_map[test.GetName()] = test
+
def GetTests(self):
return self._testname_map.values()
-
+
def GetContinuousTests(self):
con_tests = []
for test in self.GetTests():
if test.IsContinuous():
con_tests.append(test)
- return con_tests
-
+ return con_tests
+
def GetTest(self, name):
- try:
- return self._testname_map[name]
- except KeyError:
- return None
+ return self._testname_map.get(name, None)
-class TestSuite:
- """ Represents one test suite definition parsed from xml """
-
- _NAME_ATTR = 'name'
- _PKG_ATTR = 'package'
- _RUNNER_ATTR = 'runner'
- _CLASS_ATTR = 'class'
- _TARGET_ATTR = 'coverage_target'
- _BUILD_ATTR = 'build_path'
- _CONTINUOUS_ATTR = 'continuous'
-
- _DEFAULT_RUNNER = 'android.test.InstrumentationTestRunner'
-
+class TestSuite(object):
+ """Represents one test suite definition parsed from xml."""
+
+ _NAME_ATTR = "name"
+ _PKG_ATTR = "package"
+ _RUNNER_ATTR = "runner"
+ _CLASS_ATTR = "class"
+ _TARGET_ATTR = "coverage_target"
+ _BUILD_ATTR = "build_path"
+ _CONTINUOUS_ATTR = "continuous"
+
+ _DEFAULT_RUNNER = "android.test.InstrumentationTestRunner"
+
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._package = suite_element.getAttribute(self._PKG_ATTR)
if suite_element.hasAttribute(self._RUNNER_ATTR):
self._runner = suite_element.getAttribute(self._RUNNER_ATTR)
else:
- self._runner = self._DEFAULT_RUNNER
+ self._runner = self._DEFAULT_RUNNER
if suite_element.hasAttribute(self._CLASS_ATTR):
self._class = suite_element.getAttribute(self._CLASS_ATTR)
else:
- self._class = None
- if suite_element.hasAttribute(self._TARGET_ATTR):
+ self._class = None
+ if suite_element.hasAttribute(self._TARGET_ATTR):
self._target_name = suite_element.getAttribute(self._TARGET_ATTR)
else:
self._target_name = None
- if suite_element.hasAttribute(self._BUILD_ATTR):
+ if suite_element.hasAttribute(self._BUILD_ATTR):
self._build_path = suite_element.getAttribute(self._BUILD_ATTR)
else:
self._build_path = None
- if suite_element.hasAttribute(self._CONTINUOUS_ATTR):
+ if suite_element.hasAttribute(self._CONTINUOUS_ATTR):
self._continuous = suite_element.getAttribute(self._CONTINUOUS_ATTR)
else:
self._continuous = False
-
+
def GetName(self):
return self._name
-
+
def GetPackageName(self):
return self._package
def GetRunnerName(self):
return self._runner
-
+
def GetClassName(self):
return self._class
-
+
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
-
+
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
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
-
+
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:
file_path: string absolute file path
+ Returns:
+ a TestDefinitions object containing data parsed from file_path
Raises:
ParseError if xml format is not recognized
"""
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java
index b1b971d..6ede10d 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java
@@ -215,6 +215,11 @@
final boolean[] result = new boolean[1];
try {
workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
+ /**
+ * Run the export.
+ * @throws InvocationTargetException
+ * @throws InterruptedException
+ */
public void run(IProgressMonitor monitor) throws InvocationTargetException,
InterruptedException {
try {
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java
index 8a9703d..7fd76e9 100644
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java
@@ -397,8 +397,10 @@
/**
* Creates the list of destination filenames based on the content of the destination field
* 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) {
String filename = file.getName();