-
Notifications
You must be signed in to change notification settings - Fork 553
/
simplecov.rb
470 lines (408 loc) · 14.4 KB
/
simplecov.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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# frozen_string_literal: true
require "English"
# Coverage may be inaccurate under JRUBY.
if defined?(JRUBY_VERSION) && defined?(JRuby)
# @see https://github.com/jruby/jruby/issues/1196
# @see https://github.com/metricfu/metric_fu/pull/226
# @see https://github.com/simplecov-ruby/simplecov/issues/420
# @see https://github.com/simplecov-ruby/simplecov/issues/86
# @see https://jira.codehaus.org/browse/JRUBY-6106
unless org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED
warn 'Coverage may be inaccurate; set the "--debug" command line option,' \
' or do JRUBY_OPTS="--debug"' \
' or set the "debug.fullTrace=true" option in your .jrubyrc'
end
end
#
# Code coverage for ruby. Please check out README for a full introduction.
#
module SimpleCov
class << self
attr_accessor :running, :pid
# Basically, should we take care of at_exit behavior or something else?
# Used by the minitest plugin. See lib/minitest/simplecov_plugin.rb
attr_accessor :external_at_exit
alias external_at_exit? external_at_exit
#
# Sets up SimpleCov to run against your project.
# You can optionally specify a profile to use as well as configuration with a block:
# SimpleCov.start
# OR
# SimpleCov.start 'rails' # using rails profile
# OR
# SimpleCov.start do
# add_filter 'test'
# end
# OR
# SimpleCov.start 'rails' do
# add_filter 'test'
# end
#
# Please check out the RDoc for SimpleCov::Configuration to find about available config options
#
def start(profile = nil, &block)
require "coverage"
initial_setup(profile, &block)
require_relative "./simplecov/process" if SimpleCov.enabled_for_subprocesses? &&
::Process.respond_to?(:fork)
make_parallel_tests_available
@result = nil
self.pid = Process.pid
start_coverage_measurement
end
#
# Collate a series of SimpleCov result files into a single SimpleCov output.
# You can optionally specify configuration with a block:
# SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"]
# OR
# SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' # using rails profile
# OR
# SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"] do
# add_filter 'test'
# end
# OR
# SimpleCov.collate Dir["simplecov-resultset-*/.resultset.json"], 'rails' do
# add_filter 'test'
# end
#
# Please check out the RDoc for SimpleCov::Configuration to find about
# available config options, or checkout the README for more in-depth
# information about coverage collation
#
def collate(result_filenames, profile = nil, &block)
raise "There's no reports to be merged" if result_filenames.empty?
initial_setup(profile, &block)
results = result_filenames.flat_map do |filename|
# Re-create each included instance of SimpleCov::Result from the stored run data.
Result.from_hash(JSON.parse(File.read(filename)) || {})
end
# Use the ResultMerger to produce a single, merged result, ready to use.
@result = ResultMerger.merge_and_store(*results)
run_exit_tasks!
end
#
# Returns the result for the current coverage run, merging it across test suites
# from cache using SimpleCov::ResultMerger if use_merging is activated (default)
#
def result
return @result if result?
# Collect our coverage result
process_coverage_result if running
# If we're using merging of results, store the current result
# first (if there is one), then merge the results and return those
if use_merging
wait_for_other_processes
SimpleCov::ResultMerger.store_result(@result) if result?
@result = SimpleCov::ResultMerger.merged_result
end
@result
ensure
self.running = false
end
#
# Returns nil if the result has not been computed
# Otherwise, returns the result
#
def result?
defined?(@result) && @result
end
#
# Applies the configured filters to the given array of SimpleCov::SourceFile items
#
def filtered(files)
result = files.clone
filters.each do |filter|
result = result.reject { |source_file| filter.matches?(source_file) }
end
SimpleCov::FileList.new result
end
#
# Applies the configured groups to the given array of SimpleCov::SourceFile items
#
def grouped(files)
grouped = {}
grouped_files = []
groups.each do |name, filter|
grouped[name] = SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
grouped_files += grouped[name]
end
if !groups.empty? && !(other_files = files.reject { |source_file| grouped_files.include?(source_file) }).empty?
grouped["Ungrouped"] = SimpleCov::FileList.new(other_files)
end
grouped
end
#
# Applies the profile of given name on SimpleCov configuration
#
def load_profile(name)
profiles.load(name)
end
def load_adapter(name)
warn "#{Kernel.caller.first}: [DEPRECATION] #load_adapter is deprecated. Use #load_profile instead."
load_profile(name)
end
#
# Clear out the previously cached .result. Primarily useful in testing
#
def clear_result
@result = nil
end
def at_exit_behavior
# If we are in a different process than called start, don't interfere.
return if SimpleCov.pid != Process.pid
# If SimpleCov is no longer running then don't run exit tasks
SimpleCov.run_exit_tasks! if SimpleCov.running
end
# @api private
#
# Called from at_exit block
#
def run_exit_tasks!
error_exit_status = exit_status_from_exception
at_exit.call
exit_and_report_previous_error(error_exit_status) if previous_error?(error_exit_status)
process_results_and_report_error if ready_to_process_results?
end
#
# @api private
#
# Returns the exit status from the exit exception
#
def exit_status_from_exception
# Capture the current exception if it exists
@exit_exception = $ERROR_INFO
return nil unless @exit_exception
if @exit_exception.is_a?(SystemExit)
@exit_exception.status
else
SimpleCov::ExitCodes::EXCEPTION
end
end
# @api private
def previous_error?(error_exit_status)
# Normally it'd be enough to check for previous error but when running test_unit
# status is 0
error_exit_status && error_exit_status != SimpleCov::ExitCodes::SUCCESS
end
#
# @api private
#
# Thinking: Move this behavior earlier so if there was an error we do nothing?
def exit_and_report_previous_error(exit_status)
warn("Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected") if print_error_status
Kernel.exit(exit_status)
end
# @api private
def ready_to_process_results?
final_result_process? && result?
end
def process_results_and_report_error
exit_status = process_result(result)
# Force exit with stored status (see github issue #5)
if exit_status.positive?
warn("SimpleCov failed with exit #{exit_status} due to a coverage related error") if print_error_status
Kernel.exit exit_status
end
end
# @api private
#
# Usage:
# exit_status = SimpleCov.process_result(SimpleCov.result, exit_status)
#
def process_result(result)
result_exit_status = result_exit_status(result)
write_last_run(result) if result_exit_status == SimpleCov::ExitCodes::SUCCESS
result_exit_status
end
# @api private
CoverageLimits = Struct.new(:minimum_coverage, :minimum_coverage_by_file, :maximum_coverage_drop, keyword_init: true)
def result_exit_status(result)
coverage_limits = CoverageLimits.new(
minimum_coverage: minimum_coverage, minimum_coverage_by_file: minimum_coverage_by_file,
maximum_coverage_drop: maximum_coverage_drop
)
ExitCodes::ExitCodeHandling.call(result, coverage_limits: coverage_limits)
end
#
# @api private
#
def final_result_process?
# checking for ENV["TEST_ENV_NUMBER"] to determine if the tests are being run in parallel
!defined?(ParallelTests) || !ENV["TEST_ENV_NUMBER"] || ParallelTests.last_process?
end
#
# @api private
#
def wait_for_other_processes
return unless defined?(ParallelTests) && final_result_process?
ParallelTests.wait_for_other_processes_to_finish
end
#
# @api private
#
def write_last_run(result)
SimpleCov::LastRun.write(result:
result.coverage_statistics.transform_values do |stats|
round_coverage(stats.percent)
end)
end
#
# @api private
#
# Rounding down to be extra strict, see #679
def round_coverage(coverage)
coverage.floor(2)
end
private
def initial_setup(profile, &block)
load_profile(profile) if profile
configure(&block) if block_given?
self.running = true
end
#
# Trigger Coverage.start depends on given config coverage_criterion
#
# With Positive branch it supports all coverage measurement types
# With Negative branch it supports only line coverage measurement type
#
def start_coverage_measurement
# This blog post gives a good run down of the coverage criterias introduced
# in Ruby 2.5: https://blog.bigbinary.com/2018/04/11/ruby-2-5-supports-measuring-branch-and-method-coverages.html
# There is also a nice writeup of the different coverage criteria made in this
# comment https://github.com/simplecov-ruby/simplecov/pull/692#discussion_r281836176 :
# Ruby < 2.5:
# https://github.com/ruby/ruby/blob/v1_9_3_374/ext/coverage/coverage.c
# traditional mode (Array)
#
# Ruby 2.5:
# https://bugs.ruby-lang.org/issues/13901
# https://github.com/ruby/ruby/blob/v2_5_3/ext/coverage/coverage.c
# default: traditional/compatible mode (Array)
# :lines - like traditional mode but using Hash
# :branches
# :methods
# :all - same as lines + branches + methods
#
# Ruby >= 2.6:
# https://bugs.ruby-lang.org/issues/15022
# https://github.com/ruby/ruby/blob/v2_6_3/ext/coverage/coverage.c
# default: traditional/compatible mode (Array)
# :lines - like traditional mode but using Hash
# :branches
# :methods
# :oneshot_lines - can not be combined with lines
# :all - same as lines + branches + methods
#
if coverage_start_arguments_supported?
start_coverage_with_criteria
else
Coverage.start
end
end
def start_coverage_with_criteria
start_arguments = coverage_criteria.map do |criterion|
[lookup_corresponding_ruby_coverage_name(criterion), true]
end.to_h
Coverage.start(start_arguments)
end
CRITERION_TO_RUBY_COVERAGE = {
branch: :branches,
line: :lines
}.freeze
def lookup_corresponding_ruby_coverage_name(criterion)
CRITERION_TO_RUBY_COVERAGE.fetch(criterion)
end
#
# Finds files that were to be tracked but were not loaded and initializes
# the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
#
def add_not_loaded_files(result)
if tracked_files
result = result.dup
Dir[tracked_files].each do |file|
absolute_path = File.expand_path(file)
result[absolute_path] ||= SimulateCoverage.call(absolute_path)
end
end
result
end
#
# Call steps that handle process coverage result
#
# @return [Hash]
#
def process_coverage_result
adapt_coverage_result
remove_useless_results
result_with_not_loaded_files
end
#
# Unite the result so it wouldn't matter what coverage type was called
#
# @return [Hash]
#
def adapt_coverage_result
@result = SimpleCov::ResultAdapter.call(Coverage.result)
end
#
# Filter coverage result
# The result before filter also has result of coverage for files
# are not related to the project like loaded gems coverage.
#
# @return [Hash]
#
def remove_useless_results
@result = SimpleCov::UselessResultsRemover.call(@result)
end
#
# Initialize result with files that are not included by coverage
# and added inside the config block
#
# @return [Hash]
#
def result_with_not_loaded_files
@result = SimpleCov::Result.new(add_not_loaded_files(@result))
end
# parallel_tests isn't always available, see: https://github.com/grosser/parallel_tests/issues/772
def make_parallel_tests_available
return if defined?(ParallelTests)
return unless probably_running_parallel_tests?
require "parallel_tests"
rescue LoadError
warn("SimpleCov guessed you were running inside parallel tests but couldn't load it. Please file a bug report with us!")
end
def probably_running_parallel_tests?
ENV["TEST_ENV_NUMBER"] && ENV["PARALLEL_TEST_GROUPS"]
end
end
end
# requires are down here here for a load order reason I'm not sure what it is about
require "set"
require "forwardable"
require_relative "simplecov/configuration"
SimpleCov.extend SimpleCov::Configuration
require_relative "simplecov/coverage_statistics"
require_relative "simplecov/exit_codes"
require_relative "simplecov/profiles"
require_relative "simplecov/source_file/line"
require_relative "simplecov/source_file/branch"
require_relative "simplecov/source_file"
require_relative "simplecov/file_list"
require_relative "simplecov/result"
require_relative "simplecov/filter"
require_relative "simplecov/formatter"
require_relative "simplecov/last_run"
require_relative "simplecov/lines_classifier"
require_relative "simplecov/result_merger"
require_relative "simplecov/command_guesser"
require_relative "simplecov/version"
require_relative "simplecov/result_adapter"
require_relative "simplecov/combine"
require_relative "simplecov/combine/branches_combiner"
require_relative "simplecov/combine/files_combiner"
require_relative "simplecov/combine/lines_combiner"
require_relative "simplecov/combine/results_combiner"
require_relative "simplecov/useless_results_remover"
require_relative "simplecov/simulate_coverage"
# Load default config
require_relative "simplecov/defaults" unless ENV["SIMPLECOV_NO_DEFAULTS"]