simplecov 0.18.5 → 0.21.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 +75 -520
- data/README.md +112 -29
- data/doc/alternate-formatters.md +12 -2
- data/doc/commercial-services.md +5 -0
- data/lib/minitest/simplecov_plugin.rb +1 -1
- data/lib/simplecov.rb +126 -137
- data/lib/simplecov/configuration.rb +90 -15
- data/lib/simplecov/default_formatter.rb +20 -0
- data/lib/simplecov/defaults.rb +11 -8
- 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 +73 -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 +12 -6
- data/lib/simplecov/filter.rb +7 -5
- data/lib/simplecov/formatter.rb +2 -2
- data/lib/simplecov/formatter/multi_formatter.rb +5 -7
- data/lib/simplecov/lines_classifier.rb +3 -3
- data/lib/simplecov/no_defaults.rb +1 -1
- data/lib/simplecov/process.rb +19 -0
- data/lib/simplecov/result.rb +8 -35
- data/lib/simplecov/result_merger.rb +121 -57
- data/lib/simplecov/source_file/line.rb +1 -1
- data/lib/simplecov/useless_results_remover.rb +5 -3
- data/lib/simplecov/version.rb +1 -1
- metadata +31 -12
- data/CODE_OF_CONDUCT.md +0 -76
- data/CONTRIBUTING.md +0 -51
- data/ISSUE_TEMPLATE.md +0 -23
data/lib/simplecov/file_list.rb
CHANGED
@@ -27,6 +27,10 @@ module SimpleCov
|
|
27
27
|
@coverage_statistics ||= compute_coverage_statistics
|
28
28
|
end
|
29
29
|
|
30
|
+
def coverage_statistics_by_file
|
31
|
+
@coverage_statistics_by_file ||= compute_coverage_statistics_by_file
|
32
|
+
end
|
33
|
+
|
30
34
|
# Returns the count of lines that have coverage
|
31
35
|
def covered_lines
|
32
36
|
coverage_statistics[:line]&.covered
|
@@ -100,14 +104,16 @@ module SimpleCov
|
|
100
104
|
|
101
105
|
private
|
102
106
|
|
103
|
-
def
|
104
|
-
|
105
|
-
together[:line] << file.coverage_statistics
|
106
|
-
together[:branch] << file.coverage_statistics
|
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?
|
107
111
|
end
|
112
|
+
end
|
108
113
|
|
109
|
-
|
110
|
-
coverage_statistics
|
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?
|
111
117
|
coverage_statistics
|
112
118
|
end
|
113
119
|
end
|
data/lib/simplecov/filter.rb
CHANGED
@@ -14,6 +14,7 @@ module SimpleCov
|
|
14
14
|
#
|
15
15
|
class Filter
|
16
16
|
attr_reader :filter_argument
|
17
|
+
|
17
18
|
def initialize(filter_argument)
|
18
19
|
@filter_argument = filter_argument
|
19
20
|
end
|
@@ -34,13 +35,14 @@ module SimpleCov
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def self.class_for_argument(filter_argument)
|
37
|
-
|
38
|
+
case filter_argument
|
39
|
+
when String
|
38
40
|
SimpleCov::StringFilter
|
39
|
-
|
41
|
+
when Regexp
|
40
42
|
SimpleCov::RegexFilter
|
41
|
-
|
43
|
+
when Array
|
42
44
|
SimpleCov::ArrayFilter
|
43
|
-
|
45
|
+
when Proc
|
44
46
|
SimpleCov::BlockFilter
|
45
47
|
else
|
46
48
|
raise ArgumentError, "You have provided an unrecognized filter type"
|
@@ -50,7 +52,7 @@ module SimpleCov
|
|
50
52
|
|
51
53
|
class StringFilter < SimpleCov::Filter
|
52
54
|
# Returns true when the given source file's filename matches the
|
53
|
-
# string configured when initializing this Filter with StringFilter.new('somestring)
|
55
|
+
# string configured when initializing this Filter with StringFilter.new('somestring')
|
54
56
|
def matches?(source_file)
|
55
57
|
source_file.project_filename.include?(filter_argument)
|
56
58
|
end
|
data/lib/simplecov/formatter.rb
CHANGED
@@ -6,12 +6,10 @@ module SimpleCov
|
|
6
6
|
module InstanceMethods
|
7
7
|
def format(result)
|
8
8
|
formatters.map do |formatter|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
nil
|
14
|
-
end
|
9
|
+
formatter.new.format(result)
|
10
|
+
rescue StandardError => e
|
11
|
+
warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
|
12
|
+
nil
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
@@ -27,7 +25,7 @@ module SimpleCov
|
|
27
25
|
|
28
26
|
def self.[](*args)
|
29
27
|
warn "#{Kernel.caller.first}: [DEPRECATION] ::[] is deprecated. Use ::new instead."
|
30
|
-
new(Array(
|
28
|
+
new(Array(args))
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -13,18 +13,18 @@ module SimpleCov
|
|
13
13
|
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
14
14
|
|
15
15
|
def self.no_cov_line
|
16
|
-
/^(\s*)#(\s*)(
|
16
|
+
/^(\s*)#(\s*)(:#{SimpleCov.nocov_token}:)/o
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.no_cov_line?(line)
|
20
|
-
line
|
20
|
+
no_cov_line.match?(line)
|
21
21
|
rescue ArgumentError
|
22
22
|
# E.g., line contains an invalid byte sequence in UTF-8
|
23
23
|
false
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.whitespace_line?(line)
|
27
|
-
line
|
27
|
+
WHITESPACE_OR_COMMENT_LINE.match?(line)
|
28
28
|
rescue ArgumentError
|
29
29
|
# E.g., line contains an invalid byte sequence in UTF-8
|
30
30
|
false
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Process
|
4
|
+
class << self
|
5
|
+
def fork_with_simplecov(&block)
|
6
|
+
if defined?(SimpleCov) && SimpleCov.running
|
7
|
+
fork_without_simplecov do
|
8
|
+
SimpleCov.at_fork.call(Process.pid)
|
9
|
+
block.call if block_given?
|
10
|
+
end
|
11
|
+
else
|
12
|
+
fork_without_simplecov(&block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
alias fork_without_simplecov fork
|
17
|
+
alias fork fork_with_simplecov
|
18
|
+
end
|
19
|
+
end
|
data/lib/simplecov/result.rb
CHANGED
@@ -20,14 +20,16 @@ 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, :total_branches, :covered_branches, :missed_branches, :coverage_statistics
|
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
|
-
result =
|
28
|
+
def initialize(original_result, command_name: nil, created_at: nil)
|
29
|
+
result = original_result
|
30
30
|
@original_result = result.freeze
|
31
|
+
@command_name = command_name
|
32
|
+
@created_at = created_at
|
31
33
|
@files = SimpleCov::FileList.new(result.map do |filename, coverage|
|
32
34
|
SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
|
33
35
|
end.compact.sort_by(&:filename))
|
@@ -72,41 +74,12 @@ module SimpleCov
|
|
72
74
|
|
73
75
|
# Loads a SimpleCov::Result#to_hash dump
|
74
76
|
def self.from_hash(hash)
|
75
|
-
command_name, data
|
76
|
-
|
77
|
-
result = SimpleCov::Result.new(data["coverage"])
|
78
|
-
|
79
|
-
result.command_name = command_name
|
80
|
-
result.created_at = Time.at(data["timestamp"])
|
81
|
-
result
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
# We changed the format of the raw result data in simplecov, as people are likely
|
87
|
-
# to have "old" resultsets lying around (but not too old so that they're still
|
88
|
-
# considered we can adapt them).
|
89
|
-
# See https://github.com/colszowka/simplecov/pull/824#issuecomment-576049747
|
90
|
-
def adapt_result(result)
|
91
|
-
if pre_simplecov_0_18_result?(result)
|
92
|
-
adapt_pre_simplecov_0_18_result(result)
|
93
|
-
else
|
94
|
-
result
|
77
|
+
hash.map do |command_name, data|
|
78
|
+
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
|
95
79
|
end
|
96
80
|
end
|
97
81
|
|
98
|
-
|
99
|
-
def pre_simplecov_0_18_result?(result)
|
100
|
-
_key, data = result.first
|
101
|
-
|
102
|
-
data.is_a?(Array)
|
103
|
-
end
|
104
|
-
|
105
|
-
def adapt_pre_simplecov_0_18_result(result)
|
106
|
-
result.map do |file_path, line_coverage_data|
|
107
|
-
[file_path, {"lines" => line_coverage_data}]
|
108
|
-
end.to_h
|
109
|
-
end
|
82
|
+
private
|
110
83
|
|
111
84
|
def coverage
|
112
85
|
keys = original_result.keys & filenames
|
@@ -19,86 +19,130 @@ module SimpleCov
|
|
19
19
|
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
26
|
+
end
|
27
|
+
|
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))
|
36
41
|
end
|
42
|
+
|
43
|
+
create_result(command_names, coverage)
|
37
44
|
end
|
38
45
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
43
50
|
|
44
|
-
|
45
|
-
|
51
|
+
def parse_file(path)
|
52
|
+
data = read_file(path)
|
53
|
+
parse_json(data)
|
54
|
+
end
|
46
55
|
|
47
|
-
|
48
|
-
|
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
|
+
{}
|
49
72
|
end
|
50
73
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
results = []
|
57
|
-
resultset.each do |command_name, data|
|
58
|
-
result = SimpleCov::Result.from_hash(command_name => data)
|
59
|
-
# Only add result if the timeout is above the configured threshold
|
60
|
-
results << result if (Time.now - result.created_at) < SimpleCov.merge_timeout
|
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
|
-
def
|
66
|
-
|
67
|
-
store_result(result) if result
|
68
|
-
result
|
85
|
+
def within_merge_timeout?(data)
|
86
|
+
time_since_result_creation(data) < SimpleCov.merge_timeout
|
69
87
|
end
|
70
88
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
81
111
|
end
|
82
112
|
|
83
113
|
#
|
84
|
-
# Gets all SimpleCov::Results
|
114
|
+
# Gets all SimpleCov::Results stored in resultset, merges them and produces a new
|
85
115
|
# SimpleCov::Result with merged coverage data and the command_name
|
86
116
|
# for the result consisting of a join on all source result's names
|
87
|
-
#
|
88
117
|
def merged_result
|
89
|
-
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)
|
90
133
|
end
|
91
134
|
|
92
135
|
# Saves the given SimpleCov::Result in the resultset cache
|
93
136
|
def store_result(result)
|
94
137
|
synchronize_resultset do
|
95
138
|
# Ensure we have the latest, in case it was already cached
|
96
|
-
|
97
|
-
|
139
|
+
new_resultset = read_resultset
|
140
|
+
|
141
|
+
# A single result only ever has one command_name, see `SimpleCov::Result#to_hash`
|
98
142
|
command_name, data = result.to_hash.first
|
99
|
-
|
143
|
+
new_resultset[command_name] = data
|
100
144
|
File.open(resultset_path, "w+") do |f_|
|
101
|
-
f_.puts JSON.pretty_generate(
|
145
|
+
f_.puts JSON.pretty_generate(new_resultset)
|
102
146
|
end
|
103
147
|
end
|
104
148
|
true
|
@@ -121,9 +165,29 @@ module SimpleCov
|
|
121
165
|
end
|
122
166
|
end
|
123
167
|
|
124
|
-
#
|
125
|
-
|
126
|
-
|
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
|
127
191
|
end
|
128
192
|
end
|
129
193
|
end
|
@@ -56,7 +56,7 @@ module SimpleCov
|
|
56
56
|
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
57
57
|
# # :nocov: comment lines.
|
58
58
|
def skipped?
|
59
|
-
|
59
|
+
skipped
|
60
60
|
end
|
61
61
|
|
62
62
|
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
@@ -5,12 +5,14 @@ module SimpleCov
|
|
5
5
|
# Select the files that related to working scope directory of SimpleCov
|
6
6
|
#
|
7
7
|
module UselessResultsRemover
|
8
|
-
ROOT_REGX = /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/io.freeze
|
9
|
-
|
10
8
|
def self.call(coverage_result)
|
11
9
|
coverage_result.select do |path, _coverage|
|
12
|
-
path =~
|
10
|
+
path =~ root_regx
|
13
11
|
end
|
14
12
|
end
|
13
|
+
|
14
|
+
def self.root_regx
|
15
|
+
@root_regx ||= /\A#{Regexp.escape(SimpleCov.root + File::SEPARATOR)}/i.freeze
|
16
|
+
end
|
15
17
|
end
|
16
18
|
end
|