simplecov 1.0.3 → 1.1.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/README.md +65 -1439
- 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 +21 -28
- data/lib/simplecov/cli/coverage_file.rb +65 -0
- data/lib/simplecov/cli/diff.rb +43 -48
- data/lib/simplecov/cli/dotfile.rb +12 -6
- data/lib/simplecov/cli/merge.rb +12 -10
- data/lib/simplecov/cli/open.rb +3 -5
- data/lib/simplecov/cli/report.rb +31 -23
- 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 +30 -94
- data/lib/simplecov/cli/uncovered.rb +20 -25
- data/lib/simplecov/cli.rb +15 -2
- data/lib/simplecov/combine/branches_combiner.rb +43 -19
- 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 +36 -12
- data/lib/simplecov/combine/results_combiner.rb +12 -37
- data/lib/simplecov/combine.rb +5 -23
- data/lib/simplecov/command_guesser.rb +66 -9
- data/lib/simplecov/configuration/coverage.rb +12 -15
- data/lib/simplecov/configuration/coverage_criteria.rb +34 -38
- data/lib/simplecov/configuration/eval_coverage.rb +41 -0
- data/lib/simplecov/configuration/filters.rb +11 -44
- data/lib/simplecov/configuration/formatting.rb +6 -3
- data/lib/simplecov/configuration/groups.rb +42 -0
- data/lib/simplecov/configuration/merging.rb +8 -7
- data/lib/simplecov/configuration/thresholds.rb +14 -13
- data/lib/simplecov/configuration.rb +13 -43
- data/lib/simplecov/coverage_json.rb +24 -0
- data/lib/simplecov/coverage_statistics.rb +1 -1
- data/lib/simplecov/coverage_violations.rb +15 -5
- data/lib/simplecov/defaults.rb +7 -3
- data/lib/simplecov/directive.rb +1 -1
- 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 +3 -16
- 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 +4 -17
- data/lib/simplecov/exit_codes.rb +1 -0
- data/lib/simplecov/exit_handling.rb +11 -41
- data/lib/simplecov/file_list.rb +5 -10
- data/lib/simplecov/filter.rb +43 -9
- data/lib/simplecov/formatter/base.rb +11 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +97 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +35 -5
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +96 -0
- data/lib/simplecov/formatter/html_formatter.rb +67 -47
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +54 -56
- 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 -48
- data/lib/simplecov/formatter/multi_formatter.rb +1 -1
- data/lib/simplecov/formatter/simple_formatter.rb +8 -5
- data/lib/simplecov/formatter.rb +5 -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 +2 -2
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +2 -2
- data/lib/simplecov/parallel_coordination.rb +6 -1
- data/lib/simplecov/parallel_result_merger.rb +230 -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_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 +82 -43
- data/lib/simplecov/run_identity.rb +77 -0
- data/lib/simplecov/simulate_coverage.rb +35 -11
- data/lib/simplecov/source_file/method.rb +7 -1
- data/lib/simplecov/source_file/ruby_data_parser.rb +6 -2
- data/lib/simplecov/source_file/skip_chunks.rb +7 -10
- data/lib/simplecov/source_file/source_loader.rb +23 -7
- data/lib/simplecov/source_file/statistics.rb +24 -16
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +203 -13
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +19 -30
- 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 +6 -14
- data/lib/simplecov/static_coverage_extractor/visitor.rb +23 -36
- data/lib/simplecov/unloaded_file_injector.rb +75 -0
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +11 -5
- data/schemas/coverage-v1.0.schema.json +2 -2
- data/schemas/coverage.schema.json +2 -2
- data/sig/simplecov.rbs +200 -66
- metadata +28 -14
- 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 -70
- 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
|
@@ -13,13 +13,27 @@ module SimpleCov
|
|
|
13
13
|
# so all results in all files specified will be merged. Pass
|
|
14
14
|
# `ignore_timeout: false` to honor it.
|
|
15
15
|
#
|
|
16
|
-
|
|
16
|
+
# `processes:` above 1 fans the merge out across that many forked worker
|
|
17
|
+
# processes, for a collate big enough that reading and parsing the
|
|
18
|
+
# resultsets dominates it. The report is identical either way, not merely
|
|
19
|
+
# equivalent — the workers visit the resultsets in the order the
|
|
20
|
+
# single-process merge visits them — and one process never forks at all.
|
|
21
|
+
# The count is deliberately not clamped to the machine's core count nor
|
|
22
|
+
# gated on some minimum number of resultsets: how many processes a collate
|
|
23
|
+
# job can afford is the caller's call, not SimpleCov's. Anything below 1 is
|
|
24
|
+
# taken as 1, so a count computed from arithmetic that can reach zero needs
|
|
25
|
+
# no guarding. Defaults to `SIMPLECOV_CONCURRENCY`, or 1 when that is
|
|
26
|
+
# unset. See `SimpleCov::ParallelResultMerger`.
|
|
27
|
+
#
|
|
28
|
+
def collate(result_filenames, profile = nil, processes: ENV.fetch("SIMPLECOV_CONCURRENCY", 1).to_i,
|
|
29
|
+
ignore_timeout: true, &)
|
|
17
30
|
raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
|
|
18
31
|
|
|
19
32
|
initial_setup(profile, &)
|
|
20
33
|
|
|
21
34
|
# Use the ResultMerger to produce a single, merged result, ready to use.
|
|
22
|
-
@result =
|
|
35
|
+
@result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
|
|
36
|
+
ignore_timeout: ignore_timeout)
|
|
23
37
|
|
|
24
38
|
@collating_result = true
|
|
25
39
|
run_exit_tasks!
|
|
@@ -28,24 +42,22 @@ module SimpleCov
|
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
#
|
|
31
|
-
# Returns the result for the current coverage run
|
|
32
|
-
#
|
|
45
|
+
# Returns the result for the current coverage run. With merging enabled,
|
|
46
|
+
# every process stores its own slice, but only the finalization owner reads
|
|
47
|
+
# and caches the merged result.
|
|
33
48
|
#
|
|
34
49
|
def result
|
|
35
50
|
return @result if result?
|
|
36
51
|
|
|
37
52
|
use_merging = merging
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
# step, so this per-process result is the final one and reports any
|
|
41
|
-
# dropped source files; otherwise the merged result does the reporting.
|
|
42
|
-
process_coverage_result(report: !use_merging) if defined?(Coverage) && Coverage.running?
|
|
54
|
+
collect_own_coverage(standalone: !use_merging)
|
|
43
55
|
|
|
44
56
|
# If we're using merging of results, store the current result
|
|
45
57
|
# first (if there is one), then merge the results and return those
|
|
46
58
|
if use_merging
|
|
47
59
|
SimpleCov::ResultMerger.store_result(@result) if result?
|
|
48
|
-
return @result unless
|
|
60
|
+
return @result unless merge_finalization_owner?
|
|
49
61
|
|
|
50
62
|
wait_for_other_processes
|
|
51
63
|
@result = SimpleCov::ResultMerger.merged_result
|
|
@@ -54,6 +66,16 @@ module SimpleCov
|
|
|
54
66
|
@result
|
|
55
67
|
end
|
|
56
68
|
|
|
69
|
+
# Build this process's slice of the coverage result. `standalone` is true
|
|
70
|
+
# when no merge step follows, which makes this the final result: it is the
|
|
71
|
+
# one that reports dropped source files, and the one that injects unloaded
|
|
72
|
+
# files. When a merge does follow, both jobs belong to the merged result.
|
|
73
|
+
def collect_own_coverage(standalone:)
|
|
74
|
+
return unless defined?(Coverage) && Coverage.running?
|
|
75
|
+
|
|
76
|
+
process_coverage_result(report: standalone, inject_unloaded: standalone)
|
|
77
|
+
end
|
|
78
|
+
|
|
57
79
|
# Returns nil if the result has not been computed, otherwise the result.
|
|
58
80
|
def result?
|
|
59
81
|
defined?(@result) && @result
|
|
@@ -79,7 +101,7 @@ module SimpleCov
|
|
|
79
101
|
# construction). Files matched by no group fall into the implicit
|
|
80
102
|
# "Ungrouped" bucket.
|
|
81
103
|
def grouped(files, groups: SimpleCov.groups)
|
|
82
|
-
return {} if groups.empty?
|
|
104
|
+
return {} if GroupNames.validate!(groups.keys).empty?
|
|
83
105
|
|
|
84
106
|
grouped = groups.transform_values do |filter|
|
|
85
107
|
SimpleCov::FileList.new(files.select { |source_file| filter.matches?(source_file) })
|
|
@@ -87,7 +109,7 @@ module SimpleCov
|
|
|
87
109
|
|
|
88
110
|
in_group = grouped_file_set(grouped)
|
|
89
111
|
ungrouped = files.reject { |source_file| in_group.include?(source_file) }
|
|
90
|
-
grouped[
|
|
112
|
+
grouped[GroupNames::UNGROUPED] = SimpleCov::FileList.new(ungrouped) if ungrouped.any?
|
|
91
113
|
|
|
92
114
|
grouped
|
|
93
115
|
end
|
|
@@ -115,6 +137,29 @@ module SimpleCov
|
|
|
115
137
|
coverage.floor(2)
|
|
116
138
|
end
|
|
117
139
|
|
|
140
|
+
# Add simulated coverage for each of `candidate_paths` the result doesn't
|
|
141
|
+
# already carry, and return it with the set of paths added.
|
|
142
|
+
#
|
|
143
|
+
# @api private — the seam `SimpleCov::ResultMerger` injects through. Public
|
|
144
|
+
# only because the merge runs in another object, on behalf of processes
|
|
145
|
+
# whose configuration it may not share, so it supplies the paths itself.
|
|
146
|
+
def inject_unloaded_files(result, candidate_paths, synthesize: nil, lines: nil)
|
|
147
|
+
return [result, Set.new] if candidate_paths.empty?
|
|
148
|
+
|
|
149
|
+
# Synthesizing branch and method tuples means parsing every tracked file
|
|
150
|
+
# that wasn't loaded, which is about half the cost of simulating one.
|
|
151
|
+
# Nothing reads those tuples when neither criterion is enabled —
|
|
152
|
+
# `Combine::CoverageAccumulator` only combines them per criterion, and the
|
|
153
|
+
# statistics drop them the same way — so skip the parse. The same goes
|
|
154
|
+
# for line data, which a branch-only or method-only run neither reports
|
|
155
|
+
# nor receives from `Coverage` for the files it loaded. See #1250.
|
|
156
|
+
UnloadedFileInjector.call(
|
|
157
|
+
result, candidate_paths,
|
|
158
|
+
synthesize: synthesize.nil? ? branch_coverage? || method_coverage? : synthesize,
|
|
159
|
+
lines: lines.nil? ? line_coverage? : lines
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
118
163
|
private
|
|
119
164
|
|
|
120
165
|
def initial_setup(profile, &block)
|
|
@@ -126,14 +171,15 @@ module SimpleCov
|
|
|
126
171
|
grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
|
|
127
172
|
end
|
|
128
173
|
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
# Every path this process was told to track, whether or not it loaded them.
|
|
175
|
+
# Recorded on the result (and from there into the resultset) so that a merge
|
|
176
|
+
# in another process can inject the ones nobody loaded without needing this
|
|
177
|
+
# process's `cover` / `track_files` configuration. A standalone `collate`
|
|
178
|
+
# never ran `SimpleCov.start` and so has none of its own. See #1250.
|
|
179
|
+
def tracked_file_paths
|
|
180
|
+
UnloadedFileInjector.discover(
|
|
181
|
+
unloaded_file_discovery_globs, root: root, reject: filters.select(&:path_only?)
|
|
182
|
+
)
|
|
137
183
|
end
|
|
138
184
|
|
|
139
185
|
# Globs to expand on disk when injecting unloaded files into the
|
|
@@ -144,36 +190,29 @@ module SimpleCov
|
|
|
144
190
|
[tracked_files, *cover_globs].compact
|
|
145
191
|
end
|
|
146
192
|
|
|
147
|
-
# Expand the given globs relative to SimpleCov.root, not Dir.pwd —
|
|
148
|
-
# test runners that chdir (or CI scripts that invoke the suite
|
|
149
|
-
# from a subdir) would otherwise silently miss the unloaded-file
|
|
150
|
-
# injection and produce a different file set per environment. See
|
|
151
|
-
# issue #1106.
|
|
152
|
-
def discover_unloaded_paths(globs)
|
|
153
|
-
globs.flat_map { |glob| Dir.glob(glob, base: root) }.uniq
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def inject_unloaded_files(result, candidate_paths)
|
|
157
|
-
not_loaded_files = candidate_paths.each_with_object(Set.new) do |file, set|
|
|
158
|
-
absolute_path = File.expand_path(file, root)
|
|
159
|
-
next if result.key?(absolute_path)
|
|
160
|
-
|
|
161
|
-
result[absolute_path] = SimulateCoverage.call(absolute_path)
|
|
162
|
-
set << absolute_path
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
[result, not_loaded_files]
|
|
166
|
-
end
|
|
167
|
-
|
|
168
193
|
# Run all the steps that handle processing the raw coverage result.
|
|
169
194
|
# `report:` is true only when this slice is the final result (merging
|
|
170
195
|
# off); with merging on the merged result reports dropped source files,
|
|
171
196
|
# so the per-process slice stays quiet to avoid one warning per worker.
|
|
172
|
-
|
|
197
|
+
#
|
|
198
|
+
# `inject_unloaded:` is likewise false when a merge step follows. Only the
|
|
199
|
+
# union of every process's loaded files says what was really never loaded,
|
|
200
|
+
# so injecting here means each worker simulates nearly the whole project
|
|
201
|
+
# and all but one of those passes is merged away. See #1250.
|
|
202
|
+
def process_coverage_result(report:, inject_unloaded: true)
|
|
173
203
|
raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
|
|
174
204
|
adapted = SimpleCov::ResultAdapter.call(raw)
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
# What this process was told to track and did not load. Subtracting what
|
|
206
|
+
# it loaded matters because `Result#to_hash` serializes coverage after
|
|
207
|
+
# filtering: a file this process loaded and then filtered out would
|
|
208
|
+
# otherwise be recorded as tracked while absent from the stored coverage,
|
|
209
|
+
# and the merge would simulate it back in as never-loaded. See #1250.
|
|
210
|
+
tracked = tracked_file_paths - adapted.keys
|
|
211
|
+
result, not_loaded = inject_unloaded ? inject_unloaded_files(adapted, tracked) : [adapted, Set.new]
|
|
212
|
+
@result = SimpleCov::Result.new(
|
|
213
|
+
result, not_loaded_files: not_loaded, tracked_files: tracked,
|
|
214
|
+
run_id: SimpleCov.run_id, worker_id: SimpleCov.worker_id, report: report
|
|
215
|
+
)
|
|
177
216
|
end
|
|
178
217
|
end
|
|
179
218
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Identifies one test invocation and each top-level parallel worker within
|
|
7
|
+
# it. Forked subprocesses inherit both values from their parent worker.
|
|
8
|
+
module RunIdentity
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Returns the run id together with its provenance: `true` when the id
|
|
12
|
+
# alone proves same-run membership (explicitly configured, derived from
|
|
13
|
+
# parallel_tests' per-invocation pid file, or freshly random), `false`
|
|
14
|
+
# when it was inferred from the parent pid, which consecutive runs
|
|
15
|
+
# launched by the same long-lived parent process share.
|
|
16
|
+
def generate
|
|
17
|
+
explicit = ENV.fetch("SIMPLECOV_RUN_ID", nil)
|
|
18
|
+
return [explicit, true] if explicit && !explicit.empty?
|
|
19
|
+
|
|
20
|
+
pid_file = ENV.fetch("PARALLEL_PID_FILE", nil)
|
|
21
|
+
return ["parallel-tests:#{File.basename(pid_file)}", true] if pid_file && !pid_file.empty?
|
|
22
|
+
|
|
23
|
+
return ["parallel-parent:#{Process.ppid}", false] if SimpleCov::ParallelAdapters.current
|
|
24
|
+
|
|
25
|
+
[SecureRandom.uuid, true]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def worker_id
|
|
29
|
+
explicit = ENV.fetch("SIMPLECOV_WORKER_ID", nil)
|
|
30
|
+
return explicit if explicit && !explicit.empty?
|
|
31
|
+
|
|
32
|
+
number = ENV.fetch("TEST_ENV_NUMBER", nil)
|
|
33
|
+
return number.empty? ? "1" : number if number
|
|
34
|
+
|
|
35
|
+
Process.pid.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether the current run id alone proves same-run membership. Decided
|
|
39
|
+
# where the id is generated, never re-inferred from the id's shape, so
|
|
40
|
+
# an explicit SIMPLECOV_RUN_ID that happens to look like an inferred
|
|
41
|
+
# one is still trusted.
|
|
42
|
+
def authoritative?
|
|
43
|
+
materialize_current
|
|
44
|
+
@authoritative
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current
|
|
48
|
+
materialize_current
|
|
49
|
+
@current
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def materialize_current
|
|
53
|
+
@current, @authoritative = generate unless defined?(@current)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def current_worker_id
|
|
57
|
+
@current_worker_id ||= worker_id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prepare
|
|
61
|
+
SimpleCov::ParallelAdapters.current
|
|
62
|
+
SimpleCov.run_id
|
|
63
|
+
SimpleCov.worker_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Mixed into SimpleCov after ParallelAdapters has loaded.
|
|
67
|
+
module Accessors
|
|
68
|
+
def run_id
|
|
69
|
+
SimpleCov::RunIdentity.current
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def worker_id
|
|
73
|
+
SimpleCov::RunIdentity.current_worker_id
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -33,25 +33,46 @@ module SimpleCov
|
|
|
33
33
|
# can't be parsed, fall back to the old empty hashes — old behavior,
|
|
34
34
|
# old tradeoff.
|
|
35
35
|
#
|
|
36
|
+
# Pass `synthesize: false` to skip the static analysis and return the
|
|
37
|
+
# empty hashes directly. Callers use it when neither branch nor method
|
|
38
|
+
# coverage is enabled, since nothing will read the tuples and the Prism
|
|
39
|
+
# parse is about half the cost of simulating a file. See #1250.
|
|
40
|
+
#
|
|
41
|
+
# Pass `lines: false` to omit the `"lines"` key entirely, mirroring what
|
|
42
|
+
# `Coverage.result` reports for a file loaded under a branch-only or
|
|
43
|
+
# method-only run. Emitting zeroed lines there would make a simulated
|
|
44
|
+
# file indistinguishable from one a sibling process actually loaded once
|
|
45
|
+
# the two are merged.
|
|
46
|
+
#
|
|
36
47
|
# @return [Hash]
|
|
37
48
|
#
|
|
38
|
-
def call(absolute_path)
|
|
49
|
+
def call(absolute_path, synthesize: true, lines: true)
|
|
39
50
|
source_lines = read_lines(absolute_path)
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
simulated = synthesized_tuples(source_lines, synthesize)
|
|
52
|
+
return simulated unless lines
|
|
53
|
+
|
|
54
|
+
classified = coverage_stub(absolute_path, source_lines) ||
|
|
55
|
+
LinesClassifier.new.classify(source_lines)
|
|
56
|
+
{"lines" => classified}.merge(simulated)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The branch and method tuples for a file, or empty hashes when the static
|
|
60
|
+
# analysis is skipped (nothing enabled reads them) or unavailable (no Prism,
|
|
61
|
+
# or the file doesn't parse).
|
|
62
|
+
def synthesized_tuples(source_lines, synthesize)
|
|
42
63
|
empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
|
|
43
|
-
synthesized = StaticCoverageExtractor.call(source_lines.join) || empty
|
|
64
|
+
synthesized = (StaticCoverageExtractor.call(source_lines.join) if synthesize) || empty
|
|
44
65
|
|
|
45
|
-
{
|
|
46
|
-
"lines" => lines,
|
|
47
|
-
"branches" => synthesized["branches"],
|
|
48
|
-
"methods" => synthesized["methods"]
|
|
49
|
-
}
|
|
66
|
+
{"branches" => synthesized["branches"], "methods" => synthesized["methods"]}
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# SystemCallError, not just ENOENT: a `track_files` glob can sweep
|
|
70
|
+
# up an unreadable file or a directory named like a Ruby file
|
|
71
|
+
# (EACCES, EISDIR), and simulation must degrade to "empty file"
|
|
72
|
+
# rather than crash the merge or report step.
|
|
52
73
|
def read_lines(path)
|
|
53
74
|
File.readlines(path)
|
|
54
|
-
rescue
|
|
75
|
+
rescue SystemCallError
|
|
55
76
|
[]
|
|
56
77
|
end
|
|
57
78
|
|
|
@@ -68,7 +89,10 @@ module SimpleCov
|
|
|
68
89
|
classifier_output = LinesClassifier.new.classify(source_lines)
|
|
69
90
|
stub.each_index { |idx| stub[idx] = nil if classifier_output[idx].nil? }
|
|
70
91
|
stub
|
|
71
|
-
rescue
|
|
92
|
+
rescue SystemCallError, SyntaxError
|
|
93
|
+
# SystemCallError for the same reason as read_lines above:
|
|
94
|
+
# line_stub reads the file itself, so EACCES/EISDIR surface here
|
|
95
|
+
# too.
|
|
72
96
|
nil
|
|
73
97
|
end
|
|
74
98
|
end
|
|
@@ -18,10 +18,16 @@ module SimpleCov
|
|
|
18
18
|
!skipped? && coverage.positive?
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Criterion-level skips (nocov chunks, `# simplecov:disable` /
|
|
22
|
+
# `# simplecov:disable method` regions) arrive via `skipped!` from
|
|
23
|
+
# MethodBuilder. Deliberately NOT derived from the lines' skip
|
|
24
|
+
# state: a line-only directive around a def must not remove the
|
|
25
|
+
# method from method totals. Without line info there is nothing to
|
|
26
|
+
# report against, so such a method stays skipped.
|
|
21
27
|
def skipped?
|
|
22
28
|
return @skipped if defined?(@skipped)
|
|
23
29
|
|
|
24
|
-
@skipped = lines.
|
|
30
|
+
@skipped = lines.empty?
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# Flag the method as skipped directly, without going through its lines.
|
|
@@ -97,9 +97,13 @@ module SimpleCov
|
|
|
97
97
|
# Method coverage keys can contain inspect-format class references
|
|
98
98
|
# like `#<Class:Foo>` or `#<Class:0x...>`, which aren't valid Ruby
|
|
99
99
|
# syntax. Wrap them in quotes so Ripper can parse the surrounding
|
|
100
|
-
# array literal; downstream we treat them as opaque strings.
|
|
100
|
+
# array literal; downstream we treat them as opaque strings. The
|
|
101
|
+
# pattern recurses because singleton methods on instances nest one
|
|
102
|
+
# inspect segment inside another (`#<Class:#<Object:0x...>>`), and
|
|
103
|
+
# stopping at the first `>` leaves a dangling `>` that fails both
|
|
104
|
+
# Ripper passes and crashes the merge.
|
|
101
105
|
def quote_inspected_class_segments(str)
|
|
102
|
-
str.gsub(/#<[
|
|
106
|
+
str.gsub(/#<(?:[^<>]|\g<0>)*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
|
|
103
107
|
end
|
|
104
108
|
end
|
|
105
109
|
end
|
|
@@ -22,19 +22,16 @@ module SimpleCov
|
|
|
22
22
|
@src = src
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# Every criterion honors the deprecated all-criteria `# :nocov:`
|
|
26
|
+
# chunks plus its own per-criterion directive ranges. Methods
|
|
27
|
+
# included: they carry a source range, and nocov has always meant
|
|
28
|
+
# "exclude everything here".
|
|
28
29
|
def for(criterion)
|
|
29
|
-
|
|
30
|
-
directive_chunks.fetch(:method)
|
|
31
|
-
else
|
|
32
|
-
nocov_chunks + directive_chunks.fetch(criterion)
|
|
33
|
-
end
|
|
30
|
+
nocov_chunks + directive_chunks.fetch(criterion)
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
33
|
+
# Ranges of 1-based line numbers (see `with_index(1)` below);
|
|
34
|
+
# consumers subtract 1 to index into the zero-based lines array.
|
|
38
35
|
def nocov_chunks
|
|
39
36
|
@nocov_chunks ||= build_nocov_chunks
|
|
40
37
|
end
|
|
@@ -17,17 +17,27 @@ module SimpleCov
|
|
|
17
17
|
lines = [] #: Array[String]
|
|
18
18
|
# The default encoding is UTF-8
|
|
19
19
|
File.open(filename, "rb:UTF-8") do |file|
|
|
20
|
-
current_line = file.gets
|
|
20
|
+
current_line = scrub_invalid(file.gets)
|
|
21
21
|
|
|
22
22
|
if current_line && shebang?(current_line)
|
|
23
23
|
lines << current_line
|
|
24
|
-
current_line = file.gets
|
|
24
|
+
current_line = scrub_invalid(file.gets)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
read_lines(file, lines, current_line)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# A line read as UTF-8 can still carry invalid bytes (a Latin-1
|
|
32
|
+
# source file without a magic comment, say). Replace them before
|
|
33
|
+
# any regex sees the line: the shebang and magic-comment checks
|
|
34
|
+
# would otherwise raise ArgumentError and take the report down.
|
|
35
|
+
def scrub_invalid(line)
|
|
36
|
+
return line if line.nil? || line.valid_encoding?
|
|
37
|
+
|
|
38
|
+
line.scrub
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
def shebang?(line)
|
|
32
42
|
SHEBANG_REGEX.match?(line)
|
|
33
43
|
end
|
|
@@ -47,14 +57,20 @@ module SimpleCov
|
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# properly,
|
|
60
|
+
# Guarantee every line leaves the loader as valid UTF-8, replacing
|
|
61
|
+
# what can't be represented: transcode non-UTF-8 lines (setting
|
|
62
|
+
# invalid/undef options on `file.set_encoding` doesn't work
|
|
63
|
+
# properly, and this also works around a JRuby incompatibility)
|
|
64
|
+
# and scrub UTF-8-tagged lines that carry invalid bytes, which
|
|
65
|
+
# would otherwise raise from every regex the classifier runs.
|
|
54
66
|
def ensure_remove_undefs(file_lines)
|
|
55
67
|
file_lines.each do |line|
|
|
56
68
|
# simplecov:disable — defensive: only fires for non-UTF-8 source files
|
|
57
|
-
|
|
69
|
+
if line.encoding == Encoding::UTF_8
|
|
70
|
+
line.scrub! unless line.valid_encoding?
|
|
71
|
+
else
|
|
72
|
+
line.encode!("UTF-8", invalid: :replace, undef: :replace)
|
|
73
|
+
end
|
|
58
74
|
# simplecov:enable
|
|
59
75
|
end
|
|
60
76
|
end
|
|
@@ -22,35 +22,43 @@ module SimpleCov
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def line_statistics
|
|
25
|
-
sf = @source_file
|
|
26
25
|
{
|
|
27
|
-
line:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
omitted: sf.never_lines.size
|
|
26
|
+
line: coverage_statistics(
|
|
27
|
+
@source_file.covered_lines,
|
|
28
|
+
@source_file.missed_lines,
|
|
29
|
+
omitted: @source_file.never_lines.size
|
|
32
30
|
)
|
|
33
31
|
}
|
|
34
32
|
end
|
|
35
33
|
|
|
34
|
+
# Files added via track_files but never loaded have no branch/method
|
|
35
|
+
# data. Report 0% instead of the empty-set default of 100% (see #902).
|
|
36
36
|
def branch_statistics
|
|
37
37
|
sf = @source_file
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if sf.not_loaded? &&
|
|
41
|
-
return {branch: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
|
|
42
|
-
end
|
|
38
|
+
covered = sf.covered_branches
|
|
39
|
+
missed = sf.missed_branches
|
|
40
|
+
percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
|
|
43
41
|
|
|
44
|
-
{branch:
|
|
42
|
+
{branch: coverage_statistics(covered, missed, percent: percent)}
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def method_statistics
|
|
48
46
|
sf = @source_file
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
covered = sf.covered_methods
|
|
48
|
+
missed = sf.missed_methods
|
|
49
|
+
percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
|
|
50
|
+
|
|
51
|
+
{method: coverage_statistics(covered, missed, percent: percent)}
|
|
52
|
+
end
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
def coverage_statistics(covered, missed, omitted: 0, percent: nil)
|
|
55
|
+
CoverageStatistics.new(
|
|
56
|
+
total_strength: covered.sum { |item| item.coverage.to_i },
|
|
57
|
+
covered: covered.size,
|
|
58
|
+
missed: missed.size,
|
|
59
|
+
omitted: omitted,
|
|
60
|
+
percent: percent
|
|
61
|
+
)
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
64
|
end
|