Commit b55b9a83 authored by Yuke Liao's avatar Yuke Liao Committed by Commit Bot

[Coverage] Support negative filter and filter by build targets.

This CL adds support to the code coverage tool to include files by
build targets and also exclude files by paths or build targets.

The motivation of this change is that some view controllers are not
supposed to have unit tests, and instead, they should run agaist 
integration tests in terms of code coverage. Given that view 
controllers are often grouped together by build targets, so it is 
desirable to have a way to include or exclude the sources of a list
of build targets in the code coverage report.

Bug: 763959
Change-Id: I68079e0e6e5952883259b8806cb250166d0a50dc
Reviewed-on: https://chromium-review.googlesource.com/669979
Commit-Queue: Yuke Liao <liaoyuke@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Cr-Commit-Position: refs/heads/master@{#502922}
parent f4a85e0d
...@@ -14,23 +14,43 @@ ...@@ -14,23 +14,43 @@
such as ios_chrome_unittests. To simply play with this tool, you are such as ios_chrome_unittests. To simply play with this tool, you are
suggested to start with 'url_unittests'. suggested to start with 'url_unittests'.
ios/tools/coverage/coverage.py -p path1 -p path2 target Example usages:
Generate code coverage report for |target| for |path1| and |path2|. ios/tools/coverage/coverage.py ios_clean_chrome_unittests
-t ios/clean/chrome/browser/ui/find_in_page/
ios/tools/coverage/coverage.py target -p path --reuse-profdata -i ios/clean/chrome/browser/ui/find_in_page/
out/Coverage-iphonesimulator/coverage.profdata # Generate code coverage report for ios_clean_chrome_unittests for
Skip running tests and reuse the specified profile data file to generate # ios/clean/chrome/browser/ui/find_in_page/ and only include files under
code coverage report. # ios/clean/chrome/browser/ui/find_in_page/.
ios/tools/coverage/coverage.py ios_clean_chrome_unittests
-t ios/clean/chrome/browser/ui/find_in_page/
-i ios/clean/chrome/browser/ui/find_in_page/
-r out/Coverage-iphonesimulator/coverage.profdata
# Skip running tests and reuse the specified profile data file to generate
# code coverage report.
ios/tools/coverage/coverage.py ios_clean_chrome_unittests
-t ios/clean/chrome/browser/ui/find_in_page/
-i ios/clean/chrome/browser/ui/find_in_page/
-e //ios/clean/chrome/browser/ui/find_in_page:find_in_page_ui
-r out/Coverage-iphonesimulator/coverage.profdata
# Exclude the 'sources' of find_in_page_ui build target.
ios/tools/coverage/coverage.py ios_showcase_egtests
-t ios/showcase/content_suggestions
-i //ios/showcase/content_suggestions:content_suggestions
# Generate code coverage report for ios_showcase_egtests for
# ios/showcase/content_suggestions and only include the 'sources' of
# content_suggestions build target.
For more options, please refer to ios/tools/coverage/coverage.py -h For more options, please refer to ios/tools/coverage/coverage.py -h
""" """
import sys import sys
import argparse import argparse
import collections import collections
import ConfigParser import ConfigParser
import json
import os import os
import subprocess import subprocess
...@@ -80,7 +100,8 @@ def _CreateCoverageProfileDataForTarget(target, jobs_count=None, ...@@ -80,7 +100,8 @@ def _CreateCoverageProfileDataForTarget(target, jobs_count=None,
return profdata_path return profdata_path
def _DisplayLineCoverageReport(target, profdata_path, paths): def _DisplayLineCoverageReport(target, profdata_path, top_level_dir,
include_sources, exclude_sources):
"""Generates and displays line coverage report. """Generates and displays line coverage report.
The output has the following format: The output has the following format:
...@@ -95,39 +116,28 @@ def _DisplayLineCoverageReport(target, profdata_path, paths): ...@@ -95,39 +116,28 @@ def _DisplayLineCoverageReport(target, profdata_path, paths):
Args: Args:
target: A string representing the name of the target to be tested. target: A string representing the name of the target to be tested.
profdata_path: A string representing the path to the profdata file. profdata_path: A string representing the path to the profdata file.
paths: A list of directories to generate code coverage for. top_level_dir: The top level directory to show code coverage report.
include_sources: A list of paths.
exclude_sources: A list of paths.
""" """
print 'Generating code coverge report' print 'Generating code coverge report'
raw_line_coverage_report = _GenerateLineCoverageReport(target, profdata_path) raw_line_coverage_report = _GenerateLineCoverageReport(target, profdata_path)
line_coverage_report_with_test = _FilterLineCoverageReport( line_coverage_report_with_test = _FilterLineCoverageReport(
raw_line_coverage_report, paths) raw_line_coverage_report, include_sources, exclude_sources)
line_coverage_report = _ExcludeTestFiles(line_coverage_report_with_test) line_coverage_report = _ExcludeTestFiles(line_coverage_report_with_test)
coverage_by_path = collections.defaultdict( top_level_dir_coverage = collections.defaultdict(lambda: 0)
lambda: collections.defaultdict(lambda: 0))
for file_name in line_coverage_report: for file_name in line_coverage_report:
total_lines = line_coverage_report[file_name]['total_lines'] total_lines = line_coverage_report[file_name]['total_lines']
executed_lines = line_coverage_report[file_name]['executed_lines'] executed_lines = line_coverage_report[file_name]['executed_lines']
matched_paths = _MatchFilePathWithDirectories(file_name, paths) if file_name.startswith(top_level_dir):
for matched_path in matched_paths: top_level_dir_coverage['total_lines'] += total_lines
coverage_by_path[matched_path]['total_lines'] += total_lines top_level_dir_coverage['executed_lines'] += executed_lines
coverage_by_path[matched_path]['executed_lines'] += executed_lines
if matched_paths:
coverage_by_path['aggregate']['total_lines'] += total_lines
coverage_by_path['aggregate']['executed_lines'] += executed_lines
print '\nLine Coverage Report for Following Directories: ' + str(paths)
for path in paths:
print path + ':'
_PrintLineCoverageStats(coverage_by_path[path]['total_lines'],
coverage_by_path[path]['executed_lines'])
if len(paths) > 1: print '\nLine Coverage Report for Directory: ' + str(top_level_dir)
print 'In Aggregate:' _PrintLineCoverageStats(top_level_dir_coverage['total_lines'],
_PrintLineCoverageStats(coverage_by_path['aggregate']['total_lines'], top_level_dir_coverage['executed_lines'])
coverage_by_path['aggregate']['executed_lines'])
def _GenerateLineCoverageReport(target, profdata_path): def _GenerateLineCoverageReport(target, profdata_path):
...@@ -200,14 +210,13 @@ def _GenerateLineCoverageReport(target, profdata_path): ...@@ -200,14 +210,13 @@ def _GenerateLineCoverageReport(target, profdata_path):
continue continue
executed_lines = total_lines - missed_lines executed_lines = total_lines - missed_lines
line_coverage_report[file_name]['total_lines'] = total_lines line_coverage_report[file_name]['total_lines'] = total_lines
line_coverage_report[file_name]['executed_lines'] = executed_lines line_coverage_report[file_name]['executed_lines'] = executed_lines
return line_coverage_report return line_coverage_report
def _FilterLineCoverageReport(raw_report, paths): def _FilterLineCoverageReport(raw_report, include_sources, exclude_sources):
"""Filter line coverage report to only include directories in |paths|. """Filter line coverage report to only include directories in |paths|.
Args: Args:
...@@ -216,7 +225,8 @@ def _FilterLineCoverageReport(raw_report, paths): ...@@ -216,7 +225,8 @@ def _FilterLineCoverageReport(raw_report, paths):
-- file_name: dict => Line coverage summary. -- file_name: dict => Line coverage summary.
---- total_lines: int => Number of total lines. ---- total_lines: int => Number of total lines.
---- executed_lines: int => Number of executed lines. ---- executed_lines: int => Number of executed lines.
paths: A list of directories to generate code coverage for. include_sources: A list of paths.
exclude_sources: A list of paths.
Returns: Returns:
A json object with the following format: A json object with the following format:
...@@ -228,7 +238,11 @@ def _FilterLineCoverageReport(raw_report, paths): ...@@ -228,7 +238,11 @@ def _FilterLineCoverageReport(raw_report, paths):
""" """
filtered_report = {} filtered_report = {}
for file_name in raw_report: for file_name in raw_report:
if _MatchFilePathWithDirectories(file_name, paths): should_include = (any(file_name.startswith(source)
for source in include_sources))
should_exclude = (any(file_name.startswith(source)
for source in exclude_sources))
if should_include and not should_exclude:
filtered_report[file_name] = raw_report[file_name] filtered_report[file_name] = raw_report[file_name]
return filtered_report return filtered_report
...@@ -279,33 +293,13 @@ def _PrintLineCoverageStats(total_lines, executed_lines): ...@@ -279,33 +293,13 @@ def _PrintLineCoverageStats(total_lines, executed_lines):
coverage = float(executed_lines) / total_lines if total_lines > 0 else None coverage = float(executed_lines) / total_lines if total_lines > 0 else None
percentage_coverage = '{}%'.format(int(coverage * 100)) if coverage else None percentage_coverage = '{}%'.format(int(coverage * 100)) if coverage else None
output = ('\tTotal Lines: {}\tExecuted Lines: {}\tMissed Lines: {}\t' output = ('Total Lines: {}\tExecuted Lines: {}\tMissed Lines: {}\t'
'Coverage: {}\n') 'Coverage: {}\n')
print output.format(total_lines, executed_lines, missed_lines, print output.format(total_lines, executed_lines, missed_lines,
percentage_coverage or 'NA') percentage_coverage or 'NA')
def _MatchFilePathWithDirectories(file_path, directories): def _BuildTargetWithCoverageConfiguration(target, jobs_count):
"""Returns the directories that contains the file.
Args:
file_path: the absolute path of a file that is to be matched.
directories: A list of directories that are relative to source root.
Returns:
A list of directories that contains the file.
"""
matched_directories = []
src_root = _GetSrcRootPath()
relative_file_path = os.path.relpath(file_path, src_root)
for directory in directories:
if relative_file_path.startswith(directory):
matched_directories.append(directory)
return matched_directories
def _BuildTargetWithCoverageConfiguration(target, jobs_count=None):
"""Builds target with coverage configuration. """Builds target with coverage configuration.
This function requires current working directory to be the root of checkout. This function requires current working directory to be the root of checkout.
...@@ -505,6 +499,156 @@ def _TargetNameIsValidTestTarget(target): ...@@ -505,6 +499,156 @@ def _TargetNameIsValidTestTarget(target):
VALID_TEST_TARGET_POSTFIXES)) VALID_TEST_TARGET_POSTFIXES))
def _AssertCoverageBuildDirectoryExists():
"""Asserts that the build directory with converage configuration exists."""
src_root = _GetSrcRootPath()
build_dir_path = os.path.join(src_root, BUILD_DIRECTORY)
assert os.path.exists(build_dir_path), (build_dir_path + " doesn't exist."
'Hint: run gclient runhooks or '
'ios/build/tools/setup-gn.py.')
def _SeparatePathsAndBuildTargets(paths_or_build_targets):
"""Separate file/directory paths from build target paths.
Args:
paths_or_build_targets: A list of file/directory or build target paths.
Returns:
Two lists contain the file/directory and build target paths respectively.
"""
paths = []
build_targets = []
for path_or_build_target in paths_or_build_targets:
if path_or_build_target.startswith('//'):
build_targets.append(path_or_build_target)
else:
paths.append(path_or_build_target)
return paths, build_targets
def _FormatBuildTargetPaths(build_targets):
"""Formats build target paths to explicitly specify target name.
Build target paths may have target name omitted, this method adds a target
name for the path if it is.
For example, //url is converted to //url:url.
Args:
build_targets: A list of build targets.
Returns:
A list of build targets.
"""
formatted_build_targets = []
for build_target in build_targets:
if ':' not in os.path.basename(build_target):
formatted_build_targets.append(
build_target + ':' + os.path.basename(build_target))
else:
formatted_build_targets.append(build_target)
return formatted_build_targets
def _AssertBuildTargetsExist(build_targets):
"""Asserts that the build targets specified in |build_targets| exist.
Args:
build_targets: A list of build targets.
"""
# The returned json objec has the following format:
# Root: dict => A dictionary of sources of build targets.
# -- target: dict => A dictionary that describes the target.
# ---- sources: list => A list of source files.
#
# For example:
# {u'//url:url': {u'sources': [u'//url/gurl.cc', u'//url/url_canon_icu.cc']}}
#
target_source_descriptions = _GetSourcesDescriptionOfBuildTargets(
build_targets)
for build_target in build_targets:
assert build_target in target_source_descriptions, (('{} is not a valid '
'build target. Please '
'run \'gn desc {} '
'sources\' to debug.')
.format(build_target,
build_target))
def _AssertPathsExist(paths):
"""Asserts that the paths specified in |paths| exist.
Args:
paths: A list of files or directories.
"""
src_root = _GetSrcRootPath()
for path in paths:
abspath = os.path.join(src_root, path)
assert os.path.exists(abspath), (('Path: {} doesn\'t exist.\nA valid '
'path must exist and be relative to the '
'root of source, which is {}. For '
'example, \'ios/\' is a valid path.').
format(abspath, src_root))
def _GetSourcesOfBuildTargets(build_targets):
"""Returns a list of paths corresponding to the sources of the build targets.
Args:
build_targets: A list of build targets.
Returns:
A list of os paths relative to the root of checkout, and en empty list if
|build_targets| is empty.
"""
if not build_targets:
return []
target_sources_description = _GetSourcesDescriptionOfBuildTargets(
build_targets)
sources = []
for build_target in build_targets:
sources.extend(_ConvertBuildFilePathsToOsPaths(
target_sources_description[build_target]['sources']))
return sources
def _GetSourcesDescriptionOfBuildTargets(build_targets):
"""Returns the description of sources of the build targets using 'gn desc'.
Args:
build_targets: A list of build targets.
Returns:
A json object with the following format:
Root: dict => A dictionary of sources of build targets.
-- target: dict => A dictionary that describes the target.
---- sources: list => A list of source files.
"""
cmd = ['gn', 'desc', BUILD_DIRECTORY]
for build_target in build_targets:
cmd.append(build_target)
cmd.extend(['sources', '--format=json'])
return json.loads(subprocess.check_output(cmd))
def _ConvertBuildFilePathsToOsPaths(build_file_paths):
"""Converts paths in build file format to os path format.
Args:
build_file_paths: A list of paths starts with '//'.
Returns:
A list of os paths relative to the root of checkout.
"""
return [build_file_path[2:] for build_file_path in build_file_paths]
def _ParseCommandArguments(): def _ParseCommandArguments():
"""Add and parse relevant arguments for tool commands. """Add and parse relevant arguments for tool commands.
...@@ -514,8 +658,30 @@ def _ParseCommandArguments(): ...@@ -514,8 +658,30 @@ def _ParseCommandArguments():
arg_parser = argparse.ArgumentParser() arg_parser = argparse.ArgumentParser()
arg_parser.usage = __doc__ arg_parser.usage = __doc__
arg_parser.add_argument('-p', '--path', action='append', required=True, arg_parser.add_argument('-t', '--top-level-dir', type=str, required=True,
help='Directories to get code coverage for.') help='The top level directory to show code coverage '
'report, the path needs to be relative to the '
'root of the checkout.')
arg_parser.add_argument('-i', '--include', action='append', required=True,
help='Directories or build targets to get code '
'coverage for. For directories, paths need to '
'be relative to the root of the checkoutand and '
'all files under them are included recursively; '
'for build targets, only the \'sources\' of the '
'targets are included, and the format of '
'specifying build targets is the same as in '
'\'deps\' in BUILD.gn.')
arg_parser.add_argument('-e', '--exclude', action='append',
help='Directories or build targets to get code '
'coverage for. For directories, paths need to '
'be relative to the root of the checkoutand and '
'all files under them are excluded recursively; '
'for build targets, only the \'sources\' of the '
'targets are excluded, and the format of '
'specifying build targets is the same as in '
'\'deps\' in BUILD.gn.')
arg_parser.add_argument('-j', '--jobs', type=int, default=None, arg_parser.add_argument('-j', '--jobs', type=int, default=None,
help='Run N jobs to build in parallel. If not ' help='Run N jobs to build in parallel. If not '
...@@ -523,7 +689,7 @@ def _ParseCommandArguments(): ...@@ -523,7 +689,7 @@ def _ParseCommandArguments():
'based on CPUs availability. Please refer to ' 'based on CPUs availability. Please refer to '
'\'ninja -h\' for more details.') '\'ninja -h\' for more details.')
arg_parser.add_argument('-r', '--reuse-profdata', arg_parser.add_argument('-r', '--reuse-profdata', type=str,
help='Skip building test target and running tests ' help='Skip building test target and running tests '
'and re-use the specified profile data file.') 'and re-use the specified profile data file.')
...@@ -538,31 +704,6 @@ def _ParseCommandArguments(): ...@@ -538,31 +704,6 @@ def _ParseCommandArguments():
return args return args
def _AssertCoverageBuildDirectoryExists():
"""Asserts that the build directory with converage configuration exists."""
src_root = _GetSrcRootPath()
build_dir_path = os.path.join(src_root, BUILD_DIRECTORY)
assert os.path.exists(build_dir_path), (build_dir_path + " doesn't exist."
'Hint: run gclient runhooks or '
'ios/build/tools/setup-gn.py.')
def _AssertPathsExist(paths):
"""Asserts that paths specified in |paths| exist.
Args:
paths: A list of directories.
"""
src_root = _GetSrcRootPath()
for path in paths:
abspath = os.path.join(src_root, path)
assert os.path.exists(abspath), (('Path: {} doesn\'t exist.\n A valid '
'path must exist and be relative to the '
'root of source, which is {} \nFor '
'example, \'ios/\' is a valid path.').
format(abspath, src_root))
def Main(): def Main():
"""Executes tool commands.""" """Executes tool commands."""
args = _ParseCommandArguments() args = _ParseCommandArguments()
...@@ -580,8 +721,28 @@ def Main(): ...@@ -580,8 +721,28 @@ def Main():
if not jobs and _IsGomaConfigured(): if not jobs and _IsGomaConfigured():
jobs = DEFAULT_GOMA_JOBS jobs = DEFAULT_GOMA_JOBS
print 'Validating inputs'
_AssertCoverageBuildDirectoryExists() _AssertCoverageBuildDirectoryExists()
_AssertPathsExist(args.path) _AssertPathsExist([args.top_level_dir])
include_paths, raw_include_targets = _SeparatePathsAndBuildTargets(
args.include)
exclude_paths, raw_exclude_targets = _SeparatePathsAndBuildTargets(
args.exclude or [])
include_targets = _FormatBuildTargetPaths(raw_include_targets)
exclude_targets = _FormatBuildTargetPaths(raw_exclude_targets)
if include_paths:
_AssertPathsExist(include_paths)
if exclude_paths:
_AssertPathsExist(exclude_paths)
if include_targets:
_AssertBuildTargetsExist(include_targets)
if exclude_targets:
_AssertBuildTargetsExist(exclude_targets)
include_sources = include_paths + _GetSourcesOfBuildTargets(include_targets)
exclude_sources = exclude_paths + _GetSourcesOfBuildTargets(exclude_targets)
profdata_path = args.reuse_profdata profdata_path = args.reuse_profdata
if profdata_path: if profdata_path:
...@@ -592,7 +753,8 @@ def Main(): ...@@ -592,7 +753,8 @@ def Main():
profdata_path = _CreateCoverageProfileDataForTarget(target, jobs, profdata_path = _CreateCoverageProfileDataForTarget(target, jobs,
args.gtest_filter) args.gtest_filter)
_DisplayLineCoverageReport(target, profdata_path, args.path) _DisplayLineCoverageReport(target, profdata_path, args.top_level_dir,
include_sources, exclude_sources)
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(Main()) sys.exit(Main())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment