Merge "Make coverage work without test defs." into jb-mr1-dev

This commit is contained in:
Brett Chabot
2012-09-21 12:33:54 -07:00
committed by Android (Google) Code Review
9 changed files with 296 additions and 99 deletions

View File

@@ -115,6 +115,10 @@ class AndroidMK(object):
"""
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 @@ def CreateAndroidMK(path, filename=AndroidMK.FILENAME):
mk._ParseMK(mk_path)
return mk
else:
return None
return None

View File

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

View File

@@ -24,6 +24,8 @@ import os
# local imports
import android_build
import android_mk
import coverage_target
import coverage_targets
import errors
import logger
@@ -62,7 +64,9 @@ class CoverageGenerator(object):
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 @@ class CoverageGenerator(object):
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 @@ class CoverageGenerator(object):
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 @@ class CoverageGenerator(object):
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 @@ class CoverageGenerator(object):
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

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

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

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
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 @@ class TestRunner(object):
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 @@ class TestRunner(object):
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 @@ class TestRunner(object):
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 @@ class TestRunner(object):
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):

View File

@@ -22,7 +22,7 @@ import re
# local imports
import android_manifest
import coverage
from coverage import coverage
import errors
import logger
import test_suite
@@ -84,6 +84,8 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
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 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
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 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
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 @@ class InstrumentationTestSuite(test_suite.AbstractTestSuite):
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)

View File

@@ -141,6 +141,9 @@ class TestWalker(object):
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 @@ class TestWalker(object):
# 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: