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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
#
|
|
5
|
+
# Records each test as a context in a `ContextMap`. Enabled by the
|
|
6
|
+
# `track_tests` configuration; installed into RSpec by
|
|
7
|
+
# `SimpleCov.start_tracking` and into Minitest by the minitest plugin.
|
|
8
|
+
# Any other runner can wrap its own units of work with
|
|
9
|
+
# `SimpleCov.track_test`.
|
|
10
|
+
#
|
|
11
|
+
# The contract is `track(id) { }`: attribute the lines the block executed to
|
|
12
|
+
# `id`, or abstain (`recorded_map` nil), never guess. How the lines are
|
|
13
|
+
# obtained is this class's private business, and today's engine is a
|
|
14
|
+
# peek-diff: sample `Coverage.peek_result` around the block and take the
|
|
15
|
+
# lines whose execution count grew. If Ruby's Coverage ever grows native
|
|
16
|
+
# contexts the engine is what gets replaced; the contract, the `ContextMap`,
|
|
17
|
+
# and everything downstream stay.
|
|
18
|
+
#
|
|
19
|
+
# Two limits are the peek-diff engine's, not the contract's. It needs
|
|
20
|
+
# per-line execution counts, which is why `track_tests` refuses to run under
|
|
21
|
+
# `:oneshot_line`. And tests overlapping across threads cannot be told
|
|
22
|
+
# apart, because the counters being diffed are process-global.
|
|
23
|
+
#
|
|
24
|
+
#
|
|
25
|
+
class TestTracker
|
|
26
|
+
attr_reader :map
|
|
27
|
+
|
|
28
|
+
# The context id everything since the last boundary belongs to, and the
|
|
29
|
+
# peek that boundary took.
|
|
30
|
+
Segment = Struct.new(:id, :opening)
|
|
31
|
+
private_constant :Segment
|
|
32
|
+
|
|
33
|
+
def initialize(root_regex: UselessResultsRemover.root_regex, granularity: :test)
|
|
34
|
+
unless Configuration::TRACK_TESTS_GRANULARITIES.include?(granularity)
|
|
35
|
+
raise ArgumentError, "unknown granularity #{granularity.inspect}, " \
|
|
36
|
+
"expected one of #{Configuration::TRACK_TESTS_GRANULARITIES.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@map = ContextMap.new
|
|
40
|
+
# Only files under the project root are attributed, judged by the same
|
|
41
|
+
# regex the report's own universe is rooted with. A peek covers every
|
|
42
|
+
# file the process loaded, every gem included, and diffing all of them on
|
|
43
|
+
# every test is the cost that would make tracking unaffordable.
|
|
44
|
+
@delta = Delta.new(root_regex: root_regex)
|
|
45
|
+
@granularity = granularity
|
|
46
|
+
@lock = Mutex.new
|
|
47
|
+
@depth = 0
|
|
48
|
+
@poisoned = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Records the lines the block executed as covered by `test_id`'s context
|
|
52
|
+
# (the test itself at :test granularity, its file at :file). A failing
|
|
53
|
+
# test's lines are still attributed when its segment closes.
|
|
54
|
+
def track(test_id)
|
|
55
|
+
id = context_id(test_id)
|
|
56
|
+
nested_opening = begin_track(id)
|
|
57
|
+
begin
|
|
58
|
+
yield
|
|
59
|
+
ensure
|
|
60
|
+
settle(id, nested_opening)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def poisoned?
|
|
65
|
+
@poisoned
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# nil once poisoned: a poisoned recording must vanish like one that never
|
|
69
|
+
# happened, so the merge's all-or-nothing rule drops the run's map instead
|
|
70
|
+
# of keeping a wrong one. Flushes the open segment, so the answer carries
|
|
71
|
+
# every finished track. At process exit `Coverage.result` has already
|
|
72
|
+
# stopped measurement, so result-building passes the final coverage it just
|
|
73
|
+
# took as the closing snapshot instead of peeking again.
|
|
74
|
+
def recorded_map(closing: nil)
|
|
75
|
+
return nil if poisoned?
|
|
76
|
+
|
|
77
|
+
flush_segment(closing)
|
|
78
|
+
@map
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Same-thread reentrancy is fine, since a line the inner test executed was
|
|
84
|
+
# executed on the outer test's watch too, but a second thread means
|
|
85
|
+
# concurrent tests and `Coverage`'s counters are process-global: their
|
|
86
|
+
# deltas cannot be told apart, so recording shuts off rather than
|
|
87
|
+
# misattribute.
|
|
88
|
+
def begin_track(id)
|
|
89
|
+
outermost = note_entry
|
|
90
|
+
return nil if @poisoned
|
|
91
|
+
return Coverage.peek_result unless outermost
|
|
92
|
+
|
|
93
|
+
open_segment(id)
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def note_entry
|
|
98
|
+
@lock.synchronize do
|
|
99
|
+
if @depth.zero?
|
|
100
|
+
@owner = Thread.current
|
|
101
|
+
elsif !@owner.equal?(Thread.current)
|
|
102
|
+
poison
|
|
103
|
+
end
|
|
104
|
+
@depth += 1
|
|
105
|
+
@depth.equal?(1)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A nested track records immediately against its own opening peek; an
|
|
110
|
+
# outermost one leaves its segment open for the next boundary. The owner
|
|
111
|
+
# needs no clearing: the next track to find the depth back at zero takes
|
|
112
|
+
# ownership itself.
|
|
113
|
+
def settle(id, nested_opening)
|
|
114
|
+
@map.record(id, @delta.call(nested_opening, Coverage.peek_result)) if nested_opening && !@poisoned
|
|
115
|
+
ensure
|
|
116
|
+
@lock.synchronize { @depth -= 1 }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Peeking is the dominant cost of tracking (the copy covers every file the
|
|
120
|
+
# process loaded, branch and method tables included), so recording settles
|
|
121
|
+
# at segment boundaries: consecutive tracks with one context id share one
|
|
122
|
+
# open segment and pay nothing in between. In-root code that runs between
|
|
123
|
+
# two segments is attributed to the later one, the documented price.
|
|
124
|
+
def open_segment(id)
|
|
125
|
+
return if @segment&.id.eql?(id)
|
|
126
|
+
|
|
127
|
+
boundary = Coverage.peek_result
|
|
128
|
+
close_segment(boundary)
|
|
129
|
+
@segment = Segment.new(id, boundary)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def close_segment(closing)
|
|
133
|
+
segment = @segment
|
|
134
|
+
@map.record(segment.id, @delta.call(segment.opening, closing)) if segment
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def flush_segment(closing)
|
|
138
|
+
return unless @segment
|
|
139
|
+
|
|
140
|
+
close_segment(closing || Coverage.peek_result)
|
|
141
|
+
@segment = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# An id with no `:line` tail (an exotic runner's fallback) stays whole.
|
|
145
|
+
def context_id(test_id)
|
|
146
|
+
return test_id unless @granularity.equal?(:file)
|
|
147
|
+
|
|
148
|
+
path, sep, tail = test_id.rpartition(":")
|
|
149
|
+
(sep.empty? || !tail.match?(/\A\d+\z/)) ? test_id : path
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def poison
|
|
153
|
+
return if @poisoned
|
|
154
|
+
|
|
155
|
+
@poisoned = true
|
|
156
|
+
return unless SimpleCov.print_errors
|
|
157
|
+
|
|
158
|
+
warn "[SimpleCov]: tests are running concurrently in this process (parallel threads), " \
|
|
159
|
+
"so per-test coverage cannot be attributed. Dropping this process's test map."
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -3,46 +3,36 @@
|
|
|
3
3
|
require_relative "simulate_coverage"
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
|
-
# Fills in coverage for files that were tracked
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# This is the mechanism; the merge-time policy of *when* to inject and
|
|
11
|
-
# which criteria the simulated files carry lives in
|
|
12
|
-
# `ResultMerger::UnloadedFiles` — easy names to confuse.
|
|
6
|
+
# Fills in coverage for files that were tracked but never loaded, so they
|
|
7
|
+
# count toward the denominators instead of being absent from the report.
|
|
8
|
+
# This is the mechanism; the merge-time policy of when to inject lives in
|
|
9
|
+
# `ResultMerger::UnloadedFiles`.
|
|
13
10
|
#
|
|
14
11
|
# Everything arrives as arguments rather than being read from the SimpleCov
|
|
15
|
-
# singleton
|
|
16
|
-
#
|
|
17
|
-
# configuration: a standalone `collate` never ran `SimpleCov.start`, so it has
|
|
18
|
-
# no `cover` glob of its own and takes the tracked paths from the resultsets
|
|
19
|
-
# instead. See #1250.
|
|
12
|
+
# singleton, because the merge performs this injection on behalf of
|
|
13
|
+
# processes whose configuration it does not necessarily share (#1250).
|
|
20
14
|
module UnloadedFileInjector
|
|
21
|
-
|
|
15
|
+
extend self
|
|
22
16
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
17
|
+
# Globs expand relative to `root` rather than `Dir.pwd`: runners that chdir
|
|
18
|
+
# would otherwise silently miss files and produce a different set per
|
|
19
|
+
# environment (#1106).
|
|
20
|
+
#
|
|
27
21
|
# `reject:` are the producing process's path-decidable filters. Paths its
|
|
28
22
|
# own report would exclude must not be recorded as tracked, or a merge that
|
|
29
|
-
# does not share the configuration would simulate them back in.
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# source file and may consult coverage that does not exist yet, so those
|
|
33
|
-
# still fall to the merging process's filter chain. See #1250.
|
|
23
|
+
# does not share the configuration would simulate them back in. A block
|
|
24
|
+
# filter is handed the source file and may consult coverage that does not
|
|
25
|
+
# exist yet, so those still fall to the merging process's filter chain.
|
|
34
26
|
def discover(globs, root:, reject: [])
|
|
35
27
|
paths = globs.compact
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
.flat_map { |glob| Dir.glob(glob, base: root) }
|
|
29
|
+
.uniq
|
|
30
|
+
.map { |path| File.expand_path(path, root) }
|
|
39
31
|
return paths if reject.empty?
|
|
40
32
|
|
|
41
33
|
paths.reject { |path| rejected?(path, reject) }
|
|
42
34
|
end
|
|
43
35
|
|
|
44
|
-
# `SourceFile` reads source and builds lines lazily, so a path-only filter
|
|
45
|
-
# answers without anything here touching disk.
|
|
46
36
|
def rejected?(path, filters)
|
|
47
37
|
candidate = SourceFile.new(path, NO_COVERAGE_YET, loaded: false)
|
|
48
38
|
filters.any? { |filter| filter.matches?(candidate) }
|
|
@@ -52,12 +42,9 @@ module SimpleCov
|
|
|
52
42
|
NO_COVERAGE_YET = no_coverage_yet.freeze
|
|
53
43
|
private_constant :NO_COVERAGE_YET
|
|
54
44
|
|
|
55
|
-
#
|
|
56
|
-
# Paths that are present are left alone, whoever put them there, which is
|
|
45
|
+
# Paths already present are left alone, whoever put them there, which is
|
|
57
46
|
# what lets this run over resultsets a previous SimpleCov already injected
|
|
58
47
|
# into without double-counting or overwriting real data.
|
|
59
|
-
#
|
|
60
|
-
# Returns the augmented coverage and the set of paths that were added.
|
|
61
48
|
def call(coverage, paths, synthesize:, lines:)
|
|
62
49
|
injected = Set.new
|
|
63
50
|
augmented = coverage.dup
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# run never make it into the formatted result.
|
|
4
|
+
# Drops coverage entries whose paths live outside `SimpleCov.root`, so vendored
|
|
5
|
+
# gems, stdlib files, and anything else touched during the run never make it
|
|
6
|
+
# into the formatted result.
|
|
8
7
|
module UselessResultsRemover
|
|
9
8
|
def self.call(coverage_result)
|
|
10
9
|
coverage_result.select { |path, _coverage| path.match?(root_regex) }
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
# The `/i` flag covers case-insensitive matches on Windows
|
|
12
|
+
# The `/i` flag covers case-insensitive matches on Windows and macOS-HFS+,
|
|
14
13
|
# where the on-disk path's case can differ from `SimpleCov.root`'s.
|
|
15
14
|
def self.root_regex
|
|
16
15
|
root = SimpleCov.root
|
|
17
|
-
return @root_regex if root
|
|
16
|
+
return @root_regex if root.eql?(@root_regex_root)
|
|
18
17
|
|
|
19
18
|
@root_regex_root = root
|
|
20
19
|
@root_regex = /\A#{Regexp.escape(root.chomp(File::SEPARATOR) + File::SEPARATOR)}/i
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def self.root_regx
|
|
23
|
+
Deprecation.warn("`SimpleCov::UselessResultsRemover.root_regx` is deprecated. " \
|
|
24
|
+
"Replace with `root_regex`.")
|
|
24
25
|
root_regex
|
|
25
26
|
end
|
|
26
27
|
end
|
data/lib/simplecov/version.rb
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module ViewCoverage
|
|
5
|
+
#
|
|
6
|
+
# Compiles a single ActionView template: builds a `Template` for the file on
|
|
7
|
+
# disk and defines its method into a throwaway module, without ever
|
|
8
|
+
# rendering it.
|
|
9
|
+
#
|
|
10
|
+
# Defining the method is the whole trick. `Template#compile` hands the
|
|
11
|
+
# handler's generated Ruby to `module_eval(source, identifier, offset)`,
|
|
12
|
+
# and evaluating a `def` records the body as executable-but-unexecuted
|
|
13
|
+
# rather than running it, so `Coverage` comes away with a zeroed line array
|
|
14
|
+
# and a full set of branch tuples for the template.
|
|
15
|
+
#
|
|
16
|
+
# Never compile a template the suite already rendered. Evaluating the same
|
|
17
|
+
# identifier a second time replaces that file's entry in `Coverage` rather
|
|
18
|
+
# than adding to it, so the recompile would zero the hits the run actually
|
|
19
|
+
# earned and leave a duplicate set of branch tuples behind. `ViewCoverage`
|
|
20
|
+
# filters those out before calling here.
|
|
21
|
+
#
|
|
22
|
+
module TemplateCompiler
|
|
23
|
+
extend self
|
|
24
|
+
|
|
25
|
+
# False in any project that doesn't load ActionView, which is the common
|
|
26
|
+
# case and not an error: view coverage simply has nothing to do there.
|
|
27
|
+
def available?
|
|
28
|
+
!defined?(::ActionView::Template).nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Answers whether the compile produced coverage data. A template that
|
|
32
|
+
# doesn't compile can't render either, so the suite has bigger problems
|
|
33
|
+
# than its coverage, but the report is not the place to discover that and
|
|
34
|
+
# a broken view must not take the whole run down.
|
|
35
|
+
def call(path)
|
|
36
|
+
template = build_template(path, File.binread(path))
|
|
37
|
+
return false unless template
|
|
38
|
+
|
|
39
|
+
compile_into_throwaway_module(template)
|
|
40
|
+
true
|
|
41
|
+
rescue StandardError, ScriptError => e
|
|
42
|
+
warn "[SimpleCov]: Skipping #{path}, which did not compile (#{e.class})"
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ActionView keeps `compile` private, and the module it compiles into is
|
|
47
|
+
# thrown away: what is wanted is the coverage the compile records, not the
|
|
48
|
+
# method it defines.
|
|
49
|
+
#
|
|
50
|
+
# mutant:disable -- `send` and `__send__` are the same call.
|
|
51
|
+
def compile_into_throwaway_module(template)
|
|
52
|
+
template.send(:compile, Module.new)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The format the template renders, taken from the extension in front of the
|
|
56
|
+
# handler's: `show.html.erb` is `:html`. It reaches the handler's codegen
|
|
57
|
+
# (an HTML template escapes interpolations, a text one does not), but
|
|
58
|
+
# never the line structure, so a template named without one compiles fine
|
|
59
|
+
# as nil.
|
|
60
|
+
def format_for(path)
|
|
61
|
+
segments = File.basename(path).split(".")
|
|
62
|
+
return nil if segments.length < 3
|
|
63
|
+
|
|
64
|
+
segments.fetch(-2).to_sym
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The single place ActionView is named. The handler lookup is the one the
|
|
68
|
+
# resolver performs, so a template language the project has registered a
|
|
69
|
+
# handler for compiles here the way it does when rendered. Nil when
|
|
70
|
+
# nothing is registered for the extension, which is how a default glob
|
|
71
|
+
# naming `.haml` behaves in a project that has no Haml: ActionView would
|
|
72
|
+
# hand back the raw handler and the template would report as static text.
|
|
73
|
+
def build_template(path, source)
|
|
74
|
+
extension = File.extname(path).delete_prefix(".")
|
|
75
|
+
handler = ActionView::Template.registered_template_handler(extension)
|
|
76
|
+
return nil unless handler
|
|
77
|
+
|
|
78
|
+
no_locals = [] #: Array[Symbol]
|
|
79
|
+
ActionView::Template.new(source, path, handler, locals: no_locals, format: format_for(path))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "view_coverage/template_compiler"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
# Brings ActionView templates into the report, for projects that opt in with
|
|
7
|
+
# `cover_views`.
|
|
8
|
+
#
|
|
9
|
+
# Rendering a template is already enough to measure it: ActionView compiles
|
|
10
|
+
# each one with `module_eval(source, identifier, offset)` where `identifier`
|
|
11
|
+
# is the template's own path, and the offset cancels the `def` line its
|
|
12
|
+
# wrapper adds, so eval coverage attributes the generated code back to the
|
|
13
|
+
# `.erb` file at its own line numbers.
|
|
14
|
+
#
|
|
15
|
+
# A template no test renders is never compiled, so `Coverage` never hears
|
|
16
|
+
# about it and the report omits it entirely. That is the flattering
|
|
17
|
+
# direction to be wrong in, so every template the run didn't reach is
|
|
18
|
+
# compiled here instead, which lands it in the report at 0%.
|
|
19
|
+
module ViewCoverage
|
|
20
|
+
extend self
|
|
21
|
+
|
|
22
|
+
# Called once per process, immediately before `Coverage.result`. It has to
|
|
23
|
+
# happen here rather than at the merge point, where unloaded `.rb` files are
|
|
24
|
+
# injected, because compiling a template needs ActionView and the merging
|
|
25
|
+
# process need not have it loaded. A standalone `simplecov merge` never
|
|
26
|
+
# does.
|
|
27
|
+
def compile_unrendered
|
|
28
|
+
return [] unless enabled?
|
|
29
|
+
|
|
30
|
+
rendered = measured_paths
|
|
31
|
+
discover.reject { |path| rendered.include?(path) }
|
|
32
|
+
.select { |path| TemplateCompiler.call(path) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def enabled?
|
|
36
|
+
SimpleCov.view_coverage? && Coverage.running? && TemplateCompiler.available?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Expanded against `SimpleCov.root` rather than the working directory, and
|
|
40
|
+
# filtered through the same path-only filters the rest of the report honors,
|
|
41
|
+
# so `skip "app/views/admin"` keeps those templates out instead of compiling
|
|
42
|
+
# them only to drop them later.
|
|
43
|
+
def discover
|
|
44
|
+
globs = SimpleCov.view_globs || [] #: Array[String?]
|
|
45
|
+
UnloadedFileInjector.discover(globs, root: SimpleCov.root, reject: SimpleCov.filters.select(&:path_only?))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The files `Coverage` is already holding data for, which for a template
|
|
49
|
+
# means the run rendered it. `peek_result` copies the whole coverage state
|
|
50
|
+
# to answer, so this stays a once-per-process call.
|
|
51
|
+
def measured_paths
|
|
52
|
+
Coverage.peek_result.keys.to_set
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|