simplecov 0.17.1 → 0.18.0.beta1
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 +25 -1
- data/CODE_OF_CONDUCT.md +76 -0
- data/README.md +167 -63
- data/doc/alternate-formatters.md +5 -0
- data/lib/simplecov.rb +147 -65
- data/lib/simplecov/combine.rb +30 -0
- data/lib/simplecov/combine/branches_combiner.rb +32 -0
- data/lib/simplecov/combine/files_combiner.rb +25 -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 +77 -8
- data/lib/simplecov/defaults.rb +1 -1
- data/lib/simplecov/file_list.rb +30 -3
- 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 +2 -0
- data/lib/simplecov/lines_classifier.rb +2 -2
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +21 -4
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +12 -11
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +219 -109
- data/lib/simplecov/source_file/branch.rb +106 -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 +33 -168
- 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,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
|
@@ -27,7 +27,7 @@ module SimpleCov
|
|
27
27
|
if data
|
28
28
|
begin
|
29
29
|
JSON.parse(data) || {}
|
30
|
-
rescue
|
30
|
+
rescue StandardError
|
31
31
|
{}
|
32
32
|
end
|
33
33
|
else
|
@@ -40,8 +40,10 @@ module SimpleCov
|
|
40
40
|
def stored_data
|
41
41
|
synchronize_resultset do
|
42
42
|
return unless File.exist?(resultset_path)
|
43
|
+
|
43
44
|
data = File.read(resultset_path)
|
44
45
|
return if data.nil? || data.length < 2
|
46
|
+
|
45
47
|
data
|
46
48
|
end
|
47
49
|
end
|
@@ -55,9 +57,7 @@ module SimpleCov
|
|
55
57
|
resultset.each do |command_name, data|
|
56
58
|
result = SimpleCov::Result.from_hash(command_name => data)
|
57
59
|
# Only add result if the timeout is above the configured threshold
|
58
|
-
if (Time.now - result.created_at) < SimpleCov.merge_timeout
|
59
|
-
results << result
|
60
|
-
end
|
60
|
+
results << result if (Time.now - result.created_at) < SimpleCov.merge_timeout
|
61
61
|
end
|
62
62
|
results
|
63
63
|
end
|
@@ -66,8 +66,9 @@ module SimpleCov
|
|
66
66
|
# coverage data and the command_name for the result consisting of a join
|
67
67
|
# on all source result's names
|
68
68
|
def merge_results(*results)
|
69
|
-
|
70
|
-
|
69
|
+
parsed_results = JSON.parse(JSON.dump(results.map(&:original_result)), :symbolize_names => true)
|
70
|
+
combined_result = SimpleCov::Combine::ResultsCombiner.combine(*parsed_results)
|
71
|
+
result = SimpleCov::Result.new(combined_result)
|
71
72
|
# Specify the command name
|
72
73
|
result.command_name = results.map(&:command_name).sort.join(", ")
|
73
74
|
result
|
@@ -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
|
@@ -6,79 +6,13 @@ module SimpleCov
|
|
6
6
|
# source lines and featuring helpers to interpret that data.
|
7
7
|
#
|
8
8
|
class SourceFile
|
9
|
-
# Representation of a single line in a source file including
|
10
|
-
# this specific line's source code, line_number and code coverage,
|
11
|
-
# with the coverage being either nil (coverage not applicable, e.g. comment
|
12
|
-
# line), 0 (line not covered) or >1 (the amount of times the line was
|
13
|
-
# executed)
|
14
|
-
class Line
|
15
|
-
# The source code for this line. Aliased as :source
|
16
|
-
attr_reader :src
|
17
|
-
# The line number in the source file. Aliased as :line, :number
|
18
|
-
attr_reader :line_number
|
19
|
-
# The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
|
20
|
-
attr_reader :coverage
|
21
|
-
# Whether this line was skipped
|
22
|
-
attr_reader :skipped
|
23
|
-
|
24
|
-
# Lets grab some fancy aliases, shall we?
|
25
|
-
alias source src
|
26
|
-
alias line line_number
|
27
|
-
alias number line_number
|
28
|
-
|
29
|
-
def initialize(src, line_number, coverage)
|
30
|
-
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
31
|
-
raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
|
32
|
-
raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
|
33
|
-
@src = src
|
34
|
-
@line_number = line_number
|
35
|
-
@coverage = coverage
|
36
|
-
@skipped = false
|
37
|
-
end
|
38
|
-
|
39
|
-
# Returns true if this is a line that should have been covered, but was not
|
40
|
-
def missed?
|
41
|
-
!never? && !skipped? && coverage.zero?
|
42
|
-
end
|
43
|
-
|
44
|
-
# Returns true if this is a line that has been covered
|
45
|
-
def covered?
|
46
|
-
!never? && !skipped? && coverage > 0
|
47
|
-
end
|
48
|
-
|
49
|
-
# Returns true if this line is not relevant for coverage
|
50
|
-
def never?
|
51
|
-
!skipped? && coverage.nil?
|
52
|
-
end
|
53
|
-
|
54
|
-
# Flags this line as skipped
|
55
|
-
def skipped!
|
56
|
-
@skipped = true
|
57
|
-
end
|
58
|
-
|
59
|
-
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
60
|
-
# # :nocov: comment lines.
|
61
|
-
def skipped?
|
62
|
-
!!skipped
|
63
|
-
end
|
64
|
-
|
65
|
-
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
66
|
-
# as a css class in report generation
|
67
|
-
def status
|
68
|
-
return "skipped" if skipped?
|
69
|
-
return "never" if never?
|
70
|
-
return "missed" if missed?
|
71
|
-
return "covered" if covered?
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
9
|
# The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)
|
76
10
|
attr_reader :filename
|
77
11
|
# The array of coverage data received from the Coverage.result
|
78
12
|
attr_reader :coverage
|
79
13
|
|
80
14
|
def initialize(filename, coverage)
|
81
|
-
@filename = filename
|
15
|
+
@filename = filename.to_s
|
82
16
|
@coverage = coverage
|
83
17
|
end
|
84
18
|
|
@@ -102,19 +36,68 @@ module SimpleCov
|
|
102
36
|
end
|
103
37
|
alias source_lines lines
|
104
38
|
|
105
|
-
|
106
|
-
|
39
|
+
# Returns all covered lines as SimpleCov::SourceFile::Line
|
40
|
+
def covered_lines
|
41
|
+
@covered_lines ||= lines.select(&:covered?)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns all lines that should have been, but were not covered
|
45
|
+
# as instances of SimpleCov::SourceFile::Line
|
46
|
+
def missed_lines
|
47
|
+
@missed_lines ||= lines.select(&:missed?)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns all lines that are not relevant for coverage as
|
51
|
+
# SimpleCov::SourceFile::Line instances
|
52
|
+
def never_lines
|
53
|
+
@never_lines ||= lines.select(&:never?)
|
54
|
+
end
|
107
55
|
|
56
|
+
# Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
|
57
|
+
def skipped_lines
|
58
|
+
@skipped_lines ||= lines.select(&:skipped?)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the number of relevant lines (covered + missed)
|
62
|
+
def lines_of_code
|
63
|
+
covered_lines.size + missed_lines.size
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_lines
|
67
|
+
coverage_exceeding_source_warn if coverage[:lines].size > src.size
|
108
68
|
lines = src.map.with_index(1) do |src, i|
|
109
|
-
SimpleCov::SourceFile::Line.new(src, i, coverage[i - 1])
|
69
|
+
SimpleCov::SourceFile::Line.new(src, i, coverage[:lines][i - 1])
|
110
70
|
end
|
111
|
-
|
112
71
|
process_skipped_lines(lines)
|
113
72
|
end
|
114
73
|
|
74
|
+
# no_cov_chunks is zero indexed to work directly with the array holding the lines
|
75
|
+
def no_cov_chunks
|
76
|
+
@no_cov_chunks ||= build_no_cov_chunks
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_no_cov_chunks
|
80
|
+
no_cov_lines = src.map.with_index(1).select { |line, _index| LinesClassifier.no_cov_line?(line) }
|
81
|
+
|
82
|
+
warn "uneven number of nocov comments detected" if no_cov_lines.size.odd?
|
83
|
+
|
84
|
+
no_cov_lines.each_slice(2).map do |(_line_start, index_start), (_line_end, index_end)|
|
85
|
+
index_start..index_end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_skipped_lines(lines)
|
90
|
+
# the array the lines are kept in is 0-based whereas the line numbers in the nocov
|
91
|
+
# chunks are 1-based and are expected to be like this in other parts (and it's also
|
92
|
+
# arguably more understandable)
|
93
|
+
no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
94
|
+
|
95
|
+
lines
|
96
|
+
end
|
97
|
+
|
115
98
|
# Warning to identify condition from Issue #56
|
116
99
|
def coverage_exceeding_source_warn
|
117
|
-
|
100
|
+
warn "Warning: coverage data provided by Coverage [#{coverage[:lines].size}] exceeds number of lines in #{filename} [#{src.size}]"
|
118
101
|
end
|
119
102
|
|
120
103
|
# Access SimpleCov::SourceFile::Line source lines by line number
|
@@ -122,7 +105,7 @@ module SimpleCov
|
|
122
105
|
lines[number - 1]
|
123
106
|
end
|
124
107
|
|
125
|
-
# The coverage for this file in percent. 0 if the file has no
|
108
|
+
# The coverage for this file in percent. 0 if the file has no coverage lines
|
126
109
|
def covered_percent
|
127
110
|
return 100.0 if no_lines?
|
128
111
|
|
@@ -134,7 +117,7 @@ module SimpleCov
|
|
134
117
|
def covered_strength
|
135
118
|
return 0.0 if relevant_lines.zero?
|
136
119
|
|
137
|
-
|
120
|
+
(lines_strength / relevant_lines.to_f).round(1)
|
138
121
|
end
|
139
122
|
|
140
123
|
def no_lines?
|
@@ -149,55 +132,182 @@ module SimpleCov
|
|
149
132
|
lines.size - never_lines.size - skipped_lines.size
|
150
133
|
end
|
151
134
|
|
152
|
-
#
|
153
|
-
|
154
|
-
|
135
|
+
#
|
136
|
+
# Return all the branches inside current source file
|
137
|
+
def branches
|
138
|
+
@branches ||= build_branches
|
155
139
|
end
|
156
140
|
|
157
|
-
|
158
|
-
|
159
|
-
def missed_lines
|
160
|
-
@missed_lines ||= lines.select(&:missed?)
|
141
|
+
def no_branches?
|
142
|
+
total_branches.empty?
|
161
143
|
end
|
162
144
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
145
|
+
def branches_coverage_percent
|
146
|
+
return 100.0 if no_branches?
|
147
|
+
return 0.0 if covered_branches.empty?
|
148
|
+
|
149
|
+
Float(covered_branches.size * 100.0 / total_branches.size.to_f)
|
167
150
|
end
|
168
151
|
|
169
|
-
#
|
170
|
-
|
171
|
-
|
152
|
+
#
|
153
|
+
# Return the relevant branches to source file
|
154
|
+
def total_branches
|
155
|
+
covered_branches + missed_branches
|
172
156
|
end
|
173
157
|
|
174
|
-
#
|
175
|
-
|
176
|
-
|
158
|
+
#
|
159
|
+
# Return hash with key of line number and branch coverage count as value
|
160
|
+
def branches_report
|
161
|
+
@branches_report ||= build_branches_report
|
177
162
|
end
|
178
163
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
line.skipped!
|
190
|
-
end
|
164
|
+
## Related to source file branches statistics
|
165
|
+
|
166
|
+
#
|
167
|
+
# Call recursive method that transform our static hash to array of objects
|
168
|
+
# @return [Array]
|
169
|
+
#
|
170
|
+
def build_branches
|
171
|
+
coverage_branch_data = coverage.fetch(:branches, {})
|
172
|
+
branches = coverage_branch_data.flat_map do |condition, coverage_branches|
|
173
|
+
build_branches_from(condition, coverage_branches)
|
191
174
|
end
|
175
|
+
|
176
|
+
process_skipped_branches(branches)
|
192
177
|
end
|
193
178
|
|
194
|
-
|
179
|
+
def process_skipped_branches(branches)
|
180
|
+
return branches if no_cov_chunks.empty?
|
181
|
+
|
182
|
+
branches.each do |branch|
|
183
|
+
branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
|
184
|
+
end
|
185
|
+
|
186
|
+
branches
|
187
|
+
end
|
188
|
+
|
189
|
+
# Since we are dumping to and loading from JSON, and we have arrays as keys those
|
190
|
+
# don't make their way back to us intact e.g. just as a string or a symbol (currently keys are symbolized).
|
191
|
+
#
|
192
|
+
# We should probably do something different here, but as it stands these are
|
193
|
+
# our data structures that we write so eval isn't _too_ bad.
|
194
|
+
#
|
195
|
+
# See #801
|
196
|
+
#
|
197
|
+
def restore_ruby_data_structure(structure)
|
198
|
+
# Tests use the real data structures (except for integration tests) so no need to
|
199
|
+
# put them through here.
|
200
|
+
return structure if structure.is_a?(Array)
|
201
|
+
|
202
|
+
# as of right now the keys are still symbolized
|
203
|
+
# rubocop:disable Security/Eval
|
204
|
+
eval structure.to_s
|
205
|
+
# rubocop:enable Security/Eval
|
206
|
+
end
|
195
207
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
208
|
+
def build_branches_from(condition, branches)
|
209
|
+
# the format handed in from the coverage data is like this:
|
210
|
+
#
|
211
|
+
# [:then, 4, 6, 6, 6, 10]
|
212
|
+
#
|
213
|
+
# which is [type, id, start_line, start_col, end_line, end_col]
|
214
|
+
condition_type, condition_id, condition_start_line, * = restore_ruby_data_structure(condition)
|
215
|
+
|
216
|
+
branches
|
217
|
+
.map { |branch_data, hit_count| [restore_ruby_data_structure(branch_data), hit_count] }
|
218
|
+
.reject { |branch_data, _hit_count| ignore_branch?(branch_data, condition_type, condition_start_line) }
|
219
|
+
.map { |branch_data, hit_count| build_branch(branch_data, hit_count, condition_start_line, condition_id) }
|
220
|
+
end
|
221
|
+
|
222
|
+
def build_branch(branch_data, hit_count, condition_start_line, condition_id)
|
223
|
+
type, id, start_line, _start_col, end_line, _end_col = branch_data
|
224
|
+
|
225
|
+
SourceFile::Branch.new(
|
226
|
+
# rubocop these are keyword args please let me keep them, thank you
|
227
|
+
# rubocop:disable Style/HashSyntax
|
228
|
+
start_line: start_line,
|
229
|
+
end_line: end_line,
|
230
|
+
coverage: hit_count,
|
231
|
+
inline: start_line == condition_start_line,
|
232
|
+
positive: positive_branch?(condition_id, id, type)
|
233
|
+
# rubocop:enable Style/HashSyntax
|
234
|
+
)
|
235
|
+
end
|
236
|
+
|
237
|
+
def ignore_branch?(branch_data, condition_type, condition_start_line)
|
238
|
+
branch_type = branch_data[0]
|
239
|
+
branch_start_line = branch_data[2]
|
240
|
+
|
241
|
+
# branch coverage always reports case to be with an else branch even when
|
242
|
+
# there is no else branch to be covered, it's noticable by the reported start
|
243
|
+
# line being the same as that of the condition/case
|
244
|
+
condition_type == :case &&
|
245
|
+
branch_type == :else &&
|
246
|
+
condition_start_line == branch_start_line
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# Branch is positive or negative.
|
251
|
+
# For `case` conditions, `when` always supposed as positive branch.
|
252
|
+
# For `if, else` conditions:
|
253
|
+
# coverage returns matrices ex: [:if, 0,..] => {[:then, 1,..], [:else, 2,..]},
|
254
|
+
# positive branch always has id equals to condition id incremented by 1.
|
255
|
+
#
|
256
|
+
# @return [Boolean]
|
257
|
+
#
|
258
|
+
def positive_branch?(condition_id, branch_id, branch_type)
|
259
|
+
return true if branch_type == :when
|
260
|
+
|
261
|
+
branch_id == (1 + condition_id)
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Select the covered branches
|
266
|
+
# Here we user tree schema because some conditions like case may have additional
|
267
|
+
# else that is not in declared inside the code but given by default by coverage report
|
268
|
+
#
|
269
|
+
# @return [Array]
|
270
|
+
#
|
271
|
+
def covered_branches
|
272
|
+
@covered_branches ||= branches.select(&:covered?)
|
273
|
+
end
|
274
|
+
|
275
|
+
#
|
276
|
+
# Select the missed branches with coverage equal to zero
|
277
|
+
#
|
278
|
+
# @return [Array]
|
279
|
+
#
|
280
|
+
def missed_branches
|
281
|
+
@missed_branches ||= branches.select(&:missed?)
|
282
|
+
end
|
283
|
+
|
284
|
+
def branches_for_line(line_number)
|
285
|
+
branches_report.fetch(line_number, [])
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
# Check if any branches missing on given line number
|
290
|
+
#
|
291
|
+
# @param [Integer] line_number
|
292
|
+
#
|
293
|
+
# @return [Boolean]
|
294
|
+
#
|
295
|
+
def line_with_missed_branch?(line_number)
|
296
|
+
branches_for_line(line_number).select { |count, _sign| count.zero? }.any?
|
297
|
+
end
|
298
|
+
|
299
|
+
#
|
300
|
+
# Build full branches report
|
301
|
+
# Root branches represent the wrapper of all condition state that
|
302
|
+
# have inside the branches
|
303
|
+
#
|
304
|
+
# @return [Hash]
|
305
|
+
#
|
306
|
+
def build_branches_report
|
307
|
+
branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
|
308
|
+
coverage_statistics[branch.report_line] ||= []
|
309
|
+
coverage_statistics[branch.report_line] << branch.report
|
310
|
+
end
|
201
311
|
end
|
202
312
|
end
|
203
313
|
end
|