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
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "result_merger/legacy_format_adapter"
|
|
4
|
+
require_relative "result_merger/resultset_file"
|
|
5
|
+
require_relative "result_merger/resultset_run_identity"
|
|
6
|
+
require_relative "result_merger/resultset_store"
|
|
7
|
+
require_relative "result_merger/contexts"
|
|
8
|
+
require_relative "result_merger/unloaded_files"
|
|
4
9
|
|
|
5
10
|
module SimpleCov
|
|
6
11
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# upon multiple test suites.
|
|
12
|
+
# Caching, loading and merging `SimpleCov::Result`s into a single result for
|
|
13
|
+
# coverage analysis based upon multiple test suites.
|
|
10
14
|
#
|
|
11
15
|
module ResultMerger
|
|
16
|
+
extend ResultsetRunIdentity
|
|
17
|
+
|
|
12
18
|
class << self
|
|
13
|
-
# The path to the .resultset.json cache file
|
|
14
19
|
def resultset_path
|
|
15
|
-
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def resultset_writelock
|
|
19
|
-
File.join(SimpleCov.coverage_path, ".resultset.json.lock")
|
|
20
|
+
ResultsetStore.resultset_path
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def merge_and_store(*file_paths, ignore_timeout: false)
|
|
@@ -26,168 +27,163 @@ module SimpleCov
|
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def merge_results(*file_paths, ignore_timeout: false)
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# even more data especially when it also reads in all source files.
|
|
37
|
-
initial_memo = valid_results(file_paths.shift, ignore_timeout: ignore_timeout)
|
|
38
|
-
|
|
39
|
-
command_names, coverage = file_paths.reduce(initial_memo) do |memo, file_path|
|
|
40
|
-
merge_coverage(memo, valid_results(file_path, ignore_timeout: ignore_timeout))
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
create_result(command_names, coverage)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def valid_results(file_path, ignore_timeout: false)
|
|
47
|
-
results = parse_file(file_path)
|
|
48
|
-
merge_valid_results(results, ignore_timeout: ignore_timeout)
|
|
30
|
+
# Tracked paths and test maps are collected as each resultset is parsed,
|
|
31
|
+
# so files are still read and discarded one at a time (#1250).
|
|
32
|
+
tracked_files = Set.new
|
|
33
|
+
context_maps = ContextMap::Union.new
|
|
34
|
+
command_names, coverage = absorb_results(file_paths, ignore_timeout: ignore_timeout,
|
|
35
|
+
&entry_collector(tracked_files, context_maps))
|
|
36
|
+
create_result(command_names, coverage, tracked_files: tracked_files, contexts: context_maps.map)
|
|
49
37
|
end
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
# One block over the entries each resultset contributes to the merge,
|
|
40
|
+
# feeding every merge-side accumulator in a single pass: the tracked
|
|
41
|
+
# paths and the per-test map union (#1250).
|
|
42
|
+
def entry_collector(tracked_files, context_maps)
|
|
43
|
+
collectors = [UnloadedFiles.collector(tracked_files), context_maps.collector]
|
|
44
|
+
->(surviving) { collectors.each { |collector| collector.call(surviving) } }
|
|
54
45
|
end
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
#
|
|
48
|
+
# Reads every resultset and folds it into one merged coverage, stopping
|
|
49
|
+
# short of building a `SimpleCov::Result`.
|
|
50
|
+
#
|
|
51
|
+
# Files are only read in and parsed one at a time on purpose. In big CI
|
|
52
|
+
# setups you might deal with 100s of jobs each producing megabytes of
|
|
53
|
+
# data, and reading them all in easily produces gigabytes of memory
|
|
54
|
+
# consumption. For the same reason a `Result` is only created at the end.
|
|
55
|
+
#
|
|
56
|
+
# One accumulator absorbs the whole run, rather than folding each file
|
|
57
|
+
# into the merged-so-far pairwise: the pairwise form rebuilt every file's
|
|
58
|
+
# coverage once per resultset, which is what made merging a large
|
|
59
|
+
# parallel run's results the dominant cost of `collate`.
|
|
60
|
+
#
|
|
61
|
+
# `file_paths` is only ever iterated, so a caller that wants to observe
|
|
62
|
+
# the merge as it goes can hand in any Enumerable. `benchmarks/collate`
|
|
63
|
+
# passes an Enumerator that reports progress.
|
|
64
|
+
#
|
|
65
|
+
def absorb_results(file_paths, ignore_timeout: false, &on_parse)
|
|
66
|
+
Combine::CoverageAccumulator.fold(
|
|
67
|
+
file_paths.lazy.map { |file_path| valid_results(file_path, ignore_timeout: ignore_timeout, &on_parse) }
|
|
68
|
+
)
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
JSON.parse(content) || {}
|
|
69
|
-
rescue StandardError
|
|
70
|
-
warn "[SimpleCov]: Warning! Parsing JSON content of resultset file failed"
|
|
71
|
-
{}
|
|
71
|
+
# Yields the surviving entries before they are reduced.
|
|
72
|
+
def valid_results(file_path, ignore_timeout: false, &on_parse)
|
|
73
|
+
merge_valid_results(ResultsetFile.parse(file_path), ignore_timeout: ignore_timeout, &on_parse)
|
|
72
74
|
end
|
|
73
75
|
|
|
76
|
+
# Yields the entries that survived the merge timeout, so a caller that
|
|
77
|
+
# wants to observe what a resultset carried sees only what is being
|
|
78
|
+
# merged. An expired entry contributes nothing, tracked paths included.
|
|
74
79
|
def merge_valid_results(results, ignore_timeout: false)
|
|
75
|
-
results = results
|
|
80
|
+
results = drop_expired_results(results) unless ignore_timeout
|
|
81
|
+
yield results if block_given?
|
|
76
82
|
|
|
77
83
|
command_plus_coverage = results.map do |command_name, data|
|
|
78
|
-
[[command_name],
|
|
84
|
+
[[command_name], LegacyFormatAdapter.call(data.fetch("coverage"))]
|
|
79
85
|
end
|
|
80
86
|
|
|
81
|
-
# one file itself _might_ include multiple test runs
|
|
82
87
|
merge_coverage(*command_plus_coverage)
|
|
83
88
|
end
|
|
84
89
|
|
|
90
|
+
def drop_expired_results(results)
|
|
91
|
+
fresh, expired = results.partition { |_command_name, data| within_merge_timeout?(data) }
|
|
92
|
+
return results if expired.empty?
|
|
93
|
+
|
|
94
|
+
warn_about_expired_results(expired.map(&:first))
|
|
95
|
+
fresh.to_h
|
|
96
|
+
end
|
|
97
|
+
|
|
85
98
|
def within_merge_timeout?(data)
|
|
86
|
-
|
|
99
|
+
(Time.now - Time.at(data.fetch("timestamp"))) < SimpleCov.merge_timeout
|
|
87
100
|
end
|
|
88
101
|
|
|
89
|
-
def
|
|
90
|
-
|
|
102
|
+
def warn_about_expired_results(expired_command_names)
|
|
103
|
+
# Ordinary parallel workers only store their own slice; the selected
|
|
104
|
+
# final process performs this merge.
|
|
105
|
+
return unless SimpleCov.print_errors
|
|
106
|
+
|
|
107
|
+
warn "[SimpleCov]: Excluded #{expired_command_names.size} result(s) older than " \
|
|
108
|
+
"merge_timeout (#{SimpleCov.merge_timeout}s) from the merged report: " \
|
|
109
|
+
"#{expired_command_names.sort.join(", ")}. " \
|
|
110
|
+
"Increase SimpleCov.merge_timeout to include them."
|
|
91
111
|
end
|
|
92
112
|
|
|
93
|
-
|
|
113
|
+
# `tracked_files:` has no default: only the caller knows what the
|
|
114
|
+
# contributing processes were told to track, and quietly reading an
|
|
115
|
+
# omission as "nothing was tracked" would drop every never-loaded
|
|
116
|
+
# file from the merged report.
|
|
117
|
+
def create_result(command_names, coverage, tracked_files:, contexts: nil)
|
|
94
118
|
return nil unless coverage
|
|
95
119
|
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
# Deduped: a CI matrix collates one resultset per worker, and joining
|
|
121
|
+
# every run's name verbatim rendered "RSpec" a hundred times over in the
|
|
122
|
+
# report footer (#1284).
|
|
123
|
+
distinct_names = command_names.reject(&:empty?).uniq.sort
|
|
124
|
+
coverage, injected = UnloadedFiles.inject(coverage, tracked_files)
|
|
125
|
+
# The merged result is the authoritative one users actually see, so it's
|
|
126
|
+
# the one that warns about source files dropped because they no longer
|
|
127
|
+
# exist on disk (#980). The per-process slices stay quiet to avoid one
|
|
128
|
+
# warning per worker.
|
|
129
|
+
Result.new(
|
|
130
|
+
coverage,
|
|
131
|
+
command_name: distinct_names.join(", "), contexts: contexts, report: true,
|
|
132
|
+
not_loaded_files: UnloadedFiles.never_executed(coverage) | injected,
|
|
133
|
+
tracked_files: tracked_files
|
|
134
|
+
).tap { |result| result.command_names = distinct_names }
|
|
98
135
|
end
|
|
99
136
|
|
|
100
137
|
def merge_coverage(*results)
|
|
101
138
|
return [[""], nil] if results.empty?
|
|
102
|
-
return results.first if results.size == 1
|
|
103
|
-
|
|
104
|
-
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
|
|
105
|
-
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
|
|
106
|
-
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
|
|
107
|
-
merged_command = memo_command + command
|
|
108
139
|
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
# Destructured rather than counted: the single-result fast path then
|
|
141
|
+
# hands back the very result it checked for, not a second lookup.
|
|
142
|
+
only, *rest = results
|
|
143
|
+
rest.empty? ? only : Combine::CoverageAccumulator.fold(results)
|
|
111
144
|
end
|
|
112
145
|
|
|
113
|
-
#
|
|
114
|
-
# Gets all SimpleCov::Results stored in resultset, merges them and produces a new
|
|
115
|
-
# SimpleCov::Result with merged coverage data and the command_name
|
|
116
|
-
# for the result consisting of a join on all source result's names
|
|
117
146
|
def merged_result
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
command_names, coverage
|
|
122
|
-
|
|
123
|
-
create_result(command_names, coverage)
|
|
147
|
+
tracked_files = Set.new
|
|
148
|
+
context_maps = ContextMap::Union.new
|
|
149
|
+
command_names, coverage = merge_valid_results(read_resultset, &entry_collector(tracked_files, context_maps))
|
|
150
|
+
create_result(command_names, coverage, tracked_files: tracked_files, contexts: context_maps.map)
|
|
124
151
|
end
|
|
125
152
|
|
|
126
153
|
def read_resultset
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
read_file(resultset_path)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
parse_json(resultset_content)
|
|
154
|
+
content = synchronize_resultset { ResultsetFile.read(resultset_path) }
|
|
155
|
+
ResultsetFile.decode(content)
|
|
133
156
|
end
|
|
134
157
|
|
|
135
|
-
# Saves the given SimpleCov::Result in the resultset cache
|
|
136
158
|
def store_result(result)
|
|
137
159
|
synchronize_resultset do
|
|
138
|
-
# Ensure we have the latest, in case it was already cached
|
|
139
160
|
new_resultset = read_resultset
|
|
140
161
|
|
|
141
|
-
# A single result only ever has one command_name, see `
|
|
162
|
+
# A single result only ever has one command_name, see `Result#to_hash`.
|
|
142
163
|
command_name, data = result.to_hash.first
|
|
143
|
-
new_resultset[command_name] = data
|
|
144
|
-
File.open(resultset_path, "w+") do |f_|
|
|
145
|
-
f_.puts JSON.pretty_generate(new_resultset)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
true
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Ensure only one process is reading or writing the resultset at any
|
|
152
|
-
# given time
|
|
153
|
-
def synchronize_resultset
|
|
154
|
-
# make it reentrant
|
|
155
|
-
return yield if defined?(@resultset_locked) && @resultset_locked
|
|
156
|
-
|
|
157
|
-
begin
|
|
158
|
-
@resultset_locked = true
|
|
159
|
-
File.open(resultset_writelock, "w+") do |f|
|
|
160
|
-
f.flock(File::LOCK_EX)
|
|
161
|
-
yield
|
|
162
|
-
end
|
|
163
|
-
ensure
|
|
164
|
-
@resultset_locked = false
|
|
165
|
-
end
|
|
166
|
-
end
|
|
164
|
+
new_resultset[command_name] = merged_entry(new_resultset[command_name], data)
|
|
167
165
|
|
|
168
|
-
|
|
169
|
-
# to have "old" resultsets lying around (but not too old so that they're still
|
|
170
|
-
# considered we can adapt them).
|
|
171
|
-
# See https://github.com/simplecov-ruby/simplecov/pull/824#issuecomment-576049747
|
|
172
|
-
def adapt_result(result)
|
|
173
|
-
if pre_simplecov_0_18_result?(result)
|
|
174
|
-
adapt_pre_simplecov_0_18_result(result)
|
|
175
|
-
else
|
|
176
|
-
result
|
|
166
|
+
ResultsetStore.write(new_resultset)
|
|
177
167
|
end
|
|
168
|
+
true
|
|
178
169
|
end
|
|
179
170
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
171
|
+
# If an entry with the same command_name was written AFTER our process
|
|
172
|
+
# started, a sibling test runner wrote it. Combine coverage data rather
|
|
173
|
+
# than overwriting, so an empty parent-process result doesn't clobber the
|
|
174
|
+
# subprocess's real data (#581).
|
|
175
|
+
def merged_entry(existing, incoming)
|
|
176
|
+
return incoming unless concurrent_runner_entry?(existing, incoming)
|
|
183
177
|
|
|
184
|
-
|
|
178
|
+
merged = incoming.merge(
|
|
179
|
+
"coverage" => Combine::ResultsCombiner.combine(existing.fetch("coverage"), incoming.fetch("coverage"))
|
|
180
|
+
)
|
|
181
|
+
merged = Contexts.carry(merged, existing, incoming)
|
|
182
|
+
UnloadedFiles.carry_tracked(merged, existing, incoming)
|
|
185
183
|
end
|
|
186
184
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
{"lines" => line_coverage_data}
|
|
190
|
-
end
|
|
185
|
+
def synchronize_resultset(&)
|
|
186
|
+
ResultsetStore.synchronize(&)
|
|
191
187
|
end
|
|
192
188
|
end
|
|
193
189
|
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class << self
|
|
5
|
+
# By default `collate` ignores the merge_timeout so all results in all
|
|
6
|
+
# files specified will be merged. Pass `ignore_timeout: false` to honor it.
|
|
7
|
+
#
|
|
8
|
+
# `processes:` above 1 fans the merge out across that many forked workers.
|
|
9
|
+
# The report is identical either way, not merely equivalent. The count is
|
|
10
|
+
# deliberately not clamped to the machine's core count nor gated on a
|
|
11
|
+
# minimum number of resultsets: what a collate job can afford is the
|
|
12
|
+
# caller's call. Anything below 1 is taken as 1, so an unset
|
|
13
|
+
# `SIMPLECOV_CONCURRENCY` needs no default of its own.
|
|
14
|
+
def collate(result_filenames, profile = nil, processes: ENV["SIMPLECOV_CONCURRENCY"].to_i,
|
|
15
|
+
ignore_timeout: true, &)
|
|
16
|
+
raise ArgumentError, "There are no reports to be merged" if result_filenames.empty?
|
|
17
|
+
|
|
18
|
+
initial_setup(profile, &)
|
|
19
|
+
|
|
20
|
+
current_run.result = ParallelResultMerger.merge_and_store(*result_filenames, processes: [1, processes].max,
|
|
21
|
+
ignore_timeout: ignore_timeout)
|
|
22
|
+
|
|
23
|
+
current_run.collating_result = true
|
|
24
|
+
run_exit_tasks!
|
|
25
|
+
ensure
|
|
26
|
+
current_run.collating_result = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def result
|
|
30
|
+
return current_run.result if result?
|
|
31
|
+
|
|
32
|
+
merge = merging
|
|
33
|
+
collect_own_coverage(standalone: !merge)
|
|
34
|
+
merge_own_slice if merge
|
|
35
|
+
current_run.result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def merge_own_slice
|
|
39
|
+
ResultMerger.store_result(result) if result?
|
|
40
|
+
return unless merge_finalization_owner?
|
|
41
|
+
|
|
42
|
+
wait_for_other_processes
|
|
43
|
+
current_run.result = ResultMerger.merged_result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `standalone` is true when no merge step follows, which makes this the
|
|
47
|
+
# final result: the one that reports dropped source files and injects
|
|
48
|
+
# unloaded files. When a merge does follow, both jobs belong to the merge.
|
|
49
|
+
def collect_own_coverage(standalone:)
|
|
50
|
+
return unless defined?(Coverage) && Coverage.running?
|
|
51
|
+
|
|
52
|
+
process_coverage_result(report: standalone, inject_unloaded: standalone)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filtered(files)
|
|
56
|
+
result = files.to_a
|
|
57
|
+
filters.each do |filter|
|
|
58
|
+
result = result.reject { |source_file| filter.matches?(source_file) }
|
|
59
|
+
end
|
|
60
|
+
FileList.new result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Files matched by no group fall into the implicit "Ungrouped" bucket. Any
|
|
64
|
+
# group left empty, Ungrouped included, is dropped so a profile's unused
|
|
65
|
+
# groups (#1293) don't pad the report as 100% covered.
|
|
66
|
+
def grouped(files, groups: default_groups)
|
|
67
|
+
return {} if GroupNames.validate!(groups.keys).empty?
|
|
68
|
+
|
|
69
|
+
grouped = groups.transform_values do |filter|
|
|
70
|
+
FileList.new(files.select { |source_file| filter.matches?(source_file) })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
in_group = grouped_file_set(grouped)
|
|
74
|
+
grouped[GroupNames::UNGROUPED] = FileList.new(files.reject { |source_file| in_group.include?(source_file) })
|
|
75
|
+
|
|
76
|
+
grouped.reject { |_name, group_files| group_files.empty? }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def load_profile(name)
|
|
80
|
+
profiles.load(name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear_result
|
|
84
|
+
current_run.result = nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @api private -- floored (#679) so the next run can compute drift.
|
|
88
|
+
def write_last_run(result)
|
|
89
|
+
LastRun.write(
|
|
90
|
+
result: result.coverage_statistics.transform_values { |stats| round_coverage(stats.percent) }
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private -- floored, to be extra strict.
|
|
95
|
+
def round_coverage(coverage)
|
|
96
|
+
coverage.floor(2)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @api private -- the seam `SimpleCov::ResultMerger` injects through. Public
|
|
100
|
+
# only because the merge runs in another object, on behalf of processes
|
|
101
|
+
# whose configuration it may not share, so it supplies the paths itself.
|
|
102
|
+
def inject_unloaded_files(result, candidate_paths, synthesize: nil, lines: nil)
|
|
103
|
+
return [result, Set.new] if candidate_paths.empty?
|
|
104
|
+
|
|
105
|
+
# Synthesizing branch and method tuples means parsing every tracked file
|
|
106
|
+
# that wasn't loaded, about half the cost of simulating one, and nothing
|
|
107
|
+
# reads those tuples when neither criterion is enabled. The same goes for
|
|
108
|
+
# line data, which a branch-only or method-only run neither reports nor
|
|
109
|
+
# receives from `Coverage` for the files it loaded (#1250).
|
|
110
|
+
UnloadedFileInjector.call(
|
|
111
|
+
result, candidate_paths,
|
|
112
|
+
synthesize: synthesize.nil? ? branch_coverage? || method_coverage? : synthesize,
|
|
113
|
+
lines: lines.nil? ? line_coverage? : lines
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def initial_setup(profile, &block)
|
|
120
|
+
load_profile(profile) if profile
|
|
121
|
+
configure(&block) if block
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def grouped_file_set(grouped)
|
|
125
|
+
grouped.values.each_with_object(Set.new) { |file_list, set| set.merge(file_list) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Every path this process was told to track, whether or not it loaded them.
|
|
129
|
+
# Recorded on the result so a merge in another process can inject the ones
|
|
130
|
+
# nobody loaded without needing this process's `cover` / `track_files`
|
|
131
|
+
# configuration. A standalone `collate` never ran `SimpleCov.start` (#1250).
|
|
132
|
+
def tracked_file_paths
|
|
133
|
+
paths = UnloadedFileInjector.discover(
|
|
134
|
+
unloaded_file_discovery_globs, root: root, reject: filters.select(&:path_only?)
|
|
135
|
+
)
|
|
136
|
+
templates = paths.select { |path| Directive::Template.template?(path) }
|
|
137
|
+
return paths if templates.empty?
|
|
138
|
+
|
|
139
|
+
warn_templates_tracked(templates)
|
|
140
|
+
paths - templates
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Simulating a template means classifying its lines as Ruby and parsing it
|
|
144
|
+
# for branches, both of which produce a wrong shape, whereas `cover_views`
|
|
145
|
+
# compiles it and measures the real thing.
|
|
146
|
+
def warn_templates_tracked(templates)
|
|
147
|
+
names = templates.map { |path| path.delete_prefix(root).sub(%r{\A[/\\]}, "") }
|
|
148
|
+
warn "[SimpleCov]: `cover` matched templates it cannot simulate (#{names.join(", ")}). " \
|
|
149
|
+
"Add `cover_views` to measure templates."
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The legacy `track_files` glob (additive only) plus every string glob
|
|
153
|
+
# declared via `cover` (also restrictive, but the restriction lives in
|
|
154
|
+
# `Result#apply_cover_filters!`).
|
|
155
|
+
def unloaded_file_discovery_globs
|
|
156
|
+
[tracked_files, *cover_globs].compact
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `report:` and `inject_unloaded:` are false when a merge step follows.
|
|
160
|
+
# Only the union of every process's loaded files says what was really never
|
|
161
|
+
# loaded, and the merged result is the one that reports dropped source
|
|
162
|
+
# files, so a per-process slice would warn once per worker and simulate
|
|
163
|
+
# nearly the whole project only to have it merged away (#1250).
|
|
164
|
+
def process_coverage_result(report:, inject_unloaded: true)
|
|
165
|
+
# Templates nobody rendered are compiled into the running Coverage so they
|
|
166
|
+
# arrive at 0% below rather than being absent. Needs a live Coverage and
|
|
167
|
+
# ActionView, which rules out the merge point that does the same job for
|
|
168
|
+
# unloaded `.rb` files.
|
|
169
|
+
ViewCoverage.compile_unrendered
|
|
170
|
+
raw = UselessResultsRemover.call(Coverage.result)
|
|
171
|
+
adapted = ResultAdapter.call(raw)
|
|
172
|
+
# `Result#to_hash` serializes coverage after filtering, so a file this
|
|
173
|
+
# process loaded and then filtered out would otherwise be recorded as
|
|
174
|
+
# tracked while absent from the stored coverage, and the merge would
|
|
175
|
+
# simulate it back in as never-loaded (#1250).
|
|
176
|
+
tracked = tracked_file_paths - adapted.keys
|
|
177
|
+
result, not_loaded = inject_unloaded ? inject_unloaded_files(adapted, tracked) : [adapted, Set.new]
|
|
178
|
+
current_run.result = build_result(raw, result, not_loaded: not_loaded, tracked: tracked, report: report)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def build_result(raw, coverage, not_loaded:, tracked:, report:)
|
|
182
|
+
Result.new(
|
|
183
|
+
coverage, not_loaded_files: not_loaded, tracked_files: tracked, run_id: run_id,
|
|
184
|
+
worker_id: worker_id, contexts: test_tracker&.recorded_map(closing: raw), report: report
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
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 it.
|
|
7
|
+
# Forked subprocesses inherit both values from their parent worker.
|
|
8
|
+
module RunIdentity
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# Answers the run id together with its provenance: true when the id alone
|
|
12
|
+
# proves same-run membership (explicitly configured, derived from
|
|
13
|
+
# parallel_tests' per-invocation pid file, or freshly random), false when it
|
|
14
|
+
# was inferred from the parent pid, which consecutive runs launched by the
|
|
15
|
+
# 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 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
|
+
# Decided where the id is generated, never re-inferred from the id's shape,
|
|
39
|
+
# so an explicit SIMPLECOV_RUN_ID that happens to look like an inferred one
|
|
40
|
+
# is still trusted.
|
|
41
|
+
def authoritative?
|
|
42
|
+
materialize_current
|
|
43
|
+
@authoritative
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current
|
|
47
|
+
materialize_current
|
|
48
|
+
@current
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def materialize_current
|
|
52
|
+
@current, @authoritative = generate unless instance_variable_defined?(:@current)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def current_worker_id
|
|
56
|
+
@current_worker_id ||= worker_id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prepare
|
|
60
|
+
ParallelAdapters.current
|
|
61
|
+
SimpleCov.run_id
|
|
62
|
+
SimpleCov.worker_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module Accessors
|
|
66
|
+
def run_id
|
|
67
|
+
RunIdentity.current
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def worker_id
|
|
71
|
+
RunIdentity.current_worker_id
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -1,29 +1,84 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "coverage"
|
|
4
|
+
require_relative "static_coverage_extractor"
|
|
5
|
+
|
|
3
6
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
# Responsible for producing file coverage metrics.
|
|
6
|
-
#
|
|
7
7
|
module SimulateCoverage
|
|
8
|
-
|
|
8
|
+
extend self
|
|
9
9
|
|
|
10
10
|
#
|
|
11
|
-
# Simulate
|
|
12
|
-
#
|
|
11
|
+
# Simulate a file coverage report for a file that was tracked but never
|
|
12
|
+
# required, in the same hash shape as `Coverage.result`.
|
|
13
|
+
#
|
|
14
|
+
# The line classification comes from `Coverage.line_stub`, the same
|
|
15
|
+
# classification the runtime would have produced if the file had been
|
|
16
|
+
# required, overlaid with SimpleCov's `# :nocov:` toggles and
|
|
17
|
+
# `# simplecov:disable line` directive ranges, which `Coverage` doesn't
|
|
18
|
+
# know about. This keeps "relevant lines" identical whether a file was
|
|
19
|
+
# loaded or just tracked (#654).
|
|
20
|
+
#
|
|
21
|
+
# Branches and methods are enumerated by static analysis. Earlier behavior
|
|
22
|
+
# left both as empty hashes, which made unloaded files invisible to the
|
|
23
|
+
# branch/method denominators while their lines did count, so a glob that
|
|
24
|
+
# picked up files without specs silently inflated branch% relative to
|
|
25
|
+
# line% (#1059). When the file can't be parsed, fall back to the old empty
|
|
26
|
+
# hashes.
|
|
13
27
|
#
|
|
14
|
-
#
|
|
28
|
+
# Pass `synthesize: false` to skip the static analysis. Callers use it when
|
|
29
|
+
# neither branch nor method coverage is enabled, since nothing will read
|
|
30
|
+
# the tuples and the Prism parse is about half the cost of simulating a
|
|
31
|
+
# file (#1250).
|
|
15
32
|
#
|
|
16
|
-
#
|
|
33
|
+
# Pass `lines: false` to omit the `"lines"` key entirely, mirroring what
|
|
34
|
+
# `Coverage.result` reports for a file loaded under a branch-only or
|
|
35
|
+
# method-only run. Emitting zeroed lines there would make a simulated file
|
|
36
|
+
# indistinguishable from one a sibling process actually loaded.
|
|
17
37
|
#
|
|
18
|
-
def call(absolute_path)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
def call(absolute_path, synthesize: true, lines: true)
|
|
39
|
+
source_lines = read_lines(absolute_path)
|
|
40
|
+
simulated = synthesized_tuples(source_lines, synthesize)
|
|
41
|
+
return simulated unless lines
|
|
42
|
+
|
|
43
|
+
classified = coverage_stub(absolute_path, source_lines) ||
|
|
44
|
+
LinesClassifier.new.classify(source_lines)
|
|
45
|
+
{"lines" => classified}.merge(simulated)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Empty hashes when the static analysis is skipped (nothing enabled reads
|
|
49
|
+
# them) or unavailable (no Prism, or the file doesn't parse).
|
|
50
|
+
def synthesized_tuples(source_lines, synthesize)
|
|
51
|
+
empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
|
|
52
|
+
synthesized = (StaticCoverageExtractor.call(source_lines.join) if synthesize) || empty
|
|
53
|
+
|
|
54
|
+
{"branches" => synthesized.fetch("branches"), "methods" => synthesized.fetch("methods")}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# SystemCallError, not just ENOENT: a `track_files` glob can sweep up an
|
|
58
|
+
# unreadable file or a directory named like a Ruby file, and simulation must
|
|
59
|
+
# degrade to "empty file" rather than crash the merge or report step.
|
|
60
|
+
def read_lines(path)
|
|
61
|
+
File.readlines(path)
|
|
62
|
+
rescue SystemCallError
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Combines `Coverage.line_stub` (which gets multi-line statements right)
|
|
67
|
+
# with `LinesClassifier` (which knows about `# :nocov:` toggles and
|
|
68
|
+
# `# simplecov:disable line` ranges). Answers nil when `Coverage` can't
|
|
69
|
+
# read or parse the file, or the runtime doesn't expose `line_stub`.
|
|
70
|
+
def coverage_stub(path, source_lines)
|
|
71
|
+
return nil unless Coverage.respond_to?(:line_stub)
|
|
72
|
+
|
|
73
|
+
# Paired off rather than indexed: the two lists describe the same file
|
|
74
|
+
# line for line. A line the classifier has no verdict for keeps the nil
|
|
75
|
+
# it would have been set to.
|
|
76
|
+
relevance = LinesClassifier.new.classify(source_lines)
|
|
77
|
+
Coverage.line_stub(path).zip(relevance).map { |hits, relevant| hits if relevant }
|
|
78
|
+
rescue SystemCallError, SyntaxError
|
|
79
|
+
# SystemCallError for the same reason as read_lines: line_stub reads the
|
|
80
|
+
# file itself, so EACCES/EISDIR surface here too.
|
|
81
|
+
nil
|
|
27
82
|
end
|
|
28
83
|
end
|
|
29
84
|
end
|