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