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: