Make coverage work without test defs.

Change-Id: I946df038e97dc5c2f40a4610d4076e13ab6bde37
diff --git a/testrunner/android_mk.py b/testrunner/android_mk.py
index 2c265ee..7a7769f 100644
--- a/testrunner/android_mk.py
+++ b/testrunner/android_mk.py
@@ -115,6 +115,10 @@
     """
     return identifier in self._includes
 
+  def IncludesMakefilesUnder(self):
+    """Check if makefile has a 'include makefiles under here' rule"""
+    return self.HasInclude('call all-makefiles-under,$(LOCAL_PATH)')
+
   def HasJavaLibrary(self, library_name):
     """Check if library is specified as a local java library in makefile.
 
@@ -168,4 +172,4 @@
     mk._ParseMK(mk_path)
     return mk
   else:
-    return None
\ No newline at end of file
+    return None
diff --git a/testrunner/coverage/__init__.py b/testrunner/coverage/__init__.py
new file mode 100644
index 0000000..4be2078
--- /dev/null
+++ b/testrunner/coverage/__init__.py
@@ -0,0 +1 @@
+__all__ = ['coverage', 'coverage_targets', 'coverage_target']
diff --git a/testrunner/coverage.py b/testrunner/coverage/coverage.py
similarity index 88%
rename from testrunner/coverage.py
rename to testrunner/coverage/coverage.py
index 1c8ec2f..570527d 100755
--- a/testrunner/coverage.py
+++ b/testrunner/coverage/coverage.py
@@ -24,6 +24,8 @@
 
 # local imports
 import android_build
+import android_mk
+import coverage_target
 import coverage_targets
 import errors
 import logger
@@ -62,7 +64,9 @@
     self._adb = adb_interface
     self._targets_manifest = self._ReadTargets()
 
