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
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Builds the `SourceFile::Branch` objects for a source file from
|
|
6
|
-
#
|
|
7
|
-
# `ignore_branches
|
|
8
|
-
#
|
|
9
|
-
# as skipped.
|
|
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.
|
|
10
9
|
class BranchBuilder
|
|
11
10
|
def initialize(source_file)
|
|
12
11
|
@source_file = source_file
|
|
@@ -23,17 +22,13 @@ module SimpleCov
|
|
|
23
22
|
process_skipped(branches)
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
29
|
-
# `
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# the `delegate` line. Prism never sees those branches in the static
|
|
34
|
-
# source, so a condition whose start_line isn't in the real-source
|
|
35
|
-
# branch set must be eval-generated. Only consulted when the user has
|
|
36
|
-
# opted in via `SimpleCov.ignore_branches :eval_generated`. See #1046.
|
|
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).
|
|
37
32
|
def eval_generated_condition_to_ignore?(condition)
|
|
38
33
|
return false unless SimpleCov.ignored_branch?(:eval_generated)
|
|
39
34
|
|
|
@@ -44,15 +39,10 @@ module SimpleCov
|
|
|
44
39
|
# simplecov:enable branch
|
|
45
40
|
|
|
46
41
|
_type, _id, start_line, * = RubyDataParser.call(condition)
|
|
47
|
-
!positions
|
|
42
|
+
!positions.fetch(:branches).include?(start_line)
|
|
48
43
|
end
|
|
49
44
|
|
|
50
45
|
def build_branches_from(condition, branches)
|
|
51
|
-
# the format handed in from the coverage data is like this:
|
|
52
|
-
#
|
|
53
|
-
# [:then, 4, 6, 6, 6, 10]
|
|
54
|
-
#
|
|
55
|
-
# which is [type, id, start_line, start_col, end_line, end_col]
|
|
56
46
|
_condition_type, _condition_id, *condition_range = RubyDataParser.call(condition)
|
|
57
47
|
|
|
58
48
|
branches.filter_map do |branch_data, hit_count|
|
|
@@ -60,54 +50,45 @@ module SimpleCov
|
|
|
60
50
|
end
|
|
61
51
|
end
|
|
62
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].
|
|
63
55
|
def build_branch(branch_data, hit_count, condition_range)
|
|
64
56
|
type, _id, start_line, start_col, end_line, end_col = branch_data
|
|
65
57
|
return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range)
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
Branch.new(
|
|
68
60
|
start_line: start_line,
|
|
69
61
|
end_line: end_line,
|
|
70
62
|
coverage: hit_count,
|
|
71
|
-
inline: start_line
|
|
63
|
+
inline: start_line.eql?(condition_range.first),
|
|
72
64
|
type: type
|
|
73
65
|
)
|
|
74
66
|
end
|
|
75
67
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# a
|
|
81
|
-
#
|
|
82
|
-
# explicit `else` arm carries a narrower range — its own keyword/body
|
|
83
|
-
# position rather than the whole conditional. Comparing the full range
|
|
84
|
-
# (not just `start_line`) is what distinguishes a ternary's explicit
|
|
85
|
-
# else on the same line as the condition — `arg == 42 ? :yes : :no`,
|
|
86
|
-
# where the else's columns differ from the parent's — from a postfix
|
|
87
|
-
# `return if cond` where the synthetic else inherits the full range.
|
|
88
|
-
# Only consulted when the user has opted in via
|
|
89
|
-
# `SimpleCov.ignore_branches :implicit_else`. See #1033.
|
|
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).
|
|
90
74
|
def implicit_else_to_ignore?(type, branch_range, condition_range)
|
|
91
|
-
return false unless type
|
|
75
|
+
return false unless type.equal?(:else)
|
|
92
76
|
return false unless SimpleCov.ignored_branch?(:implicit_else)
|
|
93
77
|
|
|
94
|
-
branch_range
|
|
78
|
+
branch_range.eql?(condition_range)
|
|
95
79
|
end
|
|
96
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.
|
|
97
85
|
def process_skipped(branches)
|
|
98
86
|
chunks = @source_file.skip_chunks_for(:branch)
|
|
99
|
-
return branches if chunks.empty?
|
|
100
87
|
|
|
101
|
-
# A non-inline branch's source range starts on its arm body (e.g. the
|
|
102
|
-
# `:yes` line of `if cond / :yes / else / :no / end`), but `report_line`
|
|
103
|
-
# is the condition line above it — that's where the user sees the
|
|
104
|
-
# branch in the report and where they would naturally place an inline
|
|
105
88
|
# `# simplecov:disable branch` directive. Honour both.
|
|
106
89
|
branches.each do |branch|
|
|
107
90
|
branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
|
|
108
91
|
end
|
|
109
|
-
|
|
110
|
-
branches
|
|
111
92
|
end
|
|
112
93
|
end
|
|
113
94
|
end
|
|
@@ -2,24 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Helpers shared by the per-criterion builders (LineBuilder /
|
|
6
|
-
# BranchBuilder / MethodBuilder). Mixed into SourceFile so each
|
|
7
|
-
# builder can ask the file for its skip-chunk ranges and Prism-derived
|
|
8
|
-
# real source positions without duplicating the memoization.
|
|
9
5
|
module BuilderContext
|
|
10
|
-
# Skip-chunk lookup for the named criterion (`:line`, `:branch`,
|
|
11
|
-
# `:method`).
|
|
12
6
|
def skip_chunks_for(criterion)
|
|
13
|
-
(@skip_chunks ||= SkipChunks.new(filename, src)).
|
|
7
|
+
(@skip_chunks ||= SkipChunks.new(filename, src)).chunks_for(criterion)
|
|
14
8
|
end
|
|
15
9
|
|
|
16
|
-
# Memoized set of real source positions
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# nil memoization across calls.
|
|
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.
|
|
21
14
|
def real_source_positions
|
|
22
|
-
return @real_source_positions if
|
|
15
|
+
return @real_source_positions if instance_variable_defined?(:@real_source_positions)
|
|
23
16
|
|
|
24
17
|
@real_source_positions = StaticCoverageExtractor.real_source_positions(src.join)
|
|
25
18
|
end
|
|
@@ -2,25 +2,12 @@
|
|
|
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)
|
|
@@ -29,48 +16,41 @@ module SimpleCov
|
|
|
29
16
|
raise ArgumentError, "Only Integer and nil accepted for coverage"
|
|
30
17
|
end
|
|
31
18
|
|
|
32
|
-
@src
|
|
19
|
+
@src = src
|
|
33
20
|
@line_number = line_number
|
|
34
|
-
@coverage
|
|
35
|
-
@skipped
|
|
21
|
+
@coverage = coverage
|
|
22
|
+
@skipped = false
|
|
36
23
|
end
|
|
37
24
|
|
|
38
|
-
#
|
|
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.
|
|
39
28
|
def missed?
|
|
40
|
-
!never? && !skipped? &&
|
|
29
|
+
!never? && !skipped? && !covered?
|
|
41
30
|
end
|
|
42
31
|
|
|
43
|
-
# Returns true if this is a line that has been covered
|
|
44
32
|
def covered?
|
|
45
|
-
!
|
|
33
|
+
!skipped? && coverage.to_i.positive?
|
|
46
34
|
end
|
|
47
35
|
|
|
48
|
-
# Returns true if this line is not relevant for coverage
|
|
49
36
|
def never?
|
|
50
37
|
!skipped? && coverage.nil?
|
|
51
38
|
end
|
|
52
39
|
|
|
53
|
-
# Flags this line as skipped
|
|
54
40
|
def skipped!
|
|
55
41
|
@skipped = true
|
|
56
42
|
end
|
|
57
43
|
|
|
58
|
-
# Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
|
|
59
|
-
# # :nocov: comment lines.
|
|
60
44
|
def skipped?
|
|
61
45
|
skipped
|
|
62
46
|
end
|
|
63
47
|
|
|
64
|
-
# The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
|
|
65
|
-
# as a css class in report generation
|
|
66
48
|
def status
|
|
67
49
|
return "skipped" if skipped?
|
|
68
50
|
return "never" if never?
|
|
69
51
|
return "missed" if missed?
|
|
70
52
|
|
|
71
|
-
|
|
72
|
-
"covered" if covered?
|
|
73
|
-
# simplecov:enable
|
|
53
|
+
"covered"
|
|
74
54
|
end
|
|
75
55
|
end
|
|
76
56
|
end
|
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Builds the `SourceFile::Line` objects for a source file from the
|
|
6
|
-
#
|
|
7
|
-
# 1-based line number, and the Coverage hit count (or nil for
|
|
8
|
-
# never-counted lines). Applies `# simplecov:disable` /
|
|
9
|
-
# `# :nocov:` block ranges via `skipped!`.
|
|
5
|
+
# Builds the `SourceFile::Line` objects for a source file from the raw
|
|
6
|
+
# line-coverage array, applying the disabled block ranges via `skipped!`.
|
|
10
7
|
class LineBuilder
|
|
11
8
|
def initialize(source_file)
|
|
12
9
|
@source_file = source_file
|
|
@@ -18,23 +15,20 @@ module SimpleCov
|
|
|
18
15
|
lines
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
private
|
|
22
19
|
|
|
23
|
-
# When `:line` coverage is disabled
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# HTML report's source view — even without per-line hits.
|
|
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.
|
|
27
23
|
def build_lines
|
|
28
24
|
line_coverage = @source_file.coverage_data["lines"] || []
|
|
29
25
|
@source_file.src.map.with_index(1) do |src, i|
|
|
30
|
-
|
|
26
|
+
Line.new(src, i, line_coverage.at(i - 1))
|
|
31
27
|
end
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
# The
|
|
35
|
-
#
|
|
36
|
-
# so each range needs to be shifted down by one to slice into the
|
|
37
|
-
# `lines` array.
|
|
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.
|
|
38
32
|
def mark_skipped(lines, chunks)
|
|
39
33
|
chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
|
40
34
|
end
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Represents a single method detected in coverage data.
|
|
6
|
-
# Provides helpers similar to Branch for coverage status.
|
|
7
5
|
class Method
|
|
8
6
|
attr_reader :source_file, :coverage, :class_name, :method_name,
|
|
9
|
-
|
|
7
|
+
:start_line, :start_col, :end_line, :end_col
|
|
10
8
|
|
|
11
9
|
def initialize(source_file, info, coverage)
|
|
12
10
|
@source_file = source_file
|
|
@@ -25,12 +23,11 @@ module SimpleCov
|
|
|
25
23
|
# method from method totals. Without line info there is nothing to
|
|
26
24
|
# report against, so such a method stays skipped.
|
|
27
25
|
def skipped?
|
|
28
|
-
return @skipped if
|
|
26
|
+
return @skipped if instance_variable_defined?(:@skipped)
|
|
29
27
|
|
|
30
28
|
@skipped = lines.empty?
|
|
31
29
|
end
|
|
32
30
|
|
|
33
|
-
# Flag the method as skipped directly, without going through its lines.
|
|
34
31
|
def skipped!
|
|
35
32
|
@skipped = true
|
|
36
33
|
end
|
|
@@ -40,10 +37,9 @@ module SimpleCov
|
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
def lines
|
|
43
|
-
@lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
|
|
40
|
+
@lines ||= (start_line && end_line) ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
|
|
44
41
|
end
|
|
45
42
|
|
|
46
|
-
# Whether this method's source range intersects the given inclusive line range.
|
|
47
43
|
def overlaps_with?(line_range)
|
|
48
44
|
return false unless start_line && end_line
|
|
49
45
|
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
# Builds the `SourceFile::Method` objects for a source file from
|
|
6
|
-
# the raw method-coverage data. Applies the
|
|
7
|
-
# `ignore_methods :eval_generated` filter and marks methods inside
|
|
8
5
|
# `# simplecov:disable` / `# :nocov:` chunks as skipped.
|
|
9
6
|
class MethodBuilder
|
|
10
7
|
def initialize(source_file)
|
|
@@ -18,20 +15,17 @@ module SimpleCov
|
|
|
18
15
|
info = RubyDataParser.call(info)
|
|
19
16
|
next if eval_generated_to_ignore?(info)
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
Method.new(@source_file, info, hit_count)
|
|
22
19
|
end
|
|
23
20
|
|
|
24
21
|
process_skipped(methods)
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
private
|
|
28
25
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# from the real-source `def` set is eval-generated. Only consulted
|
|
33
|
-
# when the user has opted in via
|
|
34
|
-
# `SimpleCov.ignore_methods :eval_generated`. See #1046.
|
|
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).
|
|
35
29
|
def eval_generated_to_ignore?(info)
|
|
36
30
|
return false unless SimpleCov.ignored_method?(:eval_generated)
|
|
37
31
|
|
|
@@ -42,18 +36,15 @@ module SimpleCov
|
|
|
42
36
|
# simplecov:enable branch
|
|
43
37
|
|
|
44
38
|
_class_name, name, start_line, * = info
|
|
45
|
-
!positions
|
|
39
|
+
!positions.fetch(:methods).include?([name, start_line])
|
|
46
40
|
end
|
|
47
41
|
|
|
48
42
|
def process_skipped(methods)
|
|
49
43
|
chunks = @source_file.skip_chunks_for(:method)
|
|
50
|
-
return methods if chunks.empty?
|
|
51
44
|
|
|
52
45
|
methods.each do |method|
|
|
53
46
|
method.skipped! if chunks.any? { |chunk| method.overlaps_with?(chunk) }
|
|
54
47
|
end
|
|
55
|
-
|
|
56
|
-
methods
|
|
57
48
|
end
|
|
58
49
|
end
|
|
59
50
|
end
|
|
@@ -4,106 +4,99 @@ require "ripper"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
class SourceFile
|
|
7
|
-
# `Coverage.result` reports condition and method keys as Ruby
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
13
|
module RubyDataParser
|
|
14
|
-
|
|
14
|
+
extend self
|
|
15
15
|
|
|
16
|
-
# Tests use the real data structures (except for integration tests)
|
|
17
|
-
# so no need to put them through here.
|
|
18
|
-
#
|
|
19
16
|
# String parses are memoized: `Combine::BranchesCombiner` and
|
|
20
17
|
# `Combine::MethodsCombiner` derive a merge identity from every key of
|
|
21
18
|
# both sides on every pairwise merge, so collating N resultsets parses
|
|
22
|
-
# each key string N-1 times
|
|
23
|
-
# large collate.
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# element-wise (String class names included, not just the tuple):
|
|
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:
|
|
28
24
|
# every caller destructures without mutating, and sharing one array
|
|
29
|
-
# across callers must stay that way. The cache key needs no such
|
|
30
|
-
#
|
|
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.
|
|
31
27
|
def call(structure)
|
|
32
28
|
return structure if structure.is_a?(Array)
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
parse_cache[string] ||= parse_array_string(string).each(&:freeze).freeze
|
|
30
|
+
parse_cache[structure] ||= parse_array_string(structure).each(&:freeze).freeze
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
def parse_cache
|
|
39
34
|
@parse_cache ||= {} #: Hash[String, untyped]
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
#
|
|
37
|
+
# Parses a string like '[:if, 0, 3, 4, 3, 21]' or
|
|
43
38
|
# '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
|
|
44
39
|
def parse_array_string(str)
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# the shape simplecov-on-simplecov method-coverage keys take).
|
|
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.
|
|
49
43
|
sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
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
54
|
|
|
55
|
-
Array(array_node
|
|
55
|
+
Array(array_node.fetch(1)).map { |element| parse_element(element) }
|
|
56
56
|
end
|
|
57
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.
|
|
58
61
|
def parse_element(node)
|
|
59
|
-
case node
|
|
60
|
-
when :@int
|
|
61
|
-
when :
|
|
62
|
-
when :
|
|
63
|
-
when :
|
|
64
|
-
when :
|
|
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`
|
|
65
69
|
else
|
|
66
|
-
|
|
67
|
-
raise ArgumentError, "unexpected element: #{node.inspect}"
|
|
68
|
-
# simplecov:enable
|
|
70
|
+
raise ArgumentError, "unexpected element: #{node}"
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def parse_symbol_node(node)
|
|
77
|
-
if node[0] == :symbol_literal
|
|
78
|
-
node.dig(1, 1, 1).to_sym
|
|
79
|
-
else
|
|
80
|
-
unescape_ruby(string_literal_text(node[1])).to_sym
|
|
81
|
-
end
|
|
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)}"
|
|
82
78
|
end
|
|
83
79
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Array(string_content[1..]).map { |child| child[1] }.join
|
|
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)
|
|
89
84
|
end
|
|
90
85
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
86
|
+
# Undoes the same backslash-prefix escapes the previous hand-rolled parser
|
|
87
|
+
# undid: `\X` -> `X` for any X.
|
|
93
88
|
def unescape_ruby(raw)
|
|
94
|
-
raw.gsub(/\\(.)/) {
|
|
89
|
+
raw.gsub(/\\(.)/) { Regexp.last_match(1) }
|
|
95
90
|
end
|
|
96
91
|
|
|
97
|
-
# Method coverage keys can contain inspect-format class references
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# stopping at the first `>` leaves a dangling `>` that fails both
|
|
104
|
-
# Ripper passes and crashes the merge.
|
|
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.
|
|
105
98
|
def quote_inspected_class_segments(str)
|
|
106
|
-
str.gsub(/#<(?:[^<>]|\g<0>)
|
|
99
|
+
str.gsub(/#<(?:[^<>]|\g<0>)+>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
|
|
107
100
|
end
|
|
108
101
|
end
|
|
109
102
|
end
|
|
@@ -2,15 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
class SourceFile
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# * The deprecated `# :nocov:` block toggle (lines wrapped between
|
|
9
|
-
# even-numbered pairs of nocov markers are excluded from line and
|
|
10
|
-
# branch coverage).
|
|
11
|
-
# * `# simplecov:disable` / `# simplecov:enable` block directives,
|
|
12
|
-
# which can be scoped per-criterion (`# simplecov:disable branch`,
|
|
13
|
-
# etc.) — see `SimpleCov::Directive`.
|
|
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.
|
|
14
8
|
class SkipChunks
|
|
15
9
|
@nocov_warned = Set.new
|
|
16
10
|
class << self
|
|
@@ -22,16 +16,15 @@ module SimpleCov
|
|
|
22
16
|
@src = src
|
|
23
17
|
end
|
|
24
18
|
|
|
25
|
-
# Every criterion honors the deprecated all-criteria
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
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)
|
|
30
25
|
nocov_chunks + directive_chunks.fetch(criterion)
|
|
31
26
|
end
|
|
32
27
|
|
|
33
|
-
# Ranges of 1-based line numbers (see `with_index(1)` below);
|
|
34
|
-
# consumers subtract 1 to index into the zero-based lines array.
|
|
35
28
|
def nocov_chunks
|
|
36
29
|
@nocov_chunks ||= build_nocov_chunks
|
|
37
30
|
end
|
|
@@ -40,25 +33,23 @@ module SimpleCov
|
|
|
40
33
|
@directive_chunks ||= Directive.disabled_ranges(@src)
|
|
41
34
|
end
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
private
|
|
44
37
|
|
|
38
|
+
# An uneven number of nocovs is assumed to run to the end of the file. It
|
|
39
|
+
# cannot be handled inside the each_slice because JRuby behaves differently
|
|
40
|
+
# there (jruby/jruby#6048).
|
|
45
41
|
def build_nocov_chunks
|
|
46
|
-
no_cov_lines = @src.
|
|
42
|
+
no_cov_lines = @src.filter_map.with_index(1) do |line_src, index|
|
|
43
|
+
index if LinesClassifier.no_cov_line?(line_src)
|
|
44
|
+
end
|
|
47
45
|
|
|
48
|
-
warn_nocov_deprecation(no_cov_lines.first
|
|
46
|
+
warn_nocov_deprecation(no_cov_lines.first) if no_cov_lines.any?
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
# end of the file, the source doesn't really matter. Can't deal
|
|
52
|
-
# with this within the each_slice due to differing behavior in
|
|
53
|
-
# JRuby: jruby/jruby#6048
|
|
54
|
-
no_cov_lines << ["", @src.size] if no_cov_lines.size.odd?
|
|
48
|
+
no_cov_lines << @src.size if no_cov_lines.size.odd?
|
|
55
49
|
|
|
56
|
-
no_cov_lines.each_slice(2).map
|
|
57
|
-
index_start..index_end
|
|
58
|
-
end
|
|
50
|
+
no_cov_lines.each_slice(2).map { |chunk_start, chunk_end| chunk_start..chunk_end }
|
|
59
51
|
end
|
|
60
52
|
|
|
61
|
-
# Emit a one-time-per-file deprecation warning pointing the user
|
|
62
53
|
# at the `# simplecov:disable` / `# simplecov:enable` replacement.
|
|
63
54
|
def warn_nocov_deprecation(first_line_number)
|
|
64
55
|
return unless self.class.nocov_warned.add?(@filename)
|