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