simplecov 0.16.1 → 0.22.0

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.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +112 -402
  3. data/README.md +391 -127
  4. data/doc/alternate-formatters.md +21 -1
  5. data/doc/commercial-services.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  8. data/lib/simplecov/combine/files_combiner.rb +24 -0
  9. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  10. data/lib/simplecov/combine/results_combiner.rb +60 -0
  11. data/lib/simplecov/combine.rb +30 -0
  12. data/lib/simplecov/command_guesser.rb +6 -3
  13. data/lib/simplecov/configuration.rb +210 -15
  14. data/lib/simplecov/coverage_statistics.rb +56 -0
  15. data/lib/simplecov/default_formatter.rb +20 -0
  16. data/lib/simplecov/defaults.rb +15 -12
  17. data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
  18. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +83 -0
  19. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
  20. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
  21. data/lib/simplecov/exit_codes.rb +5 -0
  22. data/lib/simplecov/file_list.rb +72 -13
  23. data/lib/simplecov/filter.rb +9 -6
  24. data/lib/simplecov/formatter/multi_formatter.rb +5 -7
  25. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  26. data/lib/simplecov/formatter.rb +2 -2
  27. data/lib/simplecov/last_run.rb +3 -1
  28. data/lib/simplecov/lines_classifier.rb +5 -5
  29. data/lib/simplecov/no_defaults.rb +1 -1
  30. data/lib/simplecov/process.rb +19 -0
  31. data/lib/simplecov/profiles/hidden_filter.rb +5 -0
  32. data/lib/simplecov/profiles/rails.rb +1 -1
  33. data/lib/simplecov/profiles.rb +9 -7
  34. data/lib/simplecov/result.rb +18 -12
  35. data/lib/simplecov/result_adapter.rb +30 -0
  36. data/lib/simplecov/result_merger.rb +130 -59
  37. data/lib/simplecov/simulate_coverage.rb +29 -0
  38. data/lib/simplecov/source_file/branch.rb +84 -0
  39. data/lib/simplecov/source_file/line.rb +72 -0
  40. data/lib/simplecov/source_file.rb +279 -127
  41. data/lib/simplecov/useless_results_remover.rb +18 -0
  42. data/lib/simplecov/version.rb +1 -1
  43. data/lib/simplecov.rb +310 -121
  44. metadata +48 -50
  45. data/CONTRIBUTING.md +0 -51
  46. data/ISSUE_TEMPLATE.md +0 -23
  47. data/lib/simplecov/jruby_fix.rb +0 -44
  48. data/lib/simplecov/railtie.rb +0 -9
  49. data/lib/simplecov/railties/tasks.rake +0 -13
  50. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -6,95 +6,37 @@ 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 :coverage
12
+ attr_reader :coverage_data
79
13
 
80
- def initialize(filename, coverage)
14
+ def initialize(filename, coverage_data)
81
15
  @filename = filename
82
- @coverage = coverage
16
+ @coverage_data = coverage_data
83
17
  end
84
18
 
85
19
  # The path to this source file relative to the projects directory
86
20
  def project_filename
