diff --git a/testrunner/android_mk.py b/testrunner/android_mk.py index 2c265eea2..7a7769f68 100644 --- a/testrunner/android_mk.py +++ b/testrunner/android_mk.py @@ -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 \ 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 000000000..4be2078b0 --- /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 1c8ec2fd5..570527d56 100755 --- a/testrunner/coverage.py +++ b/testrunner/coverage/coverage.py @@ -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 /<_COVERAGE_REPORT_PATH>// @@ -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 diff --git a/testrunner/coverage/coverage_target.py b/testrunner/coverage/coverage_target.py new file mode 100644 index 000000000..9d08a5c32 --- /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 bc826de7a..a62767114 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: - ] (0..*) - These are relative to build_path. If missing, + + [] (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 000000000..c8bac17e8 --- /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 0c3ce1514..f7a4759a1 100755 --- a/testrunner/runtest.py +++ b/testrunner/runtest.py @@ -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): diff --git a/testrunner/test_defs/instrumentation_test.py b/testrunner/test_defs/instrumentation_test.py index c87fffdb3..092a77322 100644 --- a/testrunner/test_defs/instrumentation_test.py +++ b/testrunner/test_defs/instrumentation_test.py @@ -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) diff --git a/testrunner/test_defs/test_walker.py b/testrunner/test_defs/test_walker.py index 572f8b6a4..fa6ea1baa 100755 --- a/testrunner/test_defs/test_walker.py +++ b/testrunner/test_defs/test_walker.py @@ -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: