Make coverage work without test defs.

Change-Id: I946df038e97dc5c2f40a4610d4076e13ab6bde37
This commit is contained in:
Brett Chabot
2012-09-19 07:35:35 -07:00
parent 91615e7f5a
commit 8ac51186a6
9 changed files with 296 additions and 99 deletions

View File

@@ -115,6 +115,10 @@ class AndroidMK(object):
""" """
return identifier in self._includes 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): def HasJavaLibrary(self, library_name):
"""Check if library is specified as a local java library in makefile. """Check if library is specified as a local java library in makefile.

View File

@@ -0,0 +1 @@
__all__ = ['coverage', 'coverage_targets', 'coverage_target']

View File

@@ -24,6 +24,8 @@ import os
# local imports # local imports
import android_build import android_build
import android_mk
import coverage_target
import coverage_targets import coverage_targets
import errors import errors
import logger import logger
@@ -62,7 +64,9 @@ class CoverageGenerator(object):
self._adb = adb_interface self._adb = adb_interface
self._targets_manifest = self._ReadTargets() self._targets_manifest = self._ReadTargets()
def ExtractReport(self, test_suite, def ExtractReport(self,
test_suite_name,
target,
device_coverage_path, device_coverage_path,
output_path=None, output_path=None,
test_qualifier=None): test_qualifier=None):
@@ -70,7 +74,8 @@ class CoverageGenerator(object):
Assumes test has just been executed. Assumes test has just been executed.
Args: 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 device_coverage_path: location of coverage file on device
output_path: path to place output files in. If None will use output_path: path to place output files in. If None will use
<android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test[-qualifier]> <android_root_path>/<_COVERAGE_REPORT_PATH>/<target>/<test[-qualifier]>
@@ -81,12 +86,12 @@ class CoverageGenerator(object):
absolute file path string of generated html report file. absolute file path string of generated html report file.
""" """
if output_path is None: if output_path is None:
report_name = test_suite.GetName() report_name = test_suite_name
if test_qualifier: if test_qualifier:
report_name = report_name + "-" + test_qualifier report_name = report_name + "-" + test_qualifier
output_path = os.path.join(self._root_path, output_path = os.path.join(self._root_path,
self._COVERAGE_REPORT_PATH, self._COVERAGE_REPORT_PATH,
test_suite.GetTargetName(), target.GetName(),
report_name) report_name)
coverage_local_name = "%s.%s" % (report_name, coverage_local_name = "%s.%s" % (report_name,
@@ -97,15 +102,8 @@ class CoverageGenerator(object):
report_path = os.path.join(output_path, report_path = os.path.join(output_path,
report_name) report_name)
target = self._targets_manifest.GetTarget(test_suite.GetTargetName()) return self._GenerateReport(report_path, coverage_local_path, [target],
if target is None: do_src=True)
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 None return None
def _GenerateReport(self, report_path, coverage_file_path, targets, def _GenerateReport(self, report_path, coverage_file_path, targets,
@@ -283,12 +281,34 @@ class CoverageGenerator(object):
self._CombineTestCoverage() self._CombineTestCoverage()
self._CombineTargetCoverage() 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(): def EnableCoverageBuild():
"""Enable building an Android target with code coverage instrumentation.""" """Enable building an Android target with code coverage instrumentation."""
os.environ["EMMA_INSTRUMENT"] = "true" os.environ["EMMA_INSTRUMENT"] = "true"
def Run(): def Run():
"""Does coverage operations based on command line args.""" """Does coverage operations based on command line args."""
# TODO: do we want to support combining coverage for a single target # TODO: do we want to support combining coverage for a single target

View File

@@ -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

View File

@@ -18,6 +18,8 @@ import xml.dom.minidom
import xml.parsers import xml.parsers
import os import os
import coverage_target
import logger import logger
import errors import errors
@@ -38,12 +40,17 @@ class CoverageTargets:
""" """
_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, ): def __init__(self, ):
self._target_map= {} self._target_map= {}
def __iter__(self): def __iter__(self):
return iter(self._target_map.values()) return iter(self._target_map.values()
def Parse(self, file_path): 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
@@ -66,7 +73,8 @@ class CoverageTargets:
target_elements = doc.getElementsByTagName(self._TARGET_TAG_NAME) target_elements = doc.getElementsByTagName(self._TARGET_TAG_NAME)
for target_element in target_elements: for target_element in target_elements:
target = CoverageTarget(target_element) target = coverage_target.CoverageTarget()
self._ParseCoverageTarget(target, target_element)
self._AddTarget(target) self._AddTarget(target)
def _AddTarget(self, target): def _AddTarget(self, target):
@@ -90,42 +98,28 @@ class CoverageTargets:
except KeyError: except KeyError:
return None return None
class CoverageTarget: def _ParseCoverageTarget(self, target, target_element):
""" Represents one coverage target definition parsed from xml """ """Parse coverage data from XML.
_NAME_ATTR = 'name' Args:
_TYPE_ATTR = 'type' target: the Coverage object to populate
_BUILD_ATTR = 'build_path' target_element: the XML element to get data from
_SRC_TAG = 'src' """
_PATH_ATTR = 'path' target.SetName(target_element.getAttribute(self._NAME_ATTR))
target.SetType(target_element.getAttribute(self._TYPE_ATTR))
def __init__(self, target_element): target.SetBuildPath(target_element.getAttribute(self._BUILD_ATTR))
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)
self._paths = [] self._paths = []
self._ParsePaths(target_element) self._ParsePaths(target, target_element)
def GetName(self): def _ParsePaths(self, target, target_element):
return self._name
def GetPaths(self):
return self._paths
def GetType(self):
return self._type
def GetBuildPath(self):
return self._build_path
def _ParsePaths(self, target_element):
src_elements = target_element.getElementsByTagName(self._SRC_TAG) src_elements = target_element.getElementsByTagName(self._SRC_TAG)
if len(src_elements) <= 0: if len(src_elements) <= 0:
# no src tags specified. Assume build_path + src # 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: for src_element in src_elements:
rel_path = src_element.getAttribute(self._PATH_ATTR) 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): def Parse(xml_file_path):
"""parses out a file_path class from given path to xml""" """parses out a file_path class from given path to xml"""

116
testrunner/make_tree.py Normal file
View File

@@ -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

View File

@@ -41,9 +41,10 @@ import time
# local imports # local imports
import adb_interface import adb_interface
import android_build import android_build
import coverage from coverage import coverage
import errors import errors
import logger import logger
import make_tree
import run_command import run_command
from test_defs import test_defs from test_defs import test_defs
from test_defs import test_walker from test_defs import test_walker
@@ -143,6 +144,9 @@ class TestRunner(object):
parser.add_option("-o", "--coverage", dest="coverage", parser.add_option("-o", "--coverage", dest="coverage",
default=False, action="store_true", default=False, action="store_true",
help="Generate code coverage metrics for test(s)") 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", parser.add_option("-x", "--path", dest="test_path",
help="Run test(s) at given file system path") help="Run test(s) at given file system path")
parser.add_option("-t", "--all-tests", dest="all_tests", parser.add_option("-t", "--all-tests", dest="all_tests",
@@ -191,6 +195,9 @@ class TestRunner(object):
if self._options.verbose: if self._options.verbose:
logger.SetVerbose(True) logger.SetVerbose(True)
if self._options.coverage_target_path:
self._options.coverage = True
self._known_tests = self._ReadTests() self._known_tests = self._ReadTests()
self._options.host_lib_path = android_build.GetHostLibraryPath() self._options.host_lib_path = android_build.GetHostLibraryPath()
@@ -241,23 +248,24 @@ class TestRunner(object):
self._TurnOffVerifier(tests) self._TurnOffVerifier(tests)
self._DoFullBuild(tests) self._DoFullBuild(tests)
target_set = [] target_tree = make_tree.MakeTree()
extra_args_set = [] extra_args_set = []
for test_suite in tests: 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: if not self._options.preview:
self._adb.EnableAdbRoot() self._adb.EnableAdbRoot()
else: else:
logger.Log("adb root") logger.Log("adb root")
if target_set:
if not target_tree.IsEmpty():
if self._options.coverage: if self._options.coverage:
coverage.EnableCoverageBuild() coverage.EnableCoverageBuild()
target_set.append("external/emma/Android.mk") target_tree.AddPath("external/emma")
# TODO: detect if external/emma exists
target_build_string = " ".join(target_set) target_list = target_tree.GetPrunedMakeList()
target_build_string = " ".join(target_list)
extra_args_string = " ".join(extra_args_set) extra_args_string = " ".join(extra_args_set)
# mmm cannot be used from python, so perform a similar operation using # mmm cannot be used from python, so perform a similar operation using
@@ -330,22 +338,18 @@ class TestRunner(object):
os.chdir(old_dir) os.chdir(old_dir)
self._DoInstall(output) 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(): if not test_suite.IsFullMake():
build_dir = test_suite.GetBuildPath() 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()) extra_args_set.append(test_suite.GetExtraBuildArgs())
for path in test_suite.GetBuildDependencies(self._options): 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: if build_dir is not None:
build_file_path = os.path.join(build_dir, "Android.mk") target_tree.AddPath(build_dir)
if os.path.isfile(os.path.join(self._root_path, build_file_path)): return True
target_set.append(build_file_path)
return True
else:
logger.Log("%s has no Android.mk, skipping" % build_dir)
return False return False
def _GetTestsToRun(self): def _GetTestsToRun(self):

View File

@@ -22,7 +22,7 @@ import re
# local imports # local imports
import android_manifest import android_manifest
import coverage from coverage import coverage
import errors import errors
import logger import logger
import test_suite import test_suite
@@ -84,6 +84,8 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
return self return self
def GetBuildDependencies(self, options): def GetBuildDependencies(self, options):
if options.coverage_target_path:
return [options.coverage_target_path]
return [] return []
def Run(self, options, adb): def Run(self, options, adb):
@@ -140,6 +142,10 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
logger.Log(adb_cmd) logger.Log(adb_cmd)
elif options.coverage: elif options.coverage:
coverage_gen = coverage.CoverageGenerator(adb) 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) self._CheckInstrumentationInstalled(adb)
# need to parse test output to determine path to coverage file # need to parse test output to determine path to coverage file
logger.Log("Running in coverage mode, suppressing test output") logger.Log("Running in coverage mode, suppressing test output")
@@ -158,7 +164,8 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
return return
coverage_file = coverage_gen.ExtractReport( 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: if coverage_file is not None:
logger.Log("Coverage report generated at %s" % coverage_file) logger.Log("Coverage report generated at %s" % coverage_file)

View File

@@ -141,6 +141,9 @@ class TestWalker(object):
else: else:
tests.extend(self._CreateSuites(android_mk_parser, path, tests.extend(self._CreateSuites(android_mk_parser, path,
upstream_build_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 # Try to build as much of original path as possible, so
# keep track of upper-most parent directory where Android.mk was found # keep track of upper-most parent directory where Android.mk was found
# that has rule to build sub-directory makefiles. # that has rule to build sub-directory makefiles.
@@ -148,7 +151,7 @@ class TestWalker(object):
# ie if a test exists at 'foo' directory and 'foo/sub', attempting to # ie if a test exists at 'foo' directory and 'foo/sub', attempting to
# build both 'foo' and 'foo/sub' will fail. # 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, # found rule to build sub-directories. The parent path can be used,
# or if not set, use current path # or if not set, use current path
if not upstream_build_path: if not upstream_build_path: