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. ...@@ -8,19 +8,19 @@ instrumentation and junit tests.
## How Jacoco coverage works ## How Jacoco coverage works
In order to use Jacoco code coverage, we need to create build time pre-instrumented 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. **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: 1. Use the following GN build arguments:
```gn ```gn
target_os = "android" target_os = "android"
jacoco_coverage = true 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 2. Run tests, with option `--coverage-dir <directory>`, to specify where to save
the .exec file. For example, you can run chrome junit tests: 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 ...@@ -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 3. The coverage results of junit and instrumentation tests will be merged
automatically if they are in the same directory. automatically if they are in the same directory.
4. Now we have generated .exec files already. We can create a html/xml/csv report ## How to generate coverage report
using `generate_jacoco_report.py`, for example:
```shell 1. Now we have generated .exec files already. We can create a Jacoco html/xml/csv
build/android/generate_jacoco_report.py \ report using `generate_jacoco_report.py`, for example:
--format html \
--output-dir tmp/coverage_report/ \ ```shell
--coverage-dir /tmp/coverage/ \ build/android/generate_jacoco_report.py \
--metadata-dir out/Debug/ \ --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: 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/testTitle.exec.
[INFO] Loading execution data file /tmp/coverage/testSelected.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/testClickToSelect.exec.
[INFO] Loading execution data file /tmp/coverage/testClickToClose.exec. [INFO] Loading execution data file /tmp/coverage/testClickToClose.exec.
[INFO] Loading execution data file /tmp/coverage/testThumbnail.exec. [INFO] Loading execution data file /tmp/coverage/testThumbnail.exec.
[INFO] Analyzing 58 classes. [INFO] Analyzing 58 classes.
``` ```
For xml and csv reports, we need to specify `--output-file` instead of `--output-dir` since 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. only one file will be generated as xml or csv report.
``` ```shell
--format xml \ build/android/generate_jacoco_report.py \
--output-file tmp/coverage_report/report.xml \ --format xml \
``` --output-file tmp/coverage_report/report.xml \
--coverage-dir /tmp/coverage/ \
``` --sources-json-dir out/Debug/ \
--format csv \ ```
--output-file tmp/coverage_report/report.csv \
``` 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 ...@@ -13,6 +13,8 @@ import fnmatch
import json import json
import os import os
import sys import sys
import tempfile
import xml.etree.ElementTree as ET
import devil_chromium import devil_chromium
from devil.utils import cmd_helper from devil.utils import cmd_helper
...@@ -53,28 +55,34 @@ def _GetFilesWithSuffix(root_dir, suffix): ...@@ -53,28 +55,34 @@ def _GetFilesWithSuffix(root_dir, suffix):
return files return files
def _AddArguments(parser): def _ParseArguments(parser):
"""Adds arguments related to parser. """Parses the command line arguments.
Args: Args:
parser: ArgumentParser object. parser: ArgumentParser object.
Returns:
The parsed arguments.
""" """
parser.add_argument( parser.add_argument(
'--format', '--format',
required=True, required=True,
choices=['html', 'xml', 'csv'], choices=['html', 'xml', 'csv', 'json'],
help='Output report format. Choose one from html, xml and csv.') 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-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( parser.add_argument(
'--coverage-dir', '--coverage-dir',
required=True, required=True,
help=('Root of the directory in which to search for ' help='Root of the directory in which to search for '
'coverage data (.exec) files.')) 'coverage data (.exec) files.')
parser.add_argument( parser.add_argument(
'--metadata-dir', '--sources-json-dir',
help=('Root of the directory in which to search for ' help='Root of the directory in which to search for '
'*-filtered.jar and *__jacoco_sources.txt files.')) '*__jacoco_sources.json files.')
parser.add_argument( parser.add_argument(
'--class-files', '--class-files',
nargs='+', nargs='+',
...@@ -92,90 +100,190 @@ def _AddArguments(parser): ...@@ -92,90 +100,190 @@ def _AddArguments(parser):
parser.add_argument( parser.add_argument(
'--cleanup', '--cleanup',
action='store_true', action='store_true',
help=('If set, removes coverage files generated at ' help='If set, removes coverage files generated at '
'runtime.')) 'runtime.')
def main():
parser = argparse.ArgumentParser()
_AddArguments(parser)
args = parser.parse_args() args = parser.parse_args()
if args.format == 'html': if args.format == 'html':
if not args.output_dir: if not args.output_dir:
parser.error('--output-dir needed for html report.') parser.error('--output-dir needed for html report.')
elif not args.output_file: 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): # Find package directory according to src root.
parser.error('At least either --metadata-dir or --class-files needed.') 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() devil_chromium.Initialize()
coverage_files = _GetFilesWithSuffix(args.coverage_dir, '.exec') coverage_files = _GetFilesWithSuffix(args.coverage_dir, '.exec')
if not coverage_files: 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)) print('Found coverage files: %s' % str(coverage_files))
class_files = [] class_files = []
sources = [] source_dirs = []
if args.metadata_dir: if args.sources_json_dir:
sources_json_files = _GetFilesWithSuffix(args.metadata_dir, sources_json_files = _GetFilesWithSuffix(args.sources_json_dir,
_SOURCES_JSON_FILES_SUFFIX) _SOURCES_JSON_FILES_SUFFIX)
for f in sources_json_files: for f in sources_json_files:
with open(f, 'r') as json_file: with open(f, 'r') as json_file:
data = json.load(json_file) data = json.load(json_file)
class_files.append(data['input_path']) class_files.append(data['input_path'])
sources.extend(data['source_dirs']) source_dirs.extend(data['source_dirs'])
fixed_source_paths = set()
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: for partial in _PARTIAL_PACKAGE_NAMES:
if partial in path: if partial in path:
fixed_path = os.path.join(host_paths.DIR_SOURCE_ROOT, fixed_dir = os.path.join(host_paths.DIR_SOURCE_ROOT,
path[:path.index(partial)]) path[:path.index(partial)])
fixed_source_paths.add(fixed_path) fixed_source_dirs.add(fixed_dir)
break break
sources = list(fixed_source_paths)
if args.class_files: if args.class_files:
class_files += args.class_files class_files += args.class_files
if args.sources: if args.sources:
sources += args.sources fixed_source_dirs.update(args.sources)
cmd = [ cmd = [
'java', '-jar', 'java', '-jar',
os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'jacoco', 'lib', os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party', 'jacoco', 'lib',
'jacococli.jar'), 'report' 'jacococli.jar'), 'report'
] + coverage_files ] + coverage_files
for f in class_files: for f in class_files:
cmd += ['--classfiles', f] cmd += ['--classfiles', f]
for source in sources: for source in fixed_source_dirs:
cmd += ['--sourcefiles', source] 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 json format, xml report will be generated first temporarily
for f in coverage_files: # then parsed to json metadata to --output-file.
os.remove(f) with tempfile.NamedTemporaryFile() as temp:
# Command tends to exit with status 0 when it actually failed.
if not exit_code:
if args.format == 'html': if args.format == 'html':
if not os.path.exists(args.output_dir) or not os.listdir(args.output_dir): out_cmd = ['--html', args.output_dir]
print('No report generated at %s' % 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 exit_code = 1
elif not os.path.exists(args.output_file):
print('No report generated at %s' % args.output_file) if args.format == 'json':
exit_code = 1 _GenerateJsonCoverageMetadata(args.output_file, temp.name, source_dirs)
return exit_code 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