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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +165 -57
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/badge/svg.rb +70 -0
  13. data/lib/simplecov/cli/badge.rb +62 -0
  14. data/lib/simplecov/cli/clean.rb +23 -24
  15. data/lib/simplecov/cli/command_helpers.rb +55 -19
  16. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  17. data/lib/simplecov/cli/completions.rb +83 -0
  18. data/lib/simplecov/cli/coverage.rb +25 -28
  19. data/lib/simplecov/cli/coverage_file.rb +58 -14
  20. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  21. data/lib/simplecov/cli/dead_code.rb +139 -0
  22. data/lib/simplecov/cli/diff/output.rb +45 -0
  23. data/lib/simplecov/cli/diff.rb +35 -67
  24. data/lib/simplecov/cli/dotfile.rb +83 -38
  25. data/lib/simplecov/cli/git.rb +41 -0
  26. data/lib/simplecov/cli/history/output.rb +135 -0
  27. data/lib/simplecov/cli/history.rb +67 -0
  28. data/lib/simplecov/cli/merge.rb +34 -39
  29. data/lib/simplecov/cli/open.rb +12 -15
  30. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  31. data/lib/simplecov/cli/patch/output.rb +135 -0
  32. data/lib/simplecov/cli/patch.rb +169 -0
  33. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  34. data/lib/simplecov/cli/ratchet.rb +83 -0
  35. data/lib/simplecov/cli/report.rb +18 -24
  36. data/lib/simplecov/cli/run.rb +8 -9
  37. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  38. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  39. data/lib/simplecov/cli/serve.rb +27 -24
  40. data/lib/simplecov/cli/show/annotator.rb +87 -0
  41. data/lib/simplecov/cli/show/sweep.rb +36 -0
  42. data/lib/simplecov/cli/show.rb +133 -0
  43. data/lib/simplecov/cli/status/facts.rb +87 -0
  44. data/lib/simplecov/cli/status.rb +93 -0
  45. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  46. data/lib/simplecov/cli/tests.rb +148 -0
  47. data/lib/simplecov/cli/uncovered/misses.rb +37 -0
  48. data/lib/simplecov/cli/uncovered.rb +70 -32
  49. data/lib/simplecov/cli/usage.rb +152 -0
  50. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  51. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  52. data/lib/simplecov/cli/watch/poller.rb +45 -0
  53. data/lib/simplecov/cli/watch/session.rb +134 -0
  54. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  55. data/lib/simplecov/cli/watch.rb +85 -0
  56. data/lib/simplecov/cli.rb +43 -92
  57. data/lib/simplecov/color.rb +21 -35
  58. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  59. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  60. data/lib/simplecov/combine/identity_interner.rb +10 -13
  61. data/lib/simplecov/combine/interned_counts.rb +7 -9
  62. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  63. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  64. data/lib/simplecov/combine/results_combiner.rb +4 -21
  65. data/lib/simplecov/combine.rb +0 -6
  66. data/lib/simplecov/command_guesser.rb +20 -34
  67. data/lib/simplecov/configuration/baseline.rb +31 -0
  68. data/lib/simplecov/configuration/coverage.rb +91 -79
  69. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  70. data/lib/simplecov/configuration/deprecations.rb +21 -0
  71. data/lib/simplecov/configuration/eval_coverage.rb +6 -10
  72. data/lib/simplecov/configuration/filters.rb +40 -73
  73. data/lib/simplecov/configuration/formatting.rb +74 -83
  74. data/lib/simplecov/configuration/groups.rb +19 -13
  75. data/lib/simplecov/configuration/history.rb +44 -0
  76. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  77. data/lib/simplecov/configuration/merging.rb +94 -62
  78. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  79. data/lib/simplecov/configuration/production.rb +23 -0
  80. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  81. data/lib/simplecov/configuration/thresholds.rb +33 -62
  82. data/lib/simplecov/configuration/view_coverage.rb +38 -0
  83. data/lib/simplecov/configuration.rb +70 -76
  84. data/lib/simplecov/configuration_error.rb +5 -0
  85. data/lib/simplecov/context_map/union.rb +81 -0
  86. data/lib/simplecov/context_map.rb +187 -0
  87. data/lib/simplecov/coverage_json.rb +2 -5
  88. data/lib/simplecov/coverage_statistics.rb +14 -24
  89. data/lib/simplecov/coverage_violations.rb +157 -64
  90. data/lib/simplecov/current_run.rb +61 -0
  91. data/lib/simplecov/defaults.rb +4 -23
  92. data/lib/simplecov/deprecation.rb +45 -27
  93. data/lib/simplecov/directive.rb +46 -62
  94. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  95. data/lib/simplecov/exit_codes/check.rb +17 -11
  96. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  97. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  98. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  99. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  100. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  101. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  102. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  103. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  104. data/lib/simplecov/exit_codes.rb +17 -7
  105. data/lib/simplecov/exit_handling.rb +94 -59
  106. data/lib/simplecov/file_list.rb +20 -51
  107. data/lib/simplecov/filter.rb +43 -66
  108. data/lib/simplecov/formatter/base.rb +38 -51
  109. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  110. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  111. data/lib/simplecov/formatter/html_formatter/public/index.html +17 -10
  112. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  113. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  114. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  115. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  116. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  117. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  118. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  119. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  120. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  121. data/lib/simplecov/formatter.rb +14 -10
  122. data/lib/simplecov/group_names.rb +13 -12
  123. data/lib/simplecov/history.rb +115 -0
  124. data/lib/simplecov/last_run.rb +6 -9
  125. data/lib/simplecov/lines_classifier.rb +31 -16
  126. data/lib/simplecov/load_global_config.rb +7 -7
  127. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  128. data/lib/simplecov/parallel_adapters/generic.rb +7 -18
  129. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  130. data/lib/simplecov/parallel_adapters.rb +24 -53
  131. data/lib/simplecov/parallel_coordination.rb +39 -49
  132. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  133. data/lib/simplecov/parallel_result_merger.rb +67 -126
  134. data/lib/simplecov/process.rb +14 -32
  135. data/lib/simplecov/production/error.rb +11 -0
  136. data/lib/simplecov/production/file_sink.rb +130 -0
  137. data/lib/simplecov/production.rb +249 -0
  138. data/lib/simplecov/profiles/rails.rb +1 -12
  139. data/lib/simplecov/profiles/root_filter.rb +0 -4
  140. data/lib/simplecov/profiles/strict.rb +9 -19
  141. data/lib/simplecov/profiles.rb +11 -33
  142. data/lib/simplecov/report_deferral.rb +11 -15
  143. data/lib/simplecov/report_stamp.rb +6 -9
  144. data/lib/simplecov/result/filter_config.rb +19 -0
  145. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  146. data/lib/simplecov/result/serialization.rb +43 -0
  147. data/lib/simplecov/result/source_file_builder.rb +7 -12
  148. data/lib/simplecov/result.rb +68 -117
  149. data/lib/simplecov/result_adapter.rb +58 -68
  150. data/lib/simplecov/result_merger/contexts.rb +22 -0
  151. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  152. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  153. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  154. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  155. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  156. data/lib/simplecov/result_merger.rb +59 -58
  157. data/lib/simplecov/result_processing.rb +74 -119
  158. data/lib/simplecov/run_identity.rb +16 -18
  159. data/lib/simplecov/simulate_coverage.rb +35 -50
  160. data/lib/simplecov/source_file/branch.rb +8 -33
  161. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  162. data/lib/simplecov/source_file/builder_context.rb +6 -13
  163. data/lib/simplecov/source_file/line.rb +13 -33
  164. data/lib/simplecov/source_file/line_builder.rb +9 -15
  165. data/lib/simplecov/source_file/method.rb +3 -7
  166. data/lib/simplecov/source_file/method_builder.rb +6 -15
  167. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  168. data/lib/simplecov/source_file/skip_chunks.rb +19 -28
  169. data/lib/simplecov/source_file/source_loader.rb +45 -35
  170. data/lib/simplecov/source_file/statistics.rb +10 -9
  171. data/lib/simplecov/source_file.rb +21 -51
  172. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
  173. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  174. data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
  175. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  176. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  177. data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
  178. data/lib/simplecov/static_coverage_extractor.rb +46 -62
  179. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  180. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  181. data/lib/simplecov/test_tracker/delta.rb +69 -0
  182. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  183. data/lib/simplecov/test_tracker.rb +162 -0
  184. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  185. data/lib/simplecov/useless_results_remover.rb +7 -6
  186. data/lib/simplecov/version.rb +1 -1
  187. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  188. data/lib/simplecov/view_coverage.rb +55 -0
  189. data/lib/simplecov-html.rb +1 -0
  190. data/lib/simplecov.rb +59 -114
  191. data/lib/simplecov_json_formatter.rb +1 -0
  192. data/man/simplecov.1 +349 -0
  193. data/sig/simplecov.rbs +523 -595
  194. metadata +72 -7
  195. data/schemas/coverage-v1.0.schema.json +0 -306
  196. 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 (via `cover` / `track_files`)
7
- # but never loaded, so they count toward the denominators instead of being
8
- # absent from the report entirely.
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. The merge step performs this injection on behalf of the processes
16
- # that contributed to it, and it does not necessarily share their
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
- module_function
15
+ extend self
22
16
 
23
- # Expand `globs` into absolute paths, relative to `root` rather than
24
- # `Dir.pwd` test runners that chdir (or CI scripts that invoke the suite
25
- # from a subdirectory) would otherwise silently miss files and produce a
26
- # different set per environment. See #1106.
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. Before
30
- # injection moved to the merge the producer filtered them out of its own
31
- # result and they never reached a resultset. A block filter is handed the
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
- .flat_map { |glob| Dir.glob(glob, base: root) }
37
- .uniq
38
- .map { |path| File.expand_path(path, root) }
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
- # Add simulated coverage for every path `coverage` does not already carry.
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
- # Drop coverage entries whose paths live outside `SimpleCov.root` so the
5
- # report only reflects the project's own source. Vendored gems, stdlib
6
- # files, and anything else that happens to have been touched during the
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 / macOS-HFS+
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 == @root_regex_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Backward-compatibility shim: simplecov-html is now part of simplecov.
4
+
4
5
  require_relative "simplecov/formatter/html_formatter"