simplecov 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +165 -57
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- 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/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- 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 +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- 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 +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- 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 +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -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 +43 -92
- data/lib/simplecov/color.rb +21 -35
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- 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 +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- 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 +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- 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 +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +17 -7
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +38 -51
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -18
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -53
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- 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/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +58 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +35 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- 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 +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- 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 +1 -0
- data/lib/simplecov.rb +59 -114
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -7
- data/schemas/coverage-v1.0.schema.json +0 -306
- data/schemas/coverage.schema.json +0 -306
|
@@ -3,82 +3,81 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
|
-
#
|
|
7
|
-
# Bundles the configuration options used for SimpleCov. All methods
|
|
8
|
-
# defined here are usable from SimpleCov directly. Please check out
|
|
9
|
-
# SimpleCov documentation for further info.
|
|
10
|
-
#
|
|
11
6
|
module Configuration
|
|
12
|
-
#
|
|
13
|
-
# The root for the project. This defaults to the
|
|
14
|
-
# current working directory.
|
|
15
|
-
#
|
|
16
|
-
# Configure with SimpleCov.root('/my/project/path')
|
|
17
|
-
#
|
|
18
7
|
def root(root = nil)
|
|
19
|
-
return @root if
|
|
8
|
+
return @root if instance_variable_defined?(:@root) && root.nil?
|
|
20
9
|
|
|
10
|
+
self.root = root
|
|
11
|
+
@root
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# nil means the working directory, the default the reader would have derived.
|
|
15
|
+
def root=(root)
|
|
21
16
|
@coverage_path = nil unless @coverage_path_explicit # invalidate cache
|
|
22
17
|
@root = File.expand_path(root || Dir.getwd)
|
|
23
18
|
end
|
|
24
19
|
|
|
25
|
-
#
|
|
26
|
-
# The name of the output and cache directory. Defaults to 'coverage'
|
|
27
|
-
#
|
|
28
|
-
# Configure with SimpleCov.coverage_dir('cov')
|
|
29
|
-
#
|
|
30
20
|
def coverage_dir(dir = nil)
|
|
31
|
-
return @coverage_dir if
|
|
21
|
+
return @coverage_dir if instance_variable_defined?(:@coverage_dir) && dir.nil?
|
|
22
|
+
|
|
23
|
+
self.coverage_dir = dir
|
|
24
|
+
@coverage_dir
|
|
25
|
+
end
|
|
32
26
|
|
|
27
|
+
# nil means the default 'coverage', recorded as a derivation rather than a
|
|
28
|
+
# choice.
|
|
29
|
+
def coverage_dir=(dir)
|
|
33
30
|
@coverage_path = nil unless @coverage_path_explicit # invalidate cache
|
|
34
31
|
@coverage_dir_explicit = true unless dir.nil?
|
|
35
32
|
@coverage_dir = dir || "coverage"
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# out-of-tree build directories. See #716.
|
|
36
|
+
# Defaults to `SimpleCov.root` + `SimpleCov.coverage_dir`, but callers can
|
|
37
|
+
# override with an arbitrary absolute path, handy for out-of-tree build
|
|
38
|
+
# directories (#716).
|
|
43
39
|
#
|
|
44
|
-
# Reading is pure: the directory is only created when a path is
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# themselves, so read-only CLI subcommands that interpolate the
|
|
49
|
-
# path into status text don't materialize a stray `coverage/`
|
|
50
|
-
# directory.
|
|
40
|
+
# Reading is pure: the directory is only created when a path is explicitly
|
|
41
|
+
# assigned. The codepaths that write into it ensure existence themselves,
|
|
42
|
+
# so read-only CLI subcommands that interpolate the path into status text
|
|
43
|
+
# don't materialize a stray `coverage/` directory.
|
|
51
44
|
#
|
|
52
45
|
def coverage_path(path = nil)
|
|
53
|
-
if path
|
|
54
|
-
expanded = File.expand_path(path)
|
|
55
|
-
@coverage_path = expanded
|
|
56
|
-
@coverage_path_explicit = true
|
|
57
|
-
FileUtils.mkdir_p expanded
|
|
58
|
-
end
|
|
46
|
+
self.coverage_path = path if path
|
|
59
47
|
|
|
60
48
|
@coverage_path ||= File.expand_path(coverage_dir, root)
|
|
61
49
|
end
|
|
62
50
|
|
|
51
|
+
# Assigning is the signal the caller intends to write there, so the
|
|
52
|
+
# directory is created.
|
|
53
|
+
def coverage_path=(path)
|
|
54
|
+
expanded = File.expand_path(path)
|
|
55
|
+
@coverage_path = expanded
|
|
56
|
+
@coverage_path_explicit = true
|
|
57
|
+
FileUtils.mkdir_p expanded
|
|
58
|
+
end
|
|
59
|
+
|
|
63
60
|
#
|
|
64
|
-
# The name of the command (a.k.a. Test Suite) currently running.
|
|
65
|
-
#
|
|
66
|
-
# with SimpleCov.command_name("test:units").
|
|
61
|
+
# The name of the command (a.k.a. Test Suite) currently running. Used for
|
|
62
|
+
# result merging and caching. Auto-detected.
|
|
67
63
|
#
|
|
68
64
|
def command_name(name = nil)
|
|
69
|
-
|
|
70
|
-
@command_name ||=
|
|
65
|
+
self.command_name = name unless name.nil?
|
|
66
|
+
@command_name ||= CommandGuesser.guess
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def command_name=(name)
|
|
70
|
+
@command_name = name
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
# Returns the hash of available profiles
|
|
74
73
|
def profiles
|
|
75
|
-
@profiles ||=
|
|
74
|
+
@profiles ||= Profiles.new
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
78
|
+
# Configure simplecov in a block instead of prepending SimpleCov to each
|
|
79
|
+
# config method. Parameterized blocks retain their caller context and
|
|
80
|
+
# receive this configuration target explicitly.
|
|
82
81
|
#
|
|
83
82
|
def configure(&block)
|
|
84
83
|
raise ArgumentError, "configuration block required" unless block
|
|
@@ -91,61 +90,49 @@ module SimpleCov
|
|
|
91
90
|
self
|
|
92
91
|
end
|
|
93
92
|
|
|
94
|
-
#
|
|
95
|
-
# Gets or sets the behavior to process coverage results.
|
|
96
|
-
# By default, it stores/merges the current result and formats only
|
|
97
|
-
# from the final reporting process.
|
|
98
|
-
#
|
|
99
93
|
def at_exit(&block)
|
|
100
94
|
@at_exit = block if block
|
|
101
95
|
configured = @at_exit
|
|
102
96
|
return configured if configured
|
|
103
|
-
return
|
|
97
|
+
return -> {} unless active_session?
|
|
104
98
|
|
|
105
|
-
@at_exit =
|
|
99
|
+
@at_exit = lambda do
|
|
106
100
|
result = SimpleCov.result
|
|
107
101
|
result.format! if result && SimpleCov.merge_finalization_owner?
|
|
108
102
|
end
|
|
109
103
|
end
|
|
110
104
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# (e.g. by `SimpleCov.collate`, which never starts Coverage).
|
|
105
|
+
# The Coverage module is actively tracking, or a `@result` has already been
|
|
106
|
+
# assembled (`SimpleCov.collate` never starts Coverage).
|
|
114
107
|
def active_session?
|
|
115
|
-
SimpleCov.result? ||
|
|
108
|
+
!!SimpleCov.result? || coverage_running?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A configuration loaded on its own may have no Coverage to ask, which is
|
|
112
|
+
# not something a suite that measures coverage can be.
|
|
113
|
+
def coverage_running?
|
|
114
|
+
defined?(Coverage) ? Coverage.running? : false
|
|
116
115
|
end
|
|
117
116
|
|
|
118
|
-
#
|
|
119
|
-
# Gets or sets the behavior to start a new forked Process.
|
|
120
|
-
# Defaults to adding " (subprocess: #{serial})" to command_name and
|
|
121
|
-
# starting SimpleCov in quiet mode.
|
|
122
|
-
#
|
|
123
117
|
def at_fork(&block)
|
|
124
118
|
@at_fork = block if block
|
|
125
119
|
@at_fork ||= lambda { |_pid|
|
|
126
|
-
# Needs a name that's unique per worker within a run yet identical
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
120
|
+
# Needs a name that's unique per worker within a run yet identical across
|
|
121
|
+
# runs. Built from SimpleCov's stable fork serial rather than the OS pid:
|
|
122
|
+
# with the pid, every run produced uniquely-named results that never
|
|
123
|
+
# overwrote the previous run's, so they piled up in .resultset.json
|
|
124
|
+
# until merge_timeout and the merged report's file set drifted from run
|
|
125
|
+
# to run (#1171).
|
|
132
126
|
SimpleCov.command_name "#{SimpleCov.command_name} (subprocess: #{SimpleCov.subprocess_serial})"
|
|
133
|
-
# be quiet, the parent process will use the regular formatter
|
|
134
127
|
SimpleCov.print_errors false
|
|
135
|
-
SimpleCov.formatter
|
|
128
|
+
SimpleCov.formatter Formatter::SimpleFormatter
|
|
136
129
|
SimpleCov.minimum_coverage 0
|
|
137
130
|
SimpleCov.start
|
|
138
131
|
}
|
|
139
132
|
end
|
|
140
133
|
|
|
141
|
-
#
|
|
142
|
-
# Returns the project name — defaults to the last dirname in
|
|
143
|
-
# SimpleCov.root, capitalized with underscores → spaces.
|
|
144
|
-
#
|
|
134
|
+
# Only a String renames, and a name already chosen survives anything else.
|
|
145
135
|
def project_name(new_name = nil)
|
|
146
|
-
current = defined?(@project_name) ? @project_name : nil
|
|
147
|
-
return current if current && new_name.nil?
|
|
148
|
-
|
|
149
136
|
@project_name = new_name if new_name.is_a?(String)
|
|
150
137
|
@project_name ||= File.basename(root).capitalize.tr("_", " ")
|
|
151
138
|
end
|
|
@@ -160,4 +147,11 @@ require_relative "configuration/groups"
|
|
|
160
147
|
require_relative "configuration/formatting"
|
|
161
148
|
require_relative "configuration/ignored_entries"
|
|
162
149
|
require_relative "configuration/merging"
|
|
150
|
+
require_relative "configuration/test_tracking"
|
|
163
151
|
require_relative "configuration/thresholds"
|
|
152
|
+
require_relative "configuration/baseline"
|
|
153
|
+
require_relative "configuration/deprecations"
|
|
154
|
+
require_relative "configuration/history"
|
|
155
|
+
require_relative "configuration/missed_caps"
|
|
156
|
+
require_relative "configuration/production"
|
|
157
|
+
require_relative "configuration/view_coverage"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class ContextMap
|
|
5
|
+
# Accumulates the context maps of every result a merge absorbs, under the
|
|
6
|
+
# all-or-nothing rule the resultset format needs: the merged result carries
|
|
7
|
+
# the union of the maps when every merged result recorded one, and no map at
|
|
8
|
+
# all otherwise. Keeping a partial union would present one worker's map as
|
|
9
|
+
# the whole run's, silently omitting the tests of the results that didn't
|
|
10
|
+
# record.
|
|
11
|
+
class Union
|
|
12
|
+
attr_reader :carrying, :entries
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@map = ContextMap.new
|
|
16
|
+
@complete = true
|
|
17
|
+
@carrying = 0
|
|
18
|
+
@entries = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def collector
|
|
22
|
+
->(surviving) { absorb_resultset(surviving) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def absorb_resultset(resultset)
|
|
26
|
+
resultset.each_value { |data| absorb_entry(data) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def absorb_entry(data)
|
|
30
|
+
@entries += 1
|
|
31
|
+
map = ContextMap.from_hash(data["contexts"])
|
|
32
|
+
return @complete = false unless map
|
|
33
|
+
|
|
34
|
+
@carrying += 1
|
|
35
|
+
@map.absorb(map)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The parallel merge builds one union per worker over its slice and ships it
|
|
39
|
+
# back, so the parent combines unions rather than re-reading entries.
|
|
40
|
+
def absorb_union(other)
|
|
41
|
+
@entries += other.entries
|
|
42
|
+
@carrying += other.carrying
|
|
43
|
+
@complete &&= other.complete?
|
|
44
|
+
@map.absorb(other.partial_map)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def complete?
|
|
49
|
+
@complete
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The union when every absorbed result carried a map, nil otherwise. Dropping
|
|
53
|
+
# warns when the merge mixed results with and without maps, since that
|
|
54
|
+
# usually means `track_tests` is on in some workers and not others; a merge
|
|
55
|
+
# where nothing recorded stays quiet.
|
|
56
|
+
def map
|
|
57
|
+
return @map if @complete
|
|
58
|
+
|
|
59
|
+
warn_about_partial_maps
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
protected
|
|
63
|
+
|
|
64
|
+
# Protected: only another union folding this one in may read it.
|
|
65
|
+
def partial_map
|
|
66
|
+
@map
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def warn_about_partial_maps
|
|
72
|
+
return unless @carrying.positive? && SimpleCov.print_errors
|
|
73
|
+
|
|
74
|
+
warn "[SimpleCov]: Dropped the per-test map from the merged result: " \
|
|
75
|
+
"only #{@carrying} of #{@entries} merged results carry one. The map is " \
|
|
76
|
+
"kept only when every merged result records it, so enable `track_tests` " \
|
|
77
|
+
"in every suite that should contribute."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
#
|
|
5
|
+
# Records which context covered which line: a list of context ids and, per
|
|
6
|
+
# source file, a bitmap of covered lines for each context that touched the
|
|
7
|
+
# file. A context is any labeled region of execution, and the vocabulary is
|
|
8
|
+
# deliberately the general one (coverage.py calls the same idea dynamic
|
|
9
|
+
# contexts), so the stored format doesn't bake simplecov's current use into
|
|
10
|
+
# a name.
|
|
11
|
+
#
|
|
12
|
+
# The naive shape of this data, a list of ids on every line, is
|
|
13
|
+
# O(contexts x lines) strings. Context ids are interned instead, and each
|
|
14
|
+
# context's covered lines within a file are a single Integer bitmap with bit
|
|
15
|
+
# N set when line N+1 was executed, which packs a thousand-line file into
|
|
16
|
+
# ~125 bytes and makes the union of two recordings a bitwise OR.
|
|
17
|
+
#
|
|
18
|
+
# Serialized into `.resultset.json` as `{"version" => 1, "contexts" =>
|
|
19
|
+
# [...ids...], "files" => {path => {context index => bitmap as hex}}}`. The
|
|
20
|
+
# format is tolerated, not trusted: `.from_hash` returns nil for anything
|
|
21
|
+
# malformed or future-versioned, which the merge treats the same as a map
|
|
22
|
+
# that was never recorded.
|
|
23
|
+
#
|
|
24
|
+
class ContextMap
|
|
25
|
+
# Bumped on any change an older reader could misread, so `.from_hash` can
|
|
26
|
+
# treat a future format as absent instead of answering from it wrongly.
|
|
27
|
+
VERSION = 1
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@contexts = [] #: Array[String]
|
|
31
|
+
@indices = {} #: Hash[String, Integer]
|
|
32
|
+
@files = {} #: Hash[String, Hash[Integer, Integer]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `lines_by_file` maps a source path to the bitmap of lines the context
|
|
36
|
+
# executed there. The id is interned even when the delta is empty: the
|
|
37
|
+
# context ran, and keeping it distinguishes "covered nothing of its own"
|
|
38
|
+
# from "never recorded".
|
|
39
|
+
def record(context_id, lines_by_file)
|
|
40
|
+
index = intern(context_id)
|
|
41
|
+
lines_by_file.each do |path, bitmap|
|
|
42
|
+
next if bitmap.zero?
|
|
43
|
+
|
|
44
|
+
table = (@files[path] ||= {})
|
|
45
|
+
table[index] = (table[index] || 0) | bitmap
|
|
46
|
+
end
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sorted for stable output, since recording order varies between runs and
|
|
51
|
+
# merges. The path is resolved against `SimpleCov.root`, so callers can
|
|
52
|
+
# pass either an absolute or a project-relative one.
|
|
53
|
+
#
|
|
54
|
+
# A line number below 1 needs no guard of its own: it shifts the probe bit
|
|
55
|
+
# right off the end of the bitmap, leaving a mask no line can match.
|
|
56
|
+
def covering(path, line)
|
|
57
|
+
table = @files[File.expand_path(path, SimpleCov.root)]
|
|
58
|
+
return [] unless table
|
|
59
|
+
|
|
60
|
+
bit = 1 << (line - 1)
|
|
61
|
+
table.filter_map { |index, bitmap| @contexts.fetch(index) if bitmap.anybits?(bit) }.sort
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def contexts
|
|
65
|
+
@contexts.dup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def empty?
|
|
69
|
+
@contexts.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Other's contexts are re-interned, so maps recorded by different processes
|
|
73
|
+
# merge by id, and a context both sides saw contributes one entry.
|
|
74
|
+
def absorb(other)
|
|
75
|
+
remap = other.interned_contexts.map { |context_id| intern(context_id) }
|
|
76
|
+
other.file_tables.each do |path, table|
|
|
77
|
+
target = (@files[path] ||= {})
|
|
78
|
+
table.each do |index, bitmap|
|
|
79
|
+
key = remap.fetch(index)
|
|
80
|
+
target[key] = (target[key] || 0) | bitmap
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `only:` restricts the files to a given set of paths, so the map follows
|
|
87
|
+
# the same universe as the coverage it sits beside. The context list is
|
|
88
|
+
# never restricted: which contexts ran is true regardless of which files
|
|
89
|
+
# survived filtering.
|
|
90
|
+
def to_h(only: nil)
|
|
91
|
+
# Selected rather than sliced: `only` is a Set, and splatting one into
|
|
92
|
+
# `slice` is a call `to_a` answers for, which makes dropping the `to_a` a
|
|
93
|
+
# mutation nothing can observe.
|
|
94
|
+
tables = only ? @files.select { |path, _table| only.include?(path) } : @files # rubocop:disable Style/HashSlice
|
|
95
|
+
serialized = tables.transform_values { |table| serialize_table(table) }
|
|
96
|
+
{"version" => VERSION, "contexts" => @contexts.dup, "files" => serialized}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Index string => hex bitmap, the wire encoding `to_h` writes.
|
|
100
|
+
# coverage.json shares the resultset's encoding through this, so the format
|
|
101
|
+
# has one owner.
|
|
102
|
+
def serialized_bitmaps_for(path)
|
|
103
|
+
serialize_table(@files[File.expand_path(path, SimpleCov.root)] || {})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @api private
|
|
107
|
+
def intern(context_id)
|
|
108
|
+
@indices[context_id] ||= begin
|
|
109
|
+
@contexts << context_id
|
|
110
|
+
@contexts.size - 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
# nil when the data is not a well-formed map of this format version.
|
|
116
|
+
# All-or-nothing on purpose: a partially salvaged map would answer
|
|
117
|
+
# `covering` queries with silent gaps, and the merge already drops an
|
|
118
|
+
# absent map consistently.
|
|
119
|
+
def from_hash(data)
|
|
120
|
+
# `eql?` rather than `==`: a version written as 1.0 is not this format,
|
|
121
|
+
# and reading it as if it were is what the version gate exists to stop.
|
|
122
|
+
return nil unless data.instance_of?(Hash) && data["version"].eql?(VERSION)
|
|
123
|
+
|
|
124
|
+
contexts = data["contexts"]
|
|
125
|
+
files = data["files"]
|
|
126
|
+
return nil unless contexts.instance_of?(Array) && contexts.all?(String) && files.instance_of?(Hash)
|
|
127
|
+
|
|
128
|
+
build(contexts, files)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def build(contexts, files)
|
|
134
|
+
map = new
|
|
135
|
+
contexts.each { |context_id| map.intern(context_id) }
|
|
136
|
+
files.each do |path, table|
|
|
137
|
+
return nil unless path.instance_of?(String) && table.instance_of?(Hash)
|
|
138
|
+
return nil unless absorb_table(map, contexts, path, table)
|
|
139
|
+
end
|
|
140
|
+
map
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def absorb_table(map, contexts, path, table)
|
|
144
|
+
table.all? do |index, encoded|
|
|
145
|
+
position = parse_index(index, contexts.size)
|
|
146
|
+
bitmap = parse_bitmap(encoded)
|
|
147
|
+
map.record(contexts.fetch(position), path => bitmap) if position && bitmap
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A serialized context index is a decimal string pointing into the entry's
|
|
152
|
+
# own context list. An out-of-range one makes the whole map malformed.
|
|
153
|
+
def parse_index(index, size)
|
|
154
|
+
return nil unless index.instance_of?(String) && index.match?(/\A\d+\z/)
|
|
155
|
+
|
|
156
|
+
value = index.to_i
|
|
157
|
+
value if value < size
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def parse_bitmap(encoded)
|
|
161
|
+
return nil unless encoded.instance_of?(String) && encoded.match?(/\A\h+\z/)
|
|
162
|
+
|
|
163
|
+
encoded.to_i(16)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Sorted by index: recording and merge order vary run to run, and a
|
|
170
|
+
# deterministic serialization keeps stored resultsets diffable.
|
|
171
|
+
def serialize_table(table)
|
|
172
|
+
table.sort.to_h { |index, bitmap| [index.to_s, bitmap.to_s(16)] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
protected
|
|
176
|
+
|
|
177
|
+
# Live state, not copies, which is why these are protected rather than
|
|
178
|
+
# public.
|
|
179
|
+
def interned_contexts
|
|
180
|
+
@contexts
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def file_tables
|
|
184
|
+
@files
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
|
-
# Shared parser for coverage.json consumers. It enforces the encoding and
|
|
7
|
-
# stable outermost shape while leaving command- or viewer-specific fields to
|
|
8
|
-
# their respective callers.
|
|
9
6
|
module CoverageJSON
|
|
10
7
|
class Error < StandardError; end
|
|
11
8
|
|
|
@@ -14,11 +11,11 @@ module SimpleCov
|
|
|
14
11
|
raise Error, "input is not valid UTF-8" unless source.valid_encoding?
|
|
15
12
|
|
|
16
13
|
document = JSON.parse(source)
|
|
17
|
-
return document if document.
|
|
14
|
+
return document if document.instance_of?(Hash)
|
|
18
15
|
|
|
19
16
|
raise Error, "top-level value must be an object"
|
|
20
17
|
rescue JSON::ParserError => e
|
|
21
|
-
raise Error, e
|
|
18
|
+
raise Error, e
|
|
22
19
|
end
|
|
23
20
|
end
|
|
24
21
|
end
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# * total - how many things to cover there are (total relevant loc/branches)
|
|
9
|
-
# * covered - how many of the coverables are hit
|
|
10
|
-
# * missed - how many of the coverables are missed
|
|
11
|
-
# * omitted - how many lines cannot be covered (blank lines/comments); only meaningful for line coverage
|
|
12
|
-
# * percent - percentage as covered/missed
|
|
13
|
-
# * strength - average hits per coverable unit (will not exist for one shot lines format)
|
|
4
|
+
# One coverage criterion's numbers, uniform across criteria: total, covered,
|
|
5
|
+
# missed, omitted (only meaningful for line coverage), percent, and strength,
|
|
6
|
+
# the average hits per coverable unit, which the oneshot-lines format has no
|
|
7
|
+
# way to produce.
|
|
14
8
|
class CoverageStatistics
|
|
15
9
|
attr_reader :total, :covered, :missed, :omitted, :strength, :percent
|
|
16
10
|
|
|
17
|
-
# Seed for the reduce in `.from`: covered, missed, omitted, strength.
|
|
18
11
|
ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
|
|
19
12
|
|
|
13
|
+
# Strength is remultiplied by loc because files have different strengths and
|
|
14
|
+
# line counts, giving them a different weight in the total.
|
|
20
15
|
def self.from(coverage_statistics)
|
|
21
16
|
sum_covered, sum_missed, sum_omitted, sum_total_strength =
|
|
22
17
|
coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
|
|
@@ -24,8 +19,6 @@ module SimpleCov
|
|
|
24
19
|
covered + stats.covered,
|
|
25
20
|
missed + stats.missed,
|
|
26
21
|
omitted + stats.omitted,
|
|
27
|
-
# gotta remultiply with loc because files have different strength and loc
|
|
28
|
-
# giving them a different "weight" in total
|
|
29
22
|
total_strength + (stats.strength * stats.total)
|
|
30
23
|
]
|
|
31
24
|
end
|
|
@@ -33,19 +26,16 @@ module SimpleCov
|
|
|
33
26
|
new(covered: sum_covered, missed: sum_missed, omitted: sum_omitted, total_strength: sum_total_strength)
|
|
34
27
|
end
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@
|
|
41
|
-
@
|
|
42
|
-
@omitted = omitted
|
|
43
|
-
@total = covered + missed
|
|
44
|
-
@percent = percent || compute_percent(covered, missed, total)
|
|
29
|
+
def initialize(covered:, missed:, omitted: 0, total_strength: 0, percent: nil)
|
|
30
|
+
@covered = covered
|
|
31
|
+
@missed = missed
|
|
32
|
+
@omitted = omitted
|
|
33
|
+
@total = covered + missed
|
|
34
|
+
@percent = percent || compute_percent(covered, missed, total)
|
|
45
35
|
@strength = compute_strength(total_strength, total)
|
|
46
36
|
end
|
|
47
37
|
|
|
48
|
-
|
|
38
|
+
private
|
|
49
39
|
|
|
50
40
|
def compute_percent(covered, missed, total)
|
|
51
41
|
return 100.0 if missed.zero?
|
|
@@ -56,7 +46,7 @@ module SimpleCov
|
|
|
56
46
|
def compute_strength(total_strength, total)
|
|
57
47
|
return 0.0 if total.zero?
|
|
58
48
|
|
|
59
|
-
total_strength.
|
|
49
|
+
total_strength.fdiv(total)
|
|
60
50
|
end
|
|
61
51
|
end
|
|
62
52
|
end
|