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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "atomic_file"
|
|
7
|
+
|
|
8
|
+
module SimpleCov
|
|
9
|
+
# A bounded record of past runs, coverage/.history.json (#1267): one entry
|
|
10
|
+
# per reported run carrying the per-criterion totals, the per-group and
|
|
11
|
+
# per-file percentages, a timestamp, and the branch and commit when the
|
|
12
|
+
# project is a git checkout. It is what turns "did the number move since the
|
|
13
|
+
# very last run" into direction of travel.
|
|
14
|
+
#
|
|
15
|
+
# Entries are capped and written atomically, and a corrupt or foreign file
|
|
16
|
+
# degrades to an empty history with a warning rather than crashing the
|
|
17
|
+
# at_exit hook, the same tolerance `.last_run.json` has. The file is
|
|
18
|
+
# deliberately plain committable JSON.
|
|
19
|
+
module History
|
|
20
|
+
ENVELOPE = "simplecov_history"
|
|
21
|
+
FORMAT_VERSION = 1
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def history_path
|
|
25
|
+
File.join(SimpleCov.coverage_path, ".history.json")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record(result)
|
|
29
|
+
limit = SimpleCov.history_limit
|
|
30
|
+
return if limit.zero?
|
|
31
|
+
|
|
32
|
+
entries = read
|
|
33
|
+
entries << entry_for(result)
|
|
34
|
+
write(entries.last(limit))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read
|
|
38
|
+
return [] unless File.exist?(history_path)
|
|
39
|
+
|
|
40
|
+
content = File.read(history_path)
|
|
41
|
+
return [] if content.match?(/\A\s*\z/)
|
|
42
|
+
|
|
43
|
+
entries = JSON.parse(content).dig(ENVELOPE, "entries")
|
|
44
|
+
entries.instance_of?(Array) ? entries : invalid_history
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
invalid_history
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The recorded entries plus `result` as the newest, capped, and without
|
|
50
|
+
# writing anything: what the report artifacts embed, so the embedded history
|
|
51
|
+
# includes the run being reported even though recording happens later.
|
|
52
|
+
def entries_with(result)
|
|
53
|
+
(read << entry_for(result)).last(SimpleCov.history_limit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A detached HEAD reports its branch as nil rather than the literal "HEAD"
|
|
57
|
+
# most CI checkouts would record.
|
|
58
|
+
def git_info
|
|
59
|
+
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
|
60
|
+
branch = nil if branch.eql?("HEAD")
|
|
61
|
+
[branch, git("rev-parse", "HEAD")]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def entry_for(result)
|
|
67
|
+
branch, commit = git_info
|
|
68
|
+
{
|
|
69
|
+
# getutc, not utc: the destructive variant would rezone the Time the result
|
|
70
|
+
# still holds.
|
|
71
|
+
"created_at" => result.created_at.getutc.iso8601,
|
|
72
|
+
"branch" => branch,
|
|
73
|
+
"commit" => commit,
|
|
74
|
+
"totals" => measured_percents(result),
|
|
75
|
+
"groups" => result.groups.transform_values { |files| measured_percents(files) },
|
|
76
|
+
"files" => files_percents(result)
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def measured_percents(stats_source)
|
|
81
|
+
stats_source.coverage_statistics.to_h do |criterion, stats|
|
|
82
|
+
[criterion.to_s, SimpleCov.round_coverage(stats.percent)]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def files_percents(result)
|
|
87
|
+
measured = result.coverage_statistics
|
|
88
|
+
initial = {} #: Hash[String, Hash[String, Numeric]]
|
|
89
|
+
result.files.each_with_object(initial) do |file, percents|
|
|
90
|
+
stats = file.coverage_statistics
|
|
91
|
+
percents[file.project_filename] = measured.to_h do |criterion, _run_total|
|
|
92
|
+
[criterion.to_s, SimpleCov.round_coverage(stats.fetch(criterion).percent)]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def write(entries)
|
|
98
|
+
payload = {ENVELOPE => {"format_version" => FORMAT_VERSION, "entries" => entries}}
|
|
99
|
+
AtomicFile.write(history_path, "#{JSON.pretty_generate(payload)}\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def invalid_history
|
|
103
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of .history.json failed, starting a fresh history"
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def git(*)
|
|
108
|
+
output, status = Open3.capture2e("git", "-C", SimpleCov.root.to_s, *)
|
|
109
|
+
output.rstrip if status.success?
|
|
110
|
+
rescue
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/simplecov/last_run.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "atomic_file"
|
|
4
5
|
|
|
5
6
|
module SimpleCov
|
|
6
7
|
module LastRun
|
|
@@ -9,19 +10,29 @@ module SimpleCov
|
|
|
9
10
|
File.join(SimpleCov.coverage_path, ".last_run.json")
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
# The maximum_coverage_drop check digs into a Hash, so anything else in a
|
|
14
|
+
# corrupt or hand-edited file counts as "no previous run" rather than
|
|
15
|
+
# crashing the at_exit hook.
|
|
12
16
|
def read
|
|
13
17
|
return nil unless File.exist?(last_run_path)
|
|
14
18
|
|
|
15
19
|
json = File.read(last_run_path)
|
|
16
|
-
return nil if json.
|
|
20
|
+
return nil if json.match?(/\A\s*\z/)
|
|
17
21
|
|
|
18
|
-
JSON.parse(json, symbolize_names: true)
|
|
22
|
+
parsed = JSON.parse(json, symbolize_names: true)
|
|
23
|
+
parsed.instance_of?(Hash) ? parsed : invalid_last_run
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
invalid_last_run
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
def write(json)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
AtomicFile.write(last_run_path, "#{JSON.pretty_generate(json)}\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def invalid_last_run
|
|
35
|
+
warn "[SimpleCov]: Warning! Parsing JSON content of .last_run.json failed, ignoring the previous run"
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
|
@@ -1,47 +1,95 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# Classifies whether lines are relevant for code coverage analysis.
|
|
5
|
-
# Comments & whitespace lines, and :nocov: token blocks, are considered not relevant.
|
|
3
|
+
require_relative "directive"
|
|
6
4
|
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Classifies whether lines are relevant for code coverage analysis. Comments,
|
|
7
|
+
# whitespace lines, and disabled blocks are considered not relevant.
|
|
7
8
|
class LinesClassifier
|
|
8
9
|
RELEVANT = 0
|
|
9
10
|
NOT_RELEVANT = nil
|
|
10
11
|
|
|
11
|
-
WHITESPACE_LINE = /^\s
|
|
12
|
-
COMMENT_LINE = /^\s
|
|
12
|
+
WHITESPACE_LINE = /^\s*$/
|
|
13
|
+
COMMENT_LINE = /^\s*#/
|
|
13
14
|
WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
|
|
14
15
|
|
|
16
|
+
# The leading `^(\s*)#` anchor is load-bearing beyond matching the marker:
|
|
17
|
+
# `classify_line` only reaches this for lines that already passed
|
|
18
|
+
# `whitespace_line?`, which is sound only while every marker is also a
|
|
19
|
+
# comment. Loosening this to match a trailing `x = 1 # :nocov:` would stop
|
|
20
|
+
# the toggle firing. `lines_classifier_spec.rb` pins the implication.
|
|
21
|
+
#
|
|
22
|
+
# The `/o` flag freezes the interpolated nocov token at this
|
|
23
|
+
# process's first classification; a `nocov_token` configured after
|
|
24
|
+
# coverage has classified a file would be silently ignored. Sound
|
|
25
|
+
# today because configuration always precedes classification.
|
|
15
26
|
def self.no_cov_line
|
|
16
|
-
/^(\s*)#(\s*)(:#{SimpleCov.
|
|
27
|
+
/^(\s*)#(\s*)(:#{SimpleCov.current_nocov_token}:)/o
|
|
17
28
|
end
|
|
18
29
|
|
|
30
|
+
# A marker is itself a comment, so the cheap whitespace-or-comment test gates
|
|
31
|
+
# the token match: a line of real code cannot be a marker, and no longer pays
|
|
32
|
+
# to be checked against one. That matters because this runs per line of every
|
|
33
|
+
# tracked-but-unloaded file, once in every process of a parallel run.
|
|
19
34
|
def self.no_cov_line?(line)
|
|
20
35
|
no_cov_line.match?(line)
|
|
21
36
|
rescue ArgumentError
|
|
22
|
-
# E.g., line contains an invalid byte sequence in UTF-8
|
|
23
37
|
false
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
def self.whitespace_line?(line)
|
|
27
41
|
WHITESPACE_OR_COMMENT_LINE.match?(line)
|
|
28
42
|
rescue ArgumentError
|
|
29
|
-
# E.g., line contains an invalid byte sequence in UTF-8
|
|
30
43
|
false
|
|
31
44
|
end
|
|
32
45
|
|
|
46
|
+
class SkipState
|
|
47
|
+
def initialize
|
|
48
|
+
@skipping = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def skipping?
|
|
52
|
+
@skipping
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def toggle
|
|
56
|
+
@skipping = !@skipping
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The skip state lives in a one-slot box owned by this call rather than in an
|
|
61
|
+
# ivar, so one classifier instance can serve concurrent `classify` calls
|
|
62
|
+
# without the toggles interleaving.
|
|
33
63
|
def classify(lines)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
lines = lines.to_a
|
|
65
|
+
directive_disabled = directive_disabled_line_set(lines)
|
|
66
|
+
skip_state = SkipState.new
|
|
67
|
+
|
|
68
|
+
lines.map.with_index(1) do |line, line_number|
|
|
69
|
+
classify_line(line, line_number, directive_disabled, skip_state)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# mutant:disable — `NOT_RELEVANT` is nil, so naming it and writing
|
|
76
|
+
# the literal are the same value, and so is an `unless` that falls
|
|
77
|
+
# off its end. The name is kept because it says which of the two
|
|
78
|
+
# verdicts a line got, which nil alone does not.
|
|
79
|
+
def classify_line(line, line_number, directive_disabled, skip_state)
|
|
80
|
+
if self.class.whitespace_line?(line)
|
|
81
|
+
skip_state.toggle if self.class.no_cov_line?(line)
|
|
82
|
+
NOT_RELEVANT
|
|
83
|
+
elsif skip_state.skipping? || directive_disabled.include?(line_number)
|
|
84
|
+
NOT_RELEVANT
|
|
85
|
+
else
|
|
86
|
+
RELEVANT
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def directive_disabled_line_set(lines)
|
|
91
|
+
Directive.disabled_ranges(lines).fetch(:line).each_with_object(Set.new) do |range, set|
|
|
92
|
+
range.each { |line_number| set.add(line_number) }
|
|
45
93
|
end
|
|
46
94
|
end
|
|
47
95
|
end
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
# `~/.simplecov` was historically resolved through a three-step fallback chain
|
|
4
|
+
# for hostile container environments circa 2017. Modern CRuby and JRuby both set
|
|
5
|
+
# HOME reliably, so trust it and skip silently when it isn't there. An ENV check
|
|
6
|
+
# rather than `Dir.home`: JRuby raises from `Dir.home` with HOME unset, and
|
|
7
|
+
# `File.expand_path("~")` raises for a set-but-empty HOME, which must not break
|
|
8
|
+
# `require "simplecov"`.
|
|
9
|
+
|
|
10
|
+
unless ENV.fetch("HOME", "").empty?
|
|
11
|
+
# simplecov:disable — only fires when ~/.simplecov exists, which is
|
|
12
|
+
# developer-machine-dependent (we can't rely on it for the dogfood).
|
|
13
|
+
global_config_path = File.join(File.expand_path("~"), ".simplecov")
|
|
7
14
|
load global_config_path if File.exist?(global_config_path)
|
|
15
|
+
# simplecov:enable
|
|
8
16
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ParallelAdapters
|
|
5
|
+
# Default no-op implementations for a parallel-test-runner adapter.
|
|
6
|
+
# Everything not overridden falls back to behaving like a single-process
|
|
7
|
+
# run. Adapters are classes, used as singletons and never instantiated.
|
|
8
|
+
class Base
|
|
9
|
+
class << self
|
|
10
|
+
def active?
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def first_worker?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def wait_for_siblings
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Whether `wait_for_siblings` blocks until every sibling process has exited,
|
|
22
|
+
# so no further resultset can appear. When true, the reporting worker can
|
|
23
|
+
# accept a settled resultset count below `expected_worker_count` as final
|
|
24
|
+
# instead of waiting out the whole `parallel_wait_timeout`.
|
|
25
|
+
def native_wait?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def expected_worker_count
|
|
30
|
+
1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# mutant:disable — `false` is a singleton, so comparing against
|
|
34
|
+
# it answers the same through ==, eql? and equal?, and no
|
|
35
|
+
# example can tell the three apart.
|
|
36
|
+
def forced_off?
|
|
37
|
+
SimpleCov.parallel_tests == false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Unset, empty, non-numeric, and non-positive values all mean 1: an
|
|
41
|
+
# unparseable value must not yield 0 workers, which would end the sibling
|
|
42
|
+
# wait before it started.
|
|
43
|
+
def parallel_test_groups_count
|
|
44
|
+
count = Integer(ENV.fetch("PARALLEL_TEST_GROUPS", nil), exception: false)
|
|
45
|
+
count&.positive? ? count : 1
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ParallelAdapters
|
|
7
|
+
# Catch-all adapter for parallel test runners that follow the
|
|
8
|
+
# `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var convention but ship no
|
|
9
|
+
# Ruby API for SimpleCov to hook. `wait_for_siblings` is inherited from Base
|
|
10
|
+
# as a no-op: without a runner-provided API the only synchronization
|
|
11
|
+
# available is polling the resultset cache.
|
|
12
|
+
class GenericAdapter < Base
|
|
13
|
+
class << self
|
|
14
|
+
def active?
|
|
15
|
+
return false if forced_off?
|
|
16
|
+
|
|
17
|
+
ENV.key?("TEST_ENV_NUMBER")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# parallel_tests sets the first worker's TEST_ENV_NUMBER to "", and runners
|
|
21
|
+
# that number from 1 use "1". Both shapes match; any other value is a
|
|
22
|
+
# non-first worker.
|
|
23
|
+
def first_worker?
|
|
24
|
+
["", "1"].include?(ENV.fetch("TEST_ENV_NUMBER", nil))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def expected_worker_count
|
|
28
|
+
parallel_test_groups_count
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
module ParallelAdapters
|
|
7
|
+
# Adapter for grosser/parallel_tests, the historical default and still the
|
|
8
|
+
# most precise option for projects on it. Detection requires the full native
|
|
9
|
+
# coordination contract: the `ParallelTests` constant loaded,
|
|
10
|
+
# `TEST_ENV_NUMBER` set, and `PARALLEL_PID_FILE` set, because the native
|
|
11
|
+
# wait API reads the pid-file path with `ENV.fetch`. A runner that only
|
|
12
|
+
# provides the env-var convention belongs to GenericAdapter.
|
|
13
|
+
class ParallelTestsAdapter < Base
|
|
14
|
+
class << self
|
|
15
|
+
def active?
|
|
16
|
+
return false if forced_off?
|
|
17
|
+
|
|
18
|
+
ensure_loaded
|
|
19
|
+
!!(defined?(::ParallelTests) && native_parallel_tests_environment?)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The first started process, not the last. The parallel_tests README
|
|
23
|
+
# recommends `first_process?` for "do something once after every worker
|
|
24
|
+
# finishes" hooks, so user code with its own
|
|
25
|
+
# `wait_for_other_processes_to_finish` overwhelmingly waits in the first
|
|
26
|
+
# process, and picking the same side avoids the cross-process deadlock #922
|
|
27
|
+
# reported.
|
|
28
|
+
def first_worker?
|
|
29
|
+
ParallelTests.first_process?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def wait_for_siblings
|
|
33
|
+
return unless native_parallel_tests_environment?
|
|
34
|
+
|
|
35
|
+
ParallelTests.wait_for_other_processes_to_finish
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def native_wait?
|
|
39
|
+
native_parallel_tests_environment?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def expected_worker_count
|
|
43
|
+
parallel_test_groups_count
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# parallel_tests is an optional dependency, and `TEST_ENV_NUMBER` /
|
|
47
|
+
# `PARALLEL_TEST_GROUPS` are commonly set for other reasons, so a missing
|
|
48
|
+
# gem is treated as "user isn't using parallel_tests": stay quiet and let
|
|
49
|
+
# GenericAdapter handle it. Warning here regressed users who use those env
|
|
50
|
+
# vars for their own subprocess coordination (#1018).
|
|
51
|
+
def ensure_loaded
|
|
52
|
+
return if defined?(::ParallelTests)
|
|
53
|
+
return if forced_off?
|
|
54
|
+
return unless SimpleCov.parallel_tests || env_suggests_parallel_tests?
|
|
55
|
+
|
|
56
|
+
# simplecov:disable — only fires under a real parallel_tests setup
|
|
57
|
+
require "parallel_tests"
|
|
58
|
+
rescue LoadError
|
|
59
|
+
# simplecov:enable
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def env_suggests_parallel_tests?
|
|
63
|
+
ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def native_parallel_tests_environment?
|
|
67
|
+
ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_PID_FILE")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parallel_adapters/base"
|
|
4
|
+
require_relative "parallel_adapters/parallel_tests"
|
|
5
|
+
require_relative "parallel_adapters/generic"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
# Registry and selection for parallel-test-runner adapters. An adapter
|
|
9
|
+
# answers a small fixed set of questions on SimpleCov's behalf: `active?`,
|
|
10
|
+
# `first_worker?`, `wait_for_siblings`, and `expected_worker_count`.
|
|
11
|
+
#
|
|
12
|
+
# `Base` provides safe no-op defaults. `ParallelTestsAdapter` wraps the
|
|
13
|
+
# grosser/parallel_tests gem; `GenericAdapter` is env-var-only detection for
|
|
14
|
+
# runners that follow the `TEST_ENV_NUMBER` convention without shipping a
|
|
15
|
+
# Ruby API (#1065, #1156).
|
|
16
|
+
#
|
|
17
|
+
# Users register their own with `ParallelAdapters.register MyAdapter`.
|
|
18
|
+
# Subclass `Base` to inherit the no-op defaults: the contract methods are
|
|
19
|
+
# class methods, so plain inheritance is what carries them through and
|
|
20
|
+
# `extend Base` won't pick them up.
|
|
21
|
+
module ParallelAdapters
|
|
22
|
+
extend self
|
|
23
|
+
|
|
24
|
+
# ParallelTestsAdapter first (most specific: it uses the gem's own API when
|
|
25
|
+
# the gem is loaded), then GenericAdapter as the env-var fallback.
|
|
26
|
+
# User-registered adapters are prepended.
|
|
27
|
+
def adapters
|
|
28
|
+
@adapters ||= [ParallelTestsAdapter, GenericAdapter]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Newly registered adapters are inserted at the front of the selection
|
|
32
|
+
# list, so a custom adapter for a specific runner takes precedence over the
|
|
33
|
+
# built-ins.
|
|
34
|
+
def register(adapter)
|
|
35
|
+
reset_current!
|
|
36
|
+
adapters.unshift(adapter) unless adapters.include?(adapter)
|
|
37
|
+
adapter
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The first registered adapter whose `active?` answers true, or nil when
|
|
41
|
+
# none is, in which case the caller treats the process as single-worker.
|
|
42
|
+
def current
|
|
43
|
+
return @current if instance_variable_defined?(:@current)
|
|
44
|
+
|
|
45
|
+
@current = adapters.find(&:active?)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Primarily for tests that mutate env vars between examples; production
|
|
49
|
+
# runs are single-shot.
|
|
50
|
+
def reset_current!
|
|
51
|
+
remove_instance_variable(:@current) if instance_variable_defined?(:@current)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# simplecov:disable
|
|
4
|
+
# Methods below only fire under a parallel test runner; not reachable
|
|
5
|
+
# from a single-process rspec run. Cucumber's test_projects exercise
|
|
6
|
+
# the parallel_tests integration end-to-end in subprocesses, but
|
|
7
|
+
# those subprocesses don't merge their Coverage data back into the
|
|
8
|
+
# parent this dogfood report measures.
|
|
9
|
+
module SimpleCov
|
|
10
|
+
class << self
|
|
11
|
+
# @api private
|
|
12
|
+
def final_result_process?
|
|
13
|
+
adapter = ParallelAdapters.current
|
|
14
|
+
# A subprocess forked while coverage was running is never the final
|
|
15
|
+
# reporter: the process that spawned it merges every slice and produces
|
|
16
|
+
# the report. Without this, fork-based runners that don't set
|
|
17
|
+
# TEST_ENV_NUMBER (Minitest's `parallelize`) have every worker produce
|
|
18
|
+
# the final report and its warnings (#1171).
|
|
19
|
+
return !forked_subprocess? unless adapter
|
|
20
|
+
|
|
21
|
+
adapter.first_worker?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @api private
|
|
25
|
+
def wait_for_other_processes
|
|
26
|
+
adapter = ParallelAdapters.current
|
|
27
|
+
return unless adapter && final_result_process?
|
|
28
|
+
|
|
29
|
+
# Adapters that wrap a runner with a real "wait" primitive implement
|
|
30
|
+
# this; the rest no-op and rely on the polling fallback below.
|
|
31
|
+
adapter.wait_for_siblings
|
|
32
|
+
|
|
33
|
+
# The native wait can return before sibling at_exit handlers finish
|
|
34
|
+
# writing resultsets, and adapters without a native wait have nothing
|
|
35
|
+
# else. Either way, poll the resultset cache until all expected workers
|
|
36
|
+
# have reported or a timeout is reached. The outcome is captured so
|
|
37
|
+
# threshold checks can be suppressed against a partial total.
|
|
38
|
+
@parallel_results_complete =
|
|
39
|
+
wait_for_parallel_results(adapter.expected_worker_count, native_wait: adapter.native_wait?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @api private -- true when every sibling reported its resultset before the
|
|
43
|
+
# wait deadline. Defaults to true outside a parallel run.
|
|
44
|
+
def parallel_results_complete?
|
|
45
|
+
instance_variable_defined?(:@parallel_results_complete) ? @parallel_results_complete : true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private -- seconds the resultset count must hold steady, after a
|
|
49
|
+
# native wait, before we accept fewer than `expected` workers as final.
|
|
50
|
+
PARALLEL_RESULTS_SETTLE = 0.5
|
|
51
|
+
private_constant :PARALLEL_RESULTS_SETTLE
|
|
52
|
+
|
|
53
|
+
# @api private -- true when the reporting worker has every resultset it's
|
|
54
|
+
# going to get, false on timeout. Single-process runs short-circuit.
|
|
55
|
+
#
|
|
56
|
+
# Normally we poll until `expected` workers have reported or
|
|
57
|
+
# `SimpleCov.parallel_wait_timeout` elapses. When a native wait already
|
|
58
|
+
# confirmed every sibling process exited, no further resultset will appear,
|
|
59
|
+
# so a count below `expected` just means some workers produced none, and
|
|
60
|
+
# once the count holds steady for `PARALLEL_RESULTS_SETTLE` we accept it as
|
|
61
|
+
# final. Without a native wait we can't tell an idle worker from a slow
|
|
62
|
+
# one, so we keep waiting the full timeout.
|
|
63
|
+
def wait_for_parallel_results(expected, native_wait: false)
|
|
64
|
+
return true unless expected > 1 # simplecov:disable branch — only false in real parallel runs
|
|
65
|
+
|
|
66
|
+
deadline = monotonic_time + parallel_wait_timeout
|
|
67
|
+
tracker = {count: 0, since: monotonic_time}
|
|
68
|
+
loop do
|
|
69
|
+
seen = current_parallel_worker_count
|
|
70
|
+
return true if seen >= expected
|
|
71
|
+
return true if native_wait && resultset_count_settled?(tracker, seen)
|
|
72
|
+
return false if parallel_wait_timed_out?(deadline, expected, seen)
|
|
73
|
+
|
|
74
|
+
sleep 0.1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def current_parallel_worker_count
|
|
79
|
+
resultset = ResultMerger.read_resultset
|
|
80
|
+
ResultMerger.worker_identities_for_run(resultset, run_id, process_start_time).size
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# `tracker` carries the last count and the time it last changed across poll
|
|
84
|
+
# iterations. Answered through an if/else rather than an early return
|
|
85
|
+
# because falling through after a climb would read the clock a second time
|
|
86
|
+
# and answer the same `false` anyway.
|
|
87
|
+
def resultset_count_settled?(tracker, count)
|
|
88
|
+
if count > tracker.fetch(:count)
|
|
89
|
+
tracker[:count] = count
|
|
90
|
+
tracker[:since] = monotonic_time
|
|
91
|
+
false
|
|
92
|
+
else
|
|
93
|
+
count.positive? && (monotonic_time - tracker.fetch(:since)) >= PARALLEL_RESULTS_SETTLE
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def monotonic_time
|
|
98
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @api private -- warns on the first timeout so the user knows the merged
|
|
102
|
+
# total is partial.
|
|
103
|
+
def parallel_wait_timed_out?(deadline, expected, seen)
|
|
104
|
+
return false unless monotonic_time > deadline
|
|
105
|
+
|
|
106
|
+
warn_about_incomplete_parallel_results(expected, seen)
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @api private
|
|
111
|
+
def warn_about_incomplete_parallel_results(expected, seen)
|
|
112
|
+
return unless print_errors
|
|
113
|
+
|
|
114
|
+
warn Color.colorize(
|
|
115
|
+
"Only #{seen} of #{expected} parallel-test workers reported within " \
|
|
116
|
+
"#{parallel_wait_timeout}s, so coverage totals are partial and minimum / " \
|
|
117
|
+
"maximum coverage checks are skipped for this run. Increase " \
|
|
118
|
+
"SimpleCov.parallel_wait_timeout if a worker routinely needs longer.",
|
|
119
|
+
:yellow
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
# simplecov:enable
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ParallelResultMerger
|
|
5
|
+
# What a worker ships back over its pipe: the `[command_names, coverage]` pair
|
|
6
|
+
# its slice folded to, the tracked paths the slice carried, and the slice's
|
|
7
|
+
# context-map union. Building and folding live together so the pipe format
|
|
8
|
+
# has one owner. The parent needs the union across every worker of the
|
|
9
|
+
# tracked paths, to know what nothing loaded, and of the maps, to judge the
|
|
10
|
+
# all-or-nothing rule over the whole run rather than per slice.
|
|
11
|
+
module WorkerPayload
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def build(chunk, ignore_timeout:)
|
|
15
|
+
tracked_files = Set.new
|
|
16
|
+
context_maps = ContextMap::Union.new
|
|
17
|
+
pair = ResultMerger.absorb_results(chunk, ignore_timeout: ignore_timeout,
|
|
18
|
+
&ResultMerger.entry_collector(tracked_files, context_maps))
|
|
19
|
+
[pair, tracked_files.to_a, context_maps]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def absorb(payload, tracked_files, context_maps)
|
|
23
|
+
_pair, tracked, maps = payload
|
|
24
|
+
tracked_files.merge(tracked)
|
|
25
|
+
context_maps.absorb_union(maps)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pair(payload)
|
|
29
|
+
payload.first
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|