6
6
unicode_literals , with_statement )
7
7
8
8
import functools
9
+ import os
9
10
11
+ from pants .backend .jvm .subsystems .jvm_tool_mixin import JvmToolMixin
10
12
from pants .backend .jvm .tasks .coverage .engine import CoverageEngine
13
+ from pants .base .exceptions import TaskError
14
+ from pants .java .jar .jar_dependency import JarDependency
11
15
from pants .subsystem .subsystem import Subsystem
16
+ from pants .util import desktop
17
+ from pants .util .dirutil import safe_delete , safe_mkdir
12
18
13
19
14
20
class Jacoco (CoverageEngine ):
15
21
"""Class to run coverage tests with Jacoco."""
16
22
17
- class Factory (Subsystem ):
23
+ class Factory (Subsystem , JvmToolMixin ):
18
24
options_scope = 'jacoco'
19
25
20
26
@classmethod
21
- def create (cls , settings , targets , execute_java_for_targets ):
27
+ def register_options (cls , register ):
28
+ super (Jacoco .Factory , cls ).register_options (register )
29
+
30
+ # We need to inject the jacoco agent at test runtime
31
+ cls .register_jvm_tool (register ,
32
+ 'jacoco-agent' ,
33
+ classpath = [
34
+ JarDependency (
35
+ org = 'org.jacoco' ,
36
+ name = 'org.jacoco.agent' ,
37
+ # TODO(jtrobec): get off of snapshat once jacoco release with cli is available
38
+ # see https://github.com/pantsbuild/pants/issues/5010
39
+ rev = '0.7.10-SNAPSHOT' ,
40
+ classifier = 'runtime' ,
41
+ intransitive = True )
42
+ ])
43
+
44
+ # We'll use the jacoco-cli to generate reports
45
+ cls .register_jvm_tool (register ,
46
+ 'jacoco-cli' ,
47
+ classpath = [
48
+ JarDependency (
49
+ org = 'org.jacoco' ,
50
+ name = 'org.jacoco.cli' ,
51
+ # TODO(jtrobec): get off of snapshat once jacoco release with cli is available
52
+ # see https://github.com/pantsbuild/pants/issues/5010
53
+ rev = '0.7.10-SNAPSHOT' )
54
+ ])
55
+
56
+ def create (self , settings , targets , execute_java_for_targets ):
22
57
"""
23
58
:param settings: Generic code coverage settings.
24
59
:type settings: :class:`CodeCoverageSettings`
@@ -30,9 +65,11 @@ def create(cls, settings, targets, execute_java_for_targets):
30
65
`pants.java.util.execute_java`.
31
66
"""
32
67
33
- return Jacoco (settings , targets , execute_java_for_targets )
68
+ agent_path = self .tool_jar_from_products (settings .context .products , 'jacoco-agent' , scope = 'jacoco' )
69
+ cli_path = self .tool_classpath_from_products (settings .context .products , 'jacoco-cli' , scope = 'jacoco' )
70
+ return Jacoco (settings , agent_path , cli_path , targets , execute_java_for_targets )
34
71
35
- def __init__ (self , settings , targets , execute_java_for_targets ):
72
+ def __init__ (self , settings , agent_path , cli_path , targets , execute_java_for_targets ):
36
73
"""
37
74
:param settings: Generic code coverage settings.
38
75
:type settings: :class:`CodeCoverageSettings`
@@ -44,12 +81,20 @@ def __init__(self, settings, targets, execute_java_for_targets):
44
81
`pants.java.util.execute_java`.
45
82
"""
46
83
self ._settings = settings
84
+ options = settings .options
85
+ self ._context = settings .context
47
86
self ._targets = targets
87
+ self ._coverage_targets = {t for t in targets if Jacoco .is_coverage_target (t )}
88
+ self ._agent_path = agent_path
89
+ self ._cli_path = cli_path
48
90
self ._execute_java = functools .partial (execute_java_for_targets , targets )
91
+ self ._coverage_force = options .coverage_force
92
+ self ._coverage_datafile = os .path .join (settings .coverage_dir , 'jacoco.exec' )
93
+ self ._coverage_report_dir = os .path .join (settings .coverage_dir , 'reports' )
49
94
50
95
def instrument (self ):
51
- # jacoco does runtime instrumentation, so this is a noop
52
- pass
96
+ # jacoco does runtime instrumentation, so this only does clean-up of existing run
97
+ safe_delete ( self . _coverage_datafile )
53
98
54
99
@property
55
100
def classpath_append (self ):
@@ -61,13 +106,71 @@ def classpath_prepend(self):
61
106
62
107
@property
63
108
def extra_jvm_options (self ):
64
- # TODO(jtrobec): implement code coverage using jacoco
65
- return []
109
+ agent_option = '-javaagent:{agent}=destfile={destfile}' .format (agent = self ._agent_path ,
110
+ destfile = self ._coverage_datafile )
111
+ return [agent_option ]
112
+
113
+ @staticmethod
114
+ def is_coverage_target (tgt ):
115
+ return (tgt .is_java or tgt .is_scala ) and not tgt .is_test and not tgt .is_synthetic
66
116
67
117
def report (self , execution_failed_exception = None ):
68
- # TODO(jtrobec): implement code coverage using jacoco
69
- pass
118
+ if execution_failed_exception :
119
+ self ._settings .log .warn ('Test failed: {0}' .format (execution_failed_exception ))
120
+ if self ._coverage_force :
121
+ self ._settings .log .warn ('Generating report even though tests failed, because the coverage-force flag is set.' )
122
+ else :
123
+ return
124
+
125
+ safe_mkdir (self ._coverage_report_dir , clean = True )
126
+ for report_format in ['xml' , 'csv' , 'html' ]:
127
+ target_path = os .path .join (self ._coverage_report_dir , report_format )
128
+ args = ['report' , self ._coverage_datafile ] + self ._get_target_classpaths () + self ._get_source_roots () + [
129
+ '--{report_format}={target_path}' .format (report_format = report_format ,
130
+ target_path = target_path )
131
+ ]
132
+ main = 'net.sourceforge.cobertura.reporting.ReportMain'
133
+ result = self ._execute_java (classpath = self ._cli_path ,
134
+ main = 'org.jacoco.cli.internal.Main' ,
135
+ jvm_options = self ._settings .coverage_jvm_options ,
136
+ args = args ,
137
+ workunit_factory = self ._context .new_workunit ,
138
+ workunit_name = 'jacoco-report-' + report_format )
139
+ if result != 0 :
140
+ raise TaskError ("java {0} ... exited non-zero ({1})"
141
+ " 'failed to report'" .format (main , result ))
142
+
143
+ def _get_target_classpaths (self ):
144
+ runtime_classpath = self ._context .products .get_data ('runtime_classpath' )
145
+
146
+ target_paths = []
147
+ for target in self ._coverage_targets :
148
+ paths = runtime_classpath .get_for_target (target )
149
+ for (name , path ) in paths :
150
+ target_paths .append (path )
151
+
152
+ return self ._make_multiple_arg ('--classfiles' , target_paths )
153
+
154
+ def _get_source_roots (self ):
155
+ source_roots = {t .target_base for t in self ._coverage_targets }
156
+ return self ._make_multiple_arg ('--sourcefiles' , source_roots )
157
+
158
+ def _make_multiple_arg (self , arg_name , arg_list ):
159
+ """Jacoco cli allows the specification of multiple values for certain args by repeating the argument
160
+ with a new value. E.g. --classfiles a.class --classfiles b.class, etc. This method creates a list of
161
+ strings interleaved with the arg name to satisfy that format.
162
+ """
163
+ unique_args = list (set (arg_list ))
164
+
165
+ args = [(arg_name , f ) for f in unique_args ]
166
+ flattened = list (sum (args , ()))
167
+
168
+ return flattened
70
169
71
170
def maybe_open_report (self ):
72
- # TODO(jtrobec): implement code coverage using jacoco
73
- pass
171
+ if self ._settings .coverage_open :
172
+ report_file_path = os .path .join (self ._settings .coverage_dir , 'reports/html' , 'index.html' )
173
+ try :
174
+ desktop .ui_open (report_file_path )
175
+ except desktop .OpenError as e :
176
+ raise TaskError (e )
0 commit comments