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,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
require_relative "color"
|
|
6
|
+
require_relative "cli/dotfile"
|
|
7
|
+
require_relative "cli/badge"
|
|
8
|
+
require_relative "cli/clean"
|
|
9
|
+
require_relative "cli/completions"
|
|
10
|
+
require_relative "cli/coverage_file"
|
|
11
|
+
require_relative "cli/coverage"
|
|
12
|
+
require_relative "cli/dead_code"
|
|
13
|
+
require_relative "cli/diff"
|
|
14
|
+
require_relative "cli/history"
|
|
15
|
+
require_relative "cli/merge"
|
|
16
|
+
require_relative "cli/open"
|
|
17
|
+
require_relative "cli/patch"
|
|
18
|
+
require_relative "cli/ratchet"
|
|
19
|
+
require_relative "cli/report"
|
|
20
|
+
require_relative "cli/run"
|
|
21
|
+
require_relative "cli/serve"
|
|
22
|
+
require_relative "cli/show"
|
|
23
|
+
require_relative "cli/status"
|
|
24
|
+
require_relative "cli/tests"
|
|
25
|
+
require_relative "cli/affected"
|
|
26
|
+
require_relative "cli/uncovered"
|
|
27
|
+
require_relative "cli/usage"
|
|
28
|
+
require_relative "cli/watch"
|
|
29
|
+
|
|
30
|
+
module SimpleCov
|
|
31
|
+
# Lightweight command-line front-end. Read-only subcommands consume
|
|
32
|
+
# JSONFormatter output, which the bundled HTMLFormatter already drops
|
|
33
|
+
# alongside the HTML, so no runtime hooking is needed for those. Default
|
|
34
|
+
# paths follow the project's `.simplecov` `coverage_dir` setting when one is
|
|
35
|
+
# present.
|
|
36
|
+
module CLI
|
|
37
|
+
COMMANDS = {
|
|
38
|
+
"coverage" => Coverage,
|
|
39
|
+
"show" => Show,
|
|
40
|
+
"run" => Run,
|
|
41
|
+
"open" => Open,
|
|
42
|
+
"report" => Report,
|
|
43
|
+
"status" => Status,
|
|
44
|
+
"history" => History,
|
|
45
|
+
"uncovered" => Uncovered,
|
|
46
|
+
"tests" => Tests,
|
|
47
|
+
"affected" => Affected,
|
|
48
|
+
"merge" => Merge,
|
|
49
|
+
"diff" => Diff,
|
|
50
|
+
"patch" => Patch,
|
|
51
|
+
"ratchet" => Ratchet,
|
|
52
|
+
"dead-code" => DeadCode,
|
|
53
|
+
"serve" => Serve,
|
|
54
|
+
"watch" => Watch,
|
|
55
|
+
"badge" => Badge,
|
|
56
|
+
"clean" => Clean,
|
|
57
|
+
"completions" => Completions
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
extend self
|
|
61
|
+
|
|
62
|
+
# Resolved once per process, by walking up from cwd for a `.simplecov` and
|
|
63
|
+
# loading it with `SimpleCov.start` neutered so it can't trigger tracking
|
|
64
|
+
# just because we asked it for a config value.
|
|
65
|
+
def coverage_dir
|
|
66
|
+
@coverage_dir ||= Dotfile.coverage_dir
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default_input
|
|
70
|
+
File.join(coverage_dir, "coverage.json")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def color_enabled?(opts, stream)
|
|
74
|
+
return false if opts[:no_color]
|
|
75
|
+
|
|
76
|
+
Color.enabled?(stream)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def default_report
|
|
80
|
+
File.join(coverage_dir, "index.html")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def default_resultset
|
|
84
|
+
File.join(coverage_dir, ".resultset.json")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run(argv, stdout: $stdout, stderr: $stderr)
|
|
88
|
+
command, *rest = argv
|
|
89
|
+
handler = COMMANDS[command]
|
|
90
|
+
return dispatch(handler, command, rest, stdout: stdout, stderr: stderr) if handler
|
|
91
|
+
return stdout.puts(usage) || 0 if [nil, "help", "--help", "-h"].include?(command)
|
|
92
|
+
return stdout.puts("simplecov #{VERSION}") || 0 if %w[version --version -v].include?(command)
|
|
93
|
+
|
|
94
|
+
stderr.puts("simplecov: unknown command #{command.inspect}", usage)
|
|
95
|
+
1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One rescue covers every subcommand's OptionParser, so a typo'd flag becomes
|
|
99
|
+
# a one-line error and exit status 1 instead of a backtrace.
|
|
100
|
+
def dispatch(handler, command, rest, stdout:, stderr:)
|
|
101
|
+
handler.run(rest, stdout: stdout, stderr: stderr)
|
|
102
|
+
rescue CommandHelpers::HelpRequested
|
|
103
|
+
stdout.puts(Usage.for(self, command))
|
|
104
|
+
0
|
|
105
|
+
rescue OptionParser::ParseError => e
|
|
106
|
+
stderr.puts("simplecov #{command}: #{e} (run `simplecov help` for usage)")
|
|
107
|
+
1
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def usage
|
|
111
|
+
Usage.text(self)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
# ANSI colorization for stderr diagnostics. Thresholds mirror the HTML
|
|
5
|
+
# formatter so a team's mental model of "what's the cutoff" is the same in
|
|
6
|
+
# the terminal and in the report.
|
|
7
|
+
module Color
|
|
8
|
+
GREEN_THRESHOLD = 90
|
|
9
|
+
YELLOW_THRESHOLD = 75
|
|
10
|
+
|
|
11
|
+
ANSI = {
|
|
12
|
+
red: "\e[31m",
|
|
13
|
+
yellow: "\e[33m",
|
|
14
|
+
green: "\e[32m",
|
|
15
|
+
reset: "\e[0m"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
extend self
|
|
19
|
+
|
|
20
|
+
# Precedence, highest first: an explicit `SimpleCov.color` true/false (the
|
|
21
|
+
# default `:auto` falls through), then `NO_COLOR`, then `FORCE_COLOR`, then
|
|
22
|
+
# `stream.tty?`. `NO_COLOR` wins over `FORCE_COLOR` when both are set.
|
|
23
|
+
#
|
|
24
|
+
# `stream` is the IO the colorized text is destined for. CLI subcommands
|
|
25
|
+
# that print to stdout should pass `$stdout` so a redirected pipe doesn't
|
|
26
|
+
# get ANSI sequences.
|
|
27
|
+
#
|
|
28
|
+
# `SimpleCov.color` only exists once the full library is loaded, and the
|
|
29
|
+
# standalone CLI loads this file without it, so a missing config reads as
|
|
30
|
+
# its `:auto` default. A parallel runner can also close a worker's stdio
|
|
31
|
+
# before its at_exit hooks run, and a closed stream is not a tty.
|
|
32
|
+
def enabled?(stream = $stderr)
|
|
33
|
+
config = SimpleCov.color if SimpleCov.respond_to?(:color)
|
|
34
|
+
return config if [true, false].include?(config)
|
|
35
|
+
return false if env_set?("NO_COLOR")
|
|
36
|
+
return true if env_set?("FORCE_COLOR")
|
|
37
|
+
|
|
38
|
+
stream.tty?
|
|
39
|
+
rescue IOError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def for_percent(percent)
|
|
44
|
+
return :green if percent >= GREEN_THRESHOLD
|
|
45
|
+
return :yellow if percent >= YELLOW_THRESHOLD
|
|
46
|
+
|
|
47
|
+
:red
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def colorize(text, color, enabled: enabled?)
|
|
51
|
+
return text unless enabled
|
|
52
|
+
|
|
53
|
+
"#{ANSI.fetch(color)}#{text}#{ANSI.fetch(:reset)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def colorize_percent(percent, text = nil, enabled: enabled?)
|
|
57
|
+
colorize(text || format("%.2f%%", percent), for_percent(percent), enabled: enabled)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def env_set?(name)
|
|
61
|
+
value = ENV.fetch(name, nil)
|
|
62
|
+
value && !value.empty?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../source_file/ruby_data_parser"
|
|
4
|
+
require_relative "identity_interner"
|
|
5
|
+
require_relative "interned_counts"
|
|
6
|
+
|
|
3
7
|
module SimpleCov
|
|
4
8
|
module Combine
|
|
5
|
-
#
|
|
6
|
-
# Combine different branch coverage results on single file.
|
|
7
|
-
#
|
|
8
|
-
# Should be called through `SimpleCov.combine`.
|
|
9
9
|
module BranchesCombiner
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
# Return merged branches or the existed brach if other is missing.
|
|
14
|
-
#
|
|
15
|
-
# Branches inside files are always same if they exist, the difference only in coverage count.
|
|
16
|
-
# Branch coverage report for any conditional case is built from hash, it's key is a condition and
|
|
17
|
-
# it's body is a hash << keys from condition and value is coverage rate >>.
|
|
18
|
-
# ex: branches =>{ [:if, 3, 8, 6, 8, 36] => {[:then, 4, 8, 6, 8, 12] => 1, [:else, 5, 8, 6, 8, 36]=>2}, other conditions...}
|
|
19
|
-
# We create copy of result and update it values depending on the combined branches coverage values.
|
|
20
|
-
#
|
|
21
|
-
# @return [Hash]
|
|
22
|
-
#
|
|
10
|
+
extend self
|
|
11
|
+
|
|
23
12
|
def combine(coverage_a, coverage_b)
|
|
24
|
-
coverage_a
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
merged = absorb({}, coverage_a) #: Hash[untyped, [untyped, Hash[untyped, untyped]]]
|
|
14
|
+
materialize(absorb(merged, coverage_b))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Folds `coverage` into `target`, an interned table keyed by condition
|
|
18
|
+
# identity. Kept interned rather than turned back into tuple keys per merge,
|
|
19
|
+
# so a fold over N resultsets interns each condition once instead of
|
|
20
|
+
# re-interning the whole accumulated table N times.
|
|
21
|
+
def absorb(target, coverage)
|
|
22
|
+
return target unless coverage
|
|
23
|
+
|
|
24
|
+
coverage.each do |condition, branches_inside|
|
|
25
|
+
entry = target[identities[condition]] ||= new_condition(condition)
|
|
26
|
+
InternedCounts.absorb_counts(entry.fetch(1), branches_inside, identities)
|
|
28
27
|
end
|
|
28
|
+
|
|
29
|
+
target
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def new_condition(condition)
|
|
33
|
+
arms = {} #: Hash[untyped, untyped]
|
|
34
|
+
[condition, arms]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Turns an interned table back into the tuple-keyed hash the rest of
|
|
38
|
+
# SimpleCov reads. Done once, at the end of a fold.
|
|
39
|
+
def materialize(target)
|
|
40
|
+
target.values.to_h { |condition, branches| [condition, branches.values.to_h] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def identities
|
|
44
|
+
@identities ||= IdentityInterner.build { |tuple| tuple_identity(tuple) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Branches match on source span, whatever ids the recording processes handed
|
|
48
|
+
# them (#1233).
|
|
49
|
+
def tuple_identity(tuple)
|
|
50
|
+
type, _id, start_line, start_column, end_line, end_column = SourceFile::RubyDataParser.call(tuple)
|
|
51
|
+
[type, start_line, start_column, end_line, end_column].freeze
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
54
|
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "branches_combiner"
|
|
4
|
+
require_relative "lines_combiner"
|
|
5
|
+
require_relative "methods_combiner"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module Combine
|
|
9
|
+
#
|
|
10
|
+
# Folds any number of resultsets' coverage into one, absorbing them one
|
|
11
|
+
# at a time so that a caller reading resultsets off disk never holds more
|
|
12
|
+
# than one in memory, and so the accumulated side can be updated in place
|
|
13
|
+
# rather than rebuilt once per resultset.
|
|
14
|
+
#
|
|
15
|
+
class CoverageAccumulator
|
|
16
|
+
# A file some process actually loaded has at least one executed line; a
|
|
17
|
+
# simulated (never-loaded) file's lines are all `nil` or `0`.
|
|
18
|
+
#
|
|
19
|
+
# `Array()` plus the `Numeric` test rather than `any?(&:positive?)`
|
|
20
|
+
# because this reads straight off a parsed resultset, which is external
|
|
21
|
+
# input and can carry anything under "lines".
|
|
22
|
+
def self.executed?(lines)
|
|
23
|
+
counts = Array(lines) #: Array[untyped]
|
|
24
|
+
counts.any? { |count| count.is_a?(Numeric) && count.positive? }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.fold(pairs)
|
|
28
|
+
accumulator = new
|
|
29
|
+
command_names = [] #: Array[String]
|
|
30
|
+
|
|
31
|
+
pairs.each do |names, coverage|
|
|
32
|
+
command_names.concat(names)
|
|
33
|
+
accumulator.absorb(coverage)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[command_names, accumulator.result]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize
|
|
40
|
+
# Whether a criterion is enabled can't change mid-fold, so read it once
|
|
41
|
+
# here rather than once per file. `branch_coverage?` reaches
|
|
42
|
+
# `Coverage.supported?`, and a large parallel run merges thousands of
|
|
43
|
+
# files.
|
|
44
|
+
@branch_coverage = SimpleCov.branch_coverage?
|
|
45
|
+
@method_coverage = SimpleCov.method_coverage?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def absorb(coverage)
|
|
49
|
+
return self unless coverage
|
|
50
|
+
|
|
51
|
+
files = (@files ||= {}) #: Hash[String, untyped]
|
|
52
|
+
coverage.each do |filename, file_coverage|
|
|
53
|
+
files[filename] = merge_file(files[filename], file_coverage)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `nil` until the first absorb, so callers can tell "no results" apart
|
|
60
|
+
# from "results that cover nothing": only the former means there is no
|
|
61
|
+
# report to build.
|
|
62
|
+
def result
|
|
63
|
+
@files&.transform_values do |entry|
|
|
64
|
+
case entry
|
|
65
|
+
when MergedFile then entry.to_h
|
|
66
|
+
else entry
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def merge_file(existing, file_coverage)
|
|
74
|
+
case existing
|
|
75
|
+
when nil then file_coverage
|
|
76
|
+
when MergedFile then existing.absorb(file_coverage)
|
|
77
|
+
else MergedFile.new(existing, branches: @branch_coverage, methods: @method_coverage).absorb(file_coverage)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# One file's accumulated coverage.
|
|
83
|
+
#
|
|
84
|
+
# A criterion is carried for this file when the data carries it, not when
|
|
85
|
+
# this process happens to measure it: `simplecov merge` never ran
|
|
86
|
+
# SimpleCov.start at all, and dropping a table it did not ask for would
|
|
87
|
+
# lose branch data the producers did measure. A nil table means nobody
|
|
88
|
+
# measured that criterion, an empty one means it was measured and the
|
|
89
|
+
# file has none.
|
|
90
|
+
#
|
|
91
|
+
class MergedFile
|
|
92
|
+
def initialize(coverage, branches:, methods:)
|
|
93
|
+
@branch_coverage = branches
|
|
94
|
+
@method_coverage = methods
|
|
95
|
+
@lines = LinesCombiner.merge_into(nil, coverage["lines"])
|
|
96
|
+
branches_table = coverage["branches"]
|
|
97
|
+
@branches = BranchesCombiner.absorb(new_table, branches_table) if branches || branches_table
|
|
98
|
+
@methods = MethodsCombiner.absorb(nil, coverage["methods"])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def absorb(coverage)
|
|
102
|
+
reconcile_synthesized(coverage)
|
|
103
|
+
@lines = LinesCombiner.merge_into(@lines, coverage["lines"])
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_h
|
|
108
|
+
merged = {"lines" => @lines} #: Hash[String, untyped]
|
|
109
|
+
merged["branches"] = BranchesCombiner.materialize(@branches) if @branches
|
|
110
|
+
merged["methods"] = MethodsCombiner.materialize(@methods) if @methods
|
|
111
|
+
merged
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def new_table
|
|
117
|
+
{} #: Hash[untyped, untyped]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# When exactly one side of the merge was actually executed, its branch
|
|
121
|
+
# and method tuples are authoritative and the other side's are dropped.
|
|
122
|
+
# SimulateCoverage synthesizes those tuples statically, so a location
|
|
123
|
+
# that drifts from what Coverage emits would otherwise be unioned in by
|
|
124
|
+
# position and survive as a phantom, permanently-missed branch (#1233).
|
|
125
|
+
# Lines are never dropped: a simulated file's line shape is correct and
|
|
126
|
+
# carries the unloaded-file denominator (#1059).
|
|
127
|
+
def reconcile_synthesized(coverage)
|
|
128
|
+
incoming_lines = coverage["lines"]
|
|
129
|
+
return absorb_tuples(coverage) unless lines_measured?(@lines) && lines_measured?(incoming_lines)
|
|
130
|
+
|
|
131
|
+
incoming_executed = executed?(incoming_lines)
|
|
132
|
+
|
|
133
|
+
if executed?(@lines).equal?(incoming_executed)
|
|
134
|
+
absorb_tuples(coverage)
|
|
135
|
+
elsif incoming_executed
|
|
136
|
+
replace_tuples(coverage)
|
|
137
|
+
else
|
|
138
|
+
drop_incoming_tuples(coverage)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def absorb_tuples(coverage)
|
|
143
|
+
branches_table = coverage["branches"]
|
|
144
|
+
@branches = BranchesCombiner.absorb(@branches || new_table, branches_table) if branches_table
|
|
145
|
+
@methods = MethodsCombiner.absorb(@methods, coverage["methods"])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def drop_incoming_tuples(coverage)
|
|
149
|
+
@methods ||= {} if @method_coverage && coverage["methods"] #: Hash[untyped, untyped]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def replace_tuples(coverage)
|
|
153
|
+
@branches = authoritative_table(BranchesCombiner, coverage["branches"], @branch_coverage)
|
|
154
|
+
@methods = authoritative_table(MethodsCombiner, coverage["methods"], @method_coverage && !@methods.nil?)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def authoritative_table(combiner, table, keep_empty)
|
|
158
|
+
return combiner.absorb(new_table, table) if table
|
|
159
|
+
|
|
160
|
+
new_table if keep_empty
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Absent lines do not mean the side never ran: a branch-only or
|
|
164
|
+
# method-only run omits them even for the files it loaded.
|
|
165
|
+
def lines_measured?(lines)
|
|
166
|
+
!Array(lines).empty?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def executed?(lines)
|
|
170
|
+
CoverageAccumulator.executed?(lines)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Combine
|
|
5
|
+
# Builds the raw-key => interned-identity caches the tuple combiners keep. A
|
|
6
|
+
# key is mapped on first sight and kept: the pairwise fold would otherwise
|
|
7
|
+
# re-derive the accumulator's identities on every one of its merges, always
|
|
8
|
+
# to the same answer.
|
|
9
|
+
#
|
|
10
|
+
# Identities exist only to be hash keys, and `Array#hash` is not memoized, so
|
|
11
|
+
# keying on the identity tuple itself would rehash its elements for every key
|
|
12
|
+
# of both sides of every merge. An Integer hashes as an immediate. Two keys
|
|
13
|
+
# share an id exactly when their identities are equal, so the string and
|
|
14
|
+
# array forms of one key still merge together.
|
|
15
|
+
module IdentityInterner
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
def build
|
|
19
|
+
ids = {} #: Hash[::Array[untyped], Integer]
|
|
20
|
+
Hash.new do |cache, key|
|
|
21
|
+
identity = yield(key)
|
|
22
|
+
cache[key] = ids[identity] ||= ids.size
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module Combine
|
|
5
|
+
# The inner merge loop the branches and methods combiners share: folds
|
|
6
|
+
# `source`'s tuple => count pairs into `target`, an interned table keyed by
|
|
7
|
+
# each tuple's merge identity and holding `[raw_key, count]` pairs. The
|
|
8
|
+
# first-seen raw key is kept for display; counts sum. `target` and its pairs
|
|
9
|
+
# are always built by this loop, so updating them in place can't reach data a
|
|
10
|
+
# caller still holds.
|
|
11
|
+
module InternedCounts
|
|
12
|
+
extend self
|
|
13
|
+
|
|
14
|
+
def absorb_counts(target, source, identities)
|
|
15
|
+
source.each do |key, count|
|
|
16
|
+
interned = identities[key]
|
|
17
|
+
entry = target[interned]
|
|
18
|
+
if entry
|
|
19
|
+
entry[1] += count
|
|
20
|
+
else
|
|
21
|
+
target[interned] = [key, count]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
target
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,41 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module Combine
|
|
5
|
-
#
|
|
6
|
-
# Combine two different lines coverage results on same file
|
|
7
|
-
#
|
|
8
|
-
# Should be called through `SimpleCov.combine`.
|
|
9
5
|
module LinesCombiner
|
|
10
|
-
|
|
6
|
+
extend self
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.map do |coverage_a_val, coverage_b_val|
|
|
16
|
-
merge_line_coverage(coverage_a_val, coverage_b_val)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Return depends on coverage in a specific line
|
|
21
|
-
#
|
|
22
|
-
# @param [Integer || nil] first_val
|
|
23
|
-
# @param [Integer || nil] second_val
|
|
24
|
-
#
|
|
25
|
-
# Logic:
|
|
8
|
+
# Folds `source` into `target` rather than building a third array, so only
|
|
9
|
+
# for a `target` the caller owns outright: a caller still holding a
|
|
10
|
+
# reference would see it change. `source` is never touched.
|
|
26
11
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
12
|
+
# Two runs of the same source file should agree on which lines are
|
|
13
|
+
# coverage-relevant (`nil` for comments, `0`+ for executable). When they
|
|
14
|
+
# don't, "relevant on either side" wins rather than masking a real `0` as
|
|
15
|
+
# `nil`, which would drop an uncovered line from the denominator:
|
|
30
16
|
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
# nil + nil = nil
|
|
18
|
+
# nil + int = int (preserves a relevant-but-uncovered 0)
|
|
19
|
+
# int + int = int (sum)
|
|
20
|
+
def merge_into(target, source)
|
|
21
|
+
return target unless source
|
|
22
|
+
return source.dup unless target
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
size = source.size
|
|
25
|
+
target.fill(nil, target.size, size - target.size)
|
|
26
|
+
sum_into(target, source, size)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The loop is written out rather than dispatching a block per element
|
|
30
|
+
# because this is the innermost loop of a merge: a 160-worker run over
|
|
31
|
+
# ~1,800 files folds tens of millions of line counts through it.
|
|
32
|
+
#
|
|
33
|
+
# Both sides go through `to_i`, which is what keeps the merge total over
|
|
34
|
+
# external input: a count a hand-edited or foreign resultset wrote as "3" or
|
|
35
|
+
# as a JSON float merges to a wrong answer instead of raising out of the
|
|
36
|
+
# middle of a merge. On the accumulated side it doubles as the nil arm.
|
|
37
|
+
def sum_into(target, source, size)
|
|
38
|
+
index = 0
|
|
39
|
+
while index < size
|
|
40
|
+
if (value = source[index])
|
|
41
|
+
target[index] = target.at(index).to_i + value.to_i
|
|
42
|
+
end
|
|
43
|
+
index += 1
|
|
39
44
|
end
|
|
45
|
+
target
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../source_file/ruby_data_parser"
|
|
4
|
+
require_relative "identity_interner"
|
|
5
|
+
require_relative "interned_counts"
|
|
6
|
+
|
|
7
|
+
module SimpleCov
|
|
8
|
+
module Combine
|
|
9
|
+
module MethodsCombiner
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
# Method coverage maps `[class, name, start_line, start_col, end_line,
|
|
13
|
+
# end_col]` keys to hit counts. Keys are matched on their source identity,
|
|
14
|
+
# the location alone, because Ruby records one entry per defined method: the
|
|
15
|
+
# same `define_method` block defined onto different classes, or under
|
|
16
|
+
# different names, in different processes arrives with different receivers
|
|
17
|
+
# for the same source method, and matching on the full key would keep both,
|
|
18
|
+
# letting a never-called copy's 0 shadow a covered method (#1234).
|
|
19
|
+
def combine(coverage_a, coverage_b)
|
|
20
|
+
merged = absorb({}, coverage_a) #: Hash[untyped, [untyped, Integer]]
|
|
21
|
+
materialize(absorb(merged, coverage_b))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A `nil` `target` stays `nil` until some resultset actually carries
|
|
25
|
+
# methods, so a merge can tell "no method data anywhere" apart from "method
|
|
26
|
+
# data that covers nothing".
|
|
27
|
+
def absorb(target, coverage)
|
|
28
|
+
return target unless coverage
|
|
29
|
+
|
|
30
|
+
target ||= {} #: Hash[untyped, [untyped, Integer]]
|
|
31
|
+
InternedCounts.absorb_counts(target, coverage, identities)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def materialize(target)
|
|
35
|
+
target.values.to_h
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def identities
|
|
39
|
+
@identities ||= IdentityInterner.build { |key| source_identity(key) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def source_identity(key)
|
|
43
|
+
_class_name, _method_name, *location = SourceFile::RubyDataParser.call(key)
|
|
44
|
+
location.freeze
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "coverage_accumulator"
|
|
4
|
+
|
|
3
5
|
module SimpleCov
|
|
4
6
|
module Combine
|
|
5
|
-
# There might be reports from different kinds of tests,
|
|
6
|
-
# e.g. RSpec and Cucumber. We need to combine their results
|
|
7
|
-
# into unified one. This class does that.
|
|
8
|
-
# To unite the results on file basis, it leverages
|
|
9
|
-
# the combine of lines and branches inside each file within given results.
|
|
10
7
|
module ResultsCombiner
|
|
11
|
-
|
|
8
|
+
extend self
|
|
12
9
|
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# ==> FileCombiner: collect result of next combine levels lines and branches.
|
|
17
|
-
# ===> LinesCombiner: combine lines results.
|
|
18
|
-
# ===> BranchesCombiner: combine branches results.
|
|
19
|
-
#
|
|
20
|
-
# @return [Hash]
|
|
21
|
-
#
|
|
10
|
+
# Callers that read their results one at a time, to keep a big CI run's
|
|
11
|
+
# resultsets out of memory at once, should drive a `CoverageAccumulator`
|
|
12
|
+
# directly instead of collecting the results to pass here.
|
|
22
13
|
def combine(*results)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
# Manage combining results on files level
|
|
30
|
-
#
|
|
31
|
-
# @param [Hash] combined_results
|
|
32
|
-
# @param [Hash] result
|
|
33
|
-
#
|
|
34
|
-
# @return [Hash]
|
|
35
|
-
#
|
|
36
|
-
def combine_result_sets(combined_results, result)
|
|
37
|
-
results_files = combined_results.keys | result.keys
|
|
38
|
-
|
|
39
|
-
results_files.each_with_object({}) do |file_name, file_combination|
|
|
40
|
-
file_combination[file_name] = combine_file_coverage(
|
|
41
|
-
combined_results[file_name],
|
|
42
|
-
result[file_name]
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
# Combine two files coverage results
|
|
49
|
-
#
|
|
50
|
-
# @param [Hash] coverage_a
|
|
51
|
-
# @param [Hash] coverage_b
|
|
52
|
-
#
|
|
53
|
-
# @return [Hash]
|
|
54
|
-
#
|
|
55
|
-
def combine_file_coverage(coverage_a, coverage_b)
|
|
56
|
-
Combine.combine(Combine::FilesCombiner, coverage_a, coverage_b)
|
|
14
|
+
accumulator = CoverageAccumulator.new
|
|
15
|
+
results.each { |result| accumulator.absorb(result) }
|
|
16
|
+
accumulator.result || {}
|
|
57
17
|
end
|
|
58
18
|
end
|
|
59
19
|
end
|