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
|
@@ -2,53 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
#
|
|
6
|
-
# Representing single branch that has been detected in coverage report.
|
|
7
|
-
# Give us support methods that handle needed calculations.
|
|
8
5
|
class Branch
|
|
9
6
|
attr_reader :start_line, :end_line, :coverage, :type
|
|
10
7
|
|
|
11
|
-
# rubocop:disable Metrics/ParameterLists
|
|
12
8
|
def initialize(start_line:, end_line:, coverage:, inline:, type:)
|
|
13
9
|
@start_line = start_line
|
|
14
|
-
@end_line
|
|
15
|
-
@coverage
|
|
16
|
-
@inline
|
|
17
|
-
@type
|
|
18
|
-
@skipped
|
|
10
|
+
@end_line = end_line
|
|
11
|
+
@coverage = coverage
|
|
12
|
+
@inline = inline
|
|
13
|
+
@type = type
|
|
14
|
+
@skipped = false
|
|
19
15
|
end
|
|
20
|
-
# rubocop:enable Metrics/ParameterLists
|
|
21
16
|
|
|
22
17
|
def inline?
|
|
23
18
|
@inline
|
|
24
19
|
end
|
|
25
20
|
|
|
26
|
-
#
|
|
27
|
-
# Return true if there is relevant count defined > 0
|
|
28
|
-
#
|
|
29
|
-
# @return [Boolean]
|
|
30
|
-
#
|
|
31
21
|
def covered?
|
|
32
22
|
!skipped? && coverage.positive?
|
|
33
23
|
end
|
|
34
24
|
|
|
35
|
-
#
|
|
36
|
-
# Check if branche missed or not
|
|
37
|
-
#
|
|
38
|
-
# @return [Boolean]
|
|
39
|
-
#
|
|
40
25
|
def missed?
|
|
41
26
|
!skipped? && coverage.zero?
|
|
42
27
|
end
|
|
43
28
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# at if/else) because that
|
|
48
|
-
# * highlights the condition
|
|
49
|
-
# * makes it distinguishable if the first line of the branch is an inline branch
|
|
50
|
-
# (see the nested_branches fixture)
|
|
51
|
-
#
|
|
29
|
+
# Usually the line above the start of the branch, so that it shows up at the
|
|
30
|
+
# if/else. That highlights the condition, and makes it distinguishable when
|
|
31
|
+
# the first line of the branch is itself an inline branch.
|
|
52
32
|
def report_line
|
|
53
33
|
if inline?
|
|
54
34
|
start_line
|
|
@@ -57,12 +37,10 @@ module SimpleCov
|
|
|
57
37
|
end
|
|
58
38
|
end
|
|
59
39
|
|
|
60
|
-
# Flags the branch as skipped
|
|
61
40
|
def skipped!
|
|
62
41
|
@skipped = true
|
|
63
42
|
end
|
|
64
43
|
|
|
65
|
-
# Returns true if the branch was marked skipped by virtue of nocov comments.
|
|
66
44
|
def skipped?
|
|
67
45
|
@skipped
|
|
68
46
|
end
|
|
@@ -71,11 +49,6 @@ module SimpleCov
|
|
|
71
49
|
start_line <= line_range.end && end_line >= line_range.begin
|
|
72
50
|
end
|
|
73
51
|
|
|
74
|
-
#
|
|
75
|
-
# Return array with coverage count and badge
|
|
76
|
-
#
|
|
77
|
-
# @return [Array]
|
|
78
|
-
#
|
|
79
52
|
def report
|
|
80
53
|
[type, coverage]
|
|
81
54
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `SourceFile::Branch` objects for a source file from the raw
|
|
6
|
+
# branch data Ruby's Coverage library reports, applying the
|
|
7
|
+
# `ignore_branches` filters and marking branches inside disabled chunks as
|
|
8
|
+
# skipped.
|
|
9
|
+
class BranchBuilder
|
|
10
|
+
def initialize(source_file)
|
|
11
|
+
@source_file = source_file
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
coverage_branch_data = @source_file.coverage_data["branches"] || {}
|
|
16
|
+
branches = coverage_branch_data.flat_map do |condition, coverage_branches|
|
|
17
|
+
next [] if eval_generated_condition_to_ignore?(condition)
|
|
18
|
+
|
|
19
|
+
build_branches_from(condition, coverage_branches)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
process_skipped(branches)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Coverage attributes an eval'd branch to the caller's `__FILE__` /
|
|
28
|
+
# `__LINE__`, so a Rails `delegate :foo, to: :bar` call surfaces inside the
|
|
29
|
+
# source file as if there were branches at the `delegate` line. Prism never
|
|
30
|
+
# sees those branches in the static source, so a condition whose start_line
|
|
31
|
+
# isn't in the real-source branch set must be eval-generated (#1046).
|
|
32
|
+
def eval_generated_condition_to_ignore?(condition)
|
|
33
|
+
return false unless SimpleCov.ignored_branch?(:eval_generated)
|
|
34
|
+
|
|
35
|
+
positions = @source_file.real_source_positions
|
|
36
|
+
# simplecov:disable branch — nil branch fires only when Prism is unavailable
|
|
37
|
+
return false unless positions
|
|
38
|
+
|
|
39
|
+
# simplecov:enable branch
|
|
40
|
+
|
|
41
|
+
_type, _id, start_line, * = RubyDataParser.call(condition)
|
|
42
|
+
!positions.fetch(:branches).include?(start_line)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_branches_from(condition, branches)
|
|
46
|
+
_condition_type, _condition_id, *condition_range = RubyDataParser.call(condition)
|
|
47
|
+
|
|
48
|
+
branches.filter_map do |branch_data, hit_count|
|
|
49
|
+
build_branch(RubyDataParser.call(branch_data), hit_count, condition_range)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The coverage data hands in `[:then, 4, 6, 6, 6, 10]`, which is
|
|
54
|
+
# [type, id, start_line, start_col, end_line, end_col].
|
|
55
|
+
def build_branch(branch_data, hit_count, condition_range)
|
|
56
|
+
type, _id, start_line, start_col, end_line, end_col = branch_data
|
|
57
|
+
return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range)
|
|
58
|
+
|
|
59
|
+
Branch.new(
|
|
60
|
+
start_line: start_line,
|
|
61
|
+
end_line: end_line,
|
|
62
|
+
coverage: hit_count,
|
|
63
|
+
inline: start_line.eql?(condition_range.first),
|
|
64
|
+
type: type
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Ruby's Coverage reports a synthetic `:else` for constructs with no literal
|
|
69
|
+
# `else` keyword. The signal is structural: a synthetic else reuses its
|
|
70
|
+
# parent condition's full source range, while an explicit `else` arm carries
|
|
71
|
+
# a narrower one. Comparing the full range rather than just `start_line` is
|
|
72
|
+
# what distinguishes a ternary's explicit else on the condition's own line
|
|
73
|
+
# from a postfix `return if cond` (#1033).
|
|
74
|
+
def implicit_else_to_ignore?(type, branch_range, condition_range)
|
|
75
|
+
return false unless type.equal?(:else)
|
|
76
|
+
return false unless SimpleCov.ignored_branch?(:implicit_else)
|
|
77
|
+
|
|
78
|
+
branch_range.eql?(condition_range)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A non-inline branch's source range starts on its arm body, but
|
|
82
|
+
# `report_line` is the condition line above it, which is where the user sees
|
|
83
|
+
# the branch and would naturally place an inline disable directive. Honour
|
|
84
|
+
# both.
|
|
85
|
+
def process_skipped(branches)
|
|
86
|
+
chunks = @source_file.skip_chunks_for(:branch)
|
|
87
|
+
|
|
88
|
+
# `# simplecov:disable branch` directive. Honour both.
|
|
89
|
+
branches.each do |branch|
|
|
90
|
+
branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
module BuilderContext
|
|
6
|
+
def skip_chunks_for(criterion)
|
|
7
|
+
(@skip_chunks ||= SkipChunks.new(filename, src)).chunks_for(criterion)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Memoized set of real source positions extracted via Prism. Nil when Prism is
|
|
11
|
+
# unavailable or parsing fails, signaling callers to keep every Coverage entry
|
|
12
|
+
# rather than risk false drops. The `defined?` guard preserves a nil
|
|
13
|
+
# memoization across calls.
|
|
14
|
+
#
|
|
15
|
+
# Nil for a template too, without parsing: every entry a template reports
|
|
16
|
+
# is eval-generated by construction, so the filter has nothing to
|
|
17
|
+
# distinguish there, and a template whose text happens to parse as Ruby
|
|
18
|
+
# (a Haml `#id` tag reads as a comment) would otherwise lose its real
|
|
19
|
+
# branches to a confident wrong parse.
|
|
20
|
+
def real_source_positions
|
|
21
|
+
return @real_source_positions if instance_variable_defined?(:@real_source_positions)
|
|
22
|
+
return @real_source_positions = nil if Directive::Template.template?(filename)
|
|
23
|
+
|
|
24
|
+
@real_source_positions = StaticCoverageExtractor.real_source_positions(src.join)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,70 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Representation of a single line in a source file including
|
|
6
|
-
# this specific line's source code, line_number and code coverage,
|
|
7
|
-
# with the coverage being either nil (coverage not applicable, e.g. comment
|
|
8
|
-
# line), 0 (line not covered) or >1 (the amount of times the line was
|
|
9
|
-
# executed)
|
|
10
5
|
class Line
|
|
11
|
-
|
|
12
|
-
attr_reader :src
|
|
13
|
-
# The line number in the source file. Aliased as :line, :number
|
|
14
|
-
attr_reader :line_number
|
|
15
|
-
# The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
|
|
16
|
-
attr_reader :coverage
|
|
17
|
-
# Whether this line was skipped
|
|
18
|
-
attr_reader :skipped
|
|
6
|
+
attr_reader :src, :line_number, :coverage, :skipped
|
|
19
7
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
alias number line_number
|
|
8
|
+
alias_method :source, :src
|
|
9
|
+
alias_method :line, :line_number
|
|
10
|
+
alias_method :number, :line_number
|
|
24
11
|
|
|
25
12
|
def initialize(src, line_number, coverage)
|
|
26
13
|
raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
|
|
27
14
|
raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
|
|
28
|
-
|
|
15
|
+
unless coverage.is_a?(Integer) || coverage.nil?
|
|
16
|
+
raise ArgumentError, "Only Integer and nil accepted for coverage"
|
|
17
|
+
end
|
|
29
18
|
|
|
30
|
-
@src
|
|
19
|
+
@src = src
|
|
31
20
|
@line_number = line_number
|
|
32
|
-
@coverage
|
|
33
|
-
@skipped
|
|
21
|
+
@coverage = coverage
|
|
22
|
+
@skipped = false
|
|
34
23
|
end
|
|
35
24
|
|
|
36
|
-
#
|
|
25
|
+
# Asking `covered?` rather than reading the count a second time keeps the two
|
|
26
|
+
# answers from ever disagreeing, and `never?` ahead of it is what keeps a
|
|
27
|
+
# line with no coverage at all out of the missed column.
|
|
37
28
|
def missed?
|
|
38
|
-
!never? && !skipped? &&
|
|
29
|
+
!never? && !skipped? && !covered?
|
|
39
30
|
end
|
|
40
31
|
|
|
41
|
-
# Returns true if this is a line that has been covered
|
|
42
32
|
def covered?
|
|
43
|
-
!
|
|
33
|
+
!skipped? && coverage.to_i.positive?
|
|
44
34
|
end
|
|
45
35
|
|
|
46
|
-
# Returns true if this line is not relevant for coverage
|
|
47
36
|
def never?
|
|
48
37
|
!skipped? && coverage.nil?
|
|
49
38
|
end
|
|
50
39
|
|
|
51
|
-
# Flags this line as skipped
|
|
52
40
|
def skipped!
|
|
53
41
|
@skipped = true
|
|
54
42
|
end
|
|
55
43
|
|
|
56
|
-
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
|
57
|
-
# # :nocov: comment lines.
|
|
58
44
|
def skipped?
|
|
59
45
|
skipped
|
|
60
46
|
end
|
|
61
47
|
|
|
62
|
-
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
|
63
|
-
# as a css class in report generation
|
|
64
48
|
def status
|
|
65
49
|
return "skipped" if skipped?
|
|
66
50
|
return "never" if never?
|
|
67
51
|
return "missed" if missed?
|
|
68
|
-
|
|
52
|
+
|
|
53
|
+
"covered"
|
|
69
54
|
end
|
|
70
55
|
end
|
|
71
56
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `SourceFile::Line` objects for a source file from the raw
|
|
6
|
+
# line-coverage array, applying the disabled block ranges via `skipped!`.
|
|
7
|
+
class LineBuilder
|
|
8
|
+
def initialize(source_file)
|
|
9
|
+
@source_file = source_file
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
lines = build_lines
|
|
14
|
+
mark_skipped(lines, @source_file.skip_chunks_for(:line))
|
|
15
|
+
lines
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# When `:line` coverage is disabled the Coverage module emits no "lines" data,
|
|
21
|
+
# so every position looks up nil. The source rows are still useful for the
|
|
22
|
+
# HTML report's source view even without per-line hits.
|
|
23
|
+
def build_lines
|
|
24
|
+
line_coverage = @source_file.coverage_data["lines"] || []
|
|
25
|
+
@source_file.src.map.with_index(1) do |src, i|
|
|
26
|
+
Line.new(src, i, line_coverage.at(i - 1))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The lines array is 0-based while the chunks' line numbers are 1-based, so
|
|
31
|
+
# each range is shifted down by one to slice into it.
|
|
32
|
+
def mark_skipped(lines, chunks)
|
|
33
|
+
chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
class Method
|
|
6
|
+
attr_reader :source_file, :coverage, :class_name, :method_name,
|
|
7
|
+
:start_line, :start_col, :end_line, :end_col
|
|
8
|
+
|
|
9
|
+
def initialize(source_file, info, coverage)
|
|
10
|
+
@source_file = source_file
|
|
11
|
+
@class_name, @method_name, @start_line, @start_col, @end_line, @end_col = info
|
|
12
|
+
@coverage = coverage
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def covered?
|
|
16
|
+
!skipped? && coverage.positive?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Criterion-level skips (nocov chunks, `# simplecov:disable` /
|
|
20
|
+
# `# simplecov:disable method` regions) arrive via `skipped!` from
|
|
21
|
+
# MethodBuilder. Deliberately NOT derived from the lines' skip
|
|
22
|
+
# state: a line-only directive around a def must not remove the
|
|
23
|
+
# method from method totals. Without line info there is nothing to
|
|
24
|
+
# report against, so such a method stays skipped.
|
|
25
|
+
def skipped?
|
|
26
|
+
return @skipped if instance_variable_defined?(:@skipped)
|
|
27
|
+
|
|
28
|
+
@skipped = lines.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def skipped!
|
|
32
|
+
@skipped = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def missed?
|
|
36
|
+
!skipped? && coverage.zero?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lines
|
|
40
|
+
@lines ||= (start_line && end_line) ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def overlaps_with?(line_range)
|
|
44
|
+
return false unless start_line && end_line
|
|
45
|
+
|
|
46
|
+
start_line <= line_range.end && end_line >= line_range.begin
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
"#{class_name}##{method_name}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# `# simplecov:disable` / `# :nocov:` chunks as skipped.
|
|
6
|
+
class MethodBuilder
|
|
7
|
+
def initialize(source_file)
|
|
8
|
+
@source_file = source_file
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
none = {} #: Hash[untyped, Integer]
|
|
13
|
+
raw_methods = @source_file.coverage_data.fetch("methods", none)
|
|
14
|
+
methods = raw_methods.filter_map do |info, hit_count|
|
|
15
|
+
info = RubyDataParser.call(info)
|
|
16
|
+
next if eval_generated_to_ignore?(info)
|
|
17
|
+
|
|
18
|
+
Method.new(@source_file, info, hit_count)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
process_skipped(methods)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Coverage reports an eval'd `def` at the eval caller's line and name, so a
|
|
27
|
+
# method whose `(name, start_line)` is absent from the real-source `def` set
|
|
28
|
+
# is eval-generated (#1046).
|
|
29
|
+
def eval_generated_to_ignore?(info)
|
|
30
|
+
return false unless SimpleCov.ignored_method?(:eval_generated)
|
|
31
|
+
|
|
32
|
+
positions = @source_file.real_source_positions
|
|
33
|
+
# simplecov:disable branch — nil branch fires only when Prism is unavailable
|
|
34
|
+
return false unless positions
|
|
35
|
+
|
|
36
|
+
# simplecov:enable branch
|
|
37
|
+
|
|
38
|
+
_class_name, name, start_line, * = info
|
|
39
|
+
!positions.fetch(:methods).include?([name, start_line])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def process_skipped(methods)
|
|
43
|
+
chunks = @source_file.skip_chunks_for(:method)
|
|
44
|
+
|
|
45
|
+
methods.each do |method|
|
|
46
|
+
method.skipped! if chunks.any? { |chunk| method.overlaps_with?(chunk) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
class SourceFile
|
|
7
|
+
# `Coverage.result` reports condition and method keys as Ruby arrays. When
|
|
8
|
+
# the resultset is round-tripped through JSON those array keys become their
|
|
9
|
+
# stringified inspect form, so this parser walks the literal back into a
|
|
10
|
+
# real Array without using `eval` (#801). The grammar covers symbols,
|
|
11
|
+
# strings, integers, unary minus, and constant paths: every shape Coverage
|
|
12
|
+
# ever emits.
|
|
13
|
+
module RubyDataParser
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
# String parses are memoized: `Combine::BranchesCombiner` and
|
|
17
|
+
# `Combine::MethodsCombiner` derive a merge identity from every key of
|
|
18
|
+
# both sides on every pairwise merge, so collating N resultsets parses
|
|
19
|
+
# each key string N-1 times, and Ripper dominates the wall time of a
|
|
20
|
+
# large collate. The set of unique keys is bounded by the project's
|
|
21
|
+
# branch and method count, so a permanent cache stays small.
|
|
22
|
+
#
|
|
23
|
+
# Cached arrays are frozen element-wise, String class names included:
|
|
24
|
+
# every caller destructures without mutating, and sharing one array
|
|
25
|
+
# across callers must stay that way. The cache key needs no such care,
|
|
26
|
+
# since `Hash#[]=` dups and freezes String keys on its own.
|
|
27
|
+
def call(structure)
|
|
28
|
+
return structure if structure.is_a?(Array)
|
|
29
|
+
|
|
30
|
+
parse_cache[structure] ||= parse_array_string(structure).each(&:freeze).freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_cache
|
|
34
|
+
@parse_cache ||= {} #: Hash[String, untyped]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Parses a string like '[:if, 0, 3, 4, 3, 21]' or
|
|
38
|
+
# '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
|
|
39
|
+
def parse_array_string(str)
|
|
40
|
+
# Plain Ripper first: pre-quoting `#<...>` inspect segments would corrupt
|
|
41
|
+
# `"#<Class:Foo>"` strings that are valid Ruby literals, exactly the
|
|
42
|
+
# shape simplecov-on-simplecov method-coverage keys take.
|
|
43
|
+
sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
|
|
44
|
+
# Ripper wraps what it parsed in a `:program` node holding the
|
|
45
|
+
# statements, and this input is supposed to be one array literal and
|
|
46
|
+
# nothing else. Both passes answer nil for input Ruby can't parse at
|
|
47
|
+
# all, so there may be no statement to unwrap.
|
|
48
|
+
_program, statements = sexp
|
|
49
|
+
first_statement, *extra = statements
|
|
50
|
+
array_node = Array(first_statement)
|
|
51
|
+
unless extra.empty? && array_node.first.equal?(:array)
|
|
52
|
+
raise ArgumentError, "expected array literal: #{str.inspect}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Array(array_node.fetch(1)).map { |element| parse_element(element) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Each `when` already knows the node's shape, so the elements read straight
|
|
59
|
+
# out of it rather than through a second dispatch. A symbol's text sits
|
|
60
|
+
# where a string's does, one node deeper.
|
|
61
|
+
def parse_element(node)
|
|
62
|
+
case node.fetch(0)
|
|
63
|
+
when :@int then Integer(node.fetch(1))
|
|
64
|
+
when :unary then -Integer(node.fetch(2).fetch(1)) # `-2`
|
|
65
|
+
when :symbol_literal, :dyna_symbol then literal_text(node).to_sym
|
|
66
|
+
when :string_literal then literal_text(node)
|
|
67
|
+
when :var_ref then node.fetch(1).fetch(1) # `Foo`
|
|
68
|
+
when :const_path_ref then const_path(node) # `Foo::Bar`
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "unexpected element: #{node}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A constant path's namespace is an element in its own right, so
|
|
75
|
+
# `Foo::Bar::Baz` comes back together one segment at a time.
|
|
76
|
+
def const_path(node)
|
|
77
|
+
"#{parse_element(node.fetch(1))}::#{node.fetch(2).fetch(1)}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Ripper may emit zero, one, or many fragments under a literal depending
|
|
81
|
+
# on its form, and the escapes it leaves in the source text come back out.
|
|
82
|
+
def literal_text(node)
|
|
83
|
+
unescape_ruby(node.fetch(1)[1..].map { |fragment| fragment.fetch(1) }.join)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Undoes the same backslash-prefix escapes the previous hand-rolled parser
|
|
87
|
+
# undid: `\X` -> `X` for any X.
|
|
88
|
+
def unescape_ruby(raw)
|
|
89
|
+
raw.gsub(/\\(.)/) { Regexp.last_match(1) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Method coverage keys can contain inspect-format class references like
|
|
93
|
+
# `#<Class:Foo>`, which aren't valid Ruby syntax. Wrapping them in quotes
|
|
94
|
+
# lets Ripper parse the surrounding array literal. The pattern recurses
|
|
95
|
+
# because singleton methods on instances nest one inspect segment inside
|
|
96
|
+
# another (`#<Class:#<Object:0x...>>`), and stopping at the first `>`
|
|
97
|
+
# leaves a dangling `>` that fails both Ripper passes.
|
|
98
|
+
def quote_inspected_class_segments(str)
|
|
99
|
+
str.gsub(/#<(?:[^<>]|\g<0>)+>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# The line ranges excluded from a SourceFile's coverage per criterion. Two
|
|
6
|
+
# sources contribute: the deprecated nocov block toggle, and the
|
|
7
|
+
# `SimpleCov::Directive` block directives, which can be scoped per criterion.
|
|
8
|
+
class SkipChunks
|
|
9
|
+
@nocov_warned = Set.new
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :nocov_warned
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(filename, src)
|
|
15
|
+
@filename = filename
|
|
16
|
+
@src = src
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Every criterion honors the deprecated all-criteria nocov chunks plus its
|
|
20
|
+
# own per-criterion directive ranges, methods included: they carry a
|
|
21
|
+
# source range, and nocov has always meant "exclude everything here". The
|
|
22
|
+
# ranges are 1-based line numbers, so consumers subtract 1 to index into
|
|
23
|
+
# the zero-based lines array.
|
|
24
|
+
def chunks_for(criterion)
|
|
25
|
+
nocov_chunks + directive_chunks.fetch(criterion)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def nocov_chunks
|
|
29
|
+
@nocov_chunks ||= build_nocov_chunks
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def directive_chunks
|
|
33
|
+
@directive_chunks ||= Directive.disabled_ranges(ruby_lines)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# The Ruby the file holds, which for a template means the Ruby inside its
|
|
39
|
+
# tags or script lines with its own comments turned into Ruby comments.
|
|
40
|
+
# Both kinds of marker are found there, so a toggle in a template's
|
|
41
|
+
# comment syntax gets the same treatment, and the same deprecation
|
|
42
|
+
# warning, as one in a Ruby file.
|
|
43
|
+
def ruby_lines
|
|
44
|
+
@ruby_lines ||= Directive::Template.ruby_lines(@filename, @src)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# An uneven number of nocovs is assumed to run to the end of the file. It
|
|
48
|
+
# cannot be handled inside the each_slice because JRuby behaves differently
|
|
49
|
+
# there (jruby/jruby#6048).
|
|
50
|
+
def build_nocov_chunks
|
|
51
|
+
no_cov_lines = ruby_lines.filter_map.with_index(1) do |line_src, index|
|
|
52
|
+
index if LinesClassifier.no_cov_line?(line_src)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
warn_nocov_deprecation(no_cov_lines.first) if no_cov_lines.any?
|
|
56
|
+
|
|
57
|
+
no_cov_lines << @src.size if no_cov_lines.size.odd?
|
|
58
|
+
|
|
59
|
+
no_cov_lines.each_slice(2).map { |chunk_start, chunk_end| chunk_start..chunk_end }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# at the `# simplecov:disable` / `# simplecov:enable` replacement.
|
|
63
|
+
def warn_nocov_deprecation(first_line_number)
|
|
64
|
+
return unless self.class.nocov_warned.add?(@filename)
|
|
65
|
+
|
|
66
|
+
token = SimpleCov.current_nocov_token
|
|
67
|
+
warn "#{@filename}:#{first_line_number}: [DEPRECATION] `# :#{token}:` is deprecated and will be removed " \
|
|
68
|
+
"in a future release. Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|