simplecov 0.17.1 → 0.18.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -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/simplecov.rb +225 -61
  7. data/lib/simplecov/combine.rb +30 -0
  8. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  9. data/lib/simplecov/combine/files_combiner.rb +24 -0
  10. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  11. data/lib/simplecov/combine/results_combiner.rb +60 -0
  12. data/lib/simplecov/command_guesser.rb +6 -3
  13. data/lib/simplecov/configuration.rb +110 -9
  14. data/lib/simplecov/coverage_statistics.rb +56 -0
  15. data/lib/simplecov/defaults.rb +4 -2
  16. data/lib/simplecov/file_list.rb +66 -13
  17. data/lib/simplecov/filter.rb +2 -1
  18. data/lib/simplecov/formatter/multi_formatter.rb +2 -2
  19. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  20. data/lib/simplecov/last_run.rb +3 -1
  21. data/lib/simplecov/lines_classifier.rb +2 -2
  22. data/lib/simplecov/profiles.rb +9 -7
  23. data/lib/simplecov/result.rb +39 -6
  24. data/lib/simplecov/result_adapter.rb +30 -0
  25. data/lib/simplecov/result_merger.rb +18 -11
  26. data/lib/simplecov/simulate_coverage.rb +29 -0
  27. data/lib/simplecov/source_file.rb +226 -125
  28. data/lib/simplecov/source_file/branch.rb +84 -0
  29. data/lib/simplecov/source_file/line.rb +72 -0
  30. data/lib/simplecov/useless_results_remover.rb +16 -0
  31. data/lib/simplecov/version.rb +1 -1
  32. metadata +32 -166
  33. data/lib/simplecov/jruby_fix.rb +0 -44
  34. data/lib/simplecov/railtie.rb +0 -9
  35. data/lib/simplecov/railties/tasks.rake +0 -13
  36. 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.
@@ -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,6 +17,10 @@ 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
@@ -44,35 +45,49 @@ module SimpleCov
44
45
  # Please check out the RDoc for SimpleCov::Configuration to find about available config options
45
46
  #
46
47
  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
48
+ require "coverage"
49
+ initial_setup(profile, &block)
50
+ @result = nil
51
+ self.pid = Process.pid
52
+
53
+ start_coverage_measurement
59
54
  end
60
55
 
61
56
  #
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).
57
+ # Collate a series of SimpleCov result files into a single SimpleCov output.
58
+ # You can optionally specify configuration with a block:
59
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"]
60
+ # OR
61
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' # using rails profile
62
+ # OR
63
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"] do
64
+ # add_filter 'test'
65
+ # end
66
+ # OR
67
+ # SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' do
68
+ # add_filter 'test'
69
+ # end
64
70
  #
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)
71
+ # Please check out the RDoc for SimpleCov::Configuration to find about
72
+ # available config options, or checkout the README for more in-depth
73
+ # information about coverage collation
74
+ #
75
+ def collate(result_filenames, profile = nil, &block)
76
+ raise "There's no reports to be merged" if result_filenames.empty?
70
77
 
71
- result[absolute] ||= LinesClassifier.new.classify(File.foreach(absolute))
78
+ initial_setup(profile, &block)
79
+
80
+ results = result_filenames.flat_map do |filename|
81
+ # Re-create each included instance of SimpleCov::Result from the stored run data.
82
+ (JSON.parse(File.read(filename)) || {}).map do |command_name, coverage|
83
+ SimpleCov::Result.from_hash(command_name => coverage)
72
84
  end
73
85
  end
74
86
 
75
- result
87
+ # Use the ResultMerger to produce a single, merged result, ready to use.
88
+ @result = SimpleCov::ResultMerger.merge_and_store(*results)
89
+
90
+ run_exit_tasks!
76
91
  end
77
92
 
78
93
  #
@@ -83,9 +98,8 @@ module SimpleCov
83
98
  return @result if result?
84
99
 
85
100
  # Collect our coverage result
