From 764d3fa70d42a79e2ee999b790e69fc55f12bf61 Mon Sep 17 00:00:00 2001 From: Brett Chabot Date: Thu, 25 Jun 2009 17:57:31 -0700 Subject: [PATCH] Add support for running host java tests to runtest. With this change, also refactored runtest as follows: Modified the test suite schema and python implementation to have an inheritance structure. Each test type has its own python module, which will also handle the logic of running the test. --- testrunner/__init__.py | 1 + testrunner/adb_interface.py | 16 +- testrunner/android_build.py | 41 +++ testrunner/coverage.py | 61 ++-- testrunner/errors.py | 31 +- testrunner/runtest.py | 226 +-------------- testrunner/test_defs.py | 281 ------------------- testrunner/test_defs.xml | 83 +----- testrunner/test_defs.xsd | 138 +++++++-- testrunner/test_defs/__init__.py | 1 + testrunner/test_defs/abstract_test.py | 111 ++++++++ testrunner/test_defs/host_test.py | 107 +++++++ testrunner/test_defs/instrumentation_test.py | 169 +++++++++++ testrunner/test_defs/native_test.py | 153 ++++++++++ testrunner/test_defs/test_defs.py | 146 ++++++++++ 15 files changed, 928 insertions(+), 637 deletions(-) create mode 100644 testrunner/__init__.py delete mode 100644 testrunner/test_defs.py create mode 100644 testrunner/test_defs/__init__.py create mode 100644 testrunner/test_defs/abstract_test.py create mode 100644 testrunner/test_defs/host_test.py create mode 100644 testrunner/test_defs/instrumentation_test.py create mode 100644 testrunner/test_defs/native_test.py create mode 100644 testrunner/test_defs/test_defs.py diff --git a/testrunner/__init__.py b/testrunner/__init__.py new file mode 100644 index 000000000..69ee92b04 --- /dev/null +++ b/testrunner/__init__.py @@ -0,0 +1 @@ +__all__ = ['adb_interface', 'android_build', 'errors', 'logger', 'run_command'] diff --git a/testrunner/adb_interface.py b/testrunner/adb_interface.py index 429bc27d7..33191f7d5 100755 --- a/testrunner/adb_interface.py +++ b/testrunner/adb_interface.py @@ -306,7 +306,7 @@ class AdbInterface: 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 + # 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: @@ -357,12 +357,12 @@ class AdbInterface: 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 AbortError if unrecoverable error occurred @@ -375,12 +375,12 @@ class AdbInterface: error = e output = e.msg if "Read-only file system" in output: - logger.SilentLog(output) + logger.SilentLog(output) logger.Log("Remounting read-only filesystem") self.SendCommand("remount") output = self.SendCommand("sync", retry_count=retry_count) elif "No space left on device" in output: - logger.SilentLog(output) + logger.SilentLog(output) logger.Log("Restarting device runtime") self.SendShellCommand("stop", retry_count=retry_count) output = self.SendCommand("sync", retry_count=retry_count) @@ -392,3 +392,7 @@ class AdbInterface: self.WaitForDevicePm() return output + def GetSerialNumber(self): + """Returns the serial number of the targeted device.""" + return self.SendCommand("get-serialno").strip() + diff --git a/testrunner/android_build.py b/testrunner/android_build.py index 976f2bb1f..37ddf9e65 100644 --- a/testrunner/android_build.py +++ b/testrunner/android_build.py @@ -133,3 +133,44 @@ def GetTargetSystemBin(): logger.Log("Error: Target system bin path could not be found") raise errors.AbortError return path + +def GetHostLibraryPath(): + """Returns the full pathname to the host java library output directory. + + Typically $ANDROID_BUILD_TOP/out/host//framework. + + Assumes build environment has been properly configured by envsetup & + lunch/choosecombo. + + Returns: + The absolute file path of the Android host java library directory. + + Raises: + AbortError: if Android host java library directory could not be found. + """ + (_, _, os_arch) = GetHostOsArch() + path = os.path.join(GetTop(), "out", "host", os_arch, "framework") + if not os.path.exists(path): + logger.Log("Error: Host library path could not be found %s" % path) + raise errors.AbortError + return path + +def GetTestAppPath(): + """Returns the full pathname to the test app build output directory. + + Typically $ANDROID_PRODUCT_OUT/data/app + + Assumes build environment has been properly configured by envsetup & + lunch/choosecombo. + + Returns: + The absolute file path of the Android test app build directory. + + Raises: + AbortError: if Android host java library directory could not be found. + """ + path = os.path.join(GetProductOut(), "data", "app") + if not os.path.exists(path): + logger.Log("Error: app path could not be found %s" % path) + raise errors.AbortError + return path diff --git a/testrunner/coverage.py b/testrunner/coverage.py index c80eea0a4..eb46f1f49 100755 --- a/testrunner/coverage.py +++ b/testrunner/coverage.py @@ -3,16 +3,16 @@ # # 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 +# 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 +# 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 +# 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.""" @@ -37,12 +37,8 @@ class CoverageGenerator(object): 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") + _EMMA_JAR = os.path.join("external", "emma", "lib", "emma.jar") _TEST_COVERAGE_EXT = "ec" # root path of generated coverage report files, relative to Android build root _COVERAGE_REPORT_PATH = os.path.join("out", "emma") @@ -58,19 +54,14 @@ class CoverageGenerator(object): _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 + def __init__(self, adb_interface): + self._root_path = android_build.GetTop() 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" - #TODO: can emma.jar automagically be added to bootclasspath here? - def TestDeviceCoverageSupport(self): """Check if device has support for generating code coverage metrics. @@ -80,16 +71,18 @@ class CoverageGenerator(object): Returns: True if device can support code coverage. False otherwise. """ - output = self._adb.SendShellCommand("cat init.rc | grep BOOTCLASSPATH | " - "grep emma.jar") - if len(output) > 0: - return True - else: - logger.Log("Error: Targeted device does not have emma.jar on its " - "BOOTCLASSPATH.") - logger.Log("Modify the BOOTCLASSPATH entry in system/core/rootdir/init.rc" - " to add emma.jar") - return False + try: + output = self._adb.SendShellCommand("cat init.rc | grep BOOTCLASSPATH | " + "grep emma.jar") + if len(output) > 0: + return True + except errors.AbortError: + pass + logger.Log("Error: Targeted device does not have emma.jar on its " + "BOOTCLASSPATH.") + logger.Log("Modify the BOOTCLASSPATH entry in system/core/rootdir/init.rc" + " to add emma.jar") + return False def ExtractReport(self, test_suite, device_coverage_path, @@ -259,9 +252,6 @@ class CoverageGenerator(object): 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. @@ -310,6 +300,11 @@ class CoverageGenerator(object): self._CombineTargetCoverage() +def EnableCoverageBuild(): + """Enable building an Android target with code coverage instrumentation.""" + os.environ["EMMA_INSTRUMENT"] = "true" + + def Run(): """Does coverage operations based on command line args.""" # TODO: do we want to support combining coverage for a single target diff --git a/testrunner/errors.py b/testrunner/errors.py index c04fd012c..e163dd45b 100755 --- a/testrunner/errors.py +++ b/testrunner/errors.py @@ -3,41 +3,44 @@ # # 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 +# 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 +# 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 +# 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 MsgException(Exception): + """Generic exception with an optional string msg.""" + def __init__(self, msg=""): + self.msg = msg + + 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): +class AbortError(MsgException): """Generic exception that indicates a fatal error has occurred and program execution should be aborted.""" - def __init__(self, msg=""): - self.msg = msg - -class ParseError(Exception): +class ParseError(MsgException): """Raised when xml data to parse has unrecognized format.""" diff --git a/testrunner/runtest.py b/testrunner/runtest.py index 03eddbf70..34f979fc0 100755 --- a/testrunner/runtest.py +++ b/testrunner/runtest.py @@ -34,7 +34,7 @@ import coverage import errors import logger import run_command -import test_defs +from test_defs import test_defs class TestRunner(object): @@ -154,8 +154,8 @@ class TestRunner(object): self._known_tests = self._ReadTests() - self._coverage_gen = coverage.CoverageGenerator( - android_root_path=self._root_path, adb_interface=self._adb) + self._options.host_lib_path = android_build.GetHostLibraryPath() + self._options.test_data_path = android_build.GetTestAppPath() def _ReadTests(self): """Parses the set of test definition data. @@ -196,18 +196,14 @@ class TestRunner(object): if target_set: if self._options.coverage: - self._coverage_gen.EnableCoverageBuild() - self._AddBuildTargetPath(self._coverage_gen.GetEmmaBuildPath(), - target_set) + coverage.EnableCoverageBuild() target_build_string = " ".join(list(target_set)) extra_args_string = " ".join(list(extra_args_set)) - # log the user-friendly equivalent make command, so developers can - # replicate this step - logger.Log("mmm %s %s" % (target_build_string, extra_args_string)) - # mmm cannot be used from python, so perform a similiar operation using + # mmm cannot be used from python, so perform a similar operation using # ONE_SHOT_MAKEFILE cmd = 'ONE_SHOT_MAKEFILE="%s" make -C "%s" files %s' % ( target_build_string, self._root_path, extra_args_string) + logger.Log(cmd) if self._options.preview: # in preview mode, just display to the user what command would have been @@ -221,7 +217,9 @@ class TestRunner(object): def _AddBuildTarget(self, test_suite, target_set, extra_args_set): build_dir = test_suite.GetBuildPath() if self._AddBuildTargetPath(build_dir, target_set): - extra_args_set.add(test_suite.GetExtraMakeArgs()) + extra_args_set.add(test_suite.GetExtraBuildArgs()) + for path in test_suite.GetBuildDependencies(self._options): + self._AddBuildTargetPath(path, target_set) def _AddBuildTargetPath(self, build_dir, target_set): if build_dir is not None: @@ -249,206 +247,6 @@ class TestRunner(object): tests.append(test) return tests - def _RunTest(self, test_suite): - """Run the provided test suite. - - Builds up an adb instrument command using provided input arguments. - - Args: - test_suite: TestSuite to run - """ - - test_class = test_suite.GetClassName() - if self._options.test_class is not None: - test_class = self._options.test_class.lstrip() - if test_class.startswith("."): - test_class = test_suite.GetPackageName() + test_class - if self._options.test_method is not None: - test_class = "%s#%s" % (test_class, self._options.test_method) - - instrumentation_args = {} - if test_class is not None: - instrumentation_args["class"] = test_class - if self._options.test_package: - instrumentation_args["package"] = self._options.test_package - if self._options.test_size: - instrumentation_args["size"] = self._options.test_size - if self._options.wait_for_debugger: - instrumentation_args["debug"] = "true" - if self._options.suite_assign_mode: - instrumentation_args["suiteAssignment"] = "true" - if self._options.coverage: - instrumentation_args["coverage"] = "true" - if self._options.preview: - adb_cmd = self._adb.PreviewInstrumentationCommand( - package_name=test_suite.GetPackageName(), - runner_name=test_suite.GetRunnerName(), - raw_mode=self._options.raw_mode, - instrumentation_args=instrumentation_args) - logger.Log(adb_cmd) - elif self._options.coverage: - self._adb.WaitForInstrumentation(test_suite.GetPackageName(), - test_suite.GetRunnerName()) - # need to parse test output to determine path to coverage file - logger.Log("Running in coverage mode, suppressing test output") - try: - (test_results, status_map) = self._adb.StartInstrumentationForPackage( - package_name=test_suite.GetPackageName(), - runner_name=test_suite.GetRunnerName(), - timeout_time=60*60, - instrumentation_args=instrumentation_args) - except errors.InstrumentationError, errors.DeviceUnresponsiveError: - return - self._PrintTestResults(test_results) - device_coverage_path = status_map.get("coverageFilePath", None) - if device_coverage_path is None: - logger.Log("Error: could not find coverage data on device") - return - coverage_file = self._coverage_gen.ExtractReport(test_suite, device_coverage_path) - if coverage_file is not None: - logger.Log("Coverage report generated at %s" % coverage_file) - else: - self._adb.WaitForInstrumentation(test_suite.GetPackageName(), - test_suite.GetRunnerName()) - self._adb.StartInstrumentationNoResults( - package_name=test_suite.GetPackageName(), - runner_name=test_suite.GetRunnerName(), - raw_mode=self._options.raw_mode, - instrumentation_args=instrumentation_args) - - def _PrintTestResults(self, test_results): - """Prints a summary of test result data to stdout. - - Args: - test_results: a list of am_instrument_parser.TestResult - """ - total_count = 0 - error_count = 0 - fail_count = 0 - for test_result in test_results: - if test_result.GetStatusCode() == -1: # error - logger.Log("Error in %s: %s" % (test_result.GetTestName(), - test_result.GetFailureReason())) - error_count+=1 - elif test_result.GetStatusCode() == -2: # failure - logger.Log("Failure in %s: %s" % (test_result.GetTestName(), - test_result.GetFailureReason())) - fail_count+=1 - total_count+=1 - logger.Log("Tests run: %d, Failures: %d, Errors: %d" % - (total_count, fail_count, error_count)) - - def _CollectTestSources(self, test_list, dirname, files): - """For each directory, find tests source file and add them to the list. - - Test files must match one of the following pattern: - - test_*.[cc|cpp] - - *_test.[cc|cpp] - - *_unittest.[cc|cpp] - - This method is a callback for os.path.walk. - - Args: - test_list: Where new tests should be inserted. - dirname: Current directory. - files: List of files in the current directory. - """ - for f in files: - (name, ext) = os.path.splitext(f) - if ext == ".cc" or ext == ".cpp": - if re.search("_test$|_test_$|_unittest$|_unittest_$|^test_", name): - logger.SilentLog("Found %s" % f) - test_list.append(str(os.path.join(dirname, f))) - - def _FilterOutMissing(self, path, sources): - """Filter out from the sources list missing tests. - - Sometimes some test source are not built for the target, i.e there - is no binary corresponding to the source file. We need to filter - these out. - - Args: - path: Where the binaries should be. - sources: List of tests source path. - Returns: - A list of test binaries built from the sources. - """ - binaries = [] - for f in sources: - binary = os.path.basename(f) - binary = os.path.splitext(binary)[0] - full_path = os.path.join(path, binary) - if os.path.exists(full_path): - binaries.append(binary) - return binaries - - def _RunNativeTest(self, test_suite): - """Run the provided *native* test suite. - - The test_suite must contain a build path where the native test - files are. Subdirectories are automatically scanned as well. - - Each test's name must have a .cc or .cpp extension and match one - of the following patterns: - - test_* - - *_test.[cc|cpp] - - *_unittest.[cc|cpp] - A successful test must return 0. Any other value will be considered - as an error. - - Args: - test_suite: TestSuite to run - """ - # find all test files, convert unicode names to ascii, take the basename - # and drop the .cc/.cpp extension. - source_list = [] - build_path = test_suite.GetBuildPath() - os.path.walk(build_path, self._CollectTestSources, source_list) - logger.SilentLog("Tests source %s" % source_list) - - # Host tests are under out/host/-/bin. - host_list = self._FilterOutMissing(android_build.GetHostBin(), source_list) - logger.SilentLog("Host tests %s" % host_list) - - # Target tests are under $ANDROID_PRODUCT_OUT/system/bin. - target_list = self._FilterOutMissing(android_build.GetTargetSystemBin(), - source_list) - logger.SilentLog("Target tests %s" % target_list) - - # Run on the host - logger.Log("\nRunning on host") - for f in host_list: - if run_command.RunHostCommand(f) != 0: - logger.Log("%s... failed" % f) - else: - if run_command.HasValgrind(): - if run_command.RunHostCommand(f, valgrind=True) == 0: - logger.Log("%s... ok\t\t[valgrind: ok]" % f) - else: - logger.Log("%s... ok\t\t[valgrind: failed]" % f) - else: - logger.Log("%s... ok\t\t[valgrind: missing]" % f) - - # Run on the device - logger.Log("\nRunning on target") - for f in target_list: - full_path = os.path.join(os.sep, "system", "bin", f) - - # Single quotes are needed to prevent the shell splitting it. - output = self._adb.SendShellCommand("'%s 2>&1;echo -n exit code:$?'" % - full_path, - int(self._options.timeout)) - success = output.endswith("exit code:0") - logger.Log("%s... %s" % (f, success and "ok" or "failed")) - # Print the captured output when the test failed. - if not success or self._options.verbose: - pos = output.rfind("exit code") - output = output[0:pos] - logger.Log(output) - - # Cleanup - self._adb.SendShellCommand("rm %s" % full_path) - def RunTests(self): """Main entry method - executes the tests according to command line args.""" try: @@ -462,10 +260,8 @@ class TestRunner(object): self._DoBuild() for test_suite in self._GetTestsToRun(): - if test_suite.IsNative(): - self._RunNativeTest(test_suite) - else: - self._RunTest(test_suite) + test_suite.Run(self._options, self._adb) + except KeyboardInterrupt: logger.Log("Exiting...") except errors.AbortError, e: diff --git a/testrunner/test_defs.py b/testrunner/test_defs.py deleted file mode 100644 index 0542a053d..000000000 --- a/testrunner/test_defs.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/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. - -"""Parser for test definition xml files.""" - -# Python imports -import xml.dom.minidom -import xml.parsers - -# local imports -import errors -import logger - - -class TestDefinitions(object): - """Accessor for a test definitions xml file data. - - Expected format is: - - - - - - TODO: add format checking. - """ - - # tag/attribute constants - _TEST_TAG_NAME = "test" - _TEST_NATIVE_TAG_NAME = "test-native" - - def __init__(self): - # dictionary of test name to tests - self._testname_map = {} - - def __iter__(self): - ordered_list = [] - for k in sorted(self._testname_map): - ordered_list.append(self._testname_map[k]) - return iter(ordered_list) - - def Parse(self, file_path): - """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) - raise errors.ParseError - except xml.parsers.expat.ExpatError: - logger.Log("Error Parsing xml file: %s " % file_path) - raise errors.ParseError - self._ParseDoc(doc) - - def ParseString(self, xml_string): - """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) - - 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) - - suite_elements = doc.getElementsByTagName(self._TEST_NATIVE_TAG_NAME) - - for suite_element in suite_elements: - test = self._ParseNativeTestSuite(suite_element) - self._AddTest(test) - - def _ParseTestSuite(self, suite_element): - """Parse the suite element. - - Returns: - a TestSuite object, populated with parsed data - """ - test = TestSuite(suite_element) - return test - - def _ParseNativeTestSuite(self, suite_element): - """Parse the native test element. - - Returns: - a TestSuite object, populated with parsed data - Raises: - ParseError if some required attribute is missing. - """ - test = TestSuite(suite_element, native=True) - return test - - def _AddTest(self, 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 - - def GetCtsTests(self): - """Return list of cts tests.""" - cts_tests = [] - for test in self.GetTests(): - if test.IsCts(): - cts_tests.append(test) - return cts_tests - - def GetTest(self, name): - 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" - _CTS_ATTR = "cts" - _DESCRIPTION_ATTR = "description" - _EXTRA_MAKE_ARGS_ATTR = "extra_make_args" - - _DEFAULT_RUNNER = "android.test.InstrumentationTestRunner" - - def __init__(self, suite_element, native=False): - """Populates this instance's data from given suite xml element. - Raises: - ParseError if some required attribute is missing. - """ - self._native = native - self._name = suite_element.getAttribute(self._NAME_ATTR) - - if self._native: - # For native runs, _BUILD_ATTR is required - if not suite_element.hasAttribute(self._BUILD_ATTR): - logger.Log("Error: %s is missing required build_path attribute" % - self._name) - raise errors.ParseError - else: - 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 - 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._target_name = suite_element.getAttribute(self._TARGET_ATTR) - else: - self._target_name = None - 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): - self._continuous = suite_element.getAttribute(self._CONTINUOUS_ATTR) - else: - self._continuous = False - if suite_element.hasAttribute(self._CTS_ATTR): - self._cts = suite_element.getAttribute(self._CTS_ATTR) - else: - self._cts = False - - if suite_element.hasAttribute(self._DESCRIPTION_ATTR): - self._description = suite_element.getAttribute(self._DESCRIPTION_ATTR) - else: - self._description = "" - if suite_element.hasAttribute(self._EXTRA_MAKE_ARGS_ATTR): - self._extra_make_args = suite_element.getAttribute( - self._EXTRA_MAKE_ARGS_ATTR) - else: - self._extra_make_args = "" - - 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 for generating code coverage metrics. - """ - return self._target_name - - def GetBuildPath(self): - """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 being part of the continuous tests""" - return self._continuous - - def IsCts(self): - """Returns true if test is part of the compatibility test suite""" - return self._cts - - def IsNative(self): - """Returns true if test is a native one.""" - return self._native - - def GetDescription(self): - """Returns a description if available, an empty string otherwise.""" - return self._description - - def GetExtraMakeArgs(self): - """Returns the extra make args if available, an empty string otherwise.""" - return self._extra_make_args - -def Parse(file_path): - """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 - """ - tests_result = TestDefinitions() - tests_result.Parse(file_path) - return tests_result diff --git a/testrunner/test_defs.xml b/testrunner/test_defs.xml index 08fe0e850..eaaec94b7 100644 --- a/testrunner/test_defs.xml +++ b/testrunner/test_defs.xml @@ -17,72 +17,12 @@ + extra_build_args="BIONIC_TESTS=1" /> + extra_build_args="ASTL_TESTS=1" /> + + + diff --git a/testrunner/test_defs.xsd b/testrunner/test_defs.xsd index f9647795f..65c032fde 100644 --- a/testrunner/test_defs.xsd +++ b/testrunner/test_defs.xsd @@ -1,37 +1,135 @@ + + + + + targetNamespace="http://schemas.android.com/testrunner/test_defs/1.0" + xmlns="http://schemas.android.com/testrunner/test_defs/1.0" + elementFormDefault="qualified"> - - + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + diff --git a/testrunner/test_defs/__init__.py b/testrunner/test_defs/__init__.py new file mode 100644 index 000000000..c205dcb8c --- /dev/null +++ b/testrunner/test_defs/__init__.py @@ -0,0 +1 @@ +__all__ = ['test_defs'] diff --git a/testrunner/test_defs/abstract_test.py b/testrunner/test_defs/abstract_test.py new file mode 100644 index 000000000..7c4d63dde --- /dev/null +++ b/testrunner/test_defs/abstract_test.py @@ -0,0 +1,111 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2009, 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. + +"""Abstract Android test suite.""" + +# Python imports +import xml.dom.minidom +import xml.parsers + +# local imports +import errors +import logger + + +class AbstractTestSuite(object): + """Represents a generic test suite definition parsed from xml. + + This class will parse the XML attributes common to all TestSuite's. + """ + + _NAME_ATTR = "name" + _BUILD_ATTR = "build_path" + _CONTINUOUS_ATTR = "continuous" + _CTS_ATTR = "cts" + _DESCRIPTION_ATTR = "description" + _EXTRA_BUILD_ARGS_ATTR = "extra_build_args" + + def __init__(self): + self._attr_map = {} + + def Parse(self, suite_element): + """Populates this instance's data from given suite xml element. + Raises: + ParseError if a required attribute is missing. + """ + # parse name first so it can be used for error reporting + self._ParseAttribute(suite_element, self._NAME_ATTR, True) + self._ParseAttribute(suite_element, self._BUILD_ATTR, True) + self._ParseAttribute(suite_element, self._CONTINUOUS_ATTR, False, + default_value=False) + self._ParseAttribute(suite_element, self._CTS_ATTR, False, + default_value=False) + self._ParseAttribute(suite_element, self._DESCRIPTION_ATTR, False, + default_value="") + self._ParseAttribute(suite_element, self._EXTRA_BUILD_ARGS_ATTR, False, + default_value="") + + def _ParseAttribute(self, suite_element, attribute_name, mandatory, + default_value=None): + if suite_element.hasAttribute(attribute_name): + self._attr_map[attribute_name] = \ + suite_element.getAttribute(attribute_name) + elif mandatory: + error_msg = ("Could not find attribute %s in %s %s" % + (attribute_name, self.TAG_NAME, self.GetName())) + raise errors.ParseError(msg=error_msg) + else: + self._attr_map[attribute_name] = default_value + + def GetName(self): + return self._GetAttribute(self._NAME_ATTR) + + def GetBuildPath(self): + """Returns the build path of this test, relative to source tree root.""" + return self._GetAttribute(self._BUILD_ATTR) + + def GetBuildDependencies(self, options): + """Returns a list of dependent build paths.""" + return [] + + def IsContinuous(self): + """Returns true if test is flagged as being part of the continuous tests""" + return self._GetAttribute(self._CONTINUOUS_ATTR) + + def IsCts(self): + """Returns true if test is part of the compatibility test suite""" + return self._GetAttribute(self._CTS_ATTR) + + def GetDescription(self): + """Returns a description if available, an empty string otherwise.""" + return self._GetAttribute(self._DESCRIPTION_ATTR) + + def GetExtraBuildArgs(self): + """Returns the extra build args if available, an empty string otherwise.""" + return self._GetAttribute(self._EXTRA_BUILD_ARGS_ATTR) + + def _GetAttribute(self, attribute_name): + return self._attr_map.get(attribute_name) + + def Run(self, options, adb): + """Runs the test. + + Subclasses must implement this. + Args: + options: global command line options + """ + raise NotImplementedError diff --git a/testrunner/test_defs/host_test.py b/testrunner/test_defs/host_test.py new file mode 100644 index 000000000..4aefa3a7b --- /dev/null +++ b/testrunner/test_defs/host_test.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.4 +# +# +# Copyright 2009, 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. + +"""Parser for test definition xml files.""" + +# python imports +import os + +# local imports +from abstract_test import AbstractTestSuite +import errors +import logger +import run_command + + +class HostTestSuite(AbstractTestSuite): + """A test suite for running hosttestlib java tests.""" + + TAG_NAME = "test-host" + + _CLASS_ATTR = "class" + # TODO: consider obsoleting in favor of parsing the Android.mk to find the + # jar name + _JAR_ATTR = "jar_name" + + _JUNIT_JAR_NAME = "junit.jar" + _HOSTTESTLIB_NAME = "hosttestlib.jar" + _DDMLIB_NAME = "ddmlib.jar" + _lib_names = [_JUNIT_JAR_NAME, _HOSTTESTLIB_NAME, _DDMLIB_NAME] + + _JUNIT_BUILD_PATH = os.path.join("external", "junit") + _HOSTTESTLIB_BUILD_PATH = os.path.join("development", "tools", "hosttestlib") + _DDMLIB_BUILD_PATH = os.path.join("development", "tools", "ddms", "libs", + "ddmlib") + _LIB_BUILD_PATHS = [_JUNIT_BUILD_PATH, _HOSTTESTLIB_BUILD_PATH, + _DDMLIB_BUILD_PATH] + + # main class for running host tests + # TODO: should other runners be supported, and make runner an attribute of + # the test suite? + _TEST_RUNNER = "com.android.hosttest.DeviceTestRunner" + + def Parse(self, suite_element): + super(HostTestSuite, self).Parse(suite_element) + self._ParseAttribute(suite_element, self._CLASS_ATTR, True) + self._ParseAttribute(suite_element, self._JAR_ATTR, True) + + def GetBuildDependencies(self, options): + """Override parent to tag on building host libs.""" + return self._LIB_BUILD_PATHS + + def GetClass(self): + return self._GetAttribute(self._CLASS_ATTR) + + def GetJarName(self): + """Returns the name of the host jar that contains the tests.""" + return self._GetAttribute(self._JAR_ATTR) + + def Run(self, options, adb_interface): + """Runs the host test. + + Results will be displayed on stdout. Assumes 'java' is on system path. + + Args: + options: command line options for running host tests. Expected member + fields: + host_lib_path: path to directory that contains host library files + test_data_path: path to directory that contains test data files + preview: if true, do not execute, display commands only + adb_interface: reference to device under test + """ + # get the serial number of the device under test, so it can be passed to + # hosttestlib. + serial_number = adb_interface.GetSerialNumber() + self._lib_names.append(self.GetJarName()) + # gather all the host jars that are needed to run tests + full_lib_paths = [] + for lib in self._lib_names: + path = os.path.join(options.host_lib_path, lib) + # make sure jar file exists on host + if not os.path.exists(path): + raise errors.AbortError(msg="Could not find jar %s" % path) + full_lib_paths.append(path) + + # java -cp -s + # -p + cmd = "java -cp %s %s %s -s %s -p %s" % (":".join(full_lib_paths), + self._TEST_RUNNER, + self.GetClass(), serial_number, + options.test_data_path) + logger.Log(cmd) + if not options.preview: + run_command.RunOnce(cmd, return_output=False) diff --git a/testrunner/test_defs/instrumentation_test.py b/testrunner/test_defs/instrumentation_test.py new file mode 100644 index 000000000..fcc9b42c8 --- /dev/null +++ b/testrunner/test_defs/instrumentation_test.py @@ -0,0 +1,169 @@ +#!/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. + +"""TestSuite definition for Android instrumentation tests.""" + +# python imports +import os + +# local imports +from abstract_test import AbstractTestSuite +import coverage +import errors +import logger + + +class InstrumentationTestSuite(AbstractTestSuite): + """Represents a java instrumentation test suite definition run on Android device.""" + + # for legacy reasons, the xml tag name for java (device) tests is "test: + TAG_NAME = "test" + + _PKG_ATTR = "package" + _RUNNER_ATTR = "runner" + _CLASS_ATTR = "class" + _TARGET_ATTR = "coverage_target" + + _DEFAULT_RUNNER = "android.test.InstrumentationTestRunner" + + # build path to Emma target Makefile + _EMMA_BUILD_PATH = os.path.join("external", "emma") + + def _GetTagName(self): + return self._TAG_NAME + + def GetPackageName(self): + return self._GetAttribute(self._PKG_ATTR) + + def GetRunnerName(self): + return self._GetAttribute(self._RUNNER_ATTR) + + def GetClassName(self): + return self._GetAttribute(self._CLASS_ATTR) + + def GetTargetName(self): + """Retrieve module that this test is targeting. + + Used for generating code coverage metrics. + """ + return self._GetAttribute(self._TARGET_ATTR) + + def GetBuildDependencies(self, options): + if options.coverage: + return [self._EMMA_BUILD_PATH] + return [] + + def Parse(self, suite_element): + super(InstrumentationTestSuite, self).Parse(suite_element) + self._ParseAttribute(suite_element, self._PKG_ATTR, True) + self._ParseAttribute(suite_element, self._RUNNER_ATTR, False, self._DEFAULT_RUNNER) + self._ParseAttribute(suite_element, self._CLASS_ATTR, False) + self._ParseAttribute(suite_element, self._TARGET_ATTR, False) + + def Run(self, options, adb): + """Run the provided test suite. + + Builds up an adb instrument command using provided input arguments. + + Args: + options: command line options to provide to test run + adb: adb_interface to device under test + """ + + test_class = self.GetClassName() + if options.test_class is not None: + test_class = options.test_class.lstrip() + if test_class.startswith("."): + test_class = test_suite.GetPackageName() + test_class + if options.test_method is not None: + test_class = "%s#%s" % (test_class, options.test_method) + + instrumentation_args = {} + if test_class is not None: + instrumentation_args["class"] = test_class + if options.test_package: + instrumentation_args["package"] = options.test_package + if options.test_size: + instrumentation_args["size"] = options.test_size + if options.wait_for_debugger: + instrumentation_args["debug"] = "true" + if options.suite_assign_mode: + instrumentation_args["suiteAssignment"] = "true" + if options.coverage: + instrumentation_args["coverage"] = "true" + if options.preview: + adb_cmd = adb.PreviewInstrumentationCommand( + package_name=self.GetPackageName(), + runner_name=self.GetRunnerName(), + raw_mode=options.raw_mode, + instrumentation_args=instrumentation_args) + logger.Log(adb_cmd) + elif options.coverage: + coverage_gen = coverage.CoverageGenerator(adb) + if not coverage_gen.TestDeviceCoverageSupport(): + raise errors.AbortError + adb.WaitForInstrumentation(self.GetPackageName(), + self.GetRunnerName()) + # need to parse test output to determine path to coverage file + logger.Log("Running in coverage mode, suppressing test output") + try: + (test_results, status_map) = adb.StartInstrumentationForPackage( + package_name=self.GetPackageName(), + runner_name=self.GetRunnerName(), + timeout_time=60*60, + instrumentation_args=instrumentation_args) + except errors.InstrumentationError, errors.DeviceUnresponsiveError: + return + self._PrintTestResults(test_results) + device_coverage_path = status_map.get("coverageFilePath", None) + if device_coverage_path is None: + logger.Log("Error: could not find coverage data on device") + return + + coverage_file = coverage_gen.ExtractReport(self, device_coverage_path) + if coverage_file is not None: + logger.Log("Coverage report generated at %s" % coverage_file) + else: + adb.WaitForInstrumentation(self.GetPackageName(), + self.GetRunnerName()) + adb.StartInstrumentationNoResults( + package_name=self.GetPackageName(), + runner_name=self.GetRunnerName(), + raw_mode=options.raw_mode, + instrumentation_args=instrumentation_args) + + def _PrintTestResults(self, test_results): + """Prints a summary of test result data to stdout. + + Args: + test_results: a list of am_instrument_parser.TestResult + """ + total_count = 0 + error_count = 0 + fail_count = 0 + for test_result in test_results: + if test_result.GetStatusCode() == -1: # error + logger.Log("Error in %s: %s" % (test_result.GetTestName(), + test_result.GetFailureReason())) + error_count+=1 + elif test_result.GetStatusCode() == -2: # failure + logger.Log("Failure in %s: %s" % (test_result.GetTestName(), + test_result.GetFailureReason())) + fail_count+=1 + total_count+=1 + logger.Log("Tests run: %d, Failures: %d, Errors: %d" % + (total_count, fail_count, error_count)) diff --git a/testrunner/test_defs/native_test.py b/testrunner/test_defs/native_test.py new file mode 100644 index 000000000..1e79872ff --- /dev/null +++ b/testrunner/test_defs/native_test.py @@ -0,0 +1,153 @@ +#!/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. + +"""TestSuite for running native Android tests.""" + +# python imports +import os + +# local imports +from abstract_test import AbstractTestSuite +import android_build +import errors +import logger +import run_command + + +class NativeTestSuite(AbstractTestSuite): + """A test suite for running native aka C/C++ tests on device.""" + + TAG_NAME = "test-native" + + def _GetTagName(self): + return self._TAG_NAME + + def Parse(self, suite_element): + super(NativeTestSuite, self).Parse(suite_element) + + + def Run(self, options, adb): + """Run the provided *native* test suite. + + The test_suite must contain a build path where the native test + files are. Subdirectories are automatically scanned as well. + + Each test's name must have a .cc or .cpp extension and match one + of the following patterns: + - test_* + - *_test.[cc|cpp] + - *_unittest.[cc|cpp] + A successful test must return 0. Any other value will be considered + as an error. + + Args: + options: command line options + adb: adb interface + """ + # find all test files, convert unicode names to ascii, take the basename + # and drop the .cc/.cpp extension. + source_list = [] + build_path = self.GetBuildPath() + os.path.walk(build_path, self._CollectTestSources, source_list) + logger.SilentLog("Tests source %s" % source_list) + + # Host tests are under out/host/-/bin. + host_list = self._FilterOutMissing(android_build.GetHostBin(), source_list) + logger.SilentLog("Host tests %s" % host_list) + + # Target tests are under $ANDROID_PRODUCT_OUT/system/bin. + target_list = self._FilterOutMissing(android_build.GetTargetSystemBin(), + source_list) + logger.SilentLog("Target tests %s" % target_list) + + # Run on the host + logger.Log("\nRunning on host") + for f in host_list: + if run_command.RunHostCommand(f) != 0: + logger.Log("%s... failed" % f) + else: + if run_command.HasValgrind(): + if run_command.RunHostCommand(f, valgrind=True) == 0: + logger.Log("%s... ok\t\t[valgrind: ok]" % f) + else: + logger.Log("%s... ok\t\t[valgrind: failed]" % f) + else: + logger.Log("%s... ok\t\t[valgrind: missing]" % f) + + # Run on the device + logger.Log("\nRunning on target") + for f in target_list: + full_path = os.path.join(os.sep, "system", "bin", f) + + # Single quotes are needed to prevent the shell splitting it. + output = self._adb.SendShellCommand("'%s 2>&1;echo -n exit code:$?'" % + full_path, + int(self._options.timeout)) + success = output.endswith("exit code:0") + logger.Log("%s... %s" % (f, success and "ok" or "failed")) + # Print the captured output when the test failed. + if not success or options.verbose: + pos = output.rfind("exit code") + output = output[0:pos] + logger.Log(output) + + # Cleanup + adb.SendShellCommand("rm %s" % full_path) + + def _CollectTestSources(self, test_list, dirname, files): + """For each directory, find tests source file and add them to the list. + + Test files must match one of the following pattern: + - test_*.[cc|cpp] + - *_test.[cc|cpp] + - *_unittest.[cc|cpp] + + This method is a callback for os.path.walk. + + Args: + test_list: Where new tests should be inserted. + dirname: Current directory. + files: List of files in the current directory. + """ + for f in files: + (name, ext) = os.path.splitext(f) + if ext == ".cc" or ext == ".cpp": + if re.search("_test$|_test_$|_unittest$|_unittest_$|^test_", name): + logger.SilentLog("Found %s" % f) + test_list.append(str(os.path.join(dirname, f))) + + def _FilterOutMissing(self, path, sources): + """Filter out from the sources list missing tests. + + Sometimes some test source are not built for the target, i.e there + is no binary corresponding to the source file. We need to filter + these out. + + Args: + path: Where the binaries should be. + sources: List of tests source path. + Returns: + A list of test binaries built from the sources. + """ + binaries = [] + for f in sources: + binary = os.path.basename(f) + binary = os.path.splitext(binary)[0] + full_path = os.path.join(path, binary) + if os.path.exists(full_path): + binaries.append(binary) + return binaries diff --git a/testrunner/test_defs/test_defs.py b/testrunner/test_defs/test_defs.py new file mode 100644 index 000000000..7f23b8958 --- /dev/null +++ b/testrunner/test_defs/test_defs.py @@ -0,0 +1,146 @@ +#!/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. + +"""Parser for test definition xml files.""" + +# Python imports +import xml.dom.minidom +import xml.parsers + +# local imports +import errors +import logger +from instrumentation_test import InstrumentationTestSuite +from native_test import NativeTestSuite +from host_test import HostTestSuite + + +class TestDefinitions(object): + """Accessor for a test definitions xml file data. + + See test_defs.xsd for expected format. + """ + + def __init__(self): + # dictionary of test name to tests + self._testname_map = {} + + def __iter__(self): + ordered_list = [] + for k in sorted(self._testname_map): + ordered_list.append(self._testname_map[k]) + return iter(ordered_list) + + def Parse(self, file_path): + """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) + self._ParseDoc(doc) + except IOError: + 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) + raise errors.ParseError + except errors.ParseError, e: + logger.Log("Error Parsing xml file: %s Reason: %s" % (file_path, e.msg)) + raise e + + def ParseString(self, xml_string): + """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) + + def _ParseDoc(self, doc): + root_element = self._GetRootElement(doc) + for element in root_element.childNodes: + if element.nodeType != xml.dom.Node.ELEMENT_NODE: + continue + test_suite = None + if element.nodeName == InstrumentationTestSuite.TAG_NAME: + test_suite = InstrumentationTestSuite() + elif element.nodeName == NativeTestSuite.TAG_NAME: + test_suite = NativeTestSuite() + elif element.nodeName == HostTestSuite.TAG_NAME: + test_suite = HostTestSuite() + else: + logger.Log("Unrecognized tag %s found" % element.nodeName) + continue + test_suite.Parse(element) + self._AddTest(test_suite) + + def _GetRootElement(self, doc): + root_elements = doc.getElementsByTagName("test-definitions") + if len(root_elements) != 1: + error_msg = "expected 1 and only one test-definitions tag" + raise errors.ParseError(msg=error_msg) + return root_elements[0] + + def _AddTest(self, test): + """Adds a test to this TestManifest. + + If a test already exists with the same name, it overrides it. + + Args: + test: TestSuite to add + """ + if self.GetTest(test.GetName()) is not None: + logger.Log("Overriding test definition %s" % test.GetName()) + 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 + + def GetCtsTests(self): + """Return list of cts tests.""" + cts_tests = [] + for test in self.GetTests(): + if test.IsCts(): + cts_tests.append(test) + return cts_tests + + def GetTest(self, name): + return self._testname_map.get(name, None) + + +def Parse(file_path): + """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 + """ + tests_result = TestDefinitions() + tests_result.Parse(file_path) + return tests_result