diff --git a/testrunner/adb_interface.py b/testrunner/adb_interface.py new file mode 100755 index 000000000..fb304df9d --- /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 000000000..cad87c047 --- /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-?\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.*)' + 'INSTRUMENTATION_STATUS: test=(?P\w+)\s+' + 'INSTRUMENTATION_STATUS: class=(?P[\w\.]+)\s+' + 'INSTRUMENTATION_STATUS: current=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: numtests=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: id=.*\s+' + 'INSTRUMENTATION_STATUS_CODE: 1\s*', re.DOTALL) + + re_end_block = re.compile( + r'\s*INSTRUMENTATION_STATUS: stream=(?P.*)' + 'INSTRUMENTATION_STATUS: test=(?P\w+)\s+' + '(INSTRUMENTATION_STATUS: stack=(?P.*))?' + 'INSTRUMENTATION_STATUS: class=(?P[\w\.]+)\s+' + 'INSTRUMENTATION_STATUS: current=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: numtests=(?P\d+)\s+' + 'INSTRUMENTATION_STATUS: id=.*\s+' + 'INSTRUMENTATION_STATUS_CODE: (?P0|-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 000000000..507c5c79d --- /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 + /<_COVERAGE_REPORT_PATH>// + + 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 /*/*.<_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 000000000..6d606ecf5 --- /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 000000000..762c89311 --- /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 000000000..6b72b77b3 --- /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 6039e25ff..949ad6ef8 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: - - - - - TODO: add format checking + """Accessor for a test definitions xml file data. + + Expected format is: + + + + + 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 - -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' + return self._testname_map.get(name, None) +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 b1b971dc6..6ede10dba 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 @@ public final class ExportWizard extends Wizard implements IExportWizard { 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 8a9703de8..7fd76e9e8 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 @@ final class KeyCheckPage extends ExportWizardPage { /** * 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 file and the list of APK + * configurations for the project. */ private Map getApkFileMap(File file) { String filename = file.getName();