-  def ExtractReport(self, test_suite,
+  def ExtractReport(self,
+                    test_suite_name,
+                    target,
                     device_coverage_path,
                     output_path=None,
                     test_qualifier=None):
@@ -70,7 +74,8 @@
 
     Assumes test has just been executed.
     Args:
-      test_suite: TestSuite to generate coverage data for
+      test_suite_name: name of TestSuite to generate coverage data for
+      target: the CoverageTarget to use as basis for coverage calculation
       device_coverage_path: location of coverage file on device
       output_path: path to place output files in. If None will use
         <android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test[-qualifier]>
@@ -81,12 +86,12 @@
       absolute file path string of generated html report file.
     """
     if output_path is None:
-      report_name = test_suite.GetName()
+      report_name = test_suite_name
       if test_qualifier:
         report_name = report_name + "-" + test_qualifier
       output_path = os.path.join(self._root_path,
                                  self._COVERAGE_REPORT_PATH,
-                                 test_suite.GetTargetName(),
+                                 target.GetName(),
                                  report_name)
 
     coverage_local_name = "%s.%s" % (report_name,
@@ -97,15 +102,8 @@
 
       report_path = os.path.join(output_path,
                                  report_name)
-      target = self._targets_manifest.GetTarget(test_suite.GetTargetName())
-      if target is None:
-        msg = ["Error: test %s references undefined target %s."
-                % (test_suite.GetName(), test_suite.GetTargetName())]
-        msg.append(" Ensure target is defined in %s" % self._TARGET_DEF_FILE)
-        logger.Log("".join(msg))
-      else:
-        return self._GenerateReport(report_path, coverage_local_path, [target],
-                                    do_src=True)
+      return self._GenerateReport(report_path, coverage_local_path, [target],
+                                  do_src=True)
     return None
 
   def _GenerateReport(self, report_path, coverage_file_path, targets,
@@ -283,12 +281,34 @@
     self._CombineTestCoverage()
     self._CombineTargetCoverage()
 
+  def GetCoverageTarget(self, name):
+    """Find the CoverageTarget for given name"""
+    target = self._targets_manifest.GetTarget(name)
+    if target is None:
+      msg = ["Error: test references undefined target %s." % name]
+      msg.append(" Ensure target is defined in %s" % self._TARGET_DEF_FILE)
+      raise errors.AbortError(msg)
+    return target
+
+  def GetCoverageTargetForPath(self, path):
+    """Find the CoverageTarget for given file system path"""
+    android_mk_path = os.path.join(path, "Android.mk")
+    if os.path.exists(android_mk_path):
+      android_mk_parser = android_mk.CreateAndroidMK(path)
+      target = coverage_target.CoverageTarget()
+      target.SetBuildPath(os.path.join(path, "src"))
+      target.SetName(android_mk_parser.GetVariable(android_mk_parser.PACKAGE_NAME))
+      target.SetType("APPS")
+      return target
+    else:
+      msg = "No Android.mk found at %s" % path
+      raise errors.AbortError(msg)
+
 
 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/coverage/coverage_target.py b/testrunner/coverage/coverage_target.py
new file mode 100644
index 0000000..9d08a5c
--- /dev/null
+++ b/testrunner/coverage/coverage_target.py
@@ -0,0 +1,48 @@
+#
+# Copyright 2012, 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.
+
+class CoverageTarget:
+  """ Represents a code coverage target definition"""
+
+  def __init__(self):
+    self._name = None
+    self._type = None
+    self._build_path = None
+    self._paths = []
+
+  def GetName(self):
+    return self._name
+
+  def SetName(self, name):
+    self._name = name
+
+  def GetPaths(self):
+    return self._paths
+
+  def AddPath(self, path):
+    self._paths.append(path)
+
+  def GetType(self):
+    return self._type
+
+  def SetType(self, buildtype):
+    self._type = buildtype
+
+  def GetBuildPath(self):
+    return self._build_path
+
+  def SetBuildPath(self, build_path):
+    self._build_path = build_path
+
diff --git a/testrunner/coverage_targets.py b/testrunner/coverage/coverage_targets.py
similarity index 65%
rename from testrunner/coverage_targets.py
rename to testrunner/coverage/coverage_targets.py
index bc826de..a627671 100644
--- a/testrunner/coverage_targets.py
+++ b/testrunner/coverage/coverage_targets.py
@@ -3,130 +3,124 @@
 #
 # 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.
 import xml.dom.minidom
 import xml.parsers
 import os
 
+
+import coverage_target
 import logger
 import errors
 
 class CoverageTargets:
-  """Accessor for the code coverage target xml file 
+  """Accessor for the code coverage target xml file
   Expects the following format:
   <targets>
-    <target 
+    <target
       name=""
       type="JAVA_LIBRARIES|APPS"
       build_path=""
-      
-      [<src path=""/>] (0..*)  - These are relative to build_path. If missing, 
+
+      [<src path=""/>] (0..*)  - These are relative to build_path. If missing,
                                  assumes 'src'
     >/target>
-    
-    TODO: add more format checking  
+
+    TODO: add more format checking
   """
-  
-  _TARGET_TAG_NAME = 'coverage_target' 
-  
+
+  _TARGET_TAG_NAME = 'coverage_target'
+  _NAME_ATTR = 'name'
+  _TYPE_ATTR = 'type'
+  _BUILD_ATTR = 'build_path'
+  _SRC_TAG = 'src'
+  _PATH_ATTR = 'path'
+
   def __init__(self, ):
     self._target_map= {}
-    
+
   def __iter__(self):
-    return iter(self._target_map.values())
-      
+    return iter(self._target_map.values()
+
   def Parse(self, file_path):
-    """Parse the coverage target data from from given file path, and add it to 
+    """Parse the coverage target data from from given file path, and add it to
        the current object
        Args:
          file_path: absolute file path to parse
        Raises:
-         errors.ParseError if file_path cannot be parsed  
+         errors.ParseError if file_path cannot be parsed
     """
     try:
       doc = xml.dom.minidom.parse(file_path)
     except IOError:
-      # Error: The results file does not exist 
+      # Error: The results file does not exist
       logger.Log('Results 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
-    
+
     target_elements = doc.getElementsByTagName(self._TARGET_TAG_NAME)
 
     for target_element in target_elements:
-      target = CoverageTarget(target_element)
+      target = coverage_target.CoverageTarget()
+      self._ParseCoverageTarget(target, target_element)
       self._AddTarget(target)
-    
-  def _AddTarget(self, target): 
+
+  def _AddTarget(self, target):
     self._target_map[target.GetName()] = target
-     
+
   def GetBuildTargets(self):
     """ returns list of target names """
     build_targets = []
     for target in self:
       build_targets.append(target.GetName())
-    return build_targets   
-  
+    return build_targets
+
   def GetTargets(self):
     """ returns list of CoverageTarget"""
     return self._target_map.values()
-  
+
   def GetTarget(self, name):
     """ returns CoverageTarget for given name. None if not found """
     try:
       return self._target_map[name]
     except KeyError:
       return None
-  
-class CoverageTarget:
-  """ Represents one coverage target definition parsed from xml """
-  
-  _NAME_ATTR = 'name'
-  _TYPE_ATTR = 'type'
-  _BUILD_ATTR = 'build_path'
-  _SRC_TAG = 'src'
-  _PATH_ATTR = 'path'
-  
-  def __init__(self, target_element):
-    self._name = target_element.getAttribute(self._NAME_ATTR)
-    self._type = target_element.getAttribute(self._TYPE_ATTR)
-    self._build_path = target_element.getAttribute(self._BUILD_ATTR)
+
+  def _ParseCoverageTarget(self, target, target_element):
+    """Parse coverage data from XML.
+
+    Args:
+      target: the Coverage object to populate
+      target_element: the XML element to get data from
+    """
+    target.SetName(target_element.getAttribute(self._NAME_ATTR))
+    target.SetType(target_element.getAttribute(self._TYPE_ATTR))
+    target.SetBuildPath(target_element.getAttribute(self._BUILD_ATTR))
     self._paths = []
-    self._ParsePaths(target_element)
-    
-  def GetName(self):
-    return self._name
+    self._ParsePaths(target, target_element)
 
-  def GetPaths(self):
-    return self._paths
-  
-  def GetType(self):
-    return self._type
-
-  def GetBuildPath(self):
-    return self._build_path
-  
-  def _ParsePaths(self, target_element):
+  def _ParsePaths(self, target, target_element):
     src_elements = target_element.getElementsByTagName(self._SRC_TAG)
     if len(src_elements) <= 0:
       # no src tags specified. Assume build_path + src
-      self._paths.append(os.path.join(self.GetBuildPath(), "src"))
+      target.AddPath(os.path.join(target.GetBuildPath(), "src"))
     for src_element in src_elements:
       rel_path = src_element.getAttribute(self._PATH_ATTR)
-      self._paths.append(os.path.join(self.GetBuildPath(), rel_path))
-  
+      target.AddPath(os.path.join(target.GetBuildPath(), rel_path))
+
+
 def Parse(xml_file_path):
   """parses out a file_path class from given path to xml"""
   targets = CoverageTargets()
diff --git a/testrunner/make_tree.py b/testrunner/make_tree.py
new file mode 100644
index 0000000..c8bac17
--- /dev/null
+++ b/testrunner/make_tree.py
@@ -0,0 +1,116 @@
+#
+# Copyright 2012, 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.
+
+"""Data structure for processing makefiles."""
+
+import os
+
+import android_build
+import android_mk
+import errors
+
+class MakeNode(object):
+  """Represents single node in make tree."""
+
+  def __init__(self, name, parent):
+    self._name = name
+    self._children_map = {}
+    self._is_leaf = False
+    self._parent = parent
+    self._includes_submake = None
+    if parent:
+      self._path = os.path.join(parent._GetPath(), name)
+    else:
+      self._path = ""
+
+  def _AddPath(self, path_segs):
+    """Adds given path to this node.
+
+    Args:
+      path_segs: list of path segments
+    """
+    if not path_segs:
+      # done processing path
+      return self
+    current_seg = path_segs.pop(0)
+    child = self._children_map.get(current_seg)
+    if not child:
+      child = MakeNode(current_seg, self)
+      self._children_map[current_seg] = child
+    return child._AddPath(path_segs)
+
+  def _SetLeaf(self, is_leaf):
+    self._is_leaf = is_leaf
+
+  def _GetPath(self):
+    return self._path
+
+  def _DoesIncludesSubMake(self):
+    if self._includes_submake is None:
+      if self._is_leaf:
+        path = os.path.join(android_build.GetTop(), self._path)
+        mk_parser = android_mk.CreateAndroidMK(path)
+        self._includes_submake = mk_parser.IncludesMakefilesUnder()
+      else:
+        self._includes_submake = False
+    return self._includes_submake
+
+  def _DoesParentIncludeMe(self):
+    return self._parent and self._parent._DoesIncludesSubMake()
+
+  def _BuildPrunedMakeList(self, make_list):
+    if self._is_leaf and not self._DoesParentIncludeMe():
+      make_list.append(os.path.join(self._path, "Android.mk"))
+    for child in self._children_map.itervalues():
+      child._BuildPrunedMakeList(make_list)
+
+
+class MakeTree(MakeNode):
+  """Data structure for building a non-redundant set of Android.mk paths.
+
+  Used to collapse set of Android.mk files to use to prevent issuing make
+  command that include same module multiple times due to include rules.
+  """
+
+  def __init__(self):
+    super(MakeTree, self).__init__("", None)
+
+  def AddPath(self, path):
+    """Adds make directory path to tree.
+
+    Will have no effect if path is already included in make set.
+
+    Args:
+      path: filesystem path to directory to build, relative to build root.
+    """
+    path = os.path.normpath(path)
+    mk_path = os.path.join(android_build.GetTop(), path, "Android.mk")
+    if not os.path.isfile(mk_path):
+      raise errors.AbortError("%s does not exist" % mk_path)
+    path_segs = path.split(os.sep)
+    child = self._AddPath(path_segs)
+    child._SetLeaf(True)
+
+  def GetPrunedMakeList(self):
+    """Return as list of the minimum set of Android.mk files necessary to
+    build all leaf nodes in tree.
+    """
+    make_list = []
+    self._BuildPrunedMakeList(make_list)
+    return make_list
+
+  def IsEmpty(self):
+    return not self._children_map
+
diff --git a/testrunner/runtest.py b/testrunner/runtest.py
index 1c11e27..7ddff8a 100755
--- a/testrunner/runtest.py
+++ b/testrunner/runtest.py
@@ -41,9 +41,10 @@
 # local imports
 import adb_interface
 import android_build
-import coverage
+from coverage import coverage
 import errors
 import logger
+import make_tree
 import run_command
 from test_defs import test_defs
 from test_defs import test_walker
@@ -143,6 +144,9 @@
     parser.add_option("-o", "--coverage", dest="coverage",
                       default=False, action="store_true",
                       help="Generate code coverage metrics for test(s)")
+    parser.add_option("--coverage-target", dest="coverage_target_path",
+                      default=None,
+                      help="Path to app to collect code coverage target data for.")
     parser.add_option("-x", "--path", dest="test_path",
                       help="Run test(s) at given file system path")
     parser.add_option("-t", "--all-tests", dest="all_tests",
@@ -191,6 +195,9 @@
     if self._options.verbose:
       logger.SetVerbose(True)
 
+    if self._options.coverage_target_path:
+      self._options.coverage = True
+
     self._known_tests = self._ReadTests()
 
     self._options.host_lib_path = android_build.GetHostLibraryPath()
@@ -241,23 +248,24 @@
     self._TurnOffVerifier(tests)
     self._DoFullBuild(tests)
 
-    target_set = []
+    target_tree = make_tree.MakeTree()
 
     extra_args_set = []
     for test_suite in tests:
-      self._AddBuildTarget(test_suite, target_set, extra_args_set)
+      self._AddBuildTarget(test_suite, target_tree, extra_args_set)
 
     if not self._options.preview:
       self._adb.EnableAdbRoot()
     else:
       logger.Log("adb root")
-    if target_set:
+
+    if not target_tree.IsEmpty():
       if self._options.coverage:
         coverage.EnableCoverageBuild()
-        target_set.append("external/emma/Android.mk")
-        # TODO: detect if external/emma exists
+        target_tree.AddPath("external/emma")
 
-      target_build_string = " ".join(target_set)
+      target_list = target_tree.GetPrunedMakeList()
+      target_build_string = " ".join(target_list)
       extra_args_string = " ".join(extra_args_set)
 
       # mmm cannot be used from python, so perform a similar operation using
@@ -330,22 +338,18 @@
         os.chdir(old_dir)
         self._DoInstall(output)
 
-  def _AddBuildTarget(self, test_suite, target_set, extra_args_set):
+  def _AddBuildTarget(self, test_suite, target_tree, extra_args_set):
     if not test_suite.IsFullMake():
       build_dir = test_suite.GetBuildPath()
-      if self._AddBuildTargetPath(build_dir, target_set):
+      if self._AddBuildTargetPath(build_dir, target_tree):
         extra_args_set.append(test_suite.GetExtraBuildArgs())
       for path in test_suite.GetBuildDependencies(self._options):
-        self._AddBuildTargetPath(path, target_set)
+        self._AddBuildTargetPath(path, target_tree)
 
-  def _AddBuildTargetPath(self, build_dir, target_set):
+  def _AddBuildTargetPath(self, build_dir, target_tree):
     if build_dir is not None:
-      build_file_path = os.path.join(build_dir, "Android.mk")
-      if os.path.isfile(os.path.join(self._root_path, build_file_path)):
-        target_set.append(build_file_path)
-        return True
-      else:
-        logger.Log("%s has no Android.mk, skipping" % build_dir)
+      target_tree.AddPath(build_dir)
+      return True
     return False
 
   def _GetTestsToRun(self):
diff --git a/testrunner/test_defs/instrumentation_test.py b/testrunner/test_defs/instrumentation_test.py
index c87fffd..092a773 100644
--- a/testrunner/test_defs/instrumentation_test.py
+++ b/testrunner/test_defs/instrumentation_test.py
@@ -22,7 +22,7 @@
 
 # local imports
 import android_manifest
-import coverage
+from coverage import coverage
 import errors
 import logger
 import test_suite
@@ -84,6 +84,8 @@
     return self
 
   def GetBuildDependencies(self, options):
+    if options.coverage_target_path:
+      return [options.coverage_target_path]
     return []
 
   def Run(self, options, adb):
@@ -140,6 +142,10 @@
       logger.Log(adb_cmd)
     elif options.coverage:
       coverage_gen = coverage.CoverageGenerator(adb)
+      if options.coverage_target_path:
+        coverage_target = coverage_gen.GetCoverageTargetForPath(options.coverage_target_path)
+      elif self.GetTargetName():
+        coverage_target = coverage_gen.GetCoverageTarget(self.GetTargetName())
       self._CheckInstrumentationInstalled(adb)
       # need to parse test output to determine path to coverage file
       logger.Log("Running in coverage mode, suppressing test output")
@@ -158,7 +164,8 @@
         return
 
       coverage_file = coverage_gen.ExtractReport(
-          self, device_coverage_path, test_qualifier=options.test_size)
+          self.GetName(), coverage_target, device_coverage_path,
+          test_qualifier=options.test_size)
       if coverage_file is not None:
         logger.Log("Coverage report generated at %s" % coverage_file)
 
@@ -171,10 +178,10 @@
                                         instrumentation_args)
 
   def _CheckInstrumentationInstalled(self, adb):
-    if not adb.IsInstrumentationInstalled(self.GetPackageName(),    
+    if not adb.IsInstrumentationInstalled(self.GetPackageName(),
                                           self.GetRunnerName()):
       msg=("Could not find instrumentation %s/%s on device. Try forcing a "
-           "rebuild by updating a source file, and re-executing runtest." % 
+           "rebuild by updating a source file, and re-executing runtest." %
            (self.GetPackageName(), self.GetRunnerName()))
       raise errors.AbortError(msg=msg)
 
diff --git a/testrunner/test_defs/test_walker.py b/testrunner/test_defs/test_walker.py
index 572f8b6..fa6ea1b 100755
--- a/testrunner/test_defs/test_walker.py
+++ b/testrunner/test_defs/test_walker.py
@@ -141,6 +141,9 @@
       else:
         tests.extend(self._CreateSuites(android_mk_parser, path,
                                         upstream_build_path))
+      # TODO: remove this logic, and rely on caller to collapse build
+      # paths via make_tree
+
       # 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.
@@ -148,7 +151,7 @@
       # 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)'):
+      if android_mk_parser.IncludesMakefilesUnder():
         # found rule to build sub-directories. The parent path can be used,
         # or if not set, use current path
         if not upstream_build_path: