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.
diff --git a/testrunner/test_defs/__init__.py b/testrunner/test_defs/__init__.py
new file mode 100644
index 0000000..c205dcb
--- /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 0000000..7c4d63d
--- /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 0000000..4aefa3a
--- /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 <libs> <runner class> <test suite class> -s <device serial>
+    # -p <test data path>
+    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 0000000..fcc9b42
--- /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 0000000..1e79872
--- /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/<os>-<arch>/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 0000000..7f23b89
--- /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