simplecov 1.0.0 → 1.1.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/README.md +56 -1451
- data/lib/simplecov/atomic_file.rb +70 -0
- data/lib/simplecov/cli/clean.rb +43 -3
- data/lib/simplecov/cli/command_helpers.rb +55 -0
- data/lib/simplecov/cli/coverage.rb +23 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -47
- data/lib/simplecov/cli/dotfile.rb +21 -10
- data/lib/simplecov/cli/merge.rb +17 -13
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +38 -25
- data/lib/simplecov/cli/run.rb +1 -1
- data/lib/simplecov/cli/serve/report_preparer.rb +29 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +122 -0
- data/lib/simplecov/cli/serve.rb +45 -99
- data/lib/simplecov/cli/uncovered.rb +20 -24
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/color.rb +10 -1
- data/lib/simplecov/combine/branches_combiner.rb +43 -18
- data/lib/simplecov/combine/coverage_accumulator.rb +268 -0
- data/lib/simplecov/combine/identity_interner.rb +30 -0
- data/lib/simplecov/combine/interned_counts.rb +30 -0
- data/lib/simplecov/combine/lines_combiner.rb +48 -20
- data/lib/simplecov/combine/methods_combiner.rb +53 -5
- data/lib/simplecov/combine/results_combiner.rb +12 -36
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +17 -16
- data/lib/simplecov/configuration/coverage_criteria.rb +37 -39
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +33 -14
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +27 -22
- data/lib/simplecov/configuration.rb +21 -42
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +5 -2
- data/lib/simplecov/coverage_violations.rb +17 -6
- data/lib/simplecov/defaults.rb +10 -6
- data/lib/simplecov/directive.rb +9 -4
- data/lib/simplecov/exit_codes/check.rb +33 -0
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +11 -20
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +4 -17
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +14 -23
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +14 -25
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +7 -20
- data/lib/simplecov/exit_codes.rb +17 -0
- data/lib/simplecov/exit_handling.rb +13 -43
- data/lib/simplecov/file_list.rb +14 -12
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +18 -1
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +36 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +71 -45
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -50
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +78 -88
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +70 -76
- data/lib/simplecov/formatter/json_formatter.rb +5 -46
- data/lib/simplecov/formatter/multi_formatter.rb +8 -3
- data/lib/simplecov/formatter/simple_formatter.rb +8 -3
- data/lib/simplecov/formatter.rb +12 -0
- data/lib/simplecov/group_names.rb +32 -0
- data/lib/simplecov/last_run.rb +16 -9
- data/lib/simplecov/lines_classifier.rb +29 -8
- data/lib/simplecov/load_global_config.rb +5 -2
- data/lib/simplecov/parallel_adapters/base.rb +17 -0
- data/lib/simplecov/parallel_adapters/generic.rb +9 -8
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_adapters.rb +4 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -0
- data/lib/simplecov/process.rb +22 -0
- data/lib/simplecov/report_deferral.rb +49 -0
- data/lib/simplecov/report_stamp.rb +28 -0
- data/lib/simplecov/result.rb +40 -10
- data/lib/simplecov/result_adapter.rb +79 -10
- data/lib/simplecov/result_merger/resultset_file.rb +43 -7
- data/lib/simplecov/result_merger/resultset_run_identity.rb +67 -0
- data/lib/simplecov/result_merger/resultset_store.rb +15 -12
- data/lib/simplecov/result_merger/unloaded_files.rb +103 -0
- data/lib/simplecov/result_merger.rb +69 -42
- data/lib/simplecov/result_processing.rb +85 -46
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +37 -12
- data/lib/simplecov/source_file/line.rb +2 -2
- data/lib/simplecov/source_file/method.rb +8 -2
- data/lib/simplecov/source_file/method_builder.rb +3 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +25 -3
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +25 -9
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/source_file.rb +2 -2
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +239 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +236 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +7 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +74 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +74 -62
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +37 -17
- data/sig/simplecov.rbs +1777 -0
- metadata +32 -16
- data/doc/alternate-formatters.md +0 -66
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -30
- data/lib/simplecov/formatter/html_formatter/public/application.css +0 -1
- data/lib/simplecov/formatter/html_formatter/public/application.js +0 -18
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -14,9 +14,7 @@ module SimpleCov
|
|
|
14
14
|
def minimum_coverage(coverage = nil)
|
|
15
15
|
return @minimum_coverage ||= {} unless coverage
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
raise_on_invalid_coverage(coverage, "minimum_coverage")
|
|
19
|
-
@minimum_coverage = coverage
|
|
17
|
+
@minimum_coverage = normalized_threshold(coverage, "minimum_coverage")
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def raise_on_invalid_coverage(coverage, coverage_setting)
|
|
@@ -35,9 +33,7 @@ module SimpleCov
|
|
|
35
33
|
def maximum_coverage(coverage = nil)
|
|
36
34
|
return @maximum_coverage ||= {} unless coverage
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
raise_on_invalid_coverage(coverage, "maximum_coverage")
|
|
40
|
-
@maximum_coverage = coverage
|
|
36
|
+
@maximum_coverage = normalized_threshold(coverage, "maximum_coverage")
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
#
|
|
@@ -58,9 +54,7 @@ module SimpleCov
|
|
|
58
54
|
def maximum_coverage_drop(coverage_drop = nil)
|
|
59
55
|
return @maximum_coverage_drop ||= {} unless coverage_drop
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
raise_on_invalid_coverage(coverage_drop, "maximum_coverage_drop")
|
|
63
|
-
@maximum_coverage_drop = coverage_drop
|
|
57
|
+
@maximum_coverage_drop = normalized_threshold(coverage_drop, "maximum_coverage_drop")
|
|
64
58
|
end
|
|
65
59
|
|
|
66
60
|
#
|
|
@@ -100,10 +94,8 @@ module SimpleCov
|
|
|
100
94
|
|
|
101
95
|
SimpleCov::Deprecation.warn("`SimpleCov.minimum_coverage_by_group` is deprecated. " \
|
|
102
96
|
"Replace it with:\n#{per_group_coverage_replacement(coverage)}")
|
|
103
|
-
@minimum_coverage_by_group = coverage.
|
|
104
|
-
|
|
105
|
-
raise_on_invalid_coverage(group_coverage, "minimum_coverage_by_group")
|
|
106
|
-
group_coverage
|
|
97
|
+
@minimum_coverage_by_group = coverage.to_h do |group_name, group_coverage|
|
|
98
|
+
[GroupNames.normalize(group_name), normalized_threshold(group_coverage, "minimum_coverage_by_group")]
|
|
107
99
|
end
|
|
108
100
|
end
|
|
109
101
|
|
|
@@ -117,13 +109,26 @@ module SimpleCov
|
|
|
117
109
|
|
|
118
110
|
private
|
|
119
111
|
|
|
112
|
+
# Shared normalize-and-validate step behind every threshold setter:
|
|
113
|
+
# a bare Numeric targets the primary criterion, and the resulting
|
|
114
|
+
# per-criterion hash is validated before it is stored.
|
|
115
|
+
def normalized_threshold(coverage, setting)
|
|
116
|
+
coverage = {primary_coverage => coverage} if coverage.is_a?(Numeric)
|
|
117
|
+
raise_on_invalid_coverage(coverage, setting)
|
|
118
|
+
coverage
|
|
119
|
+
end
|
|
120
|
+
|
|
120
121
|
# Split a `minimum_coverage_by_file` argument into Symbol-keyed
|
|
121
122
|
# criterion defaults and String/Regexp-keyed per-path overrides;
|
|
122
123
|
# normalize Numeric override values to `{primary_coverage => N}`
|
|
123
124
|
# so downstream code only has one shape to handle.
|
|
124
125
|
def partition_per_file_thresholds(coverage)
|
|
125
126
|
coverage.each_key { |key| validate_per_file_key(key) }
|
|
126
|
-
|
|
127
|
+
pairs = coverage.partition { |key, _| key.is_a?(Symbol) }
|
|
128
|
+
# The assertions restate what the partition predicate guarantees:
|
|
129
|
+
# Symbol keys carry per-criterion Numeric defaults, the rest are paths.
|
|
130
|
+
defaults = pairs[0].to_h #: coverage_thresholds
|
|
131
|
+
raw = pairs[1].to_h #: Hash[String | Regexp, Numeric | coverage_thresholds]
|
|
127
132
|
overrides = raw.transform_values { |value| value.is_a?(Numeric) ? {primary_coverage => value} : value }
|
|
128
133
|
[defaults, overrides]
|
|
129
134
|
end
|
|
@@ -143,11 +148,11 @@ module SimpleCov
|
|
|
143
148
|
# `minimum_coverage_by_file` argument so the deprecation warning can be
|
|
144
149
|
# copy-pasted verbatim into the user's config.
|
|
145
150
|
def per_file_coverage_replacement(defaults, overrides)
|
|
146
|
-
by_criterion =
|
|
147
|
-
defaults.each { |criterion, percent| by_criterion[criterion] << "minimum_per_file #{percent}" }
|
|
151
|
+
by_criterion = {} #: Hash[Symbol, Array[String]]
|
|
152
|
+
defaults.each { |criterion, percent| (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}" }
|
|
148
153
|
overrides.each do |target, criteria|
|
|
149
154
|
criteria.each do |criterion, percent|
|
|
150
|
-
by_criterion[criterion] << "minimum_per_file #{percent}, only: #{target.inspect}"
|
|
155
|
+
(by_criterion[criterion] ||= []) << "minimum_per_file #{percent}, only: #{target.inspect}"
|
|
151
156
|
end
|
|
152
157
|
end
|
|
153
158
|
render_coverage_blocks(by_criterion)
|
|
@@ -155,11 +160,11 @@ module SimpleCov
|
|
|
155
160
|
|
|
156
161
|
# Same, for a (deprecated) `minimum_coverage_by_group` argument.
|
|
157
162
|
def per_group_coverage_replacement(coverage)
|
|
158
|
-
by_criterion =
|
|
159
|
-
coverage.each do |group_name,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
by_criterion[criterion] << "minimum_per_group #{percent}, only: #{group_name.inspect}"
|
|
163
|
+
by_criterion = {} #: Hash[Symbol, Array[String]]
|
|
164
|
+
coverage.each do |group_name, thresholds|
|
|
165
|
+
normalized = (thresholds.is_a?(Numeric) ? {primary_coverage => thresholds} : thresholds) #: coverage_thresholds
|
|
166
|
+
normalized.each do |criterion, percent|
|
|
167
|
+
(by_criterion[criterion] ||= []) << "minimum_per_group #{percent}, only: #{group_name.inspect}"
|
|
163
168
|
end
|
|
164
169
|
end
|
|
165
170
|
render_coverage_blocks(by_criterion)
|
|
@@ -51,9 +51,10 @@ module SimpleCov
|
|
|
51
51
|
#
|
|
52
52
|
def coverage_path(path = nil)
|
|
53
53
|
if path
|
|
54
|
-
|
|
54
|
+
expanded = File.expand_path(path)
|
|
55
|
+
@coverage_path = expanded
|
|
55
56
|
@coverage_path_explicit = true
|
|
56
|
-
FileUtils.mkdir_p
|
|
57
|
+
FileUtils.mkdir_p expanded
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
@coverage_path ||= File.expand_path(coverage_dir, root)
|
|
@@ -65,9 +66,8 @@ module SimpleCov
|
|
|
65
66
|
# with SimpleCov.command_name("test:units").
|
|
66
67
|
#
|
|
67
68
|
def command_name(name = nil)
|
|
68
|
-
@
|
|
69
|
-
@
|
|
70
|
-
@name
|
|
69
|
+
@command_name = name unless name.nil?
|
|
70
|
+
@command_name ||= SimpleCov::CommandGuesser.guess
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Returns the hash of available profiles
|
|
@@ -77,20 +77,18 @@ module SimpleCov
|
|
|
77
77
|
|
|
78
78
|
#
|
|
79
79
|
# Allows you to configure simplecov in a block instead of
|
|
80
|
-
# prepending SimpleCov to each config method.
|
|
80
|
+
# prepending SimpleCov to each config method. Parameterized blocks retain
|
|
81
|
+
# their caller context and receive this configuration target explicitly.
|
|
81
82
|
#
|
|
82
83
|
def configure(&block)
|
|
83
|
-
|
|
84
|
+
raise ArgumentError, "configuration block required" unless block
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
instance_exec(&block)
|
|
92
|
-
ensure
|
|
93
|
-
restore_ivars(block_context, saved) if defined?(saved) && saved
|
|
86
|
+
if block.parameters.empty?
|
|
87
|
+
instance_exec(&(_ = block))
|
|
88
|
+
else
|
|
89
|
+
yield self
|
|
90
|
+
end
|
|
91
|
+
self
|
|
94
92
|
end
|
|
95
93
|
|
|
96
94
|
#
|
|
@@ -100,12 +98,13 @@ module SimpleCov
|
|
|
100
98
|
#
|
|
101
99
|
def at_exit(&block)
|
|
102
100
|
@at_exit = block if block
|
|
103
|
-
|
|
101
|
+
configured = @at_exit
|
|
102
|
+
return configured if configured
|
|
104
103
|
return proc {} unless active_session?
|
|
105
104
|
|
|
106
105
|
@at_exit = proc do
|
|
107
106
|
result = SimpleCov.result
|
|
108
|
-
result.format! if result && SimpleCov.merge_finalization_owner?
|
|
107
|
+
result.format! if result && SimpleCov.merge_finalization_owner?
|
|
109
108
|
end
|
|
110
109
|
end
|
|
111
110
|
|
|
@@ -144,40 +143,20 @@ module SimpleCov
|
|
|
144
143
|
# SimpleCov.root, capitalized with underscores → spaces.
|
|
145
144
|
#
|
|
146
145
|
def project_name(new_name = nil)
|
|
147
|
-
|
|
146
|
+
current = defined?(@project_name) ? @project_name : nil
|
|
147
|
+
return current if current && new_name.nil?
|
|
148
148
|
|
|
149
149
|
@project_name = new_name if new_name.is_a?(String)
|
|
150
150
|
@project_name ||= File.basename(root).capitalize.tr("_", " ")
|
|
151
151
|
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
# Copy instance variables from block_context into self, saving any
|
|
156
|
-
# of ours that would be clobbered. Returns the saved values for
|
|
157
|
-
# later restoration.
|
|
158
|
-
def swap_ivars_from(block_context)
|
|
159
|
-
saved = {}
|
|
160
|
-
our_ivars = instance_variables
|
|
161
|
-
block_context.instance_variables.each do |ivar|
|
|
162
|
-
saved[ivar] = instance_variable_get(ivar) if our_ivars.include?(ivar)
|
|
163
|
-
instance_variable_set(ivar, block_context.instance_variable_get(ivar))
|
|
164
|
-
end
|
|
165
|
-
saved
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Copy instance variables back to block_context and restore our saved values.
|
|
169
|
-
def restore_ivars(block_context, saved)
|
|
170
|
-
block_context.instance_variables.each do |ivar|
|
|
171
|
-
block_context.instance_variable_set(ivar, instance_variable_get(ivar))
|
|
172
|
-
end
|
|
173
|
-
saved.each { |ivar, value| instance_variable_set(ivar, value) }
|
|
174
|
-
end
|
|
175
152
|
end
|
|
176
153
|
end
|
|
177
154
|
|
|
178
155
|
require_relative "configuration/coverage"
|
|
179
156
|
require_relative "configuration/coverage_criteria"
|
|
157
|
+
require_relative "configuration/eval_coverage"
|
|
180
158
|
require_relative "configuration/filters"
|
|
159
|
+
require_relative "configuration/groups"
|
|
181
160
|
require_relative "configuration/formatting"
|
|
182
161
|
require_relative "configuration/ignored_entries"
|
|
183
162
|
require_relative "configuration/merging"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Shared parser for coverage.json consumers. It enforces the encoding and
|
|
7
|
+
# stable outermost shape while leaving command- or viewer-specific fields to
|
|
8
|
+
# their respective callers.
|
|
9
|
+
module CoverageJSON
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
def self.load(path)
|
|
13
|
+
source = File.binread(path).force_encoding(Encoding::UTF_8)
|
|
14
|
+
raise Error, "input is not valid UTF-8" unless source.valid_encoding?
|
|
15
|
+
|
|
16
|
+
document = JSON.parse(source)
|
|
17
|
+
return document if document.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
raise Error, "top-level value must be an object"
|
|
20
|
+
rescue JSON::ParserError => e
|
|
21
|
+
raise Error, e.message
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -10,13 +10,16 @@ module SimpleCov
|
|
|
10
10
|
# * missed - how many of the coverables are missed
|
|
11
11
|
# * omitted - how many lines cannot be covered (blank lines/comments); only meaningful for line coverage
|
|
12
12
|
# * percent - percentage as covered/missed
|
|
13
|
-
# * strength - average hits per
|
|
13
|
+
# * strength - average hits per coverable unit (will not exist for one shot lines format)
|
|
14
14
|
class CoverageStatistics
|
|
15
15
|
attr_reader :total, :covered, :missed, :omitted, :strength, :percent
|
|
16
16
|
|
|
17
|
+
# Seed for the reduce in `.from`: covered, missed, omitted, strength.
|
|
18
|
+
ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
|
|
19
|
+
|
|
17
20
|
def self.from(coverage_statistics)
|
|
18
21
|
sum_covered, sum_missed, sum_omitted, sum_total_strength =
|
|
19
|
-
coverage_statistics.reduce(
|
|
22
|
+
coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
|
|
20
23
|
[
|
|
21
24
|
covered + stats.covered,
|
|
22
25
|
missed + stats.missed,
|
|
@@ -46,7 +46,8 @@ module SimpleCov
|
|
|
46
46
|
def minimum_by_group(result, thresholds)
|
|
47
47
|
thresholds.flat_map do |group_name, minimums|
|
|
48
48
|
group = lookup_group(result, group_name)
|
|
49
|
-
|
|
49
|
+
none = [] # : Array[Hash[Symbol, untyped]]
|
|
50
|
+
group ? group_minimum_violations(group_name, group, minimums) : none
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -121,11 +122,15 @@ module SimpleCov
|
|
|
121
122
|
end
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
# The misconfiguration notice is enforcement output, not a Ruby
|
|
126
|
+
# warning: like every other enforcement message it must survive
|
|
127
|
+
# `-W0` and `Warning.warn` hooks (see ExitCodes.print_error) and
|
|
128
|
+
# honor the `print_errors` opt-out.
|
|
124
129
|
def lookup_group(result, group_name)
|
|
125
130
|
group = result.groups[group_name]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
if group.nil? && SimpleCov.print_errors
|
|
132
|
+
ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
|
|
133
|
+
"Available groups: #{result.groups.keys.join(', ')}"
|
|
129
134
|
end
|
|
130
135
|
group
|
|
131
136
|
end
|
|
@@ -133,8 +138,14 @@ module SimpleCov
|
|
|
133
138
|
def compute_drop(criterion, result, last_run)
|
|
134
139
|
stats_key = SimpleCov.coverage_statistics_key(criterion)
|
|
135
140
|
last_coverage_percent = last_run.dig(:result, stats_key)
|
|
136
|
-
last_coverage_percent
|
|
137
|
-
|
|
141
|
+
if last_coverage_percent.nil? && stats_key == :line
|
|
142
|
+
last_coverage_percent = last_run.dig(:result, :covered_percent)
|
|
143
|
+
end
|
|
144
|
+
# A hand-edited .last_run.json can carry any value type, and
|
|
145
|
+
# LastRun.read only vouches for the top level being a Hash.
|
|
146
|
+
# Treat a non-numeric percent like a missing one instead of
|
|
147
|
+
# raising out of the at_exit hook.
|
|
148
|
+
return unless last_coverage_percent.is_a?(Numeric)
|
|
138
149
|
|
|
139
150
|
current = percent_for(result, criterion) or return
|
|
140
151
|
(last_coverage_percent - current).floor(10)
|
data/lib/simplecov/defaults.rb
CHANGED
|
@@ -24,8 +24,11 @@ SimpleCov.configure do
|
|
|
24
24
|
load_profile "test_frameworks"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info
|
|
27
|
+
# Gotta stash this a-s-a-p, see the CommandGuesser class and i.e. #110 for further info.
|
|
28
|
+
# The program name is kept separately as well: the join below is lossy for a
|
|
29
|
+
# path containing a space, and the guesser reads the executable from it.
|
|
28
30
|
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(' ')}"
|
|
31
|
+
SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
|
|
29
32
|
|
|
30
33
|
# Autoload config from ~/.simplecov if present
|
|
31
34
|
require_relative "load_global_config"
|
|
@@ -38,15 +41,16 @@ loop do
|
|
|
38
41
|
filename = config_path.join(".simplecov")
|
|
39
42
|
if filename.exist?
|
|
40
43
|
# `.simplecov` is a configuration file; SimpleCov.start calls inside
|
|
41
|
-
# it
|
|
42
|
-
#
|
|
44
|
+
# it draw a deprecation warning, and tracking still begins for
|
|
45
|
+
# backward compatibility. See `SimpleCov.with_dot_simplecov_autoload`
|
|
46
|
+
# and issue #581.
|
|
43
47
|
SimpleCov.with_dot_simplecov_autoload do
|
|
44
|
-
load filename
|
|
45
|
-
rescue LoadError, StandardError
|
|
48
|
+
load filename.to_s
|
|
49
|
+
rescue LoadError, StandardError => e
|
|
46
50
|
# simplecov:disable — only fires when .simplecov is unreadable
|
|
47
51
|
# or raises during load
|
|
48
52
|
warn "Warning: Error occurred while trying to load #{filename}. " \
|
|
49
|
-
"Error message: #{
|
|
53
|
+
"Error message: #{e.message}"
|
|
50
54
|
# simplecov:enable
|
|
51
55
|
end
|
|
52
56
|
break
|
data/lib/simplecov/directive.rb
CHANGED
|
@@ -43,7 +43,7 @@ module SimpleCov
|
|
|
43
43
|
PATTERN = /
|
|
44
44
|
\#\s*simplecov\s*:\s*
|
|
45
45
|
(?<mode>disable|enable)\b
|
|
46
|
-
(?:\s+(?<categories>#{CATEGORIES_PATTERN}))?
|
|
46
|
+
(?:\s+(?<categories>#{CATEGORIES_PATTERN})\b)?
|
|
47
47
|
.*?
|
|
48
48
|
\s*\z
|
|
49
49
|
/x
|
|
@@ -55,8 +55,11 @@ module SimpleCov
|
|
|
55
55
|
# An unclosed `disable` block extends to the end of the file.
|
|
56
56
|
def self.disabled_ranges(src_lines)
|
|
57
57
|
lines = src_lines.to_a
|
|
58
|
-
ranges = CATEGORIES.to_h
|
|
59
|
-
|
|
58
|
+
ranges = CATEGORIES.to_h do |category|
|
|
59
|
+
empty = [] # : Array[Range[Integer]]
|
|
60
|
+
[category, empty]
|
|
61
|
+
end
|
|
62
|
+
open_starts = {} # : Hash[Symbol, Integer]
|
|
60
63
|
|
|
61
64
|
directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
|
|
62
65
|
open_starts.each { |category, start| ranges[category] << (start..lines.size) }
|
|
@@ -91,7 +94,9 @@ module SimpleCov
|
|
|
91
94
|
|
|
92
95
|
new(
|
|
93
96
|
line_number: line_number,
|
|
94
|
-
|
|
97
|
+
# `to_s` is a no-op: the pattern requires the `mode` group, so it
|
|
98
|
+
# is present whenever `match` is; it also satisfies the type checker.
|
|
99
|
+
mode: match[:mode].to_s.to_sym,
|
|
95
100
|
categories: parse_categories(match[:categories]),
|
|
96
101
|
inline: inline?(lines, line_number, column + match.begin(0))
|
|
97
102
|
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ExitCodes
|
|
5
|
+
# Shared skeleton for the threshold checks: stash the result and the
|
|
6
|
+
# threshold configuration, fail when any violation exists, and
|
|
7
|
+
# report one message per violation. Subclasses supply `exit_code`,
|
|
8
|
+
# `compute_violations` (memoized here — `failing?` and `report` both
|
|
9
|
+
# consult it), and `report_violation`.
|
|
10
|
+
class Check
|
|
11
|
+
def initialize(result, thresholds)
|
|
12
|
+
@result = result
|
|
13
|
+
@thresholds = thresholds
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failing?
|
|
17
|
+
violations.any?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def report
|
|
21
|
+
violations.each { |violation| report_violation(violation) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :result, :thresholds
|
|
27
|
+
|
|
28
|
+
def violations
|
|
29
|
+
@violations ||= compute_violations
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -4,29 +4,24 @@ module SimpleCov
|
|
|
4
4
|
module ExitCodes
|
|
5
5
|
# Fails when any coverage criterion has dropped by more than the
|
|
6
6
|
# configured maximum since the last recorded run.
|
|
7
|
-
class MaximumCoverageDropCheck
|
|
8
|
-
def initialize(result, maximum_coverage_drop)
|
|
9
|
-
@result = result
|
|
10
|
-
@maximum_coverage_drop = maximum_coverage_drop
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def failing?
|
|
14
|
-
violations.any?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def report
|
|
18
|
-
violations.each { |violation| warn SimpleCov::Color.colorize(message_for(violation), :red) }
|
|
19
|
-
end
|
|
20
|
-
|
|
7
|
+
class MaximumCoverageDropCheck < Check
|
|
21
8
|
def exit_code
|
|
22
9
|
SimpleCov::ExitCodes::MAXIMUM_COVERAGE_DROP
|
|
23
10
|
end
|
|
24
11
|
|
|
25
12
|
private
|
|
26
13
|
|
|
14
|
+
def compute_violations
|
|
15
|
+
SimpleCov::CoverageViolations.maximum_drop(result, thresholds)
|
|
16
|
+
end
|
|
17
|
+
|
|
27
18
|
# The "drop percent" is a delta, not a coverage level — it has no
|
|
28
|
-
# natural green/yellow/red mapping
|
|
29
|
-
#
|
|
19
|
+
# natural green/yellow/red mapping, so the whole line goes red to
|
|
20
|
+
# keep the failure visible at a glance.
|
|
21
|
+
def report_violation(violation)
|
|
22
|
+
ExitCodes.print_error SimpleCov::Color.colorize(message_for(violation), :red)
|
|
23
|
+
end
|
|
24
|
+
|
|
30
25
|
def message_for(violation)
|
|
31
26
|
format(
|
|
32
27
|
"%<criterion>s coverage has dropped by %<drop_percent>.2f%% since the last time " \
|
|
@@ -36,10 +31,6 @@ module SimpleCov
|
|
|
36
31
|
max_drop: violation.fetch(:maximum)
|
|
37
32
|
)
|
|
38
33
|
end
|
|
39
|
-
|
|
40
|
-
def violations
|
|
41
|
-
@violations ||= SimpleCov::CoverageViolations.maximum_drop(@result, @maximum_coverage_drop)
|
|
42
|
-
end
|
|
43
34
|
end
|
|
44
35
|
end
|
|
45
36
|
end
|
|
@@ -7,32 +7,19 @@ module SimpleCov
|
|
|
7
7
|
# `SimpleCov::ExitCodes::MinimumOverallCoverageCheck` (or use
|
|
8
8
|
# `SimpleCov.expected_coverage`) to pin coverage to an exact value
|
|
9
9
|
# and surface unexpected increases instead of silently absorbing them.
|
|
10
|
-
class MaximumOverallCoverageCheck
|
|
11
|
-
def initialize(result, maximum_coverage)
|
|
12
|
-
@result = result
|
|
13
|
-
@maximum_coverage = maximum_coverage
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def failing?
|
|
17
|
-
violations.any?
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def report
|
|
21
|
-
violations.each { |violation| report_violation(violation) }
|
|
22
|
-
end
|
|
23
|
-
|
|
10
|
+
class MaximumOverallCoverageCheck < Check
|
|
24
11
|
def exit_code
|
|
25
12
|
SimpleCov::ExitCodes::MAXIMUM_COVERAGE
|
|
26
13
|
end
|
|
27
14
|
|
|
28
15
|
private
|
|
29
16
|
|
|
30
|
-
def
|
|
31
|
-
|
|
17
|
+
def compute_violations
|
|
18
|
+
SimpleCov::CoverageViolations.maximum_overall(result, thresholds)
|
|
32
19
|
end
|
|
33
20
|
|
|
34
21
|
def report_violation(violation)
|
|
35
|
-
|
|
22
|
+
ExitCodes.print_error format(
|
|
36
23
|
"%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
|
|
37
24
|
"Time to bump the threshold!",
|
|
38
25
|
criterion: violation.fetch(:criterion).capitalize,
|
|
@@ -4,39 +4,30 @@ module SimpleCov
|
|
|
4
4
|
module ExitCodes
|
|
5
5
|
# Fails when any individual file falls below the configured minimum
|
|
6
6
|
# coverage for any criterion.
|
|
7
|
-
class MinimumCoverageByFileCheck
|
|
7
|
+
class MinimumCoverageByFileCheck < Check
|
|
8
8
|
def initialize(result, minimum_coverage_by_file, overrides = {})
|
|
9
|
-
|
|
10
|
-
@minimum_coverage_by_file = minimum_coverage_by_file
|
|
9
|
+
super(result, minimum_coverage_by_file)
|
|
11
10
|
@overrides = overrides
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
def failing?
|
|
15
|
-
violations.any?
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def report
|
|
19
|
-
violations.each do |violation|
|
|
20
|
-
warn format(
|
|
21
|
-
"%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
|
|
22
|
-
"(%<expected>.2f%%) in %<filename>s.",
|
|
23
|
-
criterion: violation.fetch(:criterion).capitalize,
|
|
24
|
-
actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
|
|
25
|
-
expected: violation.fetch(:expected),
|
|
26
|
-
filename: violation.fetch(:project_filename)
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
13
|
def exit_code
|
|
32
14
|
SimpleCov::ExitCodes::MINIMUM_COVERAGE
|
|
33
15
|
end
|
|
34
16
|
|
|
35
17
|
private
|
|
36
18
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
19
|
+
def compute_violations
|
|
20
|
+
SimpleCov::CoverageViolations.minimum_by_file(result, thresholds, @overrides)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def report_violation(violation)
|
|
24
|
+
ExitCodes.print_error format(
|
|
25
|
+
"%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
|
|
26
|
+
"(%<expected>.2f%%) in %<filename>s.",
|
|
27
|
+
criterion: violation.fetch(:criterion).capitalize,
|
|
28
|
+
actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
|
|
29
|
+
expected: violation.fetch(:expected),
|
|
30
|
+
filename: violation.fetch(:project_filename)
|
|
40
31
|
)
|
|
41
32
|
end
|
|
42
33
|
end
|
|
@@ -4,37 +4,26 @@ module SimpleCov
|
|
|
4
4
|
module ExitCodes
|
|
5
5
|
# Fails when any configured group falls below its minimum coverage
|
|
6
6
|
# threshold for any criterion.
|
|
7
|
-
class MinimumCoverageByGroupCheck
|
|
8
|
-
def initialize(result, minimum_coverage_by_group)
|
|
9
|
-
@result = result
|
|
10
|
-
@minimum_coverage_by_group = minimum_coverage_by_group
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def failing?
|
|
14
|
-
violations.any?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def report
|
|
18
|
-
violations.each do |violation|
|
|
19
|
-
warn format(
|
|
20
|
-
"%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
|
|
21
|
-
"(%<expected>.2f%%) in %<group_name>s.",
|
|
22
|
-
criterion: violation.fetch(:criterion).capitalize,
|
|
23
|
-
actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
|
|
24
|
-
expected: violation.fetch(:expected),
|
|
25
|
-
group_name: violation.fetch(:group_name)
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
7
|
+
class MinimumCoverageByGroupCheck < Check
|
|
30
8
|
def exit_code
|
|
31
9
|
SimpleCov::ExitCodes::MINIMUM_COVERAGE
|
|
32
10
|
end
|
|
33
11
|
|
|
34
12
|
private
|
|
35
13
|
|
|
36
|
-
def
|
|
37
|
-
|
|
14
|
+
def compute_violations
|
|
15
|
+
SimpleCov::CoverageViolations.minimum_by_group(result, thresholds)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def report_violation(violation)
|
|
19
|
+
ExitCodes.print_error format(
|
|
20
|
+
"%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
|
|
21
|
+
"(%<expected>.2f%%) in %<group_name>s.",
|
|
22
|
+
criterion: violation.fetch(:criterion).capitalize,
|
|
23
|
+
actual: SimpleCov::Color.colorize_percent(violation.fetch(:actual)),
|
|
24
|
+
expected: violation.fetch(:expected),
|
|
25
|
+
group_name: violation.fetch(:group_name)
|
|
26
|
+
)
|
|
38
27
|
end
|
|
39
28
|
end
|
|
40
29
|
end
|
|
@@ -4,20 +4,7 @@ module SimpleCov
|
|
|
4
4
|
module ExitCodes
|
|
5
5
|
# Fails when the overall (project-wide) coverage for any criterion is
|
|
6
6
|
# below the configured minimum.
|
|
7
|
-
class MinimumOverallCoverageCheck
|
|
8
|
-
def initialize(result, minimum_coverage)
|
|
9
|
-
@result = result
|
|
10
|
-
@minimum_coverage = minimum_coverage
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def failing?
|
|
14
|
-
violations.any?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def report
|
|
18
|
-
violations.each { |violation| report_violation(violation) }
|
|
19
|
-
end
|
|
20
|
-
|
|
7
|
+
class MinimumOverallCoverageCheck < Check
|
|
21
8
|
def exit_code
|
|
22
9
|
SimpleCov::ExitCodes::MINIMUM_COVERAGE
|
|
23
10
|
end
|
|
@@ -27,14 +14,14 @@ module SimpleCov
|
|
|
27
14
|
WORST_FILES_LIMIT = 5
|
|
28
15
|
private_constant :WORST_FILES_LIMIT
|
|
29
16
|
|
|
30
|
-
def
|
|
31
|
-
|
|
17
|
+
def compute_violations
|
|
18
|
+
SimpleCov::CoverageViolations.minimum_overall(result, thresholds)
|
|
32
19
|
end
|
|
33
20
|
|
|
34
21
|
def report_violation(violation)
|
|
35
22
|
criterion = violation.fetch(:criterion)
|
|
36
23
|
actual = violation.fetch(:actual)
|
|
37
|
-
|
|
24
|
+
ExitCodes.print_error format(
|
|
38
25
|
"%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
|
|
39
26
|
criterion: criterion.capitalize,
|
|
40
27
|
actual: SimpleCov::Color.colorize_percent(actual),
|
|
@@ -47,9 +34,9 @@ module SimpleCov
|
|
|
47
34
|
worst = worst_files_for(criterion)
|
|
48
35
|
return if worst.empty?
|
|
49
36
|
|
|
50
|
-
|
|
37
|
+
ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
|
|
51
38
|
worst.each do |path, percent|
|
|
52
|
-
|
|
39
|
+
ExitCodes.print_error format(
|
|
53
40
|
" %<percent>s %<path>s",
|
|
54
41
|
percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
|
|
55
42
|
path: path
|
|
@@ -59,7 +46,7 @@ module SimpleCov
|
|
|
59
46
|
|
|
60
47
|
def worst_files_for(criterion)
|
|
61
48
|
stats_key = SimpleCov.coverage_statistics_key(criterion)
|
|
62
|
-
with_stats =
|
|
49
|
+
with_stats = result.files.filter_map do |source_file|
|
|
63
50
|
stats = source_file.coverage_statistics[stats_key]
|
|
64
51
|
[source_file.project_filename, stats.percent] if stats
|
|
65
52
|
end
|