simplecov 0.22.0 → 1.3.0
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/LICENSE +1 -1
- data/README.md +173 -875
- data/exe/simplecov +8 -0
- data/lib/minitest/simplecov_plugin.rb +18 -7
- data/lib/simplecov/atomic_file.rb +67 -0
- data/lib/simplecov/autostart.rb +9 -0
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/annotations.rb +166 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +86 -0
- data/lib/simplecov/cli/command_helpers.rb +91 -0
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +83 -0
- data/lib/simplecov/cli/coverage_file.rb +109 -0
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +115 -0
- data/lib/simplecov/cli/dotfile.rb +156 -0
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +115 -0
- data/lib/simplecov/cli/open.rb +45 -0
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +151 -0
- data/lib/simplecov/cli/patch.rb +175 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +91 -0
- data/lib/simplecov/cli/run.rb +35 -0
- data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
- data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
- data/lib/simplecov/cli/serve.rb +88 -0
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +33 -0
- data/lib/simplecov/cli/uncovered.rb +127 -0
- data/lib/simplecov/cli/usage.rb +153 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +114 -0
- data/lib/simplecov/color.rb +65 -0
- data/lib/simplecov/combine/branches_combiner.rb +44 -21
- data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
- data/lib/simplecov/combine/identity_interner.rb +27 -0
- data/lib/simplecov/combine/interned_counts.rb +28 -0
- data/lib/simplecov/combine/lines_combiner.rb +35 -29
- data/lib/simplecov/combine/methods_combiner.rb +48 -0
- data/lib/simplecov/combine/results_combiner.rb +9 -49
- data/lib/simplecov/combine.rb +0 -24
- data/lib/simplecov/command_guesser.rb +99 -42
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +184 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +37 -0
- data/lib/simplecov/configuration/filters.rb +131 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/groups.rb +48 -0
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +72 -0
- data/lib/simplecov/configuration/merging.rb +197 -0
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +150 -0
- data/lib/simplecov/configuration/view_coverage.rb +37 -0
- data/lib/simplecov/configuration.rb +100 -443
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +21 -0
- data/lib/simplecov/coverage_statistics.rb +24 -28
- data/lib/simplecov/coverage_violations.rb +254 -0
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +20 -28
- data/lib/simplecov/deprecation.rb +63 -0
- data/lib/simplecov/directive/erb.rb +44 -0
- data/lib/simplecov/directive/haml.rb +33 -0
- data/lib/simplecov/directive/indented_template.rb +37 -0
- data/lib/simplecov/directive/slim.rb +35 -0
- data/lib/simplecov/directive/template.rb +29 -0
- data/lib/simplecov/directive.rb +152 -0
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +39 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
- data/lib/simplecov/exit_codes.rb +26 -0
- data/lib/simplecov/exit_handling.rb +163 -0
- data/lib/simplecov/file_list.rb +63 -48
- data/lib/simplecov/filter.rb +98 -42
- data/lib/simplecov/formatter/base.rb +100 -0
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
- data/lib/simplecov/formatter/html_formatter.rb +88 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
- data/lib/simplecov/formatter/json_formatter.rb +40 -0
- data/lib/simplecov/formatter/multi_formatter.rb +8 -10
- data/lib/simplecov/formatter/simple_formatter.rb +12 -15
- data/lib/simplecov/formatter.rb +21 -1
- data/lib/simplecov/group_names.rb +33 -0
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +16 -5
- data/lib/simplecov/lines_classifier.rb +67 -19
- data/lib/simplecov/load_global_config.rb +12 -4
- data/lib/simplecov/parallel_adapters/base.rb +50 -0
- data/lib/simplecov/parallel_adapters/generic.rb +33 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
- data/lib/simplecov/parallel_adapters.rb +54 -0
- data/lib/simplecov/parallel_coordination.rb +124 -0
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +171 -0
- data/lib/simplecov/process.rb +34 -12
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +13 -10
- data/lib/simplecov/profiles/root_filter.rb +2 -5
- data/lib/simplecov/profiles/strict.rb +22 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +26 -19
- data/lib/simplecov/report_deferral.rb +45 -0
- data/lib/simplecov/report_stamp.rb +25 -0
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +46 -0
- data/lib/simplecov/result.rb +123 -47
- data/lib/simplecov/result_adapter.rb +130 -11
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
- data/lib/simplecov/result_merger/resultset_file.rb +67 -0
- data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
- data/lib/simplecov/result_merger/resultset_store.rb +62 -0
- data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
- data/lib/simplecov/result_merger.rb +120 -124
- data/lib/simplecov/result_processing.rb +188 -0
- data/lib/simplecov/run_identity.rb +75 -0
- data/lib/simplecov/simulate_coverage.rb +72 -17
- data/lib/simplecov/source_file/branch.rb +8 -35
- data/lib/simplecov/source_file/branch_builder.rb +95 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +17 -32
- data/lib/simplecov/source_file/line_builder.rb +37 -0
- data/lib/simplecov/source_file/method.rb +54 -0
- data/lib/simplecov/source_file/method_builder.rb +51 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
- data/lib/simplecov/source_file/skip_chunks.rb +72 -0
- data/lib/simplecov/source_file/source_loader.rb +89 -0
- data/lib/simplecov/source_file/statistics.rb +76 -0
- data/lib/simplecov/source_file.rb +64 -260
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
- data/lib/simplecov/static_coverage_extractor.rb +78 -0
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +62 -0
- data/lib/simplecov/useless_results_remover.rb +17 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +5 -0
- data/lib/simplecov.rb +143 -394
- data/lib/simplecov_json_formatter.rb +5 -0
- data/man/simplecov.1 +354 -0
- data/sig/simplecov.rbs +1710 -0
- metadata +183 -61
- data/CHANGELOG.md +0 -191
- data/doc/alternate-formatters.md +0 -71
- data/doc/commercial-services.md +0 -25
- data/doc/editor-integration.md +0 -18
- data/lib/simplecov/combine/files_combiner.rb +0 -24
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
#
|
|
5
|
+
# Records which context covered which line: a list of context ids and, per
|
|
6
|
+
# source file, a bitmap of covered lines for each context that touched the
|
|
7
|
+
# file. A context is any labeled region of execution, and the vocabulary is
|
|
8
|
+
# deliberately the general one (coverage.py calls the same idea dynamic
|
|
9
|
+
# contexts), so the stored format doesn't bake simplecov's current use into
|
|
10
|
+
# a name.
|
|
11
|
+
#
|
|
12
|
+
# The naive shape of this data, a list of ids on every line, is
|
|
13
|
+
# O(contexts x lines) strings. Context ids are interned instead, and each
|
|
14
|
+
# context's covered lines within a file are a single Integer bitmap with bit
|
|
15
|
+
# N set when line N+1 was executed, which packs a thousand-line file into
|
|
16
|
+
# ~125 bytes and makes the union of two recordings a bitwise OR.
|
|
17
|
+
#
|
|
18
|
+
# Serialized into `.resultset.json` as `{"version" => 1, "contexts" =>
|
|
19
|
+
# [...ids...], "files" => {path => {context index => bitmap as hex}}}`. The
|
|
20
|
+
# format is tolerated, not trusted: `.from_hash` returns nil for anything
|
|
21
|
+
# malformed or future-versioned, which the merge treats the same as a map
|
|
22
|
+
# that was never recorded.
|
|
23
|
+
#
|
|
24
|
+
class ContextMap
|
|
25
|
+
# Bumped on any change an older reader could misread, so `.from_hash` can
|
|
26
|
+
# treat a future format as absent instead of answering from it wrongly.
|
|
27
|
+
VERSION = 1
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@contexts = [] #: Array[String]
|
|
31
|
+
@indices = {} #: Hash[String, Integer]
|
|
32
|
+
@files = {} #: Hash[String, Hash[Integer, Integer]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `lines_by_file` maps a source path to the bitmap of lines the context
|
|
36
|
+
# executed there. The id is interned even when the delta is empty: the
|
|
37
|
+
# context ran, and keeping it distinguishes "covered nothing of its own"
|
|
38
|
+
# from "never recorded".
|
|
39
|
+
def record(context_id, lines_by_file)
|
|
40
|
+
index = intern(context_id)
|
|
41
|
+
lines_by_file.each do |path, bitmap|
|
|
42
|
+
next if bitmap.zero?
|
|
43
|
+
|
|
44
|
+
table = (@files[path] ||= {})
|
|
45
|
+
table[index] = (table[index] || 0) | bitmap
|
|
46
|
+
end
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sorted for stable output, since recording order varies between runs and
|
|
51
|
+
# merges. The path is resolved against `SimpleCov.root`, so callers can
|
|
52
|
+
# pass either an absolute or a project-relative one.
|
|
53
|
+
#
|
|
54
|
+
# A line number below 1 needs no guard of its own: it shifts the probe bit
|
|
55
|
+
# right off the end of the bitmap, leaving a mask no line can match.
|
|
56
|
+
def covering(path, line)
|
|
57
|
+
table = @files[File.expand_path(path, SimpleCov.root)]
|
|
58
|
+
return [] unless table
|
|
59
|
+
|
|
60
|
+
bit = 1 << (line - 1)
|
|
61
|
+
table.filter_map { |index, bitmap| @contexts.fetch(index) if bitmap.anybits?(bit) }.sort
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def contexts
|
|
65
|
+
@contexts.dup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def empty?
|
|
69
|
+
@contexts.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Other's contexts are re-interned, so maps recorded by different processes
|
|
73
|
+
# merge by id, and a context both sides saw contributes one entry.
|
|
74
|
+
def absorb(other)
|
|
75
|
+
remap = other.interned_contexts.map { |context_id| intern(context_id) }
|
|
76
|
+
other.file_tables.each do |path, table|
|
|
77
|
+
target = (@files[path] ||= {})
|
|
78
|
+
table.each do |index, bitmap|
|
|
79
|
+
key = remap.fetch(index)
|
|
80
|
+
target[key] = (target[key] || 0) | bitmap
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `only:` restricts the files to a given set of paths, so the map follows
|
|
87
|
+
# the same universe as the coverage it sits beside. The context list is
|
|
88
|
+
# never restricted: which contexts ran is true regardless of which files
|
|
89
|
+
# survived filtering.
|
|
90
|
+
def to_h(only: nil)
|
|
91
|
+
# Selected rather than sliced: `only` is a Set, and splatting one into
|
|
92
|
+
# `slice` is a call `to_a` answers for, which makes dropping the `to_a` a
|
|
93
|
+
# mutation nothing can observe.
|
|
94
|
+
tables = only ? @files.select { |path, _table| only.include?(path) } : @files # rubocop:disable Style/HashSlice
|
|
95
|
+
serialized = tables.transform_values { |table| serialize_table(table) }
|
|
96
|
+
{"version" => VERSION, "contexts" => @contexts.dup, "files" => serialized}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Index string => hex bitmap, the wire encoding `to_h` writes.
|
|
100
|
+
# coverage.json shares the resultset's encoding through this, so the format
|
|
101
|
+
# has one owner.
|
|
102
|
+
def serialized_bitmaps_for(path)
|
|
103
|
+
serialize_table(@files[File.expand_path(path, SimpleCov.root)] || {})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @api private
|
|
107
|
+
def intern(context_id)
|
|
108
|
+
@indices[context_id] ||= begin
|
|
109
|
+
@contexts << context_id
|
|
110
|
+
@contexts.size - 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
# nil when the data is not a well-formed map of this format version.
|
|
116
|
+
# All-or-nothing on purpose: a partially salvaged map would answer
|
|
117
|
+
# `covering` queries with silent gaps, and the merge already drops an
|
|
118
|
+
# absent map consistently.
|
|
119
|
+
def from_hash(data)
|
|
120
|
+
# `eql?` rather than `==`: a version written as 1.0 is not this format,
|
|
121
|
+
# and reading it as if it were is what the version gate exists to stop.
|
|
122
|
+
return nil unless data.instance_of?(Hash) && data["version"].eql?(VERSION)
|
|
123
|
+
|
|
124
|
+
contexts = data["contexts"]
|
|
125
|
+
files = data["files"]
|
|
126
|
+
return nil unless contexts.instance_of?(Array) && contexts.all?(String) && files.instance_of?(Hash)
|
|
127
|
+
|
|
128
|
+
build(contexts, files)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def build(contexts, files)
|
|
134
|
+
map = new
|
|
135
|
+
contexts.each { |context_id| map.intern(context_id) }
|
|
136
|
+
files.each do |path, table|
|
|
137
|
+
return nil unless path.instance_of?(String) && table.instance_of?(Hash)
|
|
138
|
+
return nil unless absorb_table(map, contexts, path, table)
|
|
139
|
+
end
|
|
140
|
+
map
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def absorb_table(map, contexts, path, table)
|
|
144
|
+
table.all? do |index, encoded|
|
|
145
|
+
position = parse_index(index, contexts.size)
|
|
146
|
+
bitmap = parse_bitmap(encoded)
|
|
147
|
+
map.record(contexts.fetch(position), path => bitmap) if position && bitmap
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A serialized context index is a decimal string pointing into the entry's
|
|
152
|
+
# own context list. An out-of-range one makes the whole map malformed.
|
|
153
|
+
def parse_index(index, size)
|
|
154
|
+
return nil unless index.instance_of?(String) && index.match?(/\A\d+\z/)
|
|
155
|
+
|
|
156
|
+
value = index.to_i
|
|
157
|
+
value if value < size
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_bitmap(encoded)
|
|
161
|
+
return nil unless encoded.instance_of?(String) && encoded.match?(/\A\h+\z/)
|
|
162
|
+
|
|
163
|
+
encoded.to_i(16)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Sorted by index: recording and merge order vary run to run, and a
|
|
170
|
+
# deterministic serialization keeps stored resultsets diffable.
|
|
171
|
+
def serialize_table(table)
|
|
172
|
+
table.sort.to_h { |index, bitmap| [index.to_s, bitmap.to_s(16)] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
protected
|
|
176
|
+
|
|
177
|
+
# Live state, not copies, which is why these are protected rather than
|
|
178
|
+
# public.
|
|
179
|
+
def interned_contexts
|
|
180
|
+
@contexts
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def file_tables
|
|
184
|
+
@files
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module CoverageJSON
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
def self.load(path)
|
|
10
|
+
source = File.binread(path).force_encoding(Encoding::UTF_8)
|
|
11
|
+
raise Error, "input is not valid UTF-8" unless source.valid_encoding?
|
|
12
|
+
|
|
13
|
+
document = JSON.parse(source)
|
|
14
|
+
return document if document.instance_of?(Hash)
|
|
15
|
+
|
|
16
|
+
raise Error, "top-level value must be an object"
|
|
17
|
+
rescue JSON::ParserError => e
|
|
18
|
+
raise Error, e
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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)
|
|
4
|
+
# One coverage criterion's numbers, uniform across criteria: total, covered,
|
|
5
|
+
# missed, omitted (only meaningful for line coverage), percent, and strength,
|
|
6
|
+
# the average hits per coverable unit, which the oneshot-lines format has no
|
|
7
|
+
# way to produce.
|
|
13
8
|
class CoverageStatistics
|
|
14
|
-
attr_reader :total, :covered, :missed, :strength, :percent
|
|
9
|
+
attr_reader :total, :covered, :missed, :omitted, :strength, :percent
|
|
15
10
|
|
|
11
|
+
ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
|
|
12
|
+
|
|
13
|
+
# Strength is remultiplied by loc because files have different strengths and
|
|
14
|
+
# line counts, giving them a different weight in the total.
|
|
16
15
|
def self.from(coverage_statistics)
|
|
17
|
-
sum_covered, sum_missed, sum_total_strength =
|
|
18
|
-
coverage_statistics.reduce(
|
|
16
|
+
sum_covered, sum_missed, sum_omitted, sum_total_strength =
|
|
17
|
+
coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
|
|
19
18
|
[
|
|
20
|
-
covered +
|
|
21
|
-
missed +
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
total_strength + (file_coverage_statistics.strength * file_coverage_statistics.total)
|
|
19
|
+
covered + stats.covered,
|
|
20
|
+
missed + stats.missed,
|
|
21
|
+
omitted + stats.omitted,
|
|
22
|
+
total_strength + (stats.strength * stats.total)
|
|
25
23
|
]
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
new(covered: sum_covered, missed: sum_missed, total_strength: sum_total_strength)
|
|
26
|
+
new(covered: sum_covered, missed: sum_missed, omitted: sum_omitted, total_strength: sum_total_strength)
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
@
|
|
37
|
-
@total = covered + missed
|
|
38
|
-
@percent = compute_percent(covered, missed, total)
|
|
29
|
+
def initialize(covered:, missed:, omitted: 0, total_strength: 0, percent: nil)
|
|
30
|
+
@covered = covered
|
|
31
|
+
@missed = missed
|
|
32
|
+
@omitted = omitted
|
|
33
|
+
@total = covered + missed
|
|
34
|
+
@percent = percent || compute_percent(covered, missed, total)
|
|
39
35
|
@strength = compute_strength(total_strength, total)
|
|
40
36
|
end
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
private
|
|
43
39
|
|
|
44
40
|
def compute_percent(covered, missed, total)
|
|
45
41
|
return 100.0 if missed.zero?
|
|
@@ -50,7 +46,7 @@ module SimpleCov
|
|
|
50
46
|
def compute_strength(total_strength, total)
|
|
51
47
|
return 0.0 if total.zero?
|
|
52
48
|
|
|
53
|
-
total_strength.
|
|
49
|
+
total_strength.fdiv(total)
|
|
54
50
|
end
|
|
55
51
|
end
|
|
56
52
|
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module CoverageViolations
|
|
5
|
+
class << self
|
|
6
|
+
def minimum_overall(result, thresholds)
|
|
7
|
+
thresholds.filter_map do |criterion, expected|
|
|
8
|
+
actual = floored_percent_for(result, criterion) or next
|
|
9
|
+
{criterion: criterion, expected: expected, actual: actual} if actual < expected
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# An actual of 95.4287 floors to 95.42, so a maximum of 95.42 passes
|
|
14
|
+
# (#187).
|
|
15
|
+
def maximum_overall(result, thresholds)
|
|
16
|
+
thresholds.filter_map do |criterion, expected|
|
|
17
|
+
actual = floored_percent_for(result, criterion) or next
|
|
18
|
+
{criterion: criterion, expected: expected, actual: actual} if actual > expected
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A file and criterion with a `baseline` floor is exempt here: the baseline
|
|
23
|
+
# check holds it to its own floor instead, which is the fall-through the
|
|
24
|
+
# ratchet workflow rests on (#1268).
|
|
25
|
+
def minimum_by_file(result, defaults, overrides = {}, baseline: nil)
|
|
26
|
+
result.files.flat_map do |file|
|
|
27
|
+
effective = effective_per_file_thresholds(file, defaults, overrides)
|
|
28
|
+
effective.filter_map do |criterion, expected|
|
|
29
|
+
next if baseline&.covers?(file.project_filename, criterion)
|
|
30
|
+
|
|
31
|
+
file_minimum_violation(file, criterion, expected)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A baseline violation needs the file below its floor on both axes: a
|
|
37
|
+
# percent under the floor's, and (when the floor records one) more misses
|
|
38
|
+
# than it allows. The missed count is what keeps percent movement from
|
|
39
|
+
# pure edits out of the answer.
|
|
40
|
+
def baseline(result, baseline)
|
|
41
|
+
return [] unless baseline
|
|
42
|
+
|
|
43
|
+
result.files.flat_map do |file|
|
|
44
|
+
entry = baseline.entry_for(file.project_filename)
|
|
45
|
+
next [] unless entry
|
|
46
|
+
|
|
47
|
+
entry.filter_map { |criterion, floor| baseline_violation(file, criterion, floor) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def maximum_missed(result, caps)
|
|
52
|
+
caps.filter_map do |criterion, maximum|
|
|
53
|
+
actual = missed_for(result, criterion) or next
|
|
54
|
+
{criterion: criterion, maximum: maximum, actual: actual} if actual > maximum
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def maximum_missed_by_file(result, defaults, overrides = {}, baseline: nil)
|
|
59
|
+
result.files.flat_map do |file|
|
|
60
|
+
effective = effective_per_file_thresholds(file, defaults, overrides)
|
|
61
|
+
effective.filter_map do |criterion, maximum|
|
|
62
|
+
next if baseline&.covers?(file.project_filename, criterion)
|
|
63
|
+
|
|
64
|
+
file_missed_cap_violation(file, criterion, maximum)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def minimum_by_group(result, thresholds)
|
|
70
|
+
thresholds.flat_map do |group_name, minimums|
|
|
71
|
+
group = lookup_group(result, group_name)
|
|
72
|
+
none = [] # : Array[Hash[Symbol, untyped]]
|
|
73
|
+
group ? group_minimum_violations(group_name, group, minimums) : none
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def maximum_drop(result, thresholds, last_run: nil, mode: SimpleCov.drop_baseline)
|
|
78
|
+
baseline = drop_baseline_percents(mode, last_run)
|
|
79
|
+
return [] unless baseline
|
|
80
|
+
|
|
81
|
+
thresholds.filter_map do |criterion, maximum|
|
|
82
|
+
actual = compute_drop(criterion, result, baseline)
|
|
83
|
+
{criterion: criterion, maximum: maximum, actual: actual} if actual && actual > maximum
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Answers nil, and the caller silently skips, when the criterion was
|
|
90
|
+
# configured but not measured by the runtime (`branch: 100` under the
|
|
91
|
+
# "strict" profile on JRuby, say). The config-time
|
|
92
|
+
# `raise_if_criterion_disabled` check catches the genuine mistake earlier.
|
|
93
|
+
def floored_percent_for(stats_source, criterion)
|
|
94
|
+
stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
95
|
+
round(stats.percent) if stats
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def effective_per_file_thresholds(file, defaults, overrides)
|
|
99
|
+
path = file.project_filename
|
|
100
|
+
overrides.reduce(defaults) do |acc, (pattern, criterion_thresholds)|
|
|
101
|
+
path_matches?(path, pattern) ? acc.merge(criterion_thresholds) : acc
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# `==` and `eql?` are indistinguishable for the String operands, so that
|
|
106
|
+
# mutation is equivalent. The override examples pin every arm instead.
|
|
107
|
+
# mutant:disable
|
|
108
|
+
# mutant:disable
|
|
109
|
+
def path_matches?(project_filename, pattern)
|
|
110
|
+
return project_filename.match?(pattern) if pattern.is_a?(Regexp)
|
|
111
|
+
return project_filename.start_with?(pattern) if pattern.end_with?("/")
|
|
112
|
+
|
|
113
|
+
project_filename == pattern
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def missed_for(stats_source, criterion)
|
|
117
|
+
stats = stats_source.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
118
|
+
stats&.missed
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def file_missed_cap_violation(file, criterion, maximum)
|
|
122
|
+
actual = missed_for(file, criterion) or return
|
|
123
|
+
return unless actual > maximum
|
|
124
|
+
|
|
125
|
+
{criterion: criterion, maximum: maximum, actual: actual,
|
|
126
|
+
filename: file.filename, project_filename: file.project_filename}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def baseline_violation(file, criterion, floor)
|
|
130
|
+
stats = file.coverage_statistics[SimpleCov.coverage_statistics_key(criterion)]
|
|
131
|
+
return unless stats
|
|
132
|
+
|
|
133
|
+
actual = round(stats.percent)
|
|
134
|
+
return unless actual < floor.percent
|
|
135
|
+
return if floor.missed && stats.missed <= floor.missed
|
|
136
|
+
|
|
137
|
+
{criterion: criterion, expected: floor.percent, allowed_missed: floor.missed, actual: actual,
|
|
138
|
+
actual_missed: stats.missed, filename: file.filename, project_filename: file.project_filename}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def file_minimum_violation(file, criterion, expected)
|
|
142
|
+
actual = floored_percent_for(file, criterion) or return
|
|
143
|
+
return unless actual < expected
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
criterion: criterion,
|
|
147
|
+
expected: expected,
|
|
148
|
+
actual: actual,
|
|
149
|
+
filename: file.filename,
|
|
150
|
+
project_filename: file.project_filename
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def group_minimum_violations(group_name, group, minimums)
|
|
155
|
+
minimums.filter_map do |criterion, expected|
|
|
156
|
+
actual = floored_percent_for(group, criterion) or next
|
|
157
|
+
{group_name: group_name, criterion: criterion, expected: expected, actual: actual} if actual < expected
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The misconfiguration notice is enforcement output, not a Ruby warning: it
|
|
162
|
+
# must survive `-W0` and `Warning.warn` hooks, and honor `print_errors`. A
|
|
163
|
+
# configured group that matched no files is absent from `result.groups`
|
|
164
|
+
# too, but has nothing to gate rather than nothing to look up.
|
|
165
|
+
def lookup_group(result, group_name)
|
|
166
|
+
group = result.groups[group_name]
|
|
167
|
+
if group.nil? && SimpleCov.print_errors && !result.configured_group?(group_name)
|
|
168
|
+
ExitCodes.print_error "minimum_coverage_by_group: no group named '#{group_name}' exists. " \
|
|
169
|
+
"Available groups: #{result.groups.keys.join(", ")}"
|
|
170
|
+
end
|
|
171
|
+
group
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def drop_baseline_percents(mode, last_run)
|
|
175
|
+
case mode
|
|
176
|
+
when :median then median_baseline
|
|
177
|
+
when :branch then branch_baseline
|
|
178
|
+
else last_run_baseline(last_run || LastRun.read)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A hand-edited .last_run.json can carry any value type, and `LastRun.read`
|
|
183
|
+
# only vouches for the top level being a Hash. `:covered_percent` is the
|
|
184
|
+
# pre-criteria file format's spelling of the line percent.
|
|
185
|
+
def last_run_baseline(last_run)
|
|
186
|
+
return nil unless last_run.is_a?(Hash)
|
|
187
|
+
|
|
188
|
+
previous = (_ = last_run)[:result]
|
|
189
|
+
return nil unless previous.is_a?(Hash)
|
|
190
|
+
|
|
191
|
+
previous = _ = previous
|
|
192
|
+
{line: previous[:line] || previous[:covered_percent],
|
|
193
|
+
branch: previous[:branch], method: previous[:method]}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def median_baseline
|
|
197
|
+
totals = history_totals
|
|
198
|
+
baseline = {} #: Hash[Symbol, untyped]
|
|
199
|
+
%i[line branch method].each do |criterion|
|
|
200
|
+
values = totals.map { |entry| entry[criterion.to_s] }.grep(Numeric)
|
|
201
|
+
baseline[criterion] = median(values) unless values.empty?
|
|
202
|
+
end
|
|
203
|
+
baseline
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def history_totals
|
|
207
|
+
History.read.filter_map do |entry|
|
|
208
|
+
entry.fetch("totals") if entry.is_a?(Hash) && entry["totals"].is_a?(Hash)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def median(values)
|
|
213
|
+
sorted = values.sort
|
|
214
|
+
mid = sorted.length / 2
|
|
215
|
+
sorted.length.odd? ? sorted.fetch(mid) : (sorted.fetch(mid - 1) + sorted.fetch(mid)).fdiv(2)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def branch_baseline
|
|
219
|
+
branch, = History.git_info
|
|
220
|
+
return nil unless branch
|
|
221
|
+
|
|
222
|
+
entry = History.read.reverse_each.find { |candidate| branch_entry?(candidate, branch) }
|
|
223
|
+
return nil unless entry
|
|
224
|
+
|
|
225
|
+
totals = {} #: Hash[Symbol, untyped]
|
|
226
|
+
entry.fetch("totals").each { |key, value| totals[key.to_sym] = value }
|
|
227
|
+
totals
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# `==` and `eql?` are indistinguishable for the String operands, so that
|
|
231
|
+
# mutation is equivalent. The branch-baseline examples pin every clause.
|
|
232
|
+
# mutant:disable
|
|
233
|
+
# mutant:disable
|
|
234
|
+
def branch_entry?(candidate, branch)
|
|
235
|
+
candidate.is_a?(Hash) && candidate["branch"] == branch && candidate["totals"].is_a?(Hash)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def compute_drop(criterion, result, baseline)
|
|
239
|
+
stats_key = SimpleCov.coverage_statistics_key(criterion)
|
|
240
|
+
last_coverage_percent = baseline[stats_key]
|
|
241
|
+
# Treat a non-numeric percent like a missing one instead of
|
|
242
|
+
# raising out of the at_exit hook.
|
|
243
|
+
return unless last_coverage_percent.is_a?(Numeric)
|
|
244
|
+
|
|
245
|
+
current = floored_percent_for(result, criterion) or return
|
|
246
|
+
(last_coverage_percent - current).floor(10)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def round(percent)
|
|
250
|
+
SimpleCov.round_coverage(percent)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
# The state one measured run accumulates in one process: the pid and start
|
|
5
|
+
# time that identify it, the result it computes, the flags the merge
|
|
6
|
+
# coordination reads, and the serial the parent hands each forked
|
|
7
|
+
# subprocess. A new run is a new object, which is what lets anything needing
|
|
8
|
+
# a clean slate begin one instead of unsetting state by hand.
|
|
9
|
+
class CurrentRun
|
|
10
|
+
attr_accessor :pid, :process_start_time, :result
|
|
11
|
+
attr_writer :collating_result
|
|
12
|
+
|
|
13
|
+
# The result, the coordination flags and the identity belong to the run that
|
|
14
|
+
# ends; the fork genealogy belongs to the process and carries over. A child
|
|
15
|
+
# that restarts tracking must not forget it was forked, or it would produce
|
|
16
|
+
# the final report itself, and a parent must not recount from zero, or its
|
|
17
|
+
# next children's names would collide with its first ones (#1171).
|
|
18
|
+
def successor
|
|
19
|
+
following = self.class.new
|
|
20
|
+
following.subprocess_serial = @subprocess_serial
|
|
21
|
+
following.mark_forked_subprocess! if forked_subprocess?
|
|
22
|
+
following
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Answers exactly false while no result was ever stored, which is how "never
|
|
26
|
+
# computed" stays distinct from "cleared to nil".
|
|
27
|
+
def result?
|
|
28
|
+
instance_variable_defined?(:@result) && (_ = @result)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collating_result?
|
|
32
|
+
!!@collating_result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reading a mark that was never set answers nil, which is the answer wanted.
|
|
36
|
+
def forked_subprocess?
|
|
37
|
+
!!@forked_subprocess
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mark_forked_subprocess!
|
|
41
|
+
@forked_subprocess = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A monotonically increasing serial the parent assigns to each forked
|
|
45
|
+
# subprocess. The default `at_fork` builds the worker's command_name from
|
|
46
|
+
# this rather than the OS pid: the serial sequence is the same from one run
|
|
47
|
+
# to the next, so a re-run overwrites the previous run's resultset entries
|
|
48
|
+
# instead of writing uniquely-named ones that pile up (#1171).
|
|
49
|
+
def subprocess_serial
|
|
50
|
+
@subprocess_serial ||= 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_subprocess_serial!
|
|
54
|
+
@subprocess_serial = subprocess_serial + 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
attr_writer :subprocess_serial
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/simplecov/defaults.rb
CHANGED
|
@@ -1,53 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "English"
|
|
4
4
|
require "pathname"
|
|
5
|
-
require_relative "
|
|
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"
|
|
5
|
+
require_relative "formatter/html_formatter"
|
|
11
6
|
|
|
12
|
-
# Default configuration
|
|
13
7
|
SimpleCov.configure do
|
|
14
|
-
formatter SimpleCov::Formatter::
|
|
15
|
-
SimpleCov::Formatter.from_env(ENV)
|
|
16
|
-
)
|
|
8
|
+
formatter SimpleCov::Formatter::HTMLFormatter
|
|
17
9
|
|
|
18
10
|
load_profile "bundler_filter"
|
|
19
11
|
load_profile "hidden_filter"
|
|
20
|
-
# Exclude files outside of SimpleCov.root
|
|
21
12
|
load_profile "root_filter"
|
|
13
|
+
load_profile "test_frameworks"
|
|
22
14
|
end
|
|
23
15
|
|
|
24
|
-
#
|
|
25
|
-
|
|
16
|
+
# Stashed as early as possible, before rake or test/unit tamper with ARGV
|
|
17
|
+
# (#110). The program name is kept separately as well: the join is lossy for
|
|
18
|
+
# a path containing a space, and the guesser reads the executable from it.
|
|
19
|
+
SimpleCov::CommandGuesser.original_run_command = "#{$PROGRAM_NAME} #{ARGV.join(" ")}"
|
|
20
|
+
SimpleCov::CommandGuesser.original_program_name = $PROGRAM_NAME
|
|
26
21
|
|
|
27
|
-
at_exit do
|
|
28
|
-
next if SimpleCov.external_at_exit?
|
|
29
|
-
|
|
30
|
-
SimpleCov.at_exit_behavior
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Autoload config from ~/.simplecov if present
|
|
34
22
|
require_relative "load_global_config"
|
|
35
23
|
|
|
36
|
-
# Autoload config from .simplecov if present
|
|
37
|
-
# Recurse upwards until we find .simplecov or reach the root directory
|
|
38
|
-
|
|
39
24
|
config_path = Pathname.new(SimpleCov.root)
|
|
40
25
|
loop do
|
|
41
26
|
filename = config_path.join(".simplecov")
|
|
42
27
|
if filename.exist?
|
|
43
|
-
|
|
44
|
-
load filename
|
|
45
|
-
rescue LoadError, StandardError
|
|
28
|
+
SimpleCov.with_dot_simplecov_autoload do
|
|
29
|
+
load filename.to_s
|
|
30
|
+
rescue LoadError, StandardError => e
|
|
31
|
+
# simplecov:disable — only fires when .simplecov is unreadable
|
|
32
|
+
# or raises during load
|
|
46
33
|
warn "Warning: Error occurred while trying to load #{filename}. " \
|
|
47
|
-
|
|
34
|
+
"Error message: #{e.message}"
|
|
35
|
+
# simplecov:enable
|
|
48
36
|
end
|
|
49
37
|
break
|
|
50
38
|
end
|
|
39
|
+
# simplecov:disable — only fires when no .simplecov is found up to
|
|
40
|
+
# the filesystem root; simplecov's own dogfood run finds the repo's
|
|
41
|
+
# .simplecov on the first iteration and breaks before getting here.
|
|
51
42
|
config_path, = config_path.split
|
|
52
43
|
break if config_path.root?
|
|
44
|
+
# simplecov:enable
|
|
53
45
|
end
|