simplecov 0.17.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -434
- data/README.md +375 -93
- data/doc/alternate-formatters.md +16 -1
- data/doc/commercial-services.md +5 -0
- data/lib/minitest/simplecov_plugin.rb +15 -0
- data/lib/simplecov.rb +294 -128
- data/lib/simplecov/combine.rb +30 -0
- data/lib/simplecov/combine/branches_combiner.rb +32 -0
- data/lib/simplecov/combine/files_combiner.rb +24 -0
- data/lib/simplecov/combine/lines_combiner.rb +43 -0
- data/lib/simplecov/combine/results_combiner.rb +60 -0
- data/lib/simplecov/command_guesser.rb +6 -3
- data/lib/simplecov/configuration.rb +191 -15
- data/lib/simplecov/coverage_statistics.rb +56 -0
- data/lib/simplecov/default_formatter.rb +20 -0
- data/lib/simplecov/defaults.rb +14 -13
- data/lib/simplecov/exit_codes.rb +5 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +73 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
- data/lib/simplecov/file_list.rb +72 -13
- data/lib/simplecov/filter.rb +9 -6
- data/lib/simplecov/formatter.rb +2 -2
- data/lib/simplecov/formatter/multi_formatter.rb +5 -7
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/last_run.rb +3 -1
- data/lib/simplecov/lines_classifier.rb +5 -5
- data/lib/simplecov/no_defaults.rb +1 -1
- data/lib/simplecov/process.rb +19 -0
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +18 -12
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +130 -59
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +272 -126
- data/lib/simplecov/source_file/branch.rb +84 -0
- data/lib/simplecov/source_file/line.rb +72 -0
- data/lib/simplecov/useless_results_remover.rb +18 -0
- data/lib/simplecov/version.rb +1 -1
- metadata +44 -158
- data/CONTRIBUTING.md +0 -51
- data/ISSUE_TEMPLATE.md +0 -23
- data/lib/simplecov/jruby_fix.rb +0 -44
- data/lib/simplecov/railtie.rb +0 -9
- data/lib/simplecov/railties/tasks.rake +0 -13
- data/lib/simplecov/raw_coverage.rb +0 -41
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
#
|
5
|
+
# Responsible for producing file coverage metrics.
|
6
|
+
#
|
7
|
+
module SimulateCoverage
|
8
|
+
module_function
|
9
|
+
|
10
|
+
#
|
11
|
+
# Simulate normal file coverage report on
|
12
|
+
# ruby 2.5 and return similar hash with lines and branches keys
|
13
|
+
#
|
14
|
+
# Happens when a file wasn't required but still tracked.
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
#
|
18
|
+
def call(absolute_path)
|
19
|
+
lines = File.foreach(absolute_path)
|
20
|
+
|
21
|
+
{
|
22
|
+
"lines" => LinesClassifier.new.classify(lines),
|
23
|
+
# we don't want to parse branches ourselves...
|
24
|
+
# requiring files can have side effects and we don't want to trigger that
|
25
|
+
"branches" => {}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -6,80 +6,14 @@ module SimpleCov
|
|
6
6
|
# source lines and featuring helpers to interpret that data.
|
7
7
|
#
|
8
8
|
class SourceFile
|
9
|
-
# Representation of a single line in a source file including
|
10
|
-
# this specific line's source code, line_number and code coverage,
|
11
|
-
# with the coverage being either nil (coverage not applicable, e.g. comment
|
12
|
-
# line), 0 (line not covered) or >1 (the amount of times the line was
|
13
|
-
# executed)
|
14
|
-
class Line
|
15
|
-
# The source code for this line. Aliased as :source
|
16
|
-
attr_reader :src
|
17
|
-
# The line number in the source file. Aliased as :line, :number
|
18
|
-
attr_reader :line_number
|
19
|
-
# The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
|
20
|
-
attr_reader :coverage
|
21
|
-
# Whether this line was skipped
|
22
|
-
attr_reader :skipped
|
23
|
-
|
24
|
-
# Lets grab some fancy aliases, shall we?
|
25
|
-
alias source src
|
26
|
-
alias line line_number
|
27
|
-
alias number line_number
|
28
|
-
|
29
|
-
def initialize(src, line_number, coverage)
|
30
|
-
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
31
|
-
raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
|
32
|
-
raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
|
33
|
-
@src = src
|
34
|
-
@line_number = line_number
|
35
|
-
@coverage = coverage
|
36
|
-
@skipped = false
|
37
|
-
end
|
38
|
-
|
39
|
-
# Returns true if this is a line that should have been covered, but was not
|
40
|
-
def missed?
|
41
|
-
!never? && !skipped? && coverage.zero?
|
42
|
-
end
|
43
|
-
|
44
|
-
# Returns true if this is a line that has been covered
|
45
|
-
def covered?
|
46
|
-
!never? && !skipped? && coverage > 0
|
47
|
-
end
|
48
|
-
|
49
|
-
# Returns true if this line is not relevant for coverage
|
50
|
-
def never?
|
51
|
-
!skipped? && coverage.nil?
|
52
|
-
end
|
53
|
-
|
54
|
-
# Flags this line as skipped
|
55
|
-
def skipped!
|
56
|
-
@skipped = true
|
57
|
-
end
|
58
|
-
|
59
|
-
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
60
|
-
# # :nocov: comment lines.
|
61
|
-
def skipped?
|
62
|
-
!!skipped
|
63
|
-
end
|
64
|
-
|
65
|
-
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
66
|
-
# as a css class in report generation
|
67
|
-
def status
|
68
|
-
return "skipped" if skipped?
|
69
|
-
return "never" if never?
|
70
|
-
return "missed" if missed?
|
71
|
-
return "covered" if covered?
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
9
|
# The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)
|
76
10
|
attr_reader :filename
|
77
11
|
# The array of coverage data received from the Coverage.result
|
78
|
-
attr_reader :
|
12
|
+
attr_reader :coverage_data
|
79
13
|
|
80
|
-
def initialize(filename,
|
14
|
+
def initialize(filename, coverage_data)
|
81
15
|
@filename = filename
|
82
|
-
@
|
16
|
+
@coverage_data = coverage_data
|
83
17
|
end
|
84
18
|
|
85
19
|
# The path to this source file relative to the projects directory
|
@@ -91,10 +25,18 @@ module SimpleCov
|
|
91
25
|
def src
|
92
26
|
# We intentionally read source code lazily to
|
93
27
|
# suppress reading unused source code.
|
94
|
-
@src ||=
|
28
|
+
@src ||= load_source
|
95
29
|
end
|
96
30
|
alias source src
|
97
31
|
|
32
|
+
def coverage_statistics
|
33
|
+
@coverage_statistics ||=
|
34
|
+
{
|
35
|
+
**line_coverage_statistics,
|
36
|
+
**branch_coverage_statistics
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
98
40
|
# Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
|
99
41
|
# and thus including coverage data. Aliased as :source_lines
|
100
42
|
def lines
|
@@ -102,19 +44,31 @@ module SimpleCov
|
|
102
44
|
end
|
103
45
|
alias source_lines lines
|
104
46
|
|
105
|
-
|
106
|
-
|
47
|
+
# Returns all covered lines as SimpleCov::SourceFile::Line
|
48
|
+
def covered_lines
|
49
|
+
@covered_lines ||= lines.select(&:covered?)
|
50
|
+
end
|
107
51
|
|
108
|
-
|
109
|
-
|
110
|
-
|
52
|
+
# Returns all lines that should have been, but were not covered
|
53
|
+
# as instances of SimpleCov::SourceFile::Line
|
54
|
+
def missed_lines
|
55
|
+
@missed_lines ||= lines.select(&:missed?)
|
56
|
+
end
|
111
57
|
|
112
|
-
|
58
|
+
# Returns all lines that are not relevant for coverage as
|
59
|
+
# SimpleCov::SourceFile::Line instances
|
60
|
+
def never_lines
|
61
|
+
@never_lines ||= lines.select(&:never?)
|
113
62
|
end
|
114
63
|
|
115
|
-
#
|
116
|
-
def
|
117
|
-
|
64
|
+
# Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
|
65
|
+
def skipped_lines
|
66
|
+
@skipped_lines ||= lines.select(&:skipped?)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the number of relevant lines (covered + missed)
|
70
|
+
def lines_of_code
|
71
|
+
coverage_statistics[:line]&.total
|
118
72
|
end
|
119
73
|
|
120
74
|
# Access SimpleCov::SourceFile::Line source lines by line number
|
@@ -122,82 +76,274 @@ module SimpleCov
|
|
122
76
|
lines[number - 1]
|
123
77
|
end
|
124
78
|
|
125
|
-
# The coverage for this file in percent. 0 if the file has no
|
79
|
+
# The coverage for this file in percent. 0 if the file has no coverage lines
|
126
80
|
def covered_percent
|
127
|
-
|
128
|
-
|
129
|
-
return 0.0 if relevant_lines.zero?
|
130
|
-
|
131
|
-
Float(covered_lines.size * 100.0 / relevant_lines.to_f)
|
81
|
+
coverage_statistics[:line]&.percent
|
132
82
|
end
|
133
83
|
|
134
84
|
def covered_strength
|
135
|
-
|
136
|
-
|
137
|
-
round_float(lines_strength / relevant_lines.to_f, 1)
|
85
|
+
coverage_statistics[:line]&.strength
|
138
86
|
end
|
139
87
|
|
140
88
|
def no_lines?
|
141
89
|
lines.length.zero? || (lines.length == never_lines.size)
|
142
90
|
end
|
143
91
|
|
144
|
-
def lines_strength
|
145
|
-
lines.map(&:coverage).compact.reduce(:+)
|
146
|
-
end
|
147
|
-
|
148
92
|
def relevant_lines
|
149
93
|
lines.size - never_lines.size - skipped_lines.size
|
150
94
|
end
|
151
95
|
|
152
|
-
#
|
153
|
-
|
154
|
-
|
96
|
+
#
|
97
|
+
# Return all the branches inside current source file
|
98
|
+
def branches
|
99
|
+
@branches ||= build_branches
|
155
100
|
end
|
156
101
|
|
157
|
-
|
158
|
-
|
159
|
-
def missed_lines
|
160
|
-
@missed_lines ||= lines.select(&:missed?)
|
102
|
+
def no_branches?
|
103
|
+
total_branches.empty?
|
161
104
|
end
|
162
105
|
|
163
|
-
|
164
|
-
|
165
|
-
def never_lines
|
166
|
-
@never_lines ||= lines.select(&:never?)
|
106
|
+
def branches_coverage_percent
|
107
|
+
coverage_statistics[:branch]&.percent
|
167
108
|
end
|
168
109
|
|
169
|
-
#
|
170
|
-
|
171
|
-
|
110
|
+
#
|
111
|
+
# Return the relevant branches to source file
|
112
|
+
def total_branches
|
113
|
+
@total_branches ||= covered_branches + missed_branches
|
172
114
|
end
|
173
115
|
|
174
|
-
#
|
175
|
-
|
176
|
-
|
116
|
+
#
|
117
|
+
# Return hash with key of line number and branch coverage count as value
|
118
|
+
def branches_report
|
119
|
+
@branches_report ||= build_branches_report
|
177
120
|
end
|
178
121
|
|
179
|
-
#
|
180
|
-
#
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
122
|
+
#
|
123
|
+
# Select the covered branches
|
124
|
+
# Here we user tree schema because some conditions like case may have additional
|
125
|
+
# else that is not in declared inside the code but given by default by coverage report
|
126
|
+
#
|
127
|
+
# @return [Array]
|
128
|
+
#
|
129
|
+
def covered_branches
|
130
|
+
@covered_branches ||= branches.select(&:covered?)
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Select the missed branches with coverage equal to zero
|
135
|
+
#
|
136
|
+
# @return [Array]
|
137
|
+
#
|
138
|
+
def missed_branches
|
139
|
+
@missed_branches ||= branches.select(&:missed?)
|
140
|
+
end
|
141
|
+
|
142
|
+
def branches_for_line(line_number)
|
143
|
+
branches_report.fetch(line_number, [])
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Check if any branches missing on given line number
|
148
|
+
#
|
149
|
+
# @param [Integer] line_number
|
150
|
+
#
|
151
|
+
# @return [Boolean]
|
152
|
+
#
|
153
|
+
def line_with_missed_branch?(line_number)
|
154
|
+
branches_for_line(line_number).select { |_type, count| count.zero? }.any?
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# no_cov_chunks is zero indexed to work directly with the array holding the lines
|
160
|
+
def no_cov_chunks
|
161
|
+
@no_cov_chunks ||= build_no_cov_chunks
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_no_cov_chunks
|
165
|
+
no_cov_lines = src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }
|
166
|
+
|
167
|
+
# if we have an uneven number of nocovs we assume they go to the
|
168
|
+
# end of the file, the source doesn't really matter
|
169
|
+
# Can't deal with this within the each_slice due to differing
|
170
|
+
# behavior in JRuby: jruby/jruby#6048
|
171
|
+
no_cov_lines << ["", src.size] if no_cov_lines.size.odd?
|
172
|
+
|
173
|
+
no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
|
174
|
+
index_start..index_end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def load_source
|
179
|
+
lines = []
|
180
|
+
# The default encoding is UTF-8
|
181
|
+
File.open(filename, "rb:UTF-8") do |file|
|
182
|
+
current_line = file.gets
|
183
|
+
|
184
|
+
if shebang?(current_line)
|
185
|
+
lines << current_line
|
186
|
+
current_line = file.gets
|
190
187
|
end
|
188
|
+
|
189
|
+
read_lines(file, lines, current_line)
|
191
190
|
end
|
192
191
|
end
|
193
192
|
|
194
|
-
|
193
|
+
SHEBANG_REGEX = /\A#!/.freeze
|
194
|
+
def shebang?(line)
|
195
|
+
SHEBANG_REGEX.match?(line)
|
196
|
+
end
|
197
|
+
|
198
|
+
def read_lines(file, lines, current_line)
|
199
|
+
return lines unless current_line
|
200
|
+
|
201
|
+
set_encoding_based_on_magic_comment(file, current_line)
|
202
|
+
lines.concat([current_line], ensure_remove_undefs(file.readlines))
|
203
|
+
end
|
204
|
+
|
205
|
+
RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX = /\A#\s*(?:-\*-)?\s*(?:en)?coding:\s*(\S+)\s*(?:-\*-)?\s*\z/.freeze
|
206
|
+
def set_encoding_based_on_magic_comment(file, line)
|
207
|
+
# Check for encoding magic comment
|
208
|
+
# Encoding magic comment must be placed at first line except for shebang
|
209
|
+
if (match = RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX.match(line))
|
210
|
+
file.set_encoding(match[1], "UTF-8")
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def ensure_remove_undefs(file_lines)
|
215
|
+
# invalid/undef replace are technically not really necessary but nice to
|
216
|
+
# have and work around a JRuby incompatibility. Also moved here from
|
217
|
+
# simplecov-html to have encoding shenaningans in one place. See #866
|
218
|
+
# also setting these option on `file.set_encoding` doesn't seem to work
|
219
|
+
# properly so it has to be done here.
|
220
|
+
file_lines.each { |line| line.encode!("UTF-8", invalid: :replace, undef: :replace) }
|
221
|
+
end
|
222
|
+
|
223
|
+
def build_lines
|
224
|
+
coverage_exceeding_source_warn if coverage_data["lines"].size > src.size
|
225
|
+
lines = src.map.with_index(1) do |src, i|
|
226
|
+
SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1])
|
227
|
+
end
|
228
|
+
process_skipped_lines(lines)
|
229
|
+
end
|
230
|
+
|
231
|
+
def process_skipped_lines(lines)
|
232
|
+
# the array the lines are kept in is 0-based whereas the line numbers in the nocov
|
233
|
+
# chunks are 1-based and are expected to be like this in other parts (and it's also
|
234
|
+
# arguably more understandable)
|
235
|
+
no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
236
|
+
|
237
|
+
lines
|
238
|
+
end
|
239
|
+
|
240
|
+
def lines_strength
|
241
|
+
lines.map(&:coverage).compact.reduce(:+)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Warning to identify condition from Issue #56
|
245
|
+
def coverage_exceeding_source_warn
|
246
|
+
warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]"
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# Build full branches report
|
251
|
+
# Root branches represent the wrapper of all condition state that
|
252
|
+
# have inside the branches
|
253
|
+
#
|
254
|
+
# @return [Hash]
|
255
|
+
#
|
256
|
+
def build_branches_report
|
257
|
+
branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
|
258
|
+
coverage_statistics[branch.report_line] ||= []
|
259
|
+
coverage_statistics[branch.report_line] << branch.report
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
#
|
264
|
+
# Call recursive method that transform our static hash to array of objects
|
265
|
+
# @return [Array]
|
266
|
+
#
|
267
|
+
def build_branches
|
268
|
+
coverage_branch_data = coverage_data.fetch("branches", {})
|
269
|
+
branches = coverage_branch_data.flat_map do |condition, coverage_branches|
|
270
|
+
build_branches_from(condition, coverage_branches)
|
271
|
+
end
|
272
|
+
|
273
|
+
process_skipped_branches(branches)
|
274
|
+
end
|
275
|
+
|
276
|
+
def process_skipped_branches(branches)
|
277
|
+
return branches if no_cov_chunks.empty?
|
278
|
+
|
279
|
+
branches.each do |branch|
|
280
|
+
branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
|
281
|
+
end
|
282
|
+
|
283
|
+
branches
|
284
|
+
end
|
285
|
+
|
286
|
+
# Since we are dumping to and loading from JSON, and we have arrays as keys those
|
287
|
+
# don't make their way back to us intact e.g. just as a string
|
288
|
+
#
|
289
|
+
# We should probably do something different here, but as it stands these are
|
290
|
+
# our data structures that we write so eval isn't _too_ bad.
|
291
|
+
#
|
292
|
+
# See #801
|
293
|
+
#
|
294
|
+
def restore_ruby_data_structure(structure)
|
295
|
+
# Tests use the real data structures (except for integration tests) so no need to
|
296
|
+
# put them through here.
|
297
|
+
return structure if structure.is_a?(Array)
|
298
|
+
|
299
|
+
# rubocop:disable Security/Eval
|
300
|
+
eval structure
|
301
|
+
# rubocop:enable Security/Eval
|
302
|
+
end
|
303
|
+
|
304
|
+
def build_branches_from(condition, branches)
|
305
|
+
# the format handed in from the coverage data is like this:
|
306
|
+
#
|
307
|
+
# [:then, 4, 6, 6, 6, 10]
|
308
|
+
#
|
309
|
+
# which is [type, id, start_line, start_col, end_line, end_col]
|
310
|
+
_condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition)
|
311
|
+
|
312
|
+
branches.map do |branch_data, hit_count|
|
313
|
+
branch_data = restore_ruby_data_structure(branch_data)
|
314
|
+
build_branch(branch_data, hit_count, condition_start_line)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def build_branch(branch_data, hit_count, condition_start_line)
|
319
|
+
type, _id, start_line, _start_col, end_line, _end_col = branch_data
|
320
|
+
|
321
|
+
SourceFile::Branch.new(
|
322
|
+
start_line: start_line,
|
323
|
+
end_line: end_line,
|
324
|
+
coverage: hit_count,
|
325
|
+
inline: start_line == condition_start_line,
|
326
|
+
type: type
|
327
|
+
)
|
328
|
+
end
|
329
|
+
|
330
|
+
def line_coverage_statistics
|
331
|
+
{
|
332
|
+
line: CoverageStatistics.new(
|
333
|
+
total_strength: lines_strength,
|
334
|
+
covered: covered_lines.size,
|
335
|
+
missed: missed_lines.size
|
336
|
+
)
|
337
|
+
}
|
338
|
+
end
|
195
339
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
340
|
+
def branch_coverage_statistics
|
341
|
+
{
|
342
|
+
branch: CoverageStatistics.new(
|
343
|
+
covered: covered_branches.size,
|
344
|
+
missed: missed_branches.size
|
345
|
+
)
|
346
|
+
}
|
201
347
|
end
|
202
348
|
end
|
203
349
|
end
|