Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoverage work - add scoverage to OSS pants #8064

Merged
merged 37 commits into from
Jul 27, 2019
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3ef8021
adding scoverage 1/2
Jul 17, 2019
705a12b
adding scoverage 1/2
Jul 17, 2019
5ea8fc5
adding scoverage 1/2
Jul 17, 2019
fc48b64
removing scoverage target from BUILD.tools
Jul 18, 2019
21a5dd4
Update src/python/pants/backend/jvm/subsystems/scala_coverage_platfor…
sammy-1234 Jul 18, 2019
2429357
removing futures
Jul 18, 2019
f432d7b
changing name to scoverage_platform
Jul 19, 2019
c7f560a
adding scoverage dependency for unit tests to pass
Jul 20, 2019
c427e21
Scoverage (2/2) - adding report generator
Jul 20, 2019
324f59d
changing report generator's name
Jul 20, 2019
1a7e8fc
scoverage report generator
Jul 23, 2019
739032a
adding scoverage report gen
Jul 23, 2019
fc4ed94
testing with new report gen local publish
Jul 23, 2019
3212da7
changing scoverage to work with new reportGen
Jul 23, 2019
f9561dc
relocatiing modifications to scala library
Jul 23, 2019
3d913e3
playing with new report gen
Jul 24, 2019
3209a9f
adding scoverage runtime environment along with target filtering and …
Jul 24, 2019
3cd1b8b
removing report generator from here
Jul 24, 2019
3099ac7
Merge branch 'master' into scoverageWork
sammy-1234 Jul 24, 2019
66ace61
resolving unwanted file changes
Jul 24, 2019
3ff84cc
Merge branch 'scoverageWork' of https://github.com/sammy-1234/pants i…
Jul 24, 2019
a47ef75
minor changes to alignment
Jul 24, 2019
7bacd7b
minor changes to alignment
Jul 24, 2019
d9a94e1
minor changes to alignment
Jul 24, 2019
37295d6
adding log statements
Jul 25, 2019
f390ee4
improving test file for blacklisted targets
Jul 25, 2019
b15f45d
adding scoverage dependency to test_rsc_compile to resolve unit test …
Jul 25, 2019
ae22fed
adding extra args in constructor rather than as a property
Jul 25, 2019
e5b0a56
removing target's scoverage instance
Jul 25, 2019
12430dd
fixing blacklisted target specification
Jul 26, 2019
04065ac
cleaning checking of blacklisted targets; fixing scoverage test
Jul 26, 2019
e13edd8
removing option; removing target spec as an option
Jul 26, 2019
4e1b5c8
init change in scala_library; linting
Jul 26, 2019
02435cc
fixing lint again
Jul 26, 2019
ed7d4b0
trying to fix lint in test_scoverage_platform
Jul 27, 2019
7acd82c
changing args indentation in scoverage.py
Jul 27, 2019
d6a2dbc
running git hooks successfully
Jul 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/python/pants/backend/jvm/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pants.backend.jvm.scala_artifact import ScalaArtifact
from pants.backend.jvm.subsystems.jar_dependency_management import JarDependencyManagementSetup
from pants.backend.jvm.subsystems.scala_platform import ScalaPlatform
from pants.backend.jvm.subsystems.scoverage_platform import ScoveragePlatform
from pants.backend.jvm.subsystems.shader import Shading
from pants.backend.jvm.targets.annotation_processor import AnnotationProcessor
from pants.backend.jvm.targets.benchmark import Benchmark
Expand Down Expand Up @@ -138,7 +139,7 @@ def build_file_aliases():


def global_subsystems():
return (ScalaPlatform,)
return (ScalaPlatform, ScoveragePlatform, )


# TODO https://github.com/pantsbuild/pants/issues/604 register_goals
Expand Down
11 changes: 11 additions & 0 deletions src/python/pants/backend/jvm/subsystems/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ python_library(
],
)

python_library(
name = 'scoverage_platform',
sources = ['scoverage_platform.py'],
dependencies = [
'src/python/pants/option',
'src/python/pants/subsystem',
'src/python/pants/java/jar',
'src/python/pants/backend/jvm/targets:jvm',
],
)

