-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
plugin.rb
218 lines (184 loc) · 8.14 KB
/
plugin.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# frozen_string_literal: true
require 'jacoco/sax_parser'
module Danger
# Verify code coverage inside your projects
# This is done using the jacoco output
# Results are passed out as a table in markdown
#
# @example Verify coverage
# jacoco.minimum_project_coverage_percentage = 50
#
# @example Verify coverage per package
# jacoco.minimum_package_coverage_map = { # optional (default is empty)
# 'com/package/' => 55,
# 'com/package/more/specific/' => 15
# }
#
# @see Anton Malinskiy/danger-jacoco
# @tags jacoco, coverage, java, android, kotlin
#
class DangerJacoco < Plugin # rubocop:disable Metrics/ClassLength
attr_accessor :minimum_project_coverage_percentage, :minimum_class_coverage_percentage, :files_extension,
:minimum_package_coverage_map, :minimum_class_coverage_map, :fail_no_coverage_data_found, :title
# Initialize the plugin with configured parameters or defaults
def setup
@minimum_project_coverage_percentage = 0 unless minimum_project_coverage_percentage
@minimum_class_coverage_percentage = 0 unless minimum_class_coverage_percentage
@minimum_package_coverage_map = {} unless minimum_package_coverage_map
@minimum_class_coverage_map = {} unless minimum_class_coverage_map
@files_extension = ['.kt', '.java'] unless files_extension
@title = 'JaCoCo' unless title
end
# Parses the xml output of jacoco to Ruby model classes
# This is slow since it's basically DOM parsing
#
# @path path to the xml output of jacoco
#
def parse(path)
Jacoco::DOMParser.read_path(path)
end
# This is a fast report based on SAX parser
#
# @path path to the xml output of jacoco
# @report_url URL where html report hosted
# @delimiter git.modified_files returns full paths to the
# changed files. We need to get the class from this path to check the
# Jacoco report,
#
# e.g. src/java/com/example/SomeJavaClass.java -> com/example/SomeJavaClass
# e.g. src/kotlin/com/example/SomeKotlinClass.kt -> com/example/SomeKotlinClass
#
# The default value supposes that you're using gradle structure,
# that is your path to source files is something like
#
# Java => blah/blah/java/slashed_package/Source.java
# Kotlin => blah/blah/kotlin/slashed_package/Source.kt
#
def report(path, report_url = '', delimiter = %r{/java/|/kotlin/}, fail_no_coverage_data_found: true)
@fail_no_coverage_data_found = fail_no_coverage_data_found
setup
classes = classes(delimiter)
parser = Jacoco::SAXParser.new(classes)
Nokogiri::XML::SAX::Parser.new(parser).parse(File.open(path))
total_covered = total_coverage(path)
report_markdown = "### #{title} Code Coverage #{total_covered[:covered]}% #{total_covered[:status]}\n"
report_markdown += "| Class | Covered | Meta | Status |\n"
report_markdown += "|:---|:---:|:---:|:---:|\n"
class_coverage_above_minimum = markdown_class(parser, report_markdown, report_url)
markdown(report_markdown)
report_fails(class_coverage_above_minimum, total_covered)
end
# Select modified and added files in this PR
def classes(delimiter)
git = @dangerfile.git
affected_files = git.modified_files + git.added_files
affected_files.select { |file| files_extension.reduce(false) { |state, el| state || file.end_with?(el) } }
.map { |file| file.split('.').first.split(delimiter)[1] }
end
# It returns a specific class code coverage and an emoji status as well
def report_class(jacoco_class)
report_result = {
covered: 'No coverage data found : -',
status: ':black_joker:',
required_coverage_percentage: 'No coverage data found : -'
}
counter = coverage_counter(jacoco_class)
unless counter.nil?
coverage = (counter.covered.fdiv(counter.covered + counter.missed) * 100).floor
required_coverage = required_class_coverage(jacoco_class)
status = coverage_status(coverage, required_coverage)
report_result = {
covered: coverage,
status: status,
required_coverage_percentage: required_coverage
}
end
report_result
end
# Determines the required coverage for the class
def required_class_coverage(jacoco_class)
key = minimum_class_coverage_map.keys.detect { |k| jacoco_class.name.match(k) } || jacoco_class.name
required_coverage = minimum_class_coverage_map[key]
required_coverage = package_coverage(jacoco_class.name) if required_coverage.nil?
required_coverage = minimum_class_coverage_percentage if required_coverage.nil?
required_coverage
end
# it returns the most suitable coverage by package name to class or nil
def package_coverage(class_name)
path = class_name
package_parts = class_name.split('/')
package_parts.reverse_each do |item|
size = item.size
path = path[0...-size]
coverage = minimum_package_coverage_map[path]
path = path[0...-1] unless path.empty?
return coverage unless coverage.nil?
end
nil
end
# it returns an emoji for coverage status
def coverage_status(coverage, minimum_percentage)
if coverage < (minimum_percentage / 2) then ':skull:'
elsif coverage < minimum_percentage then ':warning:'
else ':white_check_mark:'
end
end
# It returns total of project code coverage and an emoji status as well
def total_coverage(report_path)
jacoco_report = Nokogiri::XML(File.open(report_path))
report = jacoco_report.xpath('report/counter').select { |item| item['type'] == 'INSTRUCTION' }
missed_instructions = report.first['missed'].to_f
covered_instructions = report.first['covered'].to_f
total_instructions = missed_instructions + covered_instructions
covered_percentage = (covered_instructions * 100 / total_instructions).round(2)
coverage_status = coverage_status(covered_percentage, minimum_project_coverage_percentage)
{
covered: covered_percentage,
status: coverage_status
}
end
private
def coverage_counter(jacoco_class)
counters = jacoco_class.counters
branch_counter = counters.detect { |e| e.type.eql? 'BRANCH' }
line_counter = counters.detect { |e| e.type.eql? 'LINE' }
counter = branch_counter.nil? ? line_counter : branch_counter
if counter.nil?
no_coverage_data_found_message = "No coverage data found for #{jacoco_class.name}"
raise no_coverage_data_found_message if @fail_no_coverage_data_found.instance_of?(TrueClass)
warn no_coverage_data_found_message
end
counter
end
# rubocop:disable Style/SignalException
def report_fails(class_coverage_above_minimum, total_covered)
if total_covered[:covered] < minimum_project_coverage_percentage
# fail danger if total coverage is smaller than minimum_project_coverage_percentage
covered = total_covered[:covered]
fail("Total coverage of #{covered}%. Improve this to at least #{minimum_project_coverage_percentage}%")
end
return if class_coverage_above_minimum
fail("Class coverage is below minimum. Improve to at least #{minimum_class_coverage_percentage}%")
end
# rubocop:enable Style/SignalException
def markdown_class(parser, report_markdown, report_url)
class_coverage_above_minimum = true
parser.classes.each do |jacoco_class| # Check metrics for each classes
rp = report_class(jacoco_class)
rl = report_link(jacoco_class.name, report_url)
ln = "| #{rl} | #{rp[:covered]}% | #{rp[:required_coverage_percentage]}% | #{rp[:status]} |\n"
report_markdown << ln
class_coverage_above_minimum &&= rp[:covered] >= rp[:required_coverage_percentage]
end
class_coverage_above_minimum
end
def report_link(class_name, report_url)
if report_url.empty?
"`#{class_name}`"
else
report_filepath = "#{class_name.gsub(%r{/(?=[^/]*/.)}, '.')}.html"
"[`#{class_name}`](#{report_url + report_filepath})"
end
end
end
end