87
- @filename.sub(/^#{SimpleCov.root}/, "")
21
+ @filename.delete_prefix(SimpleCov.root)
88
22
  end
89
23
 
90
24
  # The source code for this file. Aliased as :source
91
25
  def src
92
26
  # We intentionally read source code lazily to
93
27
  # suppress reading unused source code.
94
- @src ||= File.open(filename, "rb", &:readlines)
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
- def build_lines
106
- coverage_exceeding_source_warn if coverage.size > src.size
47
+ # Returns all covered lines as SimpleCov::SourceFile::Line
48
+ def covered_lines
49
+ @covered_lines ||= lines.select(&:covered?)
50
+ end
107
51
 
108
- lines = src.map.with_index(1) do |src, i|
109
- SimpleCov::SourceFile::Line.new(src, i, coverage[i - 1])
110
- end
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
- process_skipped_lines(lines)
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
- # Warning to identify condition from Issue #56
116
- def coverage_exceeding_source_warn
117
- $stderr.puts "Warning: coverage data provided by Coverage [#{coverage.size}] exceeds number of lines in #{filename} [#{src.size}]"
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,280 @@ 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 relevant lines
79
+ # The coverage for this file in percent. 0 if the file has no coverage lines
126
80
  def covered_percent
127
- return 100.0 if no_lines?
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
- return 0.0 if relevant_lines.zero?
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
- # Returns all covered lines as SimpleCov::SourceFile::Line
153
- def covered_lines
154
- @covered_lines ||= lines.select(&:covered?)
96
+ #
97
+ # Return all the branches inside current source file
98
+ def branches
99
+ @branches ||= build_branches
155
100
  end
156
101
 
157
- # Returns all lines that should have been, but were not covered
158
- # as instances of SimpleCov::SourceFile::Line
159
- def missed_lines
160
- @missed_lines ||= lines.select(&:missed?)
102
+ def no_branches?
103
+ total_branches.empty?
161
104
  end
162
105
 
163
- # Returns all lines that are not relevant for coverage as
164
- # SimpleCov::SourceFile::Line instances
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
- # Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
170
- def skipped_lines
171
- @skipped_lines ||= lines.select(&:skipped?)
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
- # Returns the number of relevant lines (covered + missed)
175
- def lines_of_code
176
- covered_lines.size + missed_lines.size
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
- # Will go through all source files and mark lines that are wrapped within # :nocov: comment blocks
180
- # as skipped.
181
- def process_skipped_lines(lines)
182
- skipping = false
183
-
184
- lines.each do |line|
185
- if SimpleCov::LinesClassifier.no_cov_line?(line.src)
186
- skipping = !skipping
187
- line.skipped!
188
- elsif skipping
189
- line.skipped!
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
187
+ end
188
+
189
+ read_lines(file, lines, current_line)
190
+ end
191
+ end
192
+
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 do |line|
221
+ if line.encoding == Encoding::UTF_8
222
+ line
223
+ else
224
+ line.encode!("UTF-8", invalid: :replace, undef: :replace)
190
225
  end
191
226
  end
192
227
  end
193
228
 
194
- private
229
+ def build_lines
230
+ coverage_exceeding_source_warn if coverage_data["lines"].size > src.size
231
+ lines = src.map.with_index(1) do |src, i|
232
+ SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1])
233
+ end
234
+ process_skipped_lines(lines)
235
+ end
236
+
237
+ def process_skipped_lines(lines)
238
+ # the array the lines are kept in is 0-based whereas the line numbers in the nocov
239
+ # chunks are 1-based and are expected to be like this in other parts (and it's also
240
+ # arguably more understandable)
241
+ no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
242
+
243
+ lines
244
+ end
245
+
246
+ def lines_strength
247
+ lines.sum { |line| line.coverage.to_i }
248
+ end
249
+
250
+ # Warning to identify condition from Issue #56
251
+ def coverage_exceeding_source_warn
252
+ warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]"
253
+ end
254
+
255
+ #
256
+ # Build full branches report
257
+ # Root branches represent the wrapper of all condition state that
258
+ # have inside the branches
259
+ #
260
+ # @return [Hash]
261
+ #
262
+ def build_branches_report
263
+ branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
264
+ coverage_statistics[branch.report_line] ||= []
265
+ coverage_statistics[branch.report_line] << branch.report
266
+ end
267
+ end
268
+
269
+ #
270
+ # Call recursive method that transform our static hash to array of objects
271
+ # @return [Array]
272
+ #
273
+ def build_branches
274
+ coverage_branch_data = coverage_data.fetch("branches", {})
275
+ branches = coverage_branch_data.flat_map do |condition, coverage_branches|
276
+ build_branches_from(condition, coverage_branches)
277
+ end
278
+
279
+ process_skipped_branches(branches)
280
+ end
281
+
282
+ def process_skipped_branches(branches)
283
+ return branches if no_cov_chunks.empty?
284
+
285
+ branches.each do |branch|
286
+ branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
287
+ end
288
+
289
+ branches
290
+ end
291
+
292
+ # Since we are dumping to and loading from JSON, and we have arrays as keys those
293
+ # don't make their way back to us intact e.g. just as a string
294
+ #
295
+ # We should probably do something different here, but as it stands these are
296
+ # our data structures that we write so eval isn't _too_ bad.
297
+ #
298
+ # See #801
299
+ #
300
+ def restore_ruby_data_structure(structure)
301
+ # Tests use the real data structures (except for integration tests) so no need to
302
+ # put them through here.
303
+ return structure if structure.is_a?(Array)
304
+
305
+ # rubocop:disable Security/Eval
306
+ eval structure
307
+ # rubocop:enable Security/Eval
308
+ end
309
+
310
+ def build_branches_from(condition, branches)
311
+ # the format handed in from the coverage data is like this:
312
+ #
313
+ # [:then, 4, 6, 6, 6, 10]
314
+ #
315
+ # which is [type, id, start_line, start_col, end_line, end_col]
316
+ _condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition)
317
+
318
+ branches.map do |branch_data, hit_count|
319
+ branch_data = restore_ruby_data_structure(branch_data)
320
+ build_branch(branch_data, hit_count, condition_start_line)
321
+ end
322
+ end
323
+
324
+ def build_branch(branch_data, hit_count, condition_start_line)
325
+ type, _id, start_line, _start_col, end_line, _end_col = branch_data
326
+
327
+ SourceFile::Branch.new(
328
+ start_line: start_line,
329
+ end_line: end_line,
330
+ coverage: hit_count,
331
+ inline: start_line == condition_start_line,
332
+ type: type
333
+ )
334
+ end
335
+
336
+ def line_coverage_statistics
337
+ {
338
+ line: CoverageStatistics.new(
339
+ total_strength: lines_strength,
340
+ covered: covered_lines.size,
341
+ missed: missed_lines.size
342
+ )
343
+ }
344
+ end
195
345
 
196
- # ruby 1.9 could use Float#round(places) instead
197
- # @return [Float]
198
- def round_float(float, places)
199
- factor = Float(10 * places)
200
- Float((float * factor).round / factor)
346
+ def branch_coverage_statistics
347
+ {
348
+ branch: CoverageStatistics.new(
349
+ covered: covered_branches.size,
350
+ missed: missed_branches.size
351
+ )
352
+ }
201
353
  end
202
354
  end
203
355
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Select the files that related to working scope directory of SimpleCov
6
+ #
7
+ module UselessResultsRemover
8
+ def self.call(coverage_result)
9
+ coverage_result.select do |path, _coverage|
10
+ path =~ root_regx
11
+ end
12
+ end
13
+
14
+ def self.root_regx
15
+ @root_regx ||= /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/i.freeze
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "0.16.1".freeze
4
+ VERSION = "0.22.0"
5
5
  end