forked from pantsbuild/pants
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Isolating much of the coverage code from junit running.
This patch has no functional changes, being just a reorganization of existing coverage code. Isolating coverage code from junit_run will make future refactors (i.e. separating tasks) much easier to deal with, in addition to making code easier to read/understand. There's still left-over cruft in junit_run that will go away (e.g. TaskExports) but this seemed like a good place to break-up the effort. Testing Done: Local runs of junit_run UTs and integration tests. Manual testing of emma, cobertura, and no coverage. Bugs closed: 2375 Reviewed at https://rbcommons.com/s/twitter/r/2973/
- Loading branch information
1 parent
bf5e1d8
commit aacb395
Showing
6 changed files
with
467 additions
and
399 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# coding=utf-8 | ||
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import (absolute_import, division, generators, nested_scopes, print_function, | ||
unicode_literals, with_statement) | ||
|
||
import os | ||
import shutil | ||
from abc import ABCMeta, abstractmethod, abstractproperty | ||
|
||
from pants.backend.jvm.tasks.classpath_util import ClasspathUtil | ||
from pants.util.dirutil import safe_mkdir | ||
from pants.util.strutil import safe_shlex_split | ||
|
||
|
||
class Coverage(object): | ||
"""Base class for emma-like coverage processors. Do not instantiate.""" | ||
__metaclass__ = ABCMeta | ||
|
||
@classmethod | ||
def register_options(cls, register, register_jvm_tool): | ||
register('--coverage-patterns', advanced=True, action='append', | ||
help='Restrict coverage measurement. Values are class name prefixes in dotted form ' | ||
'with ? and * wildcards. If preceded with a - the pattern is excluded. For ' | ||
'example, to include all code in org.pantsbuild.raven except claws and the eye ' | ||
'you would use: {flag}=org.pantsbuild.raven.* {flag}=-org.pantsbuild.raven.claw ' | ||
'{flag}=-org.pantsbuild.raven.Eye.'.format(flag='--coverage_patterns')) | ||
register('--coverage-jvm-options', advanced=True, action='append', | ||
help='JVM flags to be added when running the coverage processor. For example: ' | ||
'{flag}=-Xmx4g {flag}=-XX:MaxPermSize=1g'.format(flag='--coverage-jvm-options')) | ||
register('--coverage-open', action='store_true', | ||
help='Open the generated HTML coverage report in a browser. Implies --coverage.') | ||
register('--coverage-force', advanced=True, action='store_true', | ||
help='Attempt to run the reporting phase of coverage even if tests failed ' | ||
'(defaults to False, as otherwise the coverage results would be unreliable).') | ||
|
||
def __init__(self, task_exports, context): | ||
options = task_exports.task_options | ||
self._task_exports = task_exports | ||
self._context = context | ||
self._coverage = options.coverage | ||
self._coverage_filters = options.coverage_patterns or [] | ||
|
||
self._coverage_jvm_options = [] | ||
for jvm_option in options.coverage_jvm_options: | ||
self._coverage_jvm_options.extend(safe_shlex_split(jvm_option)) | ||
|
||
self._coverage_dir = os.path.join(task_exports.workdir, 'coverage') | ||
self._coverage_instrument_dir = os.path.join(self._coverage_dir, 'classes') | ||
# TODO(ji): These may need to be transferred down to the Emma class, as the suffixes | ||
# may be emma-specific. Resolve when we also provide cobertura support. | ||
self._coverage_metadata_file = os.path.join(self._coverage_dir, 'coverage.em') | ||
self._coverage_file = os.path.join(self._coverage_dir, 'coverage.ec') | ||
self._coverage_console_file = os.path.join(self._coverage_dir, 'coverage.txt') | ||
self._coverage_xml_file = os.path.join(self._coverage_dir, 'coverage.xml') | ||
self._coverage_html_file = os.path.join(self._coverage_dir, 'html', 'index.html') | ||
self._coverage_open = options.coverage_open | ||
self._coverage_force = options.coverage_force | ||
|
||
@abstractmethod | ||
def instrument(self, targets, tests, compute_junit_classpath, execute_java_for_targets): | ||
pass | ||
|
||
@abstractmethod | ||
def report(self, targets, tests, execute_java_for_targets, tests_failed_exception): | ||
pass | ||
|
||
@abstractproperty | ||
def classpath_prepend(self): | ||
pass | ||
|
||
@abstractproperty | ||
def classpath_append(self): | ||
pass | ||
|
||
@abstractproperty | ||
def extra_jvm_options(self): | ||
pass | ||
|
||
# Utility methods, called from subclasses | ||
def is_coverage_target(self, tgt): | ||
return (tgt.is_java or tgt.is_scala) and not tgt.is_test and not tgt.is_codegen | ||
|
||
def get_coverage_patterns(self, targets): | ||
if self._coverage_filters: | ||
return self._coverage_filters | ||
else: | ||
classes_under_test = set() | ||
classpath_products = self._context.products.get_data('runtime_classpath') | ||
|
||
def add_sources_under_test(tgt): | ||
if self.is_coverage_target(tgt): | ||
contents = ClasspathUtil.classpath_contents( | ||
(tgt,), | ||
classpath_products, | ||
confs=self._task_exports.confs, | ||
transitive=False) | ||
for f in contents: | ||
clsname = ClasspathUtil.classname_for_rel_classfile(f) | ||
if clsname: | ||
classes_under_test.add(clsname) | ||
|
||
for target in targets: | ||
target.walk(add_sources_under_test) | ||
return classes_under_test | ||
|
||
def initialize_instrument_classpath(self, targets): | ||
"""Clones the existing runtime_classpath and corresponding binaries to instrumentation specific | ||
paths. | ||
:param targets: the targets which should be mutated. | ||
:returns the instrument_classpath ClasspathProducts containing the mutated paths. | ||
""" | ||
safe_mkdir(self._coverage_instrument_dir, clean=True) | ||
|
||
runtime_classpath = self._context.products.get_data('runtime_classpath') | ||
self._context.products.safe_create_data('instrument_classpath', runtime_classpath.copy) | ||
instrumentation_classpath = self._context.products.get_data('instrument_classpath') | ||
|
||
for target in targets: | ||
if not self.is_coverage_target(target): | ||
continue | ||
paths = instrumentation_classpath.get_for_target(target, False) | ||
for (config, path) in paths: | ||
# there are two sorts of classpath entries we see in the compile classpath: jars and dirs | ||
# the branches below handle the cloning of those respectively. | ||
if os.path.isfile(path): | ||
shutil.copy2(path, self._coverage_instrument_dir) | ||
new_path = os.path.join(self._coverage_instrument_dir, os.path.basename(path)) | ||
else: | ||
files = os.listdir(path) | ||
for file in files: | ||
shutil.copy2(file, self._coverage_instrument_dir) | ||
new_path = self._coverage_instrument_dir | ||
|
||
instrumentation_classpath.remove_for_target(target, [(config, path)]) | ||
instrumentation_classpath.add_for_target(target, [(config, new_path)]) | ||
self._context.log.debug( | ||
"runtime_classpath ({}) mutated to instrument_classpath ({})".format(path, new_path)) | ||
return instrumentation_classpath |
166 changes: 166 additions & 0 deletions
166
src/python/pants/backend/jvm/tasks/coverage/cobertura.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# coding=utf-8 | ||
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import (absolute_import, division, generators, nested_scopes, print_function, | ||
unicode_literals, with_statement) | ||
|
||
import os | ||
from collections import defaultdict | ||
|
||
from twitter.common.collections import OrderedSet | ||
|
||
from pants.backend.jvm.targets.jar_dependency import JarDependency | ||
from pants.backend.jvm.tasks.coverage.base import Coverage | ||
from pants.base.build_environment import get_buildroot | ||
from pants.base.exceptions import TaskError | ||
from pants.binaries import binary_util | ||
from pants.util.dirutil import relativize_paths, safe_delete, safe_mkdir, touch | ||
|
||
|
||
class Cobertura(Coverage): | ||
"""Class to run coverage tests with cobertura.""" | ||
|
||
@classmethod | ||
def register_options(cls, register, register_jvm_tool): | ||
slf4j_jar = JarDependency(org='org.slf4j', name='slf4j-simple', rev='1.7.5') | ||
|
||
register('--coverage-cobertura-include-classes', advanced=True, action='append', | ||
help='Regex patterns passed to cobertura specifying which classes should be ' | ||
'instrumented. (see the "includeclasses" element description here: ' | ||
'https://github.com/cobertura/cobertura/wiki/Ant-Task-Reference)') | ||
|
||
register('--coverage-cobertura-exclude-classes', advanced=True, action='append', | ||
help='Regex patterns passed to cobertura specifying which classes should NOT be ' | ||
'instrumented. (see the "excludeclasses" element description here: ' | ||
'https://github.com/cobertura/cobertura/wiki/Ant-Task-Reference') | ||
|
||
def cobertura_jar(**kwargs): | ||
return JarDependency(org='net.sourceforge.cobertura', name='cobertura', rev='2.1.1', **kwargs) | ||
|
||
# The Cobertura jar needs all its dependencies when instrumenting code. | ||
register_jvm_tool(register, | ||
'cobertura-instrument', | ||
classpath=[ | ||
cobertura_jar(), | ||
slf4j_jar | ||
]) | ||
|
||
# Instrumented code needs cobertura.jar in the classpath to run, but not most of the | ||
# dependencies. | ||
register_jvm_tool(register, | ||
'cobertura-run', | ||
classpath=[ | ||
cobertura_jar(intransitive=True), | ||
slf4j_jar | ||
]) | ||
|
||
register_jvm_tool(register, 'cobertura-report', classpath=[cobertura_jar()]) | ||
|
||
def __init__(self, task_exports, context): | ||
super(Cobertura, self).__init__(task_exports, context) | ||
options = task_exports.task_options | ||
self._coverage_datafile = os.path.join(self._coverage_dir, 'cobertura.ser') | ||
touch(self._coverage_datafile) | ||
self._rootdirs = defaultdict(OrderedSet) | ||
self._include_classes = options.coverage_cobertura_include_classes | ||
self._exclude_classes = options.coverage_cobertura_exclude_classes | ||
self._nothing_to_instrument = True | ||
|
||
def instrument(self, targets, tests, compute_junit_classpath, execute_java_for_targets): | ||
instrumentation_classpath = self.initialize_instrument_classpath(targets) | ||
junit_classpath = compute_junit_classpath() | ||
cobertura_cp = self._task_exports.tool_classpath('cobertura-instrument') | ||
aux_classpath = os.pathsep.join(relativize_paths(junit_classpath, get_buildroot())) | ||
safe_delete(self._coverage_datafile) | ||
files_to_instrument = [] | ||
for target in targets: | ||
if self.is_coverage_target(target): | ||
paths = instrumentation_classpath.get_for_target(target, False) | ||
for (name, path) in paths: | ||
files_to_instrument.append(path) | ||
|
||
if len(files_to_instrument) > 0: | ||
self._nothing_to_instrument = False | ||
args = [ | ||
'--datafile', | ||
self._coverage_datafile, | ||
'--auxClasspath', | ||
aux_classpath, | ||
] | ||
# apply class incl/excl filters | ||
if len(self._include_classes) > 0: | ||
for pattern in self._include_classes: | ||
args += ["--includeClasses", pattern] | ||
else: | ||
args += ["--includeClasses", '.*'] # default to instrumenting all classes | ||
for pattern in self._exclude_classes: | ||
args += ["--excludeClasses", pattern] | ||
|
||
args += files_to_instrument | ||
|
||
main = 'net.sourceforge.cobertura.instrument.InstrumentMain' | ||
self._context.log.debug( | ||
"executing cobertura instrumentation with the following args: {}".format(args)) | ||
result = execute_java_for_targets(targets, | ||
classpath=cobertura_cp, | ||
main=main, | ||
jvm_options=self._coverage_jvm_options, | ||
args=args, | ||
workunit_factory=self._context.new_workunit, | ||
workunit_name='cobertura-instrument') | ||
if result != 0: | ||
raise TaskError("java {0} ... exited non-zero ({1})" | ||
" 'failed to instrument'".format(main, result)) | ||
|
||
@property | ||
def classpath_append(self): | ||
return () | ||
|
||
@property | ||
def classpath_prepend(self): | ||
return self._task_exports.tool_classpath('cobertura-run') | ||
|
||
@property | ||
def extra_jvm_options(self): | ||
return ['-Dnet.sourceforge.cobertura.datafile=' + self._coverage_datafile] | ||
|
||
def report(self, targets, tests, execute_java_for_targets, tests_failed_exception=None): | ||
if self._nothing_to_instrument: | ||
self._context.log.warn('Nothing found to instrument, skipping report...') | ||
return | ||
if tests_failed_exception: | ||
self._context.log.warn('Test failed: {0}'.format(tests_failed_exception)) | ||
if self._coverage_force: | ||
self._context.log.warn('Generating report even though tests failed.') | ||
else: | ||
return | ||
cobertura_cp = self._task_exports.tool_classpath('cobertura-report') | ||
source_roots = { t.target_base for t in targets if self.is_coverage_target(t) } | ||
for report_format in ['xml', 'html']: | ||
report_dir = os.path.join(self._coverage_dir, report_format) | ||
safe_mkdir(report_dir, clean=True) | ||
args = list(source_roots) | ||
args += [ | ||
'--datafile', | ||
self._coverage_datafile, | ||
'--destination', | ||
report_dir, | ||
'--format', | ||
report_format, | ||
] | ||
main = 'net.sourceforge.cobertura.reporting.ReportMain' | ||
result = execute_java_for_targets(targets, | ||
classpath=cobertura_cp, | ||
main=main, | ||
jvm_options=self._coverage_jvm_options, | ||
args=args, | ||
workunit_factory=self._context.new_workunit, | ||
workunit_name='cobertura-report-' + report_format) | ||
if result != 0: | ||
raise TaskError("java {0} ... exited non-zero ({1})" | ||
" 'failed to report'".format(main, result)) | ||
|
||
if self._coverage_open: | ||
coverage_html_file = os.path.join(self._coverage_dir, 'html', 'index.html') | ||
binary_util.ui_open(coverage_html_file) |
Oops, something went wrong.