Commit 435bb171 authored by Yun Liu's avatar Yun Liu Committed by Commit Bot

Add option to generate JSON file for code coverage tool

JSON format defined by code coverage tool proto:
https://cs.chromium.org/chromium/infra/appengine/findit/model/proto/code_coverage.proto

Bug: 843307, 961806
Change-Id: I077fbad4693946b9238c7768a8b9346526aa0099
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1614713
Commit-Queue: Yun Liu <yliuyliu@google.com>
Reviewed-by: default avatarYuke Liao <liaoyuke@chromium.org>
Reviewed-by: default avatarEric Stevenson <estevenson@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661044}
parent 1305d3b6
......@@ -8,19 +8,19 @@ instrumentation and junit tests.
## How Jacoco coverage works
In order to use Jacoco code coverage, we need to create build time pre-instrumented
files and runtime **.exec** files. Then we need to process them using the
class files and runtime **.exec** files. Then we need to process them using the
**build/android/generate_jacoco_report.py** script.
## How to collect Jacoco coverage data
## How to collect coverage data
1. Use the following GN build arguments:
```gn
target_os = "android"
jacoco_coverage = true
```
```gn
target_os = "android"
jacoco_coverage = true
```
Now when building, pre-instrumented files will be created in the build directory.
Now when building, pre-instrumented files will be created in the build directory.
2. Run tests, with option `--coverage-dir <directory>`, to specify where to save
the .exec file. For example, you can run chrome junit tests:
......@@ -29,35 +29,59 @@ Now when building, pre-instrumented files will be created in the build directory
3. The coverage results of junit and instrumentation tests will be merged
automatically if they are in the same directory.
4. Now we have generated .exec files already. We can create a html/xml/csv report
using `generate_jacoco_report.py`, for example:
## How to generate coverage report
```shell
build/android/generate_jacoco_report.py \
--format html \
--output-dir tmp/coverage_report/ \
--coverage-dir /tmp/coverage/ \
--metadata-dir out/Debug/ \
```
1. Now we have generated .exec files already. We can create a Jacoco html/xml/csv
report using `generate_jacoco_report.py`, for example:
```shell
build/android/generate_jacoco_report.py \
--format html \
--output-dir tmp/coverage_report/ \
--coverage-dir /tmp/coverage/ \
--sources-json-dir out/Debug/ \
```
Then an index.html containing coverage info will be created in output directory:
```
[INFO] Loading execution data file /tmp/coverage/testTitle.exec.
[INFO] Loading execution data file /tmp/coverage/testSelected.exec.
[INFO] Loading execution data file /tmp/coverage/testClickToSelect.exec.
[INFO] Loading execution data file /tmp/coverage/testClickToClose.exec.
[INFO] Loading execution data file /tmp/coverage/testThumbnail.exec.
[INFO] Analyzing 58 classes.
```
```
[INFO] Loading execution data file /tmp/coverage/testTitle.exec.
[INFO] Loading execution data file /tmp/coverage/testSelected.exec.
[INFO] Loading execution data file /tmp/coverage/testClickToSelect.exec.
[INFO] Loading execution data file /tmp/coverage/testClickToClose.exec.
[INFO] Loading execution data file /tmp/coverage/testThumbnail.exec.
[INFO] Analyzing 58 classes.
```
For xml and csv reports, we need to specify `--output-file` instead of `--output-dir` since
only one file will be generated as xml or csv report.
```
--format xml \
--output-file tmp/coverage_report/report.xml \
```
```
--format csv \
--output-file tmp/coverage_report/report.csv \
```
```shell
build/android/generate_jacoco_report.py \
--format xml \
--output-file tmp/coverage_report/report.xml \
--coverage-dir /tmp/coverage/ \
--sources-json-dir out/Debug/ \
```
or
```shell
build/android/generate_jacoco_report.py \
--format csv \
--output-file tmp/coverage_report/report.csv \
--coverage-dir /tmp/coverage/ \
--sources-json-dir out/Debug/ \
```
2. We can also generate JSON format report for
[coverage tool](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/code_coverage.md)
created by Chrome Ops - CATS team.
In this case, we need to change `--format` to json, for example:
```shell
build/android/generate_jacoco_report.py \
--format json \
--output-file tmp/coverage_report/coverage.json \
--coverage-dir /tmp/coverage/ \
--sources-json-dir out/Debug/ \
```
......@@ -13,6 +13,8 @@ import fnmatch
import json
import os
import sys
import tempfile
import xml.etree.ElementTree as ET
import devil_chromium
from devil.utils import cmd_helper
......@@ -53,28 +55,34 @@ def _GetFilesWithSuffix(root_dir, suffix):
return files
def _AddArguments(parser):
"""Adds arguments related to parser.
def _ParseArguments(parser):
"""Parses the command line arguments.
Args:
parser: ArgumentParser object.
Returns:
The parsed arguments.
"""
parser.add_argument(
'--format',
required=True,
choices=['html', 'xml', 'csv'],
help='Output report format. Choose one from html, xml and csv.')
choices=['html', 'xml', 'csv', 'json'],
help='Output report format. Choose one from html, xml, csv and json.'
'json format conforms to '
'//infra/appengine/findit/model/proto/code_coverage.proto')
parser.add_argument('--output-dir', help='html report output directory.')
parser.add_argument('--output-file', help='xml or csv report output file.')
parser.add_argument(
'--output-file', help='xml, csv or json report output file.')
parser.add_argument(
'--coverage-dir',
required=True,
help=('Root of the directory in which to search for '
'coverage data (.exec) files.'))
help='Root of the directory in which to search for '
'coverage data (.exec) files.')
parser.add_argument(
'--metadata-dir',
help=('Root of the directory in which to search for '
'*-filtered.jar and *__jacoco_sources.txt files.'))
'--sources-json-dir',
help='Root of the directory in which to search for '
'*__jacoco_sources.json files.')
parser.add_argument(
'--class-files',
nargs='+',
......@@ -92,90 +100,190 @@ def _AddArguments(parser):
parser.add_argument(
'--cleanup',
action='store_true',
help=('If set, removes coverage files generated at '
'runtime.'))
def main():
parser = argparse.ArgumentParser()
_AddArguments(parser)
help='If set, removes coverage files generated at '
'runtime.')
args = parser.parse_args()
if args.format == 'html':
if not args.output_dir:
parser.error('--output-dir needed for html report.')
elif not args.output_file:
parser.error('--output-file needed for xml or csv report.')
parser.error('--output-file needed for xml, csv or json report.')
if not (args.sources_json_dir or args.class_files):
parser.error('At least either --sources-json-dir or --class-files needed.')
if args.format == 'json' and not args.sources_json_dir:
parser.error('--sources-json-dir needed for json report')
return args
def _GenerateJsonCoverageMetadata(out_file_path, jacoco_xml_path, source_dirs):
"""Generates a JSON representation based on Jacoco xml report.
JSON format conforms to the proto:
//infra/appengine/findit/model/proto/code_coverage.proto
Writes the results of the coverage analysis to the file specified by
|out_file_path|.
Args:
out_file_path: A string representing the location to write JSON metadata.
jacoco_xml_path: A string representing the file path to Jacoco xml report.
source_dirs: A list of source directories of Java source files.
Raises:
Exception: No Jacoco xml report found or
cannot find package directory according to src root.
"""
if not os.path.exists(jacoco_xml_path):
raise Exception('No Jacoco xml report found on %s' % jacoco_xml_path)
data = {}
data['files'] = []
tree = ET.parse(jacoco_xml_path)
root = tree.getroot()
for package in root.iter('package'):
package_path = package.attrib['name']
print('Processing package %s' % package_path)
if not (args.metadata_dir or args.class_files):
parser.error('At least either --metadata-dir or --class-files needed.')
# Find package directory according to src root.
package_source_dir = ''
for source_dir in source_dirs:
if package_path in source_dir:
package_source_dir = source_dir
break
if not package_source_dir:
raise Exception('Cannot find package directory according to src root')
for sourcefile in package.iter('sourcefile'):
sourcefile_name = sourcefile.attrib['name']
path = os.path.join(package_source_dir, sourcefile_name)
print('Processing file %s' % path)
file_coverage = {}
file_coverage['path'] = path
file_coverage['lines'] = []
file_coverage['branches'] = []
# Calculate file's total lines.
abs_path = os.path.join(host_paths.DIR_SOURCE_ROOT, path)
if os.path.exists(abs_path):
with open(abs_path, 'r') as f:
file_coverage['total_lines'] = sum(1 for _ in f)
for line in sourcefile.iter('line'):
line_number = int(line.attrib['nr'])
covered_instructions = int(line.attrib['ci'])
missed_branches = int(line.attrib['mb'])
covered_branches = int(line.attrib['cb'])
is_branch = False
if missed_branches > 0 or covered_branches > 0:
is_branch = True
line_coverage = {}
line_coverage['first'] = line_number
line_coverage['last'] = line_number
line_coverage['count'] = covered_instructions
file_coverage['lines'].append(line_coverage)
if is_branch:
branch_coverage = {}
branch_coverage['line'] = line_number
branch_coverage['total'] = covered_branches + missed_branches
branch_coverage['covered'] = covered_branches
file_coverage['branches'].append(branch_coverage)
data['files'].append(file_coverage)
with open(out_file_path, 'w') as f:
json.dump(data, f)
def main():
parser = argparse.ArgumentParser()
args = _ParseArguments(parser)
devil_chromium.Initialize()
coverage_files = _GetFilesWithSuffix(args.coverage_dir, '.exec')
if not coverage_files:
parser.error('Found no coverage file under %s' % args.coverage_dir)
parser.error('No coverage file found under %s' % args.coverage_dir)
print('Found coverage files: %s' % str(coverage_files))
class_files = []
sources = []
if args.metadata_dir:
sources_json_files = _GetFilesWithSuffix(args.metadata_dir,
source_dirs = []
if args.sources_json_dir:
sources_json_files = _GetFilesWithSuffix(args.sources_json_dir,
_SOURCES_JSON_FILES_SUFFIX)
for f in sources_json_files:
with open(f, 'r') as json_file:
data = json.load(json_file)
class_files.append(data['input_path'])
sources.extend(data['source_dirs'])
fixed_source_paths = set()
source_dirs.extend(data['source_dirs'])
for path in sources:
# Fix source directories as direct parent of Java packages.
fixed_source_dirs = set()
for path in source_dirs:
for partial in _PARTIAL_PACKAGE_NAMES:
if partial in path:
fixed_path = os.path.join(host_paths.DIR_SOURCE_ROOT,
path[:path.index(partial)])
fixed_source_paths.add(fixed_path)
fixed_dir = os.path.join(host_paths.DIR_SOURCE_ROOT,
path[:path.index(partial)])
fixed_source_dirs.add(fixed_dir)
break
sources = list(fixed_source_paths)
if args.class_files:
class_files += args.class_files
if args.sources:
sources += args.sources
fixed_source_dirs.update(args.sources)
cmd = [
'java', '-jar',
os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'jacoco', 'lib',
'jacococli.jar'), 'report'
] + coverage_files
for f in class_files:
cmd += ['--classfiles', f]
for source in sources:
for source in fixed_source_dirs:
cmd += ['--sourcefiles', source]
if args.format == 'html':
cmd += ['--html', args.output_dir]
elif args.format == 'xml':
cmd += ['--xml', args.output_file]
else:
cmd += ['--csv', args.output_file]
exit_code = cmd_helper.RunCmd(cmd)
if args.cleanup:
for f in coverage_files:
os.remove(f)
# Command tends to exit with status 0 when it actually failed.
if not exit_code:
# For json format, xml report will be generated first temporarily
# then parsed to json metadata to --output-file.
with tempfile.NamedTemporaryFile() as temp:
if args.format == 'html':
if not os.path.exists(args.output_dir) or not os.listdir(args.output_dir):
print('No report generated at %s' % args.output_dir)
out_cmd = ['--html', args.output_dir]
elif args.format == 'xml':
out_cmd = ['--xml', args.output_file]
elif args.format == 'csv':
out_cmd = ['--csv', args.output_file]
else:
out_cmd = ['--xml', temp.name]
cmd += out_cmd
exit_code = cmd_helper.RunCmd(cmd)
if args.cleanup:
for f in coverage_files:
os.remove(f)
# Command tends to exit with status 0 when it actually failed.
if not exit_code:
if args.format == 'html':
if not os.path.exists(args.output_dir) or not os.listdir(
args.output_dir):
print('No report generated at %s' % args.output_dir)
exit_code = 1
elif not os.path.exists(out_cmd[1]):
print('No report generated at %s' % args.output_file)
exit_code = 1
elif not os.path.exists(args.output_file):
print('No report generated at %s' % args.output_file)
exit_code = 1
if args.format == 'json':
_GenerateJsonCoverageMetadata(args.output_file, temp.name, source_dirs)
return exit_code
......
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