simplecov 0.17.1 → 0.18.5

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -1
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/README.md +275 -76
  5. data/doc/alternate-formatters.md +5 -0
  6. data/lib/minitest/simplecov_plugin.rb +15 -0
  7. data/lib/simplecov.rb +238 -61
  8. data/lib/simplecov/combine.rb +30 -0
  9. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  10. data/lib/simplecov/combine/files_combiner.rb +24 -0
  11. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  12. data/lib/simplecov/combine/results_combiner.rb +60 -0
  13. data/lib/simplecov/command_guesser.rb +6 -3
  14. data/lib/simplecov/configuration.rb +110 -9
  15. data/lib/simplecov/coverage_statistics.rb +56 -0
  16. data/lib/simplecov/defaults.rb +3 -5
  17. data/lib/simplecov/file_list.rb +66 -13
  18. data/lib/simplecov/filter.rb +2 -1
  19. data/lib/simplecov/formatter/multi_formatter.rb +2 -2
  20. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  21. data/lib/simplecov/last_run.rb +3 -1
  22. data/lib/simplecov/lines_classifier.rb +2 -2
  23. data/lib/simplecov/profiles.rb +9 -7
  24. data/lib/simplecov/result.rb +39 -6
  25. data/lib/simplecov/result_adapter.rb +30 -0
  26. data/lib/simplecov/result_merger.rb +18 -11
  27. data/lib/simplecov/simulate_coverage.rb +29 -0
  28. data/lib/simplecov/source_file.rb +272 -126
  29. data/lib/simplecov/source_file/branch.rb +84 -0
  30. data/lib/simplecov/source_file/line.rb +72 -0
  31. data/lib/simplecov/useless_results_remover.rb +16 -0
  32. data/lib/simplecov/version.rb +1 -1
  33. metadata +33 -166
  34. data/lib/simplecov/jruby_fix.rb +0 -44
  35. data/lib/simplecov/railtie.rb +0 -9
  36. data/lib/simplecov/railties/tasks.rake +0 -13
  37. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -54,3 +54,8 @@ A formatter that prints the coverage of the file under test when you run a singl