python_library(
name = 'shader',
sources = ['shader.py'],
Expand Down
95 changes: 95 additions & 0 deletions src/python/pants/backend/jvm/subsystems/scoverage_platform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import logging
import os
from pants.build_graph.injectables_mixin import InjectablesMixin
from pants.subsystem.subsystem import Subsystem
from pants.java.jar.jar_dependency import JarDependency
from pants.backend.jvm.targets.jar_library import JarLibrary
from pants.build_graph.address import Address

logger = logging.getLogger(__name__)

SCOVERAGE = "scoverage"

class ScoveragePlatform(InjectablesMixin, Subsystem):
"""The scoverage platform."""

options_scope = 'scoverage'

@classmethod
def register_options(cls, register):
super().register_options(register)
register('--enable-scoverage',
default=False,
type=bool,
help='Specifies whether to generate scoverage reports for scala test targets.'
stuhood marked this conversation as resolved.
Show resolved Hide resolved
'Default value is False. If True,'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines need spaces in them in order to avoid running together.

'implies --test-junit-coverage-processor=scoverage.')

register('--blacklist-file',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pants supports @file options natively, so you should make this option a list option:

register('--blacklist-targets',
  type=list,
  member_type=target_option,
  ..
)

And then someone who wants to put the list of targets in a file (in python list syntax) can pass --blacklist-targets=@some-blacklist-file.

You could also pass in a (string) list of regexes to match against target names.

type=str,
help='Path to files containing targets not to be instrumented.')

register('--scoverage-target-path',
default='//:scoverage',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this target is bootstrapped by default. If a user overrides this, the default //:scoverage target is still injected into the build graph.

type=str,
help='Path to the scoverage dependency.')

def __init__(self, *args, **kwargs):
super(ScoveragePlatform, self).__init__(*args, **kwargs)

# Setting up the scoverage blacklist files which contains targets
# not to be instrumented. Since the file is not expected to be really big,
# would it be ok to store it in memory?
if (self.get_options().blacklist_file and
os.path.exists(self.get_options().blacklist_file)):
self._blacklist_file_contents = open(self.get_options().blacklist_file).read()
else:
self._blacklist_file_contents = None

def scoverage_jar(self):
return [JarDependency(org='com.twitter.scoverage', name='scalac-scoverage-plugin_2.12',
rev='1.0.1-twitter'),
JarDependency(org='com.twitter.scoverage', name='scalac-scoverage-runtime_2.12',
rev='1.0.1-twitter')]

def injectables(self, build_graph):
specs_to_create = [
('scoverage', self.scoverage_jar),
]

for spec_key, create_jardep_func in specs_to_create:
spec = self.injectables_spec_for_key(spec_key)
target_address = Address.parse(spec)
if not build_graph.contains_address(target_address):
target_jars = create_jardep_func()
jars = target_jars if isinstance(target_jars, list) else [target_jars]
build_graph.inject_synthetic_target(target_address,
JarLibrary,
jars=jars,
scope='forced')
elif not build_graph.get_target(target_address).is_synthetic:
raise build_graph.ManualSyntheticTargetError(target_address)

@property
def injectables_spec_mapping(self):
return {
'scoverage': [f"{self.get_options().scoverage_target_path}"],
}


def is_blacklisted(self, target) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blacklist should be cached in-memory, currently the file is read per target.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you switch this to a list option (as above) that will be handled automatically.

"""
Checks if the [target] is blacklisted or not.
"""
# File not specified
if not self._blacklist_file_contents:
return False

if target.address.spec in self._blacklist_file_contents:
logger.warning(f"{target.address.spec} found in blacklist, not instrumented.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be at debug or info rather than warn.

return True
else:
return False
1 change: 1 addition & 0 deletions src/python/pants/backend/jvm/targets/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ python_library(
'3rdparty/python/twitter/commons:twitter.common.collections',
'src/python/pants/java/jar',
'src/python/pants/backend/jvm/subsystems:scala_platform',
'src/python/pants/backend/jvm/subsystems:scoverage_platform',
'src/python/pants/base:exceptions',
'src/python/pants/base:validation',
'src/python/pants/build_graph',
Expand Down
68 changes: 67 additions & 1 deletion src/python/pants/backend/jvm/targets/scala_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from pants.backend.jvm.subsystems.scala_platform import ScalaPlatform
from pants.backend.jvm.subsystems.scoverage_platform import ScoveragePlatform
from pants.backend.jvm.targets.exportable_jvm_library import ExportableJvmLibrary
from pants.backend.jvm.targets.junit_tests import JUnitTests
from pants.base.exceptions import TargetDefinitionException
from pants.base.payload import Payload
from pants.base.payload_field import PrimitiveField
from pants.build_graph.address import Address

SCOVERAGE = "scoverage"

class ScalaLibrary(ExportableJvmLibrary):
"""A Scala library.
Expand All @@ -25,9 +27,11 @@ class ScalaLibrary(ExportableJvmLibrary):
default_sources_globs = '*.scala'
default_sources_exclude_globs = JUnitTests.scala_test_globs



@classmethod
def subsystems(cls):
return super().subsystems() + (ScalaPlatform, )
return super().subsystems() + (ScalaPlatform, ScoveragePlatform,)

def __init__(self, java_sources=None, payload=None, **kwargs):
"""
Expand All @@ -47,6 +51,8 @@ def __init__(self, java_sources=None, payload=None, **kwargs):
})
super().__init__(payload=payload, **kwargs)

self._scoverage_instance = ScoveragePlatform.global_instance()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target itself doesn't need to hold a reference to _scoverage_instance: can get it lazily from the singleton.


@classmethod
def compute_injectable_specs(cls, kwargs=None, payload=None):
for spec in super().compute_injectable_specs(kwargs, payload):
Expand All @@ -65,6 +71,10 @@ def compute_dependency_specs(cls, kwargs=None, payload=None):
for spec in ScalaPlatform.global_instance().injectables_specs_for_key('scala-library'):
yield spec

if ScoveragePlatform.global_instance().get_options().enable_scoverage:
for spec in ScoveragePlatform.global_instance().injectables_specs_for_key('scoverage'):
yield spec

def get_jar_dependencies(self):
for jar in super().get_jar_dependencies():
yield jar
Expand All @@ -82,3 +92,59 @@ def java_sources(self):
raise TargetDefinitionException(self, 'No such java target: {}'.format(spec))
targets.append(target)
return targets

@property
def scalac_plugins(self):
"""The names of compiler plugins to use when compiling this target with scalac.
:return: See constructor.
:rtype: list of strings.
"""
if self._scoverage_instance.get_options().enable_scoverage:
# Prevent instrumenting generated targets and targets in blacklist.
if self.identifier.startswith(".pants.d.gen") or self._scoverage_instance.is_blacklisted(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than checking for .pants.d, you should use if not self.is_synthetic. As below, it would be good to do this in the target constructor though.

Copy link
Contributor Author

@sammy-1234 sammy-1234 Jul 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does doing this in the constructor means checking kwargs['address'] in kwargs['build_graph'].synthetic_addresses? I tried that, however, it is still instrumenting those targets. I think what is happening is that the current synthetic target is not added to the build_graph.synthetic_addresses until __init__ is called.

return self.payload.scalac_plugins

scalac_plugins = self.payload.scalac_plugins
if scalac_plugins:
scalac_plugins.append(SCOVERAGE)
else:
scalac_plugins = [SCOVERAGE]
return scalac_plugins

return self.payload.scalac_plugins

@property
def scalac_plugin_args(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be better decoupling here rather than checking if self._scoverage,
possible suggestion could be something similar to: (plugins, may not not need to be passed in but could be a class variable e.g.)

def scalac_plugin_args(self, plugins):
for plugin in plugins:
  # supplement args

"""Map from scalac plugin name to list of args for that plugin.
:return: See constructor.
:rtype: map from string to list of strings.
"""
if self._scoverage_instance.get_options().enable_scoverage:
scalac_plugin_args = self.payload.scalac_plugin_args
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These options should be added to the plugin args in the constructor, rather than here. That would mean that they end up in the payload, and end up in the fingerprint for the target.

if scalac_plugin_args:
scalac_plugin_args.update(
{"scoverage": ["writeToClasspath:true", f"dataDir:{self.identifier}"]})
else:
scalac_plugin_args = {
"scoverage": ["writeToClasspath:true", f"dataDir:{self.identifier}"]
}
return scalac_plugin_args

return self.payload.scalac_plugin_args

@property
def compiler_option_sets(self):
"""For every element in this list, enable the corresponding flags on compilation
of targets.
:return: See constructor.
:rtype: list
"""
if self._scoverage_instance.get_options().enable_scoverage:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto doing this in the constructor.

compiler_option_sets = self.payload.compiler_option_sets
if compiler_option_sets:
list(compiler_option_sets).append(SCOVERAGE)
else:
compiler_option_sets = [SCOVERAGE]
return tuple(compiler_option_sets)

return self.payload.compiler_option_sets
40 changes: 32 additions & 8 deletions src/python/pants/backend/jvm/tasks/coverage/manager.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import logging
import os
import shutil

from pants.backend.jvm.subsystems.scoverage_platform import ScoveragePlatform
from pants.backend.jvm.tasks.coverage.cobertura import Cobertura
from pants.backend.jvm.tasks.coverage.scoverage import Scoverage
from pants.backend.jvm.tasks.coverage.engine import NoCoverage
from pants.backend.jvm.tasks.coverage.jacoco import Jacoco
from pants.subsystem.subsystem import Subsystem
from pants.util.dirutil import safe_mkdir
from pants.util.strutil import safe_shlex_split

logger = logging.getLogger(__name__)

class CodeCoverageSettings:
"""A class containing settings for code coverage tasks."""
Expand Down Expand Up @@ -57,21 +61,24 @@ class CodeCoverage(Subsystem):

@classmethod
def subsystem_dependencies(cls):
return super().subsystem_dependencies() + (Cobertura.Factory, Jacoco.Factory)
return super().subsystem_dependencies() + (Cobertura.Factory, Jacoco.Factory, Scoverage.Factory)

# TODO(jtrobec): move these to subsystem scope after deprecating
@staticmethod
def register_junit_options(register, register_jvm_tool):
register('--coverage', type=bool, fingerprint=True, help='Collect code coverage data.')
register('--coverage-processor', advanced=True, fingerprint=True,
choices=['cobertura', 'jacoco'], default=None,
choices=['cobertura', 'jacoco', 'scoverage'], default=None,
help="Which coverage processor to use if --coverage is enabled. If this option is "
"unset but coverage is enabled implicitly or explicitly, defaults to 'cobertura'."
"If this option is explicitly set, implies --coverage.")
"unset but coverage is enabled implicitly or explicitly, defaults to 'cobertura'. "
"If this option is explicitly set, implies --coverage. If this option is set to "
"scoverage, then first scoverage MUST be enabled by passing option "
"--scoverage-enable-scoverage.")
# We need to fingerprint this even though it nominally UI-only affecting option since the
# presence of this option alone can implicitly flag on `--coverage`.
register('--coverage-open', type=bool, fingerprint=True,
help='Open the generated HTML coverage report in a browser. Implies --coverage.')
help='Open the generated HTML coverage report in a browser. Implies --coverage '
'with cobertura as the engine.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason that this can't be applied for scoverage as well? But also, it's supported by both jacoco and cobertura.

Copy link
Contributor Author

@sammy-1234 sammy-1234 Jul 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I just modified the comment here because if a user passes JUST --test-junit-coverage-open, then coverage is implied but with cobertura as a default - just making this fact explicit. I can change it back if it doesn't help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it didn't make much sense. Changed it back.


register('--coverage-jvm-options', advanced=True, type=list, fingerprint=True,
help='JVM flags to be added when running the coverage processor. For example: '
Expand All @@ -89,12 +96,29 @@ class InvalidCoverageEngine(Exception):

def get_coverage_engine(self, task, output_dir, all_targets, execute_java):
options = task.get_options()
if options.coverage or options.coverage_processor or options.is_flagged('coverage_open'):
enable_scoverage = ScoveragePlatform.global_instance().get_options().enable_scoverage
processor = options.coverage_processor
sammy-1234 marked this conversation as resolved.
Show resolved Hide resolved

if (processor == 'scoverage' and not enable_scoverage):
raise self.InvalidCoverageEngine("Cannot set processor to scoverage without first enabling "
"scoverage (by passing --scoverage-enable-scoverage option)")

if enable_scoverage:
if processor is None:
logger.info("Scoverage is enabled. Setting coverage engine to scoverage.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to log here I don't think.

elif processor != 'scoverage':
logger.warning(f"Scoverage is enabled. Cannot use {processor} as the engine. Setting "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this should just be an error asking someone to set the coverage processor to match. Having multiple flags is confusing, and allowing them to disagree is even more confusing.

f"coverage engine to scoverage.")
processor = 'scoverage'

if options.coverage or processor or options.is_flagged('coverage_open'):
settings = CodeCoverageSettings.from_task(task, workdir=output_dir)
if options.coverage_processor in ('cobertura', None):
if processor in ('cobertura', None):
return Cobertura.Factory.global_instance().create(settings, all_targets, execute_java)
elif options.coverage_processor == 'jacoco':
elif processor == 'jacoco':
return Jacoco.Factory.global_instance().create(settings, all_targets, execute_java)
elif processor == 'scoverage':
return Scoverage.Factory.global_instance().create(settings, all_targets, execute_java)
else:
# NB: We should never get here since the `--coverage-processor` is restricted by `choices`,
# but for clarity.
Expand Down
Loading