Add gtest target support for runtest --path.

This is a rough initial version. It makes several assumptions:
 - assumes each test source file is built into module with name equal
to basename of source file
 - assumes tests get installed into /data/nativetest

However, given those assumptions, both:
$ runtest --path <path to directory containing gtest source> and
$ runtest --path <path to single source file>
should work.

Coming soon: gtest host support

Bugs 4563370, 4584339

Change-Id: Ia42aeed7f9ee6402b0ceb7b5ccaaa66ac636fe49
diff --git a/testrunner/adb_interface.py b/testrunner/adb_interface.py
index feb1c89..ddc50d5 100755
--- a/testrunner/adb_interface.py
+++ b/testrunner/adb_interface.py
@@ -263,8 +263,10 @@
     inst_command_string = self._BuildInstrumentationCommand(
         package_name, runner_name, no_window_animation=no_window_animation,
         raw_mode=raw_mode, instrumentation_args=instrumentation_args)
-    command_string = "adb %s shell %s" % (self._target_arg, inst_command_string)
-    return command_string
+    return self.PreviewShellCommand(inst_command_string)
+
+  def PreviewShellCommand(self, cmd):
+    return "adb %s shell %s" % (self._target_arg, cmd)
 
   def _BuildInstrumentationCommand(
       self, package, runner_name, no_window_animation=False, profile=False,
diff --git a/testrunner/android_mk.py b/testrunner/android_mk.py
index 663aa5c..2c265ee 100644
--- a/testrunner/android_mk.py
+++ b/testrunner/android_mk.py
@@ -21,25 +21,26 @@
 development/ndk/docs/ANDROID-MK.txt
 """
 
+import os
 import re
 from sets import Set
 
+import logger
 
 class AndroidMK(object):
   """In memory representation of Android.mk file."""
 
   _RE_INCLUDE = re.compile(r'include\s+\$\((.+)\)')
+  _RE_VARIABLE_REF = re.compile(r'\$\((.+)\)')
   _VAR_DELIMITER = ":="
   FILENAME = "Android.mk"
   CERTIFICATE = "LOCAL_CERTIFICATE"
   PACKAGE_NAME = "LOCAL_PACKAGE_NAME"
 
-  def __init__(self, app_path=None):
+  def __init__(self):
     self._includes = Set() # variables included in makefile
     self._variables = {} # variables defined in makefile
-
-    if app_path:
-      self.ParseMK(app_path)
+    self._has_gtestlib = False
 
   def _ProcessMKLine(self, line):
     """Add a variable definition or include.
@@ -56,6 +57,9 @@
       parts = line.split(self._VAR_DELIMITER)
       if len(parts) > 1:
         self._variables[parts[0].strip()] = parts[1].strip()
+    # hack, look for explicit mention of libgtest_main
+    if line.find('libgtest_main') != -1:
+      self._has_gtestlib = True
 
   def GetVariable(self, identifier):
     """Retrieve makefile variable.
@@ -69,6 +73,38 @@
     # so None is returned if identifier not found
     return self._variables.get(identifier, None)
 
+  def GetExpandedVariable(self, identifier):
+    """Retrieve makefile variable.
+
+    If variable value refers to another variable, recursively expand it to
+    find its literal value
+
+    Args:
+      identifier: name of variable to retrieve
+    Returns:
+      value of specified identifier, None if identifier not found in makefile
+    """
+    # use dict.get(x) rather than dict[x] to avoid KeyError exception,
+    # so None is returned if identifier not found
+    return self.__RecursiveGetVariable(identifier, Set())
+
+  def __RecursiveGetVariable(self, identifier, visited_variables):
+    variable_value = self.GetVariable(identifier)
+    if not variable_value:
+      return None
+    if variable_value in visited_variables:
+      raise RuntimeError('recursive loop found for makefile variable %s'
+                         % variable_value)
+    m = self._RE_VARIABLE_REF.match(variable_value)
+    if m:
+      logger.SilentLog('Found variable ref %s for identifier %s'
+                       % (variable_value, identifier))
+      variable_ref = m.group(1)
+      visited_variables.add(variable_ref)
+      return self.__RecursiveGetVariable(variable_ref, visited_variables)
+    else:
+      return variable_value
+
   def HasInclude(self, identifier):
     """Check variable is included in makefile.
 
@@ -79,18 +115,57 @@
     """
     return identifier in self._includes
 
-  def ParseMK(self, app_path):
+  def HasJavaLibrary(self, library_name):
+    """Check if library is specified as a local java library in makefile.
+
+    Args:
+      library_name: name of library to check
+    Returns:
+      True if library_name is included in makefile, otherwise False
+    """
+    java_lib_string = self.GetExpandedVariable('LOCAL_JAVA_LIBRARIES')
+    if java_lib_string:
+      java_libs = java_lib_string.split(' ')
+      return library_name in java_libs
+    return False
+
+  def HasGTest(self):
+    """Check if makefile includes rule to build a native gtest.
+
+    Returns:
+      True if rule to build native test is in makefile, otherwise False
+    """
+    return self._has_gtestlib or self.HasInclude('BUILD_NATIVE_TEST')
+
+  def _ParseMK(self, mk_path):
     """Parse Android.mk at the specified path.
 
     Args:
-      app_path: path to folder containing Android.mk
+      mk_path: path to Android.mk
     Raises:
       IOError: Android.mk cannot be found at given path, or cannot be opened
           for reading
     """
-    self.app_path = app_path.rstrip("/")
-    self.mk_path = "%s/%s" % (self.app_path, self.FILENAME)
-    mk = open(self.mk_path)
+    mk = open(mk_path)
     for line in mk:
       self._ProcessMKLine(line)
     mk.close()
+
+
+def CreateAndroidMK(path, filename=AndroidMK.FILENAME):
+  """Factory method for creating a AndroidMK.
+
+  Args:
+    path: the directory of the make file
+    filename: the filename of the makefile
+
+  Return:
+    the AndroidMK or None if there was no file present
+  """
+  mk_path = os.path.join(path, filename)
+  if os.path.isfile(mk_path):
+    mk = AndroidMK()
+    mk._ParseMK(mk_path)
+    return mk
+  else:
+    return None
\ No newline at end of file
diff --git a/testrunner/test_defs/gtest.py b/testrunner/test_defs/gtest.py
new file mode 100644
index 0000000..7de674b
--- /dev/null
+++ b/testrunner/test_defs/gtest.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+#
+#
+# Copyright 2011, 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 C/C++ Android tests using gtest framework."""
+
+# python imports
+import os
+import re
+
+# local imports
+import logger
+import run_command
+import test_suite
+
+
+class GTestSuite(test_suite.AbstractTestSuite):
+  """A test suite for running gtest on device."""
+
+  def __init__(self):
+    test_suite.AbstractTestSuite.__init__(self)
+    self._target_exec_path = None
+
+  def GetTargetExecPath(self):
+    """Get the target path to gtest executable."""
+    return self._target_exec_path
+
+  def SetTargetExecPath(self, path):
+    self._target_exec_path = path
+    return self
+
+  def Run(self, options, adb):
+    """Run the provided gtest test suite.
+
+    Args:
+      options: command line options
+      adb: adb interface
+    """
+    shell_cmd = adb.PreviewShellCommand(self.GetTargetExecPath())
+    logger.Log(shell_cmd)
+    if not options.preview:
+      # gtest will log to test results to stdout, so no need to do any
+      # extra processing
+      run_command.RunCommand(shell_cmd, return_output=False)
+
+
+class GTestFactory(test_suite.AbstractTestFactory):
+
+  def __init__(self, test_root_path, upstream_build_path=None):
+    test_suite.AbstractTestFactory.__init__(self, test_root_path,
+        upstream_build_path=upstream_build_path)
+
+  def CreateTests(self, sub_tests_path=None):
+    """Create tests found in sub_tests_path.
+
+    Looks for test files matching a pattern, and assumes each one is a separate
+    binary on target.
+
+    Test files must match one of the following pattern:
+      - test_*.[c|cc|cpp]
+      - *_test.[c|cc|cpp]
+      - *_unittest.[c|cc|cpp]
+
+    """
+    if not sub_tests_path:
+      sub_tests_path = self.GetTestRootPath()
+    test_file_list = []
+    if os.path.isfile(sub_tests_path):
+      self._EvaluateFile(test_file_list, os.path.basename(sub_tests_path))
+    else:
+      os.path.walk(sub_tests_path, self._CollectTestSources, test_file_list)
+    # TODO: obtain this from makefile instead of hardcoding
+    target_root_path = os.path.join('/data', 'nativetest')
+    test_suites = []
+    for test_file in test_file_list:
+      logger.SilentLog('Creating gtest suite for file %s' % test_file)
+      suite = GTestSuite()
+      suite.SetBuildPath(self.GetBuildPath())
+      suite.SetTargetExecPath(os.path.join(target_root_path, test_file))
+      test_suites.append(suite)
+    return test_suites
+
+  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:
+      self._EvaluateFile(test_list, f)
+
+  def _EvaluateFile(self, test_list, file):
+    (name, ext) = os.path.splitext(file)
+    if ext == ".cc" or ext == ".cpp" or ext == ".c":
+      if re.search("_test$|_test_$|_unittest$|_unittest_$|^test_", name):
+        logger.SilentLog("Found native test file %s" % file)
+        test_list.append(name)
diff --git a/testrunner/test_defs/instrumentation_test.py b/testrunner/test_defs/instrumentation_test.py
index 40d6bed..64182a4 100644
--- a/testrunner/test_defs/instrumentation_test.py
+++ b/testrunner/test_defs/instrumentation_test.py
@@ -17,7 +17,11 @@
 
 """TestSuite definition for Android instrumentation tests."""
 
+import os
+import re
+
 # local imports
+import android_manifest
 import coverage
 import errors
 import logger
@@ -193,3 +197,143 @@
       total_count+=1
     logger.Log("Tests run: %d, Failures: %d, Errors: %d" %
                (total_count, fail_count, error_count))
+
+
+class InstrumentationTestFactory(test_suite.AbstractTestFactory):
+  """A factory for creating InstrumentationTestSuites"""
+
+  def __init__(self, test_root_path, upstream_build_path=None):
+    test_suite.AbstractTestFactory.__init__(self, test_root_path,
+                                            upstream_build_path=upstream_build_path)
+
+  def CreateTests(self, sub_tests_path=None):
+    """Create tests found in test_path.
+
+    Will create a single InstrumentationTestSuite based on info found in
+    AndroidManifest.xml found at build_path. Will set additional filters if
+    test_path refers to a java package or java class.
+    """
+    tests = []
+    class_name_arg = None
+    java_package_name = None
+    if sub_tests_path:
+      # if path is java file, populate class name
+      if self._IsJavaFile(sub_tests_path):
+        class_name_arg = self._GetClassNameFromFile(sub_tests_path)
+        logger.SilentLog('Using java test class %s' % class_name_arg)
+      elif self._IsJavaPackage(sub_tests_path):
+        java_package_name = self._GetPackageNameFromDir(sub_tests_path)
+        logger.SilentLog('Using java package %s' % java_package_name)
+    try:
+      manifest_parser = android_manifest.AndroidManifest(app_path=
+                                                         self.GetTestsRootPath())
+      instrs = manifest_parser.GetInstrumentationNames()
+      if not instrs:
+        logger.Log('Could not find instrumentation declarations in %s at %s' %
+                   (android_manifest.AndroidManifest.FILENAME,
+                    self.GetBuildPath()))
+        return tests
+
+      for instr_name in manifest_parser.GetInstrumentationNames():
+        pkg_name = manifest_parser.GetPackageName()
+        if instr_name.find(".") < 0:
+          instr_name = "." + instr_name
+        logger.SilentLog('Found instrumentation %s/%s' % (pkg_name, instr_name))
+        suite = InstrumentationTestSuite()
+        suite.SetPackageName(pkg_name)
+        suite.SetBuildPath(self.GetBuildPath())
+        suite.SetRunnerName(instr_name)
+        suite.SetName(pkg_name)
+        suite.SetClassName(class_name_arg)
+        suite.SetJavaPackageFilter(java_package_name)
+        # this is a bit of a hack, assume if 'com.android.cts' is in
+        # package name, this is a cts test
+        # this logic can be removed altogether when cts tests no longer require
+        # custom build steps
+        if suite.GetPackageName().startswith('com.android.cts'):
+          suite.SetSuite('cts')
+        tests.append(suite)
+      return tests
+
+    except:
+      logger.Log('Could not find or parse %s at %s' %
+                 (android_manifest.AndroidManifest.FILENAME,
+                  self.GetBuildPath()))
+    return tests
+
+  def _IsJavaFile(self, path):
+    """Returns true if given file system path is a java file."""
+    return os.path.isfile(path) and self._IsJavaFileName(path)
+
+  def _IsJavaFileName(self, filename):
+    """Returns true if given file name is a java file name."""
+    return os.path.splitext(filename)[1] == '.java'
+
+  def _IsJavaPackage(self, path):
+    """Returns true if given file path is a java package.
+
+    Currently assumes if any java file exists in this directory, than it
+    represents a java package.
+
+    Args:
+      path: file system path of directory to check
+
+    Returns:
+      True if path is a java package
+    """
+    if not os.path.isdir(path):
+      return False
+    for file_name in os.listdir(path):
+      if self._IsJavaFileName(file_name):
+        return True
+    return False
+
+  def _GetClassNameFromFile(self, java_file_path):
+    """Gets the fully qualified java class name from path.
+
+    Args:
+      java_file_path: file system path of java file
+
+    Returns:
+      fully qualified java class name or None.
+    """
+    package_name = self._GetPackageNameFromFile(java_file_path)
+    if package_name:
+      filename = os.path.basename(java_file_path)
+      class_name = os.path.splitext(filename)[0]
+      return '%s.%s' % (package_name, class_name)
+    return None
+
+  def _GetPackageNameFromDir(self, path):
+    """Gets the java package name associated with given directory path.
+
+    Caveat: currently just parses defined java package name from first java
+    file found in directory.
+
+    Args:
+      path: file system path of directory
+
+    Returns:
+      the java package name or None
+    """
+    for filename in os.listdir(path):
+      if self._IsJavaFileName(filename):
+        return self._GetPackageNameFromFile(os.path.join(path, filename))
+
+  def _GetPackageNameFromFile(self, java_file_path):
+    """Gets the java package name associated with given java file path.
+
+    Args:
+      java_file_path: file system path of java file
+
+    Returns:
+      the java package name or None
+    """
+    logger.SilentLog('Looking for java package name in %s' % java_file_path)
+    re_package = re.compile(r'package\s+(.*);')
+    file_handle = open(java_file_path, 'r')
+    for line in file_handle:
+      match = re_package.match(line)
+      if match:
+        return match.group(1)
+    return None
diff --git a/testrunner/test_defs/test_suite.py b/testrunner/test_defs/test_suite.py
index 102a738..20f9629 100644
--- a/testrunner/test_defs/test_suite.py
+++ b/testrunner/test_defs/test_suite.py
@@ -105,3 +105,41 @@
       adb: asdb_interface to device under test
     """
     raise NotImplementedError
+
+class AbstractTestFactory(object):
+  """generic test suite factory."""
+
+  def __init__(self, test_root_path, upstream_build_path=None):
+    """Creates a test suite factory.
+
+    Args:
+      test_root_path: the filesystem path to the tests build directory
+      upstream_build_path: optional filesystem path for the directory
+      to build when running tests. If unspecified, will use test_root_path
+    """
+    self._test_root_path = test_root_path
+    if upstream_build_path:
+      self._build_path = upstream_build_path
+    else:
+      self._build_path = self._test_root_path
+
+  def GetBuildPath(self):
+    return self._build_path
+
+  def GetTestsRootPath(self):
+    return self._test_root_path
+
+  def CreateTests(self, sub_tests_path=None):
+    """Creates the tests at given test_path.
+
+    Subclasses must implement this.
+
+    Args:
+      sub_tests_path: the child path of test_root_path containing the tests to
+        run. If unspecified will be set to test_root_path.
+
+    Returns:
+      an array of AbstractTestSuite, or empty AbstractTestSuite if no tests
+      were defined
+    """
+    raise NotImplementedError
diff --git a/testrunner/test_defs/test_walker.py b/testrunner/test_defs/test_walker.py
index e34dafc..de93c80 100755
--- a/testrunner/test_defs/test_walker.py
+++ b/testrunner/test_defs/test_walker.py
@@ -19,27 +19,31 @@
 
 # python imports
 import os
-import re
 
 # local imports
 import android_build
-import android_manifest
 import android_mk
+import gtest
 import instrumentation_test
 import logger
 
 
 class TestWalker(object):
-  """Finds instrumentation tests from filesystem."""
+  """Finds Android tests from filesystem."""
 
   def FindTests(self, path):
-    """Gets list of Android instrumentation tests found at given path.
+    """Gets list of Android tests found at given path.
 
-    Tests are created from the <instrumentation> tags found in
-    AndroidManifest.xml files relative to the given path.
+    Tests are created from info found in Android.mk and AndroidManifest.xml
+    files relative to the given path.
+
+    Currently supported tests are:
+    - Android application tests run via instrumentation
+    - native C/C++ tests using GTest framework. (note Android.mk must follow
+      expected GTest template)
 
     FindTests will first scan sub-folders of path for tests. If none are found,
-    it will scan the file system upwards until a AndroidManifest.xml is found
+    it will scan the file system upwards until a valid test Android.mk is found
     or the Android build root is reached.
 
     Some sample values for path:
@@ -55,6 +59,8 @@
     the instrumentation in ApiDemos/tests, with the java package filter set
     to com.example.android.apis.
 
+    TODO: add GTest examples
+
     Args:
       path: file system path to search
 
@@ -110,204 +116,143 @@
     # return string with common build_path removed
     return path[build_path_len:]
 
-  def _FindSubTests(self, path, tests, build_path=None):
+  def _FindSubTests(self, path, tests, upstream_build_path=None):
     """Recursively finds all tests within given path.
 
     Args:
       path: absolute file system path to check
       tests: current list of found tests
-      build_path: the parent directory where Android.mk that builds sub-folders
-        was found
+      upstream_build_path: the parent directory where Android.mk that builds
+        sub-folders was found
 
     Returns:
       updated list of tests
     """
     if not os.path.isdir(path):
       return tests
-    filenames = os.listdir(path)
-    if filenames.count(android_manifest.AndroidManifest.FILENAME):
-      # found a manifest! now parse it to find the test definition(s)
-      manifest = android_manifest.AndroidManifest(app_path=path)
-      if not build_path:
+    android_mk_parser = android_mk.CreateAndroidMK(path)
+    if android_mk_parser:
+      if not upstream_build_path:
         # haven't found a parent makefile which builds this dir. Use current
         # dir as build path
-        tests.extend(self._CreateSuitesFromManifest(
-            manifest, self._MakePathRelativeToBuild(path)))
+        tests.extend(self._CreateSuites(
+            android_mk_parser, path, self._MakePathRelativeToBuild(path)))
       else:
-        tests.extend(self._CreateSuitesFromManifest(manifest, build_path))
-    # Try to build as much of original path as possible, so
-    # keep track of upper-most parent directory where Android.mk was found that
-    # has rule to build sub-directory makefiles
-    # this is also necessary in case of overlapping tests
-    # ie if a test exists at 'foo' directory  and 'foo/sub', attempting to
-    # build both 'foo' and 'foo/sub' will fail.
-    if filenames.count(android_mk.AndroidMK.FILENAME):
-      android_mk_parser = android_mk.AndroidMK(app_path=path)
+        tests.extend(self._CreateSuites(android_mk_parser, path,
+                                        upstream_build_path))
+      # Try to build as much of original path as possible, so
+      # keep track of upper-most parent directory where Android.mk was found
+      # that has rule to build sub-directory makefiles.
+      # this is also necessary in case of overlapping tests
+      # ie if a test exists at 'foo' directory  and 'foo/sub', attempting to
+      # build both 'foo' and 'foo/sub' will fail.
+
       if android_mk_parser.HasInclude('call all-makefiles-under,$(LOCAL_PATH)'):
-        # found rule to build sub-directories. The parent path can be used, 
+        # found rule to build sub-directories. The parent path can be used,
         # or if not set, use current path
-        if not build_path:
-          build_path = self._MakePathRelativeToBuild(path)
+        if not upstream_build_path:
+          upstream_build_path = self._MakePathRelativeToBuild(path)
       else:
-        build_path = None
-    for filename in filenames:
-      self._FindSubTests(os.path.join(path, filename), tests, build_path)
+        upstream_build_path = None
+    for filename in os.listdir(path):
+      self._FindSubTests(os.path.join(path, filename), tests,
+                         upstream_build_path)
     return tests
 
   def _FindUpstreamTests(self, path):
     """Find tests defined upward from given path.
 
     Args:
-      path: the location to start searching. If it points to a java class file
-        or java package dir, the appropriate test suite filters will be set
+      path: the location to start searching.
 
     Returns:
       list of test_suite.AbstractTestSuite found, may be empty
     """
-    class_name_arg = None
-    package_name = None
-    # if path is java file, populate class name
-    if self._IsJavaFile(path):
-      class_name_arg = self._GetClassNameFromFile(path)
-      logger.SilentLog('Using java test class %s' % class_name_arg)
-    elif self._IsJavaPackage(path):
-      package_name = self._GetPackageNameFromDir(path)
-      logger.SilentLog('Using java package %s' % package_name)
-    manifest = self._FindUpstreamManifest(path)
-    if manifest:
-      logger.SilentLog('Found AndroidManifest at %s' % manifest.GetAppPath())
-      build_path = self._MakePathRelativeToBuild(manifest.GetAppPath())
-      return self._CreateSuitesFromManifest(manifest,
-                                            build_path,
-                                            class_name=class_name_arg,
-                                            java_package_name=package_name)
+    factory = self._FindUpstreamTestFactory(path)
+    if factory:
+      return factory.CreateTests(sub_tests_path=path)
+    else:
+      return []
 
-  def _IsJavaFile(self, path):
-    """Returns true if given file system path is a java file."""
-    return os.path.isfile(path) and self._IsJavaFileName(path)
+  def _GetTestFactory(self, android_mk_parser, path, upstream_build_path=None):
+    """Get the test factory for given makefile.
 
-  def _IsJavaFileName(self, filename):
-    """Returns true if given file name is a java file name."""
-    return os.path.splitext(filename)[1] == '.java'
-
-  def _IsJavaPackage(self, path):
-    """Returns true if given file path is a java package.
-
-    Currently assumes if any java file exists in this directory, than it
-    represents a java package.
+    If given path is a valid tests build path, will return the TestFactory
+    for creating tests.
 
     Args:
-      path: file system path of directory to check
+      android_mk_parser: the android mk to evaluate
+      path: the filesystem path of the makefile
+      upstream_build_path: optional filesystem path for the directory
+      to build when running tests. If unspecified, will use path
 
     Returns:
-      True if path is a java package
+      the TestFactory or None if path is not a valid tests build path
     """
-    if not os.path.isdir(path):
-      return False
-    for file_name in os.listdir(path):
-      if self._IsJavaFileName(file_name):
-        return True
-    return False
+    if android_mk_parser.HasGTest():
+      return gtest.GTestFactory(path, upstream_build_path=upstream_build_path)
+    elif android_mk_parser.HasJavaLibrary('android.test.runner'):
+      return instrumentation_test.InstrumentationTestFactory(path,
+          upstream_build_path=upstream_build_path)
+    else:
+      # somewhat unusual, but will continue searching
+      logger.SilentLog('Found makefile at %s, but did not detect any tests.'
+                       % path)
 
-  def _GetClassNameFromFile(self, java_file_path):
-    """Gets the fully qualified java class name from path.
-
-    Args:
-      java_file_path: file system path of java file
-
-    Returns:
-      fully qualified java class name or None.
-    """
-    package_name = self._GetPackageNameFromFile(java_file_path)
-    if package_name:
-      filename = os.path.basename(java_file_path)
-      class_name = os.path.splitext(filename)[0]
-      return '%s.%s' % (package_name, class_name)
     return None
 
-  def _GetPackageNameFromDir(self, path):
-    """Gets the java package name associated with given directory path.
+  def _GetTestFactoryForPath(self, path):
+    """Get the test factory for given path.
 
-    Caveat: currently just parses defined java package name from first java
-    file found in directory.
+    If given path is a valid tests build path, will return the TestFactory
+    for creating tests.
 
     Args:
-      path: file system path of directory
+      path: the filesystem path to evaluate
 
     Returns:
-      the java package name or None
+      the TestFactory or None if path is not a valid tests build path
     """
-    for filename in os.listdir(path):
-      if self._IsJavaFileName(filename):
-        return self._GetPackageNameFromFile(os.path.join(path, filename))
+    android_mk_parser = android_mk.CreateAndroidMK(path)
+    if android_mk_parser:
+      return self._GetTestFactory(android_mk_parser, path)
+    else:
+      return None
 
-  def _GetPackageNameFromFile(self, java_file_path):
-    """Gets the java package name associated with given java file path.
-
-    Args:
-      java_file_path: file system path of java file
-
-    Returns:
-      the java package name or None
-    """
-    logger.SilentLog('Looking for java package name in %s' % java_file_path)
-    re_package = re.compile(r'package\s+(.*);')
-    file_handle = open(java_file_path, 'r')
-    for line in file_handle:
-      match = re_package.match(line)
-      if match:
-        return match.group(1)
-    return None
-
-  def _FindUpstreamManifest(self, path):
-    """Recursively searches filesystem upwards for a AndroidManifest file.
+  def _FindUpstreamTestFactory(self, path):
+    """Recursively searches filesystem upwards for a test factory.
 
     Args:
       path: file system path to search
 
     Returns:
-      the AndroidManifest found or None
+      the TestFactory found or None
     """
-    if (os.path.isdir(path) and
-        os.listdir(path).count(android_manifest.AndroidManifest.FILENAME)):
-      return android_manifest.AndroidManifest(app_path=path)
+    factory = self._GetTestFactoryForPath(path)
+    if factory:
+      return factory
     dirpath = os.path.dirname(path)
     if self._IsPathInBuildTree(path):
-      return self._FindUpstreamManifest(dirpath)
-    logger.Log('AndroidManifest.xml not found')
+      return self._FindUpstreamTestFactory(dirpath)
+    logger.Log('A tests Android.mk was not found')
     return None
 
-  def _CreateSuitesFromManifest(self, manifest, build_path, class_name=None,
-                                java_package_name=None):
-    """Creates TestSuites from a AndroidManifest.
+  def _CreateSuites(self, android_mk_parser, path, upstream_build_path):
+    """Creates TestSuites from a AndroidMK.
 
     Args:
-      manifest: the AndroidManifest
-      build_path: the build path to use for test
-      class_name: optionally, the class filter for the suite
-      java_package_name: optionally, the java package filter for the suite
+      android_mk_parser: the AndroidMK
+      path: absolute file system path of the makefile to evaluate
+      upstream_build_path: the build path to use for test. This can be
+        different than the 'path', in cases where an upstream makefile
+        is being used.
 
     Returns:
       the list of tests created
     """
-    tests = []
-    for instr_name in manifest.GetInstrumentationNames():
-      pkg_name = manifest.GetPackageName()
-      if instr_name.find(".") < 0:
-        instr_name = "." + instr_name
-      logger.SilentLog('Found instrumentation %s/%s' % (pkg_name, instr_name))
-      suite = instrumentation_test.InstrumentationTestSuite()
-      suite.SetPackageName(pkg_name)
-      suite.SetBuildPath(build_path)
-      suite.SetRunnerName(instr_name)
-      suite.SetName(pkg_name)
-      suite.SetClassName(class_name)
-      suite.SetJavaPackageFilter(java_package_name)
-      # this is a bit of a hack, assume if 'com.android.cts' is in
-      # package name, this is a cts test
-      # this logic can be removed altogether when cts tests no longer require
-      # custom build steps
-      if suite.GetPackageName().startswith('com.android.cts'):
-        suite.SetSuite('cts')
-      tests.append(suite)
-    return tests
+    factory = self._GetTestFactory(android_mk_parser, path,
+                                   upstream_build_path=upstream_build_path)
+    if factory:
+      return factory.CreateTests(path)
+    else:
+      return []
diff --git a/testrunner/tests/Android_gtestlib.mk b/testrunner/tests/Android_gtestlib.mk
new file mode 100644
index 0000000..0d3ec8f
--- /dev/null
+++ b/testrunner/tests/Android_gtestlib.mk
@@ -0,0 +1,56 @@
+# Copyright (C) 2011 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.
+
+# Build the unit tests.
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+ifneq ($(TARGET_SIMULATOR),true)
+
+# Build the unit tests.
+test_src_files := $(call find-subdir-files, *.cpp)
+
+shared_libraries := \
+        libz \
+        liblog \
+        libcutils \
+        libutils \
+        libstlport
+
+static_libraries := \
+        libgtest \
+        libgtest_main
+
+c_includes := \
+    external/zlib \
+    external/icu4c/common \
+    bionic \
+    bionic/libstdc++/include \
+    external/gtest/include \
+    external/stlport/stlport
+
+module_tags := eng tests
+
+$(foreach file,$(test_src_files), \
+    $(eval include $(CLEAR_VARS)) \
+    $(eval LOCAL_SHARED_LIBRARIES := $(shared_libraries)) \
+    $(eval LOCAL_STATIC_LIBRARIES := $(static_libraries)) \
+    $(eval LOCAL_C_INCLUDES := $(c_includes)) \
+    $(eval LOCAL_SRC_FILES := $(file)) \
+    $(eval LOCAL_MODULE := $(notdir $(file:%.cpp=%))) \
+    $(eval LOCAL_MODULE_TAGS := $(module_tags)) \
+    $(eval include $(BUILD_EXECUTABLE)) \
+)
+
+endif
diff --git a/testrunner/tests/Android_java.mk b/testrunner/tests/Android_java.mk
new file mode 100644
index 0000000..ff9138a
--- /dev/null
+++ b/testrunner/tests/Android_java.mk
@@ -0,0 +1,26 @@
+# Copyright (C) 2011 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_JAVA_LIBRARIES := foo android.test.runner
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_PACKAGE_NAME := ApiDemosTests
+LOCAL_INSTRUMENTATION_FOR := ApiDemos
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/testrunner/tests/Android_native.mk b/testrunner/tests/Android_native.mk
new file mode 100644
index 0000000..6273ccb
--- /dev/null
+++ b/testrunner/tests/Android_native.mk
@@ -0,0 +1,21 @@
+# Copyright (C) 2011 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.
+
+include $(CLEAR_VARS)
+test_module := foo
+LOCAL_MODULE := $(test_module)
+recursive_var := $(recursive_var)
+LOCAL_MODULE_TAGS := tags
+LOCAL_SRC_FILES := src
+include $(BUILD_NATIVE_TEST)
diff --git a/testrunner/tests/android_mk_tests.py b/testrunner/tests/android_mk_tests.py
new file mode 100755
index 0000000..ff1f289
--- /dev/null
+++ b/testrunner/tests/android_mk_tests.py
@@ -0,0 +1,90 @@
+#!/usr/bin/python
+#
+#
+# Copyright 2011, 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.
+
+import sys
+import unittest
+sys.path.append('../..')
+
+from testrunner import android_mk
+
+
+class AndroidMKTest(unittest.TestCase):
+  """Unit tests for AndroidMK."""
+
+  def testHasGTest(self):
+    """Test for AndroidMK.HasGTest."""
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_native.mk')
+    self.assertTrue(mk_parser.HasGTest())
+
+  def testHasGTest_lib(self):
+    """Test for AndroidMK.HasGTest."""
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_gtestlib.mk')
+    self.assertTrue(mk_parser.HasGTest())
+
+  def testHasGTest_false(self):
+    """Negative test for AndroidMK.HasGTest."""
+    mk_parser = android_mk.CreateAndroidMK(path='.', filename='Android_java.mk')
+    self.assertFalse(mk_parser.HasGTest())
+
+  def testHasJavaLibrary(self):
+    """Test for AndroidMK.HasJavaLibrary."""
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_java.mk')
+    self.assertTrue(mk_parser.HasJavaLibrary('android.test.runner'))
+
+  def testHasJavaLibrary_missing(self):
+    """Negative test for AndroidMK.HasJavaLibrary.
+
+    Test behavior when LOCAL_JAVA_LIBARIES rule is not present in makefile.
+    """
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_native.mk')
+    self.assertFalse(mk_parser.HasJavaLibrary('android.test.runner'))
+
+  def testHasJavaLibrary_false(self):
+    """Negative test for AndroidMK.HasJavaLibrary.
+
+    Test behavior when LOCAL_JAVA_LIBARIES rule is present, but does not list
+    given library.
+    """
+    mk_parser = android_mk.CreateAndroidMK(path='.', filename='Android_java.mk')
+    self.assertFalse(mk_parser.HasJavaLibrary('doesntexist'))
+
+  def testGetExpandedVariable(self):
+    """Test for AndroidMK.GetExpandedVariable.
+    """
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_native.mk')
+    self.assertEquals('foo', mk_parser.GetExpandedVariable('LOCAL_MODULE'))
+
+  def testGetExpandedVariable_loop(self):
+    """Test for AndroidMK.GetExpandedVariable where variable expansion loops
+    """
+    mk_parser = android_mk.CreateAndroidMK(path='.',
+                                           filename='Android_native.mk')
+    try:
+      mk_parser.GetExpandedVariable('recursive_var')
+      self.assertTrue(False)
+    except RuntimeError:
+      # expected
+      pass
+
+
+if __name__ == '__main__':
+  unittest.main()