86
- if running
87
- @result = SimpleCov::Result.new add_not_loaded_files(Coverage.result)
88
- end
101
+
102
+ process_coverage_result if running
89
103
 
90
104
  # If we're using merging of results, store the current result
91
105
  # first (if there is one), then merge the results and return those
@@ -147,22 +161,6 @@ module SimpleCov
147
161
  load_profile(name)
148
162
  end
149
163
 
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
164
  #
167
165
  # Clear out the previously cached .result. Primarily useful in testing
168
166
  #
@@ -196,6 +194,8 @@ module SimpleCov
196
194
  # Called from at_exit block
197
195
  #
198
196
  def run_exit_tasks!
197
+ set_exit_exception
198
+
199
199
  exit_status = SimpleCov.exit_status_from_exception
200
200
 
201
201
  SimpleCov.at_exit.call
@@ -206,8 +206,8 @@ module SimpleCov
206
206
 
207
207
  # Force exit with stored status (see github issue #5)
208
208
  # 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)
209
+ if exit_status&.positive?
210
+ $stderr.printf("SimpleCov failed with exit %<exit_status>d\n", exit_status: exit_status) if print_error_status
211
211
  Kernel.exit exit_status
212
212
  end
213
213
  end
@@ -220,11 +220,9 @@ module SimpleCov
220
220
  def process_result(result, exit_status)
221
221
  return exit_status if exit_status != SimpleCov::ExitCodes::SUCCESS # Existing errors
222
222
 
223
- covered_percent = result.covered_percent.round(2)
223
+ covered_percent = result.covered_percent.floor(2)
224
224
  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
225
+ write_last_run(covered_percent) if result_exit_status == SimpleCov::ExitCodes::SUCCESS # No result errors
228
226
  final_result_process? ? result_exit_status : SimpleCov::ExitCodes::SUCCESS
229
227
  end
230
228
 
@@ -232,17 +230,26 @@ module SimpleCov
232
230
  #
233
231
  # rubocop:disable Metrics/MethodLength
234
232
  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)
233
+ covered_percentages = result.covered_percentages.map { |percentage| percentage.floor(2) }
234
+ if (minimum_violations = minimum_coverage_violated(result)).any?
235
+ report_minimum_violated(minimum_violations)
238
236
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
239
237
  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)
238
+ $stderr.printf(
239
+ "File (%<file>s) is only (%<least_covered_percentage>.2f%%) covered. This is below the expected minimum coverage per file of (%<min_coverage>.2f%%).\n",
240
+ file: result.least_covered_file,
241
+ least_covered_percentage: covered_percentages.min,
242
+ min_coverage: SimpleCov.minimum_coverage_by_file
243
+ )
241
244
  SimpleCov::ExitCodes::MINIMUM_COVERAGE
242
245
  elsif (last_run = SimpleCov::LastRun.read)
243
- coverage_diff = last_run["result"]["covered_percent"] - covered_percent
246
+ coverage_diff = last_run[:result][:covered_percent] - covered_percent
244
247
  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)
248
+ $stderr.printf(
249
+ "Coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
250
+ drop_percent: coverage_diff,
251
+ max_drop: SimpleCov.maximum_coverage_drop
252
+ )
246
253
  SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
247
254
  else
248
255
  SimpleCov::ExitCodes::SUCCESS
@@ -266,6 +273,7 @@ module SimpleCov
266
273
  #
267
274
  def wait_for_other_processes
268
275
  return unless defined?(ParallelTests) && final_result_process?
276
+
269
277
  ParallelTests.wait_for_other_processes_to_finish
270
278
  end
271
279
 
@@ -273,16 +281,168 @@ module SimpleCov
273
281
  # @api private
274
282
  #
275
283
  def write_last_run(covered_percent)