54
54
  *by [Yosuke Kabuto](https://github.com/ysksn)*
55
55
 
56
56
  t_wada AA formatter for SimpleCov
57
+
58
+ #### [simplecov-material(https://github.com/chiefpansancolt/simplecov-material)
59
+ *by [Chiefpansancolt](https://github.com/chiefpansancolt)*
60
+
61
+ A Material Designed HTML formatter with clean and easy search of files with a tabular left Navigation.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # How minitest plugins. See https://github.com/colszowka/simplecov/pull/756 for why we need this.
4
+ # https://github.com/seattlerb/minitest#writing-extensions
5
+ module Minitest
6
+ def self.plugin_simplecov_init(_options)
7
+ if defined?(SimpleCov)
8
+ SimpleCov.external_at_exit = true
9
+
10
+ Minitest.after_run do
11
+ SimpleCov.at_exit_behavior
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,9 +2,6 @@
2
2
 
3
3
  require "English"
4
4
 
5
- #
6
- # Code coverage for ruby 1.9. Please check out README for a full introduction.
7
- #
8
5
  # Coverage may be inaccurate under JRUBY.
9
6
  if defined?(JRUBY_VERSION) && defined?(JRuby)
10
7
 
@@ -20,12 +17,21 @@ if defined?(JRUBY_VERSION) && defined?(JRuby)
20
17
  ' or set the "debug.fullTrace=true" option in your .jrubyrc'
21
18
  end
22
19
  end
20
+
21
+ #
22
+ # Code coverage for ruby. Please check out README for a full introduction.
23
+ #
23
24
  module SimpleCov
24
25
  class << self
25
26
  attr_accessor :running
26
27
  attr_accessor :pid
27
28
  attr_reader :exit_exception
28
29
 
30
+ # Basically, should we take care of at_exit behavior or something else?
31
+ # Used by the minitest plugin. See lib/minitest/simplecov_plugin.rb
32
+ attr_accessor :external_at_exit
33
+ alias external_at_exit? external_at_exit
34
+
29
35
  #
30
36
  # Sets up SimpleCov to run against your project.
31
37
  # You can optionally specify a profile to use as well as configuration with a block:
@@ -44,35 +50,49 @@ module SimpleCov
44
50
  # Please check out the RDoc for SimpleCov::Configuration to find about available config options
45
51
  #
46
52
  def start(profile = nil, &block)
47
- if SimpleCov.usable?
48
- load_profile(profile) if profile
49
- configure(&block) if block_given?
50
- @result = nil
51
- self.running = true
52
- self.pid = Process.pid
53
- Coverage.start
54
- else
55
- warn "WARNING: SimpleCov is activated, but you're not running Ruby 1.9+ - no coverage analysis will happen"
56
- warn "Starting with SimpleCov 1.0.0, even no-op compatibility with Ruby <= 1.8 will be entirely dropped."
57
- false
58
- end
53
+ require "coverage"
54
+ initial_setup(profile, &block)
55
+ @result = nil
56
+ self.pid = Process.pid
57
+
58
+ start_coverage_measurement
59
59
  end
60
60
 
61
61
  #
62
- # Finds files that were to be tracked but were not loaded and initializes
63
- # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
62
+ # Collate a series of SimpleCov result files into a single SimpleCov output.
63
+ # You can optionally specify configuration with a block:
64
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"]
65
+ # OR
66
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' # using rails profile
67
+ # OR
68
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"] do
69
+ # add_filter 'test'
70
+ # end
71
+ # OR
72
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' do
73
+ # add_filter 'test'
74
+ # end
64
75
  #
65
- def add_not_loaded_files(result)
66
- if tracked_files
67
- result = result.dup
68
- Dir[tracked_files].each do |file|
69
- absolute = File.expand_path(file)
76
+ # Please check out the RDoc for SimpleCov::Configuration to find about
77
+ # available config options, or checkout the README for more in-depth
78
+ # information about coverage collation
79
+ #
80
+ def collate(result_filenames, profile = nil, &block)
81
+ raise "There's no reports to be merged" if result_filenames.empty?
70
82
 
71
- result[absolute] ||= LinesClassifier.new.classify(File.foreach(absolute))
83
+ initial_setup(profile, &block)
84
+
85
+ results = result_filenames.flat_map do |filename|
86
+ # Re-create each included instance of SimpleCov::Result from the stored run data.
87
+ (JSON.parse(File.read(filename)) || {}).map do |command_name, coverage|
88
+ SimpleCov::Result.from_hash(command_name => coverage)
72
89
  end
73
90
  end
74
91
 
75
- result
92
+ # Use the ResultMerger to produce a single, merged result, ready to use.
93
+ @result = SimpleCov::ResultMerger.merge_and_store(*results)
94
+
95
+ run_exit_tasks!
76
96
  end
77
97
 
78
98
  #
@@ -83,9 +103,8 @@ module SimpleCov
83
103
  return @result if result?
84
104
 
85
105
  # Collect our coverage result
86
- if running
87
- @result = SimpleCov::Result.new add_not_loaded_files(Coverage.result)
88
- end
106
+
107
+ process_coverage_result if running
89
108
 
90
109
  # If we're using merging of results, store the current result
91
110
  # first (if there is one), then merge the results and return those
@@ -147,22 +166,6 @@ module SimpleCov
147
166
  load_profile(name)
148
167
  end
149
168
 
150
- #
151
- # Checks whether we're on a proper version of Ruby (likely 1.9+) which
152
- # provides coverage support
153
- #
154
- def usable?
155
- return @usable if defined?(@usable) && !@usable.nil?
156
-
157
- @usable = begin
158
- require "coverage"
159
- require "simplecov/jruby_fix"
160
- true
161
- rescue LoadError
162
- false
163
- end
164
- end
165
-
166
169
  #
167
170
  # Clear out the previously cached .result. Primarily useful in testing
168
171
  #
@@ -191,11 +194,21 @@ module SimpleCov
191
194
  end
192
195
  end
193
196
 
197
+ def at_exit_behavior
198
+ # If we are in a different process than called start, don't interfere.
199
+ return if SimpleCov.pid != Process.pid
200
+
201
+ # If SimpleCov is no longer running then don't run exit tasks
202
+ SimpleCov.run_exit_tasks! if SimpleCov.running
203
+ end
204
+
194
205
  # @api private
195
206
  #
196
207
  # Called from at_exit block
197
208
  #
198
209
  def run_exit_tasks!
210
+ set_exit_exception
211
+
199
212
  exit_status = SimpleCov.exit_status_from_exception
200
213
 
201
214
  SimpleCov.at_exit.call
@@ -206,8 +219,8 @@ module SimpleCov
206
219
 
207
220
  # Force exit with stored status (see github issue #5)
208
221
  # unless it's nil or 0 (see github issue #281)
209
- if exit_status && exit_status > 0
210
- $stderr.printf("SimpleCov failed with exit %d", exit_status)
222
+ if exit_status&.positive?
223
+ $stderr.printf("SimpleCov failed with exit %<exit_status>d\n", exit_status: exit_status) if print_error_status
211
224
  Kernel.exit exit_status
212
225
  end
213
226
  end
@@ -220,11 +233,9 @@ module SimpleCov
220
233
  def process_result(result, exit_status)
221
234
  return exit_status if exit_status != SimpleCov::ExitCodes::SUCCESS # Existing errors
222
235
 
223
- covered_percent = result.covered_percent.round(2)
236
+ covered_percent = result.covered_percent.floor(2)
224
237
  result_exit_status = result_exit_status(result, covered_percent)
225
- if result_exit_status == SimpleCov::ExitCodes::SUCCESS # No result errors
226
- write_last_run(covered_percent)
227
- end
238
+ write_last_run(covered_percent) if result_exit_status == SimpleCov::ExitCodes::SUCCESS # No result errors
228
239
  final_result_process? ? result_exit_status : SimpleCov::ExitCodes::SUCCESS
229
240
  end
230
241
 
@@ -232,17 +243,26 @@ module SimpleCov
232
243
  #
233
244
  # rubocop:disable Metrics/MethodLength
234
245
  def result_exit_status(result, covered_percent)
235
- covered_percentages = result.covered_percentages.map { |percentage| percentage.round(2) }
236
- if covered_percent < SimpleCov.minimum_coverage
237
- $stderr.printf("Coverage (%.2f%%) is below the expected minimum coverage (%.2f%%).\n", covered_percent, SimpleCov.minimum_coverage)
246
+ covered_percentages = result.covered_percentages.map { |percentage| percentage.floor(2) }
247
+ if (minimum_violations = minimum_coverage_violated(result)).any?
248
+ report_minimum_violated(minimum_violations)
238
249
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
239
250
  elsif covered_percentages.any? { |p| p < SimpleCov.minimum_coverage_by_file }
240
- $stderr.printf("File (%s) is only (%.2f%%) covered. This is below the expected minimum coverage per file of (%.2f%%).\n", result.least_covered_file, covered_percentages.min, SimpleCov.minimum_coverage_by_file)
251
+ $stderr.printf(
252
+ "File (%<file>s) is only (%<least_covered_percentage>.2f%%) covered. This is below the expected minimum coverage per file of (%<min_coverage>.2f%%).\n",
253
+ file: result.least_covered_file,
254
+ least_covered_percentage: covered_percentages.min,
255
+ min_coverage: SimpleCov.minimum_coverage_by_file
256
+ )
241
257
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
242
258
  elsif (last_run = SimpleCov::LastRun.read)
243
- coverage_diff = last_run["result"]["covered_percent"] - covered_percent
259
+ coverage_diff = last_run[:result][:covered_percent] - covered_percent
244
260
  if coverage_diff > SimpleCov.maximum_coverage_drop
245
- $stderr.printf("Coverage has dropped by %.2f%% since the last time (maximum allowed: %.2f%%).\n", coverage_diff, SimpleCov.maximum_coverage_drop)
261
+ $stderr.printf(
262
+ "Coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
263
+ drop_percent: coverage_diff,
264
+ max_drop: SimpleCov.maximum_coverage_drop
265
+ )
246
266
  SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
247
267
  else
248
268
  SimpleCov::ExitCodes::SUCCESS
@@ -266,6 +286,7 @@ module SimpleCov
266
286
  #
267
287
  def wait_for_other_processes
268
288
  return unless defined?(ParallelTests) && final_result_process?
289
+
269
290
  ParallelTests.wait_for_other_processes_to_finish
270
291
  end
271
292
 
@@ -273,16 +294,168 @@ module SimpleCov
273
294
  # @api private
274
295
  #
275
296
  def write_last_run(covered_percent)
276
- SimpleCov::LastRun.write(:result => {:covered_percent => covered_percent})
297
+ SimpleCov::LastRun.write(result: {covered_percent: covered_percent})
298
+ end
299
+
300
+ private
301
+
302
+ def initial_setup(profile, &block)
303
+ load_profile(profile) if profile
304
+ configure(&block) if block_given?
305
+ self.running = true
306
+ end
307
+
308
+ #
309
+ # Trigger Coverage.start depends on given config coverage_criterion
310
+ #
311
+ # With Positive branch it supports all coverage measurement types
312
+ # With Negative branch it supports only line coverage measurement type
313
+ #
314
+ def start_coverage_measurement
315
+ # This blog post gives a good run down of the coverage criterias introduced
316
+ # in Ruby 2.5: https://blog.bigbinary.com/2018/04/11/ruby-2-5-supports-measuring-branch-and-method-coverages.html
317
+ # There is also a nice writeup of the different coverage criteria made in this
318
+ # comment https://github.com/colszowka/simplecov/pull/692#discussion_r281836176 :
319
+ # Ruby < 2.5:
320
+ # https://github.com/ruby/ruby/blob/v1_9_3_374/ext/coverage/coverage.c
321
+ # traditional mode (Array)
322
+ #
323
+ # Ruby 2.5:
324
+ # https://bugs.ruby-lang.org/issues/13901
325
+ # https://github.com/ruby/ruby/blob/v2_5_3/ext/coverage/coverage.c
326
+ # default: traditional/compatible mode (Array)
327
+ # :lines - like traditional mode but using Hash
328
+ # :branches
329
+ # :methods
330
+ # :all - same as lines + branches + methods
331
+ #
332
+ # Ruby >= 2.6:
333
+ # https://bugs.ruby-lang.org/issues/15022
334
+ # https://github.com/ruby/ruby/blob/v2_6_3/ext/coverage/coverage.c
335
+ # default: traditional/compatible mode (Array)
336
+ # :lines - like traditional mode but using Hash
337
+ # :branches
338
+ # :methods
339
+ # :oneshot_lines - can not be combined with lines
340
+ # :all - same as lines + branches + methods
341
+ #
342
+ if coverage_start_arguments_supported?
343
+ start_coverage_with_criteria
344
+ else
345
+ Coverage.start
346
+ end
347
+ end
348
+
349
+ def start_coverage_with_criteria
350
+ start_arguments = coverage_criteria.map do |criterion|
351
+ [lookup_corresponding_ruby_coverage_name(criterion), true]
352
+ end.to_h
353
+
354
+ Coverage.start(start_arguments)
355
+ end
356
+
357
+ CRITERION_TO_RUBY_COVERAGE = {
358
+ branch: :branches,
359
+ line: :lines
360
+ }.freeze
361
+ def lookup_corresponding_ruby_coverage_name(criterion)
362
+ CRITERION_TO_RUBY_COVERAGE.fetch(criterion)
363
+ end
364
+
365
+ #
366
+ # Finds files that were to be tracked but were not loaded and initializes
367
+ # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
368
+ #
369
+ def add_not_loaded_files(result)
370
+ if tracked_files
371
+ result = result.dup
372
+ Dir[tracked_files].each do |file|
373
+ absolute_path = File.expand_path(file)
374
+ result[absolute_path] ||= SimulateCoverage.call(absolute_path)
375
+ end
376
+ end
377
+
378
+ result
379
+ end
380
+
381
+ #
382
+ # Call steps that handle process coverage result
383
+ #
384
+ # @return [Hash]
385
+ #
386
+ def process_coverage_result
387
+ adapt_coverage_result
388
+ remove_useless_results
389
+ result_with_not_loaded_files
390
+ end
391
+
392
+ #
393
+ # Unite the result so it wouldn't matter what coverage type was called
394
+ #
395
+ # @return [Hash]
396
+ #
397
+ def adapt_coverage_result
398
+ @result = SimpleCov::ResultAdapter.call(Coverage.result)
399
+ end
400
+
401
+ #
402
+ # Filter coverage result
403
+ # The result before filter also has result of coverage for files
404
+ # are not related to the project like loaded gems coverage.
405
+ #
406
+ # @return [Hash]
407
+ #
408
+ def remove_useless_results
409
+ @result = SimpleCov::UselessResultsRemover.call(@result)
410
+ end
411
+
412
+ #
413
+ # Initialize result with files that are not included by coverage
414
+ # and added inside the config block
415
+ #
416
+ # @return [Hash]
417
+ #
418
+ def result_with_not_loaded_files
419
+ @result = SimpleCov::Result.new(add_not_loaded_files(@result))
420
+ end
421
+
422
+ def minimum_coverage_violated(result)
423
+ coverage_achieved = minimum_coverage.map do |criterion, percent|
424
+ {
425
+ criterion: criterion,
426
+ minimum_expected: percent,
427
+ actual: result.coverage_statistics[criterion].percent
428
+ }
429
+ end
430
+
431
+ coverage_achieved.select do |achieved|
432
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
433
+ end
434
+ end
435
+
436
+ def report_minimum_violated(violations)
437
+ violations.each do |violation|
438
+ $stderr.printf(
439
+ "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
440
+ covered: violation.fetch(:actual).floor(2),
441
+ minimum_coverage: violation.fetch(:minimum_expected),
442
+ criterion: violation.fetch(:criterion).capitalize
443
+ )
444
+ end
277
445
  end
278
446
  end
279
447
  end
280
448
 
281
449
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__)))
450
+ require "set"
451
+ require "forwardable"
282
452
  require "simplecov/configuration"
283
- SimpleCov.send :extend, SimpleCov::Configuration
453
+ SimpleCov.extend SimpleCov::Configuration
454
+ require "simplecov/coverage_statistics"
284
455
  require "simplecov/exit_codes"
285
456
  require "simplecov/profiles"
457
+ require "simplecov/source_file/line"
458
+ require "simplecov/source_file/branch"
286
459
  require "simplecov/source_file"
287
460
  require "simplecov/file_list"
288
461
  require "simplecov/result"
@@ -290,13 +463,17 @@ require "simplecov/filter"
290
463
  require "simplecov/formatter"
291
464
  require "simplecov/last_run"
292
465
  require "simplecov/lines_classifier"
293
- require "simplecov/raw_coverage"
294
466
  require "simplecov/result_merger"
295
467
  require "simplecov/command_guesser"
296
468
  require "simplecov/version"
469
+ require "simplecov/result_adapter"
470
+ require "simplecov/combine"
471
+ require "simplecov/combine/branches_combiner"
472
+ require "simplecov/combine/files_combiner"
473
+ require "simplecov/combine/lines_combiner"
474
+ require "simplecov/combine/results_combiner"
475
+ require "simplecov/useless_results_remover"
476
+ require "simplecov/simulate_coverage"
297
477
 
298
478
  # Load default config
299
479
  require "simplecov/defaults" unless ENV["SIMPLECOV_NO_DEFAULTS"]
300
-
301
- # Load Rails integration (only for Rails 3, see #113)
302
- require "simplecov/railtie" if defined? Rails::Railtie
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ # Functionally for combining coverage results
5
+ #
6
+ module Combine
7
+ module_function
8
+
9
+ #
10
+ # Combine two coverage based on the given combiner_module.
11
+ #
12
+ # Combiners should always be called through this interface,
13
+ # as it takes care of short-circuiting of one of the coverages is nil.
14
+ #
15
+ # @return [Hash]
16
+ def combine(combiner_module, coverage_a, coverage_b)
17
+ return existing_coverage(coverage_a, coverage_b) if empty_coverage?(coverage_a, coverage_b)
18
+
19
+ combiner_module.combine(coverage_a, coverage_b)
20
+ end
21
+
22
+ def empty_coverage?(coverage_a, coverage_b)
23
+ !(coverage_a && coverage_b)
24
+ end
25
+
26
+ def existing_coverage(coverage_a, coverage_b)
27
+ coverage_a || coverage_b
28
+ end
29
+ end
30
+ end