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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -1
- data/CODE_OF_CONDUCT.md +76 -0
- data/README.md +275 -76
- data/doc/alternate-formatters.md +5 -0
- data/lib/simplecov.rb +225 -61
- 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 +110 -9
- data/lib/simplecov/coverage_statistics.rb +56 -0
- data/lib/simplecov/defaults.rb +4 -2
- data/lib/simplecov/file_list.rb +66 -13
- data/lib/simplecov/filter.rb +2 -1
- data/lib/simplecov/formatter/multi_formatter.rb +2 -2
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/last_run.rb +3 -1
- data/lib/simplecov/lines_classifier.rb +2 -2
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +39 -6
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +18 -11
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +226 -125
- 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 +16 -0
- data/lib/simplecov/version.rb +1 -1
- metadata +32 -166
- 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
data/doc/alternate-formatters.md
CHANGED
@@ -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.
|
data/lib/simplecov.rb
CHANGED
@@ -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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
#
|
63
|
-
#
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
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
|
210
|
-
$stderr.printf("SimpleCov failed with exit
|
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.
|
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.
|
236
|
-
if
|
237
|
-
|
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(
|
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[
|
246
|
+
coverage_diff = last_run[:result][:covered_percent] - covered_percent
|
244
247
|
if coverage_diff > SimpleCov.maximum_coverage_drop
|
245
|
-
$stderr.printf(
|
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(:
|
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.
|
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
|