276
- SimpleCov::LastRun.write(:result => {:covered_percent => covered_percent})
284
+ SimpleCov::LastRun.write(result: {covered_percent: covered_percent})
285
+ end
286
+
287
+ private
288
+
289
+ def initial_setup(profile, &block)
290
+ load_profile(profile) if profile
291
+ configure(&block) if block_given?
292
+ self.running = true
293
+ end
294
+
295
+ #
296
+ # Trigger Coverage.start depends on given config coverage_criterion
297
+ #
298
+ # With Positive branch it supports all coverage measurement types
299
+ # With Negative branch it supports only line coverage measurement type
300
+ #
301
+ def start_coverage_measurement
302
+ # This blog post gives a good run down of the coverage criterias introduced
303
+ # in Ruby 2.5: https://blog.bigbinary.com/2018/04/11/ruby-2-5-supports-measuring-branch-and-method-coverages.html
304
+ # There is also a nice writeup of the different coverage criteria made in this
305
+ # comment https://github.com/colszowka/simplecov/pull/692#discussion_r281836176 :
306
+ # Ruby < 2.5:
307
+ # https://github.com/ruby/ruby/blob/v1_9_3_374/ext/coverage/coverage.c
308
+ # traditional mode (Array)
309
+ #
310
+ # Ruby 2.5:
311
+ # https://bugs.ruby-lang.org/issues/13901
312
+ # https://github.com/ruby/ruby/blob/v2_5_3/ext/coverage/coverage.c
313
+ # default: traditional/compatible mode (Array)
314
+ # :lines - like traditional mode but using Hash
315
+ # :branches
316
+ # :methods
317
+ # :all - same as lines + branches + methods
318
+ #
319
+ # Ruby >= 2.6:
320
+ # https://bugs.ruby-lang.org/issues/15022
321
+ # https://github.com/ruby/ruby/blob/v2_6_3/ext/coverage/coverage.c
322
+ # default: traditional/compatible mode (Array)
323
+ # :lines - like traditional mode but using Hash
324
+ # :branches
325
+ # :methods
326
+ # :oneshot_lines - can not be combined with lines
327
+ # :all - same as lines + branches + methods
328
+ #
329
+ if coverage_start_arguments_supported?
330
+ start_coverage_with_criteria
331
+ else
332
+ Coverage.start
333
+ end
334
+ end
335
+
336
+ def start_coverage_with_criteria
337
+ start_arguments = coverage_criteria.map do |criterion|
338
+ [lookup_corresponding_ruby_coverage_name(criterion), true]
339
+ end.to_h
340
+
341
+ Coverage.start(start_arguments)
342
+ end
343
+
344
+ CRITERION_TO_RUBY_COVERAGE = {
345
+ branch: :branches,
346
+ line: :lines
347
+ }.freeze
348
+ def lookup_corresponding_ruby_coverage_name(criterion)
349
+ CRITERION_TO_RUBY_COVERAGE.fetch(criterion)
350
+ end
351
+
352
+ #
353
+ # Finds files that were to be tracked but were not loaded and initializes
354
+ # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
355
+ #
356
+ def add_not_loaded_files(result)
357
+ if tracked_files
358
+ result = result.dup
359
+ Dir[tracked_files].each do |file|
360
+ absolute_path = File.expand_path(file)
361
+ result[absolute_path] ||= SimulateCoverage.call(absolute_path)
362
+ end
363
+ end
364
+
365
+ result
366
+ end
367
+
368
+ #
369
+ # Call steps that handle process coverage result
370
+ #
371
+ # @return [Hash]
372
+ #
373
+ def process_coverage_result
374
+ adapt_coverage_result
375
+ remove_useless_results
376
+ result_with_not_loaded_files
377
+ end
378
+
379
+ #
380
+ # Unite the result so it wouldn't matter what coverage type was called
381
+ #
382
+ # @return [Hash]
383
+ #
384
+ def adapt_coverage_result
385
+ @result = SimpleCov::ResultAdapter.call(Coverage.result)
386
+ end
387
+
388
+ #
389
+ # Filter coverage result
390
+ # The result before filter also has result of coverage for files
391
+ # are not related to the project like loaded gems coverage.
392
+ #
393
+ # @return [Hash]
394
+ #
395
+ def remove_useless_results
396
+ @result = SimpleCov::UselessResultsRemover.call(@result)
397
+ end
398
+
399
+ #
400
+ # Initialize result with files that are not included by coverage
401
+ # and added inside the config block
402
+ #
403
+ # @return [Hash]
404
+ #
405
+ def result_with_not_loaded_files
406
+ @result = SimpleCov::Result.new(add_not_loaded_files(@result))
407
+ end
408
+
409
+ def minimum_coverage_violated(result)
410
+ coverage_achieved = minimum_coverage.map do |criterion, percent|
411
+ {
412
+ criterion: criterion,
413
+ minimum_expected: percent,
414
+ actual: result.coverage_statistics[criterion].percent
415
+ }
416
+ end
417
+
418
+ coverage_achieved.select do |achieved|
419
+ achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
420
+ end
421
+ end
422
+
423
+ def report_minimum_violated(violations)
424
+ violations.each do |violation|
425
+ $stderr.printf(
426
+ "%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
427
+ covered: violation.fetch(:actual).floor(2),
428
+ minimum_coverage: violation.fetch(:minimum_expected),
429
+ criterion: violation.fetch(:criterion).capitalize
430
+ )
431
+ end
277
432
  end
278
433
  end
279
434
  end
280
435
 
281
436
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__)))
437
+ require "set"
438
+ require "forwardable"
282
439
  require "simplecov/configuration"
283
- SimpleCov.send :extend, SimpleCov::Configuration
440
+ SimpleCov.extend SimpleCov::Configuration
441
+ require "simplecov/coverage_statistics"
284
442
  require "simplecov/exit_codes"
285
443
  require "simplecov/profiles"
444
+ require "simplecov/source_file/line"
445
+ require "simplecov/source_file/branch"
286
446
  require "simplecov/source_file"
287
447
  require "simplecov/file_list"
288
448
  require "simplecov/result"
@@ -290,13 +450,17 @@ require "simplecov/filter"
290
450
  require "simplecov/formatter"
291
451
  require "simplecov/last_run"
292
452
  require "simplecov/lines_classifier"
293
- require "simplecov/raw_coverage"
294
453
  require "simplecov/result_merger"
295
454
  require "simplecov/command_guesser"
296
455
  require "simplecov/version"
456
+ require "simplecov/result_adapter"
457
+ require "simplecov/combine"
458
+ require "simplecov/combine/branches_combiner"
459
+ require "simplecov/combine/files_combiner"
460
+ require "simplecov/combine/lines_combiner"
461
+ require "simplecov/combine/results_combiner"
462
+ require "simplecov/useless_results_remover"
463
+ require "simplecov/simulate_coverage"
297
464
 
298
465
  # Load default config
299
466
  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 throught his interface,
13
+ # as it takes care of short circuting 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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module Combine
5
+ #
6
+ # Combine different branch coverage results on single file.
7
+ #
8
+ # Should be called through `SimpleCov.combine`.
9
+ module BranchesCombiner
10
+ module_function
11
+
12
+ #
13
+ # Return merged branches or the existed branche if other is missing.
14
+ #
15
+ # Branches inside files are always same if they exists, the difference only in coverage count.
16
+ # Branch coverage report for any conditional case is built from hash, it's key is a condition and
17
+ # it's body is a hash << keys from condition and value is coverage rate >>.
18
+ # ex: branches =>{ [:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 1, [:else, 5, 8, 6, 8, 36]=>2}, other conditions...}
19
+ # We create copy of result and update it values depending on the combined branches coverage values.
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ def combine(coverage_a, coverage_b)
24
+ coverage_a.merge(coverage_b) do |_condition, branches_inside_a, branches_inside_b|
25
+ branches_inside_a.merge(branches_inside_b) do |_branch, a_count, b_count|
26
+ a_count + b_count
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end