simplecov 0.16.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 +5 -5
- data/CHANGELOG.md +107 -407
- data/README.md +378 -126
- data/doc/alternate-formatters.md +21 -1
- data/doc/commercial-services.md +5 -0
- data/lib/minitest/simplecov_plugin.rb +15 -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/combine.rb +30 -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 +15 -12
- 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/exit_codes.rb +5 -0
- data/lib/simplecov/file_list.rb +72 -13
- data/lib/simplecov/filter.rb +9 -6
- data/lib/simplecov/formatter/multi_formatter.rb +5 -7
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/formatter.rb +2 -2
- 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/hidden_filter.rb +5 -0
- data/lib/simplecov/profiles/rails.rb +1 -1
- 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/branch.rb +84 -0
- data/lib/simplecov/source_file/line.rb +72 -0
- data/lib/simplecov/source_file.rb +273 -127
- data/lib/simplecov/useless_results_remover.rb +18 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +308 -121
- metadata +45 -47
- 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
data/lib/simplecov/result.rb
CHANGED
@@ -5,7 +5,7 @@ require "forwardable"
|
|
5
5
|
|
6
6
|
module SimpleCov
|
7
7
|
#
|
8
|
-
# A simplecov code coverage result, initialized from the Hash Ruby
|
8
|
+
# A simplecov code coverage result, initialized from the Hash Ruby's built-in coverage
|
9
9
|
# library generates (Coverage.result).
|
10
10
|
#
|
11
11
|
class Result
|
@@ -20,15 +20,18 @@ module SimpleCov
|
|
20
20
|
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
|
21
21
|
attr_writer :command_name
|
22
22
|
|
23
|
-
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines
|
23
|
+
def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics, :coverage_statistics_by_file
|
24
24
|
def_delegator :files, :lines_of_code, :total_lines
|
25
25
|
|
26
26
|
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
|
27
27
|
# coverage data)
|
28
|
-
def initialize(original_result)
|
29
|
-
|
30
|
-
@
|
31
|
-
|
28
|
+
def initialize(original_result, command_name: nil, created_at: nil)
|
29
|
+
result = original_result
|
30
|
+
@original_result = result.freeze
|
31
|
+
@command_name = command_name
|
32
|
+
@created_at = created_at
|
33
|
+
@files = SimpleCov::FileList.new(result.map do |filename, coverage|
|
34
|
+
SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
|
32
35
|
end.compact.sort_by(&:filename))
|
33
36
|
filter!
|
34
37
|
end
|
@@ -61,16 +64,19 @@ module SimpleCov
|
|
61
64
|
|
62
65
|
# Returns a hash representation of this Result that can be used for marshalling it into JSON
|
63
66
|
def to_hash
|
64
|
-
{
|
67
|
+
{
|
68
|
+
command_name => {
|
69
|
+
"coverage" => coverage,
|
70
|
+
"timestamp" => created_at.to_i
|
71
|
+
}
|
72
|
+
}
|
65
73
|
end
|
66
74
|
|
67
75
|
# Loads a SimpleCov::Result#to_hash dump
|
68
76
|
def self.from_hash(hash)
|
69
|
-
command_name, data
|
70
|
-
|
71
|
-
|
72
|
-
result.created_at = Time.at(data["timestamp"])
|
73
|
-
result
|
77
|
+
hash.map do |command_name, data|
|
78
|
+
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
|
79
|
+
end
|
74
80
|
end
|
75
81
|
|
76
82
|
private
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
#
|
5
|
+
# Responsible for adapting the format of the coverage result whether it's default or with statistics
|
6
|
+
#
|
7
|
+
class ResultAdapter
|
8
|
+
attr_reader :result
|
9
|
+
|
10
|
+
def initialize(result)
|
11
|
+
@result = result
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call(*args)
|
15
|
+
new(*args).adapt
|
16
|
+
end
|
17
|
+
|
18
|
+
def adapt
|
19
|
+
return unless result
|
20
|
+
|
21
|
+
result.each_with_object({}) do |(file_name, cover_statistic), adapted_result|
|
22
|
+
if cover_statistic.is_a?(Array)
|
23
|
+
adapted_result.merge!(file_name => {"lines" => cover_statistic})
|
24
|
+
else
|
25
|
+
adapted_result.merge!(file_name => cover_statistic)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require "json"
|
4
4
|
|
5
|
-
#
|
6
|
-
# Singleton that is responsible for caching, loading and merging
|
7
|
-
# SimpleCov::Results into a single result for coverage analysis based
|
8
|
-
# upon multiple test suites.
|
9
|
-
#
|
10
5
|
module SimpleCov
|
6
|
+
#
|
7
|
+
# Singleton that is responsible for caching, loading and merging
|
8
|
+
# SimpleCov::Results into a single result for coverage analysis based
|
9
|
+
# upon multiple test suites.
|
10
|
+
#
|
11
11
|
module ResultMerger
|
12
12
|
class << self
|
13
13
|
# The path to the .resultset.json cache file
|
@@ -19,79 +19,130 @@ module SimpleCov
|
|
19
19
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
data = stored_data
|
27
|
-
if data
|
28
|
-
begin
|
29
|
-
JSON.parse(data) || {}
|
30
|
-
rescue
|
31
|
-
{}
|
32
|
-
end
|
33
|
-
else
|
34
|
-
{}
|
35
|
-
end
|
36
|
-
end
|
22
|
+
def merge_and_store(*file_paths, ignore_timeout: false)
|
23
|
+
result = merge_results(*file_paths, ignore_timeout: ignore_timeout)
|
24
|
+
store_result(result) if result
|
25
|
+
result
|
37
26
|
end
|
38
27
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
28
|
+
def merge_results(*file_paths, ignore_timeout: false)
|
29
|
+
# It is intentional here that files are only read in and parsed one at a time.
|
30
|
+
#
|
31
|
+
# In big CI setups you might deal with 100s of CI jobs and each one producing Megabytes
|
32
|
+
# of data. Reading them all in easily produces Gigabytes of memory consumption which
|
33
|
+
# we want to avoid.
|
34
|
+
#
|
35
|
+
# For similar reasons a SimpleCov::Result is only created in the end as that'd create
|
36
|
+
# even more data especially when it also reads in all source files.
|
37
|
+
initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
|
38
|
+
|
39
|
+
command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
|
40
|
+
merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
|
46
41
|
end
|
42
|
+
|
43
|
+
create_result(command_names, coverage)
|
47
44
|
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
46
|
+
def valid_results(file_path, ignore_timeout: false)
|
47
|
+
results = parse_file(file_path)
|
48
|
+
merge_valid_results(results, ignore_timeout: ignore_timeout)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_file(path)
|
52
|
+
data = read_file(path)
|
53
|
+
parse_json(data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_file(path)
|
57
|
+
return unless File.exist?(path)
|
58
|
+
|
59
|
+
data = File.read(path)
|
60
|
+
return if data.nil? || data.length < 2
|
61
|
+
|
62
|
+
data
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_json(content)
|
66
|
+
return {} unless content
|
67
|
+
|
68
|
+
JSON.parse(content) || {}
|
69
|
+
rescue StandardError
|
70
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
71
|
+
{}
|
72
|
+
end
|
73
|
+
|
74
|
+
def merge_valid_results(results, ignore_timeout: false)
|
75
|
+
results = results.select { |_command_name, data| within_merge_timeout?(data) } unless ignore_timeout
|
76
|
+
|
77
|
+
command_plus_coverage = results.map do |command_name, data|
|
78
|
+
[[command_name], adapt_result(data.fetch("coverage"))]
|
61
79
|
end
|
62
|
-
|
80
|
+
|
81
|
+
# one file itself _might_ include multiple test runs
|
82
|
+
merge_coverage(*command_plus_coverage)
|
63
83
|
end
|
64
84
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
85
|
+
def within_merge_timeout?(data)
|
86
|
+
time_since_result_creation(data) < SimpleCov.merge_timeout
|
87
|
+
end
|
88
|
+
|
89
|
+
def time_since_result_creation(data)
|
90
|
+
Time.now - Time.at(data.fetch("timestamp"))
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_result(command_names, coverage)
|
94
|
+
return nil unless coverage
|
95
|
+
|
96
|
+
command_name = command_names.reject(&:empty?).sort.join(", ")
|
97
|
+
SimpleCov::Result.new(coverage, command_name: command_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def merge_coverage(*results)
|
101
|
+
return [[""], nil] if results.empty?
|
102
|
+
return results.first if results.size == 1
|
103
|
+
|
104
|
+
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
|
105
|
+
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
106
|
+
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
107
|
+
merged_command = memo_command + command
|
108
|
+
|
109
|
+
[merged_command, merged_coverage]
|
110
|
+
end
|
74
111
|
end
|
75
112
|
|
76
113
|
#
|
77
|
-
# Gets all SimpleCov::Results
|
114
|
+
# Gets all SimpleCov::Results stored in resultset, merges them and produces a new
|
78
115
|
# SimpleCov::Result with merged coverage data and the command_name
|
79
116
|
# for the result consisting of a join on all source result's names
|
80
|
-
#
|
81
117
|
def merged_result
|
82
|
-
merge_results(
|
118
|
+
# conceptually this is just doing `merge_results(resultset_path)`
|
119
|
+
# it's more involved to make syre `synchronize_resultset` is only used around reading
|
120
|
+
resultset_hash = read_resultset
|
121
|
+
command_names, coverage = merge_valid_results(resultset_hash)
|
122
|
+
|
123
|
+
create_result(command_names, coverage)
|
124
|
+
end
|
125
|
+
|
126
|
+
def read_resultset
|
127
|
+
resultset_content =
|
128
|
+
synchronize_resultset do
|
129
|
+
read_file(resultset_path)
|
130
|
+
end
|
131
|
+
|
132
|
+
parse_json(resultset_content)
|
83
133
|
end
|
84
134
|
|
85
135
|
# Saves the given SimpleCov::Result in the resultset cache
|
86
136
|
def store_result(result)
|
87
137
|
synchronize_resultset do
|
88
138
|
# Ensure we have the latest, in case it was already cached
|
89
|
-
|
90
|
-
|
139
|
+
new_resultset = read_resultset
|
140
|
+
|
141
|
+
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
91
142
|
command_name, data = result.to_hash.first
|
92
|
-
|
143
|
+
new_resultset[command_name] = data
|
93
144
|
File.open(resultset_path, "w+") do |f_|
|
94
|
-
f_.puts JSON.pretty_generate(
|
145
|
+
f_.puts JSON.pretty_generate(new_resultset)
|
95
146
|
end
|
96
147
|
end
|
97
148
|
true
|
@@ -114,9 +165,29 @@ module SimpleCov
|
|
114
165
|
end
|
115
166
|
end
|
116
167
|
|
117
|
-
#
|
118
|
-
|
119
|
-
|
168
|
+
# We changed the format of the raw result data in simplecov, as people are likely
|
169
|
+
# to have "old" resultsets lying around (but not too old so that they're still
|
170
|
+
# considered we can adapt them).
|
171
|
+
# See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
172
|
+
def adapt_result(result)
|
173
|
+
if pre_simplecov_0_18_result?(result)
|
174
|
+
adapt_pre_simplecov_0_18_result(result)
|
175
|
+
else
|
176
|
+
result
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# pre 0.18 coverage data pointed from file directly to an array of line coverage
|
181
|
+
def pre_simplecov_0_18_result?(result)
|
182
|
+
_key, data = result.first
|
183
|
+
|
184
|
+
data.is_a?(Array)
|
185
|
+
end
|
186
|
+
|
187
|
+
def adapt_pre_simplecov_0_18_result(result)
|
188
|
+
result.transform_values do |line_coverage_data|
|
189
|
+
{"lines" => line_coverage_data}
|
190
|
+
end
|
120
191
|
end
|
121
192
|
end
|
122
193
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
#
|
5
|
+
# Responsible for producing file coverage metrics.
|
6
|
+
#
|
7
|
+
module SimulateCoverage
|
8
|
+
module_function
|
9
|
+
|
10
|
+
#
|
11
|
+
# Simulate normal file coverage report on
|
12
|
+
# ruby 2.5 and return similar hash with lines and branches keys
|
13
|
+
#
|
14
|
+
# Happens when a file wasn't required but still tracked.
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
#
|
18
|
+
def call(absolute_path)
|
19
|
+
lines = File.foreach(absolute_path)
|
20
|
+
|
21
|
+
{
|
22
|
+
"lines" => LinesClassifier.new.classify(lines),
|
23
|
+
# we don't want to parse branches ourselves...
|
24
|
+
# requiring files can have side effects and we don't want to trigger that
|
25
|
+
"branches" => {}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
class SourceFile
|
5
|
+
#
|
6
|
+
# Representing single branch that has been detected in coverage report.
|
7
|
+
# Give us support methods that handle needed calculations.
|
8
|
+
class Branch
|
9
|
+
attr_reader :start_line, :end_line, :coverage, :type
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/ParameterLists
|
12
|
+
def initialize(start_line:, end_line:, coverage:, inline:, type:)
|
13
|
+
@start_line = start_line
|
14
|
+
@end_line = end_line
|
15
|
+
@coverage = coverage
|
16
|
+
@inline = inline
|
17
|
+
@type = type
|
18
|
+
@skipped = false
|
19
|
+
end
|
20
|
+
# rubocop:enable Metrics/ParameterLists
|
21
|
+
|
22
|
+
def inline?
|
23
|
+
@inline
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Return true if there is relevant count defined > 0
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
#
|
31
|
+
def covered?
|
32
|
+
!skipped? && coverage.positive?
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Check if branche missed or not
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
#
|
40
|
+
def missed?
|
41
|
+
!skipped? && coverage.zero?
|
42
|
+
end
|
43
|
+
|
44
|
+
# The line on which we want to report the coverage
|
45
|
+
#
|
46
|
+
# Usually we choose the line above the start of the branch (so that it shows up
|
47
|
+
# at if/else) because that
|
48
|
+
# * highlights the condition
|
49
|
+
# * makes it distinguishable if the first line of the branch is an inline branch
|
50
|
+
# (see the nested_branches fixture)
|
51
|
+
#
|
52
|
+
def report_line
|
53
|
+
if inline?
|
54
|
+
start_line
|
55
|
+
else
|
56
|
+
start_line - 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Flags the branch as skipped
|
61
|
+
def skipped!
|
62
|
+
@skipped = true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns true if the branch was marked skipped by virtue of nocov comments.
|
66
|
+
def skipped?
|
67
|
+
@skipped
|
68
|
+
end
|
69
|
+
|
70
|
+
def overlaps_with?(line_range)
|
71
|
+
start_line <= line_range.end && end_line >= line_range.begin
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Return array with coverage count and badge
|
76
|
+
#
|
77
|
+
# @return [Array]
|
78
|
+
#
|
79
|
+
def report
|
80
|
+
[type, coverage]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleCov
|
4
|
+
class SourceFile
|
5
|
+
# Representation of a single line in a source file including
|
6
|
+
# this specific line's source code, line_number and code coverage,
|
7
|
+
# with the coverage being either nil (coverage not applicable, e.g. comment
|
8
|
+
# line), 0 (line not covered) or >1 (the amount of times the line was
|
9
|
+
# executed)
|
10
|
+
class Line
|
11
|
+
# The source code for this line. Aliased as :source
|
12
|
+
attr_reader :src
|
13
|
+
# The line number in the source file. Aliased as :line, :number
|
14
|
+
attr_reader :line_number
|
15
|
+
# The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
|
16
|
+
attr_reader :coverage
|
17
|
+
# Whether this line was skipped
|
18
|
+
attr_reader :skipped
|
19
|
+
|
20
|
+
# Lets grab some fancy aliases, shall we?
|
21
|
+
alias source src
|
22
|
+
alias line line_number
|
23
|
+
alias number line_number
|
24
|
+
|
25
|
+
def initialize(src, line_number, coverage)
|
26
|
+
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
27
|
+
raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
|
28
|
+
raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
|
29
|
+
|
30
|
+
@src = src
|
31
|
+
@line_number = line_number
|
32
|
+
@coverage = coverage
|
33
|
+
@skipped = false
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if this is a line that should have been covered, but was not
|
37
|
+
def missed?
|
38
|
+
!never? && !skipped? && coverage.zero?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true if this is a line that has been covered
|
42
|
+
def covered?
|
43
|
+
!never? && !skipped? && coverage.positive?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if this line is not relevant for coverage
|
47
|
+
def never?
|
48
|
+
!skipped? && coverage.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Flags this line as skipped
|
52
|
+
def skipped!
|
53
|
+
@skipped = true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
57
|
+
# # :nocov: comment lines.
|
58
|
+
def skipped?
|
59
|
+
skipped
|
60
|
+
end
|
61
|
+
|
62
|
+
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
63
|
+
# as a css class in report generation
|
64
|
+
def status
|
65
|
+
return "skipped" if skipped?
|
66
|
+
return "never" if never?
|
67
|
+
return "missed" if missed?
|
68
|
+
return "covered" if covered?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|