simplecov 0.17.1 → 0.21.2
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 +106 -432
- data/README.md +375 -93
- data/doc/alternate-formatters.md +16 -1
- data/doc/commercial-services.md +5 -0
- data/lib/minitest/simplecov_plugin.rb +15 -0
- data/lib/simplecov.rb +294 -128
- 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 +191 -15
- data/lib/simplecov/coverage_statistics.rb +56 -0
- data/lib/simplecov/default_formatter.rb +20 -0
- data/lib/simplecov/defaults.rb +14 -13
- data/lib/simplecov/exit_codes.rb +5 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +29 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +83 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +54 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +53 -0
- data/lib/simplecov/file_list.rb +72 -13
- data/lib/simplecov/filter.rb +9 -6
- data/lib/simplecov/formatter.rb +2 -2
- data/lib/simplecov/formatter/multi_formatter.rb +5 -7
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/last_run.rb +3 -1
- data/lib/simplecov/lines_classifier.rb +5 -5
- data/lib/simplecov/no_defaults.rb +1 -1
- data/lib/simplecov/process.rb +19 -0
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +18 -12
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +130 -59
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +272 -126
- 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 +18 -0
- data/lib/simplecov/version.rb +1 -1
- metadata +44 -158
- data/CONTRIBUTING.md +0 -51
- data/ISSUE_TEMPLATE.md +0 -23
- 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
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
# Holds the individual data of a coverage result.
|
5
|
+
#
|
6
|
+
# This is uniform across coverage criteria as they all have:
|
7
|
+
#
|
8
|
+
# * total - how many things to cover there are (total relevant loc/branches)
|
9
|
+
# * covered - how many of the coverables are hit
|
10
|
+
# * missed - how many of the coverables are missed
|
11
|
+
# * percent - percentage as covered/missed
|
12
|
+
# * strength - average hits per/coverable (will not exist for one shot lines format)
|
13
|
+
class CoverageStatistics
|
14
|
+
attr_reader :total, :covered, :missed, :strength, :percent
|
15
|
+
|
16
|
+
def self.from(coverage_statistics)
|
17
|
+
sum_covered, sum_missed, sum_total_strength =
|
18
|
+
coverage_statistics.reduce([0, 0, 0.0]) do |(covered, missed, total_strength), file_coverage_statistics|
|
19
|
+
[
|
20
|
+
covered + file_coverage_statistics.covered,
|
21
|
+
missed + file_coverage_statistics.missed,
|
22
|
+
# gotta remultiply with loc because files have different strength and loc
|
23
|
+
# giving them a different "weight" in total
|
24
|
+
total_strength + (file_coverage_statistics.strength * file_coverage_statistics.total)
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
new(covered: sum_covered, missed: sum_missed, total_strength: sum_total_strength)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Requires only covered, missed and strength to be initialized.
|
32
|
+
#
|
33
|
+
# Other values are computed by this class.
|
34
|
+
def initialize(covered:, missed:, total_strength: 0.0)
|
35
|
+
@covered = covered
|
36
|
+
@missed = missed
|
37
|
+
@total = covered + missed
|
38
|
+
@percent = compute_percent(covered, missed, total)
|
39
|
+
@strength = compute_strength(total_strength, total)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def compute_percent(covered, missed, total)
|
45
|
+
return 100.0 if missed.zero?
|
46
|
+
|
47
|
+
covered * 100.0 / total
|
48
|
+
end
|
49
|
+
|
50
|
+
def compute_strength(total_strength, total)
|
51
|
+
return 0.0 if total.zero?
|
52
|
+
|
53
|
+
total_strength.to_f / total
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "simplecov-html"
|
4
|
+
module SimpleCov
|
5
|
+
module Formatter
|
6
|
+
class << self
|
7
|
+
def from_env(env)
|
8
|
+
formatters = [SimpleCov::Formatter::HTMLFormatter]
|
9
|
+
|
10
|
+
# When running under a CI that uses CodeClimate, JSON output is expected
|
11
|
+
if env.fetch("CC_TEST_REPORTER_ID", nil)
|
12
|
+
require "simplecov_json_formatter"
|
13
|
+
formatters.push(SimpleCov::Formatter::JSONFormatter)
|
14
|
+
end
|
15
|
+
|
16
|
+
formatters
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/simplecov/defaults.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Load default formatter gem
|
4
|
-
require "simplecov-html"
|
5
4
|
require "pathname"
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
require_relative "default_formatter"
|
6
|
+
require_relative "profiles/root_filter"
|
7
|
+
require_relative "profiles/test_frameworks"
|
8
|
+
require_relative "profiles/bundler_filter"
|
9
|
+
require_relative "profiles/hidden_filter"
|
10
|
+
require_relative "profiles/rails"
|
11
11
|
|
12
12
|
# Default configuration
|
13
13
|
SimpleCov.configure do
|
14
|
-
formatter SimpleCov::Formatter::
|
14
|
+
formatter SimpleCov::Formatter::MultiFormatter.new(
|
15
|
+
SimpleCov::Formatter.from_env(ENV)
|
16
|
+
)
|
17
|
+
|
15
18
|
load_profile "bundler_filter"
|
16
19
|
load_profile "hidden_filter"
|
17
20
|
# Exclude files outside of SimpleCov.root
|
@@ -22,15 +25,13 @@ end
|
|
22
25
|
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
|
23
26
|
|
24
27
|
at_exit do
|
25
|
-
|
26
|
-
next if SimpleCov.pid != Process.pid
|
28
|
+
next if SimpleCov.external_at_exit?
|
27
29
|
|
28
|
-
SimpleCov.
|
29
|
-
SimpleCov.run_exit_tasks!
|
30
|
+
SimpleCov.at_exit_behavior
|
30
31
|
end
|
31
32
|
|
32
33
|
# Autoload config from ~/.simplecov if present
|
33
|
-
|
34
|
+
require_relative "load_global_config"
|
34
35
|
|
35
36
|
# Autoload config from .simplecov if present
|
36
37
|
# Recurse upwards until we find .simplecov or reach the root directory
|
@@ -42,7 +43,7 @@ loop do
|
|
42
43
|
begin
|
43
44
|
load filename
|
44
45
|
rescue LoadError, StandardError
|
45
|
-
|
46
|
+
warn "Warning: Error occurred while trying to load #{filename}. " \
|
46
47
|
"Error message: #{$!.message}"
|
47
48
|
end
|
48
49
|
break
|
data/lib/simplecov/exit_codes.rb
CHANGED
@@ -8,3 +8,8 @@ module SimpleCov
|
|
8
8
|
MAXIMUM_COVERAGE_DROP = 3
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
12
|
+
require_relative "exit_codes/exit_code_handling"
|
13
|
+
require_relative "exit_codes/maximum_coverage_drop_check"
|
14
|
+
require_relative "exit_codes/minimum_coverage_by_file_check"
|
15
|
+
require_relative "exit_codes/minimum_overall_coverage_check"
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
module ExitCodes
|
5
|
+
module ExitCodeHandling
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(result, coverage_limits:)
|
9
|
+
checks = coverage_checks(result, coverage_limits)
|
10
|
+
|
11
|
+
failing_check = checks.find(&:failing?)
|
12
|
+
if failing_check
|
13
|
+
failing_check.report
|
14
|
+
failing_check.exit_code
|
15
|
+
else
|
16
|
+
SimpleCov::ExitCodes::SUCCESS
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def coverage_checks(result, coverage_limits)
|
21
|
+
[
|
22
|
+
MinimumOverallCoverageCheck.new(result, coverage_limits.minimum_coverage),
|
23
|
+
MinimumCoverageByFileCheck.new(result, coverage_limits.minimum_coverage_by_file),
|
24
|
+
MaximumCoverageDropCheck.new(result, coverage_limits.maximum_coverage_drop)
|
25
|
+
]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
module ExitCodes
|
5
|
+
class MaximumCoverageDropCheck
|
6
|
+
def initialize(result, maximum_coverage_drop)
|
7
|
+
@result = result
|
8
|
+
@maximum_coverage_drop = maximum_coverage_drop
|
9
|
+
end
|
10
|
+
|
11
|
+
def failing?
|
12
|
+
return false unless maximum_coverage_drop && last_run
|
13
|
+
|
14
|
+
coverage_drop_violations.any?
|
15
|
+
end
|
16
|
+
|
17
|
+
def report
|
18
|
+
coverage_drop_violations.each do |violation|
|
19
|
+
$stderr.printf(
|
20
|
+
"%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time (maximum allowed: %<max_drop>.2f%%).\n",
|
21
|
+
criterion: violation[:criterion].capitalize,
|
22
|
+
drop_percent: SimpleCov.round_coverage(violation[:drop_percent]),
|
23
|
+
max_drop: violation[:max_drop]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def exit_code
|
29
|
+
SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :result, :maximum_coverage_drop
|
35
|
+
|
36
|
+
def last_run
|
37
|
+
return @last_run if defined?(@last_run)
|
38
|
+
|
39
|
+
@last_run = SimpleCov::LastRun.read
|
40
|
+
end
|
41
|
+
|
42
|
+
def coverage_drop_violations
|
43
|
+
@coverage_drop_violations ||=
|
44
|
+
compute_coverage_drop_data.select do |achieved|
|
45
|
+
achieved.fetch(:max_drop) < achieved.fetch(:drop_percent)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def compute_coverage_drop_data
|
50
|
+
maximum_coverage_drop.map do |criterion, percent|
|
51
|
+
{
|
52
|
+
criterion: criterion,
|
53
|
+
max_drop: percent,
|
54
|
+
drop_percent: drop_percent(criterion)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# if anyone says "max_coverage_drop 0.000000000000000001" I appologize. Please don't.
|
60
|
+
MAX_DROP_ACCURACY = 10
|
61
|
+
def drop_percent(criterion)
|
62
|
+
drop = last_coverage(criterion) -
|
63
|
+
SimpleCov.round_coverage(
|
64
|
+
result.coverage_statistics.fetch(criterion).percent
|
65
|
+
)
|
66
|
+
|
67
|
+
# floats, I tell ya.
|
68
|
+
# irb(main):001:0* 80.01 - 80.0
|
69
|
+
# => 0.010000000000005116
|
70
|
+
drop.floor(MAX_DROP_ACCURACY)
|
71
|
+
end
|
72
|
+
|
73
|
+
def last_coverage(criterion)
|
74
|
+
last_coverage_percent = last_run[:result][criterion]
|
75
|
+
|
76
|
+
# fallback for old file format
|
77
|
+
last_coverage_percent = last_run[:result][:covered_percent] if !last_coverage_percent && criterion == :line
|
78
|
+
|
79
|
+
last_coverage_percent || 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
module ExitCodes
|
5
|
+
class MinimumCoverageByFileCheck
|
6
|
+
def initialize(result, minimum_coverage_by_file)
|
7
|
+
@result = result
|
8
|
+
@minimum_coverage_by_file = minimum_coverage_by_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def failing?
|
12
|
+
minimum_violations.any?
|
13
|
+
end
|
14
|
+
|
15
|
+
def report
|
16
|
+
minimum_violations.each do |violation|
|
17
|
+
$stderr.printf(
|
18
|
+
"%<criterion>s coverage by file (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
|
19
|
+
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
|
20
|
+
minimum_coverage: violation.fetch(:minimum_expected),
|
21
|
+
criterion: violation.fetch(:criterion).capitalize
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def exit_code
|
27
|
+
SimpleCov::ExitCodes::MINIMUM_COVERAGE
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :result, :minimum_coverage_by_file
|
33
|
+
|
34
|
+
def minimum_violations
|
35
|
+
@minimum_violations ||=
|
36
|
+
compute_minimum_coverage_data.select do |achieved|
|
37
|
+
achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def compute_minimum_coverage_data
|
42
|
+
minimum_coverage_by_file.flat_map do |criterion, expected_percent|
|
43
|
+
result.coverage_statistics_by_file.fetch(criterion).map do |actual_coverage|
|
44
|
+
{
|
45
|
+
criterion: criterion,
|
46
|
+
minimum_expected: expected_percent,
|
47
|
+
actual: SimpleCov.round_coverage(actual_coverage.percent)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
module ExitCodes
|
5
|
+
class MinimumOverallCoverageCheck
|
6
|
+
def initialize(result, minimum_coverage)
|
7
|
+
@result = result
|
8
|
+
@minimum_coverage = minimum_coverage
|
9
|
+
end
|
10
|
+
|
11
|
+
def failing?
|
12
|
+
minimum_violations.any?
|
13
|
+
end
|
14
|
+
|
15
|
+
def report
|
16
|
+
minimum_violations.each do |violation|
|
17
|
+
$stderr.printf(
|
18
|
+
"%<criterion>s coverage (%<covered>.2f%%) is below the expected minimum coverage (%<minimum_coverage>.2f%%).\n",
|
19
|
+
covered: SimpleCov.round_coverage(violation.fetch(:actual)),
|
20
|
+
minimum_coverage: violation.fetch(:minimum_expected),
|
21
|
+
criterion: violation.fetch(:criterion).capitalize
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def exit_code
|
27
|
+
SimpleCov::ExitCodes::MINIMUM_COVERAGE
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :result, :minimum_coverage
|
33
|
+
|
34
|
+
def minimum_violations
|
35
|
+
@minimum_violations ||= calculate_minimum_violations
|
36
|
+
end
|
37
|
+
|
38
|
+
def calculate_minimum_violations
|
39
|
+
coverage_achieved = minimum_coverage.map do |criterion, percent|
|
40
|
+
{
|
41
|
+
criterion: criterion,
|
42
|
+
minimum_expected: percent,
|
43
|
+
actual: result.coverage_statistics.fetch(criterion).percent
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
coverage_achieved.select do |achieved|
|
48
|
+
achieved.fetch(:actual) < achieved.fetch(:minimum_expected)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/simplecov/file_list.rb
CHANGED
@@ -1,30 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# An array of SimpleCov SourceFile instances with additional collection helper
|
4
|
-
# methods for calculating coverage across them etc.
|
5
3
|
module SimpleCov
|
6
|
-
|
4
|
+
# An array of SimpleCov SourceFile instances with additional collection helper
|
5
|
+
# methods for calculating coverage across them etc.
|
6
|
+
class FileList
|
7
|
+
include Enumerable
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@files,
|
11
|
+
# For Enumerable
|
12
|
+
:each,
|
13
|
+
# also delegating methods implemented in Enumerable as they have
|
14
|
+
# custom Array implementations which are presumably better/more
|
15
|
+
# resource efficient
|
16
|
+
:size, :map, :count,
|
17
|
+
# surprisingly not in Enumerable
|
18
|
+
:empty?, :length,
|
19
|
+
# still act like we're kinda an array
|
20
|
+
:to_a, :to_ary
|
21
|
+
|
22
|
+
def initialize(files)
|
23
|
+
@files = files
|
24
|
+
end
|
25
|
+
|
26
|
+
def coverage_statistics
|
27
|
+
@coverage_statistics ||= compute_coverage_statistics
|
28
|
+
end
|
29
|
+
|
30
|
+
def coverage_statistics_by_file
|
31
|
+
@coverage_statistics_by_file ||= compute_coverage_statistics_by_file
|
32
|
+
end
|
33
|
+
|
7
34
|
# Returns the count of lines that have coverage
|
8
35
|
def covered_lines
|
9
|
-
|
10
|
-
map { |f| f.covered_lines.count }.inject(:+)
|
36
|
+
coverage_statistics[:line]&.covered
|
11
37
|
end
|
12
38
|
|
13
39
|
# Returns the count of lines that have been missed
|
14
40
|
def missed_lines
|
15
|
-
|
16
|
-
map { |f| f.missed_lines.count }.inject(:+)
|
41
|
+
coverage_statistics[:line]&.missed
|
17
42
|
end
|
18
43
|
|
19
44
|
# Returns the count of lines that are not relevant for coverage
|
20
45
|
def never_lines
|
21
46
|
return 0.0 if empty?
|
47
|
+
|
22
48
|
map { |f| f.never_lines.count }.inject(:+)
|
23
49
|
end
|
24
50
|
|
25
51
|
# Returns the count of skipped lines
|
26
52
|
def skipped_lines
|
27
53
|
return 0.0 if empty?
|
54
|
+
|
28
55
|
map { |f| f.skipped_lines.count }.inject(:+)
|
29
56
|
end
|
30
57
|
|
@@ -36,26 +63,58 @@ module SimpleCov
|
|
36
63
|
|
37
64
|
# Finds the least covered file and returns that file's name
|
38
65
|
def least_covered_file
|
39
|
-
|
66
|
+
min_by(&:covered_percent).filename
|
40
67
|
end
|
41
68
|
|
42
69
|
# Returns the overall amount of relevant lines of code across all files in this list
|
43
70
|
def lines_of_code
|
44
|
-
|
71
|
+
coverage_statistics[:line]&.total
|
45
72
|
end
|
46
73
|
|
47
74
|
# Computes the coverage based upon lines covered and lines missed
|
48
75
|
# @return [Float]
|
49
76
|
def covered_percent
|
50
|
-
|
51
|
-
Float(covered_lines * 100.0 / lines_of_code)
|
77
|
+
coverage_statistics[:line]&.percent
|
52
78
|
end
|
53
79
|
|
54
80
|
# Computes the strength (hits / line) based upon lines covered and lines missed
|
55
81
|
# @return [Float]
|
56
82
|
def covered_strength
|
57
|
-
|
58
|
-
|
83
|
+
coverage_statistics[:line]&.strength
|
84
|
+
end
|
85
|
+
|
86
|
+
# Return total count of branches in all files
|
87
|
+
def total_branches
|
88
|
+
coverage_statistics[:branch]&.total
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return total count of covered branches
|
92
|
+
def covered_branches
|
93
|
+
coverage_statistics[:branch]&.covered
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return total count of covered branches
|
97
|
+
def missed_branches
|
98
|
+
coverage_statistics[:branch]&.missed
|
99
|
+
end
|
100
|
+
|
101
|
+
def branch_covered_percent
|
102
|
+
coverage_statistics[:branch]&.percent
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def compute_coverage_statistics_by_file
|
108
|
+
@files.each_with_object(line: [], branch: []) do |file, together|
|
109
|
+
together[:line] << file.coverage_statistics.fetch(:line)
|
110
|
+
together[:branch] << file.coverage_statistics.fetch(:branch) if SimpleCov.branch_coverage?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def compute_coverage_statistics
|
115
|
+
coverage_statistics = {line: CoverageStatistics.from(coverage_statistics_by_file[:line])}
|
116
|
+
coverage_statistics[:branch] = CoverageStatistics.from(coverage_statistics_by_file[:branch]) if SimpleCov.branch_coverage?
|
117
|
+
coverage_statistics
|
59
118
|
end
|
60
119
|
end
|
61
120
|
end
|