undercover 0.8.5 → 0.9.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.
@@ -32,7 +32,7 @@ module Undercover
32
32
  ].freeze
33
33
 
34
34
  DEFAULT_FILE_INCLUDE_GLOBS = %w[*.rb *.rake *.ru Rakefile].freeze
35
- DEFAULT_FILE_EXCLUDE_GLOBS = %w[test/* spec/* db/* config/* *_test.rb *_spec.rb].freeze
35
+ DEFAULT_FILE_EXCLUDE_GLOBS = %w[test/* spec/* db/* config/* *_test.rb *_spec.rb vendor/*].freeze
36
36
 
37
37
  FORMATS = %w[text json].freeze
38
38
 
@@ -41,19 +41,19 @@ module Undercover
41
41
  :path,
42
42
  :git_dir,
43
43
  :compare,
44
- :syntax_version,
44
+ :commits_only,
45
45
  :run_mode,
46
46
  :file_scope,
47
47
  :glob_allow_filters,
48
48
  :glob_reject_filters,
49
49
  :max_warnings_limit,
50
- :formatter
50
+ :formatter,
51
+ :no_hint
51
52
 
52
53
  def initialize
53
54
  @run_mode = DIFF_TRIGGER_LINE
54
55
  @file_scope = FILE_SCOPE_EXTENDED
55
56
  # set defaults
56
- self.path = '.'
57
57
  self.git_dir = '.git'
58
58
  self.glob_allow_filters = DEFAULT_FILE_INCLUDE_GLOBS
59
59
  self.glob_reject_filters = DEFAULT_FILE_EXCLUDE_GLOBS
@@ -61,7 +61,7 @@ module Undercover
61
61
  self.formatter = 'text'
62
62
  end
63
63
 
64
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
64
+ # rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
65
65
  def parse(args)
66
66
  args = build_opts(args)
67
67
 
@@ -74,10 +74,10 @@ module Undercover
74
74
  end
75
75
 
76
76
  opts.on_tail('--version', 'Show version') do
77
- # :nocov:
77
+ # simplecov:disable
78
78
  puts VERSION
79
79
  exit
80
- # :nocov:
80
+ # simplecov:enable
81
81
  end
82
82
 
83
83
  lcov_path_option(opts)
@@ -85,17 +85,18 @@ module Undercover
85
85
  project_path_option(opts)
86
86
  git_dir_option(opts)
87
87
  compare_option(opts)
88
+ commits_only_option(opts)
88
89
  ruby_syntax_option(opts)
89
90
  max_warnings_limit_option(opts)
90
91
  file_filters(opts)
91
92
  format_option(opts)
93
+ hint_option(opts)
92
94
  end.parse(args)
93
95
 
94
96
  guess_resultset_path unless simplecov_resultset
95
97
  guess_lcov_path unless lcov
96
98
  self
97
99
  end
98
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
99
100
 
100
101
  private
101
102
 
@@ -143,8 +144,12 @@ module Undercover
143
144
  end
144
145
  end
145
146
 
147
+ # Deprecated. The project root is now read from the git repository, and paths in the
148
+ # coverage report are matched onto it without needing to be told where either lives.
149
+ # Still parsed so that an existing .undercover config file does not fail to load.
146
150
  def project_path_option(parser)
147
- parser.on('-p', '--path path', 'Project directory') do |path|
151
+ parser.on('-p', '--path path', 'Deprecated, has no effect') do |path|
152
+ warn('undercover: --path is deprecated and ignored, it will be removed in a future release')
148
153
  self.path = path
149
154
  end
150
155
  end
@@ -163,11 +168,20 @@ module Undercover
163
168
  end
164
169
  end
165
170
 
171
+ def commits_only_option(parser)
172
+ desc = 'Only check committed changes after `--compare ref`, ignoring the index and working tree'
173
+ parser.on('--commits-only', desc) do
174
+ self.commits_only = true
175
+ end
176
+ end
177
+
178
+ # Deprecated. Kept so that a `-r ruby32` line in an existing .undercover file does not fail to load.
166
179
  def ruby_syntax_option(parser)
167
- versions = Imagen::AVAILABLE_RUBY_VERSIONS.sort.join(', ')
168
- desc = "Ruby syntax version, one of: #{versions}"
169
- parser.on('-r', '--ruby-syntax ver', desc) do |version|
170
- self.syntax_version = version.strip
180
+ parser.on('-r', '--ruby-syntax ver', 'Deprecated, has no effect') do
181
+ next if @ruby_syntax_warned
182
+
183
+ warn('--ruby-syntax is ignored, undercover parses with Prism; remove it from your config')
184
+ @ruby_syntax_warned = true
171
185
  end
172
186
  end
173
187
 
@@ -179,13 +193,12 @@ module Undercover
179
193
  end
180
194
 
181
195
  def guess_resultset_path
182
- cwd = Pathname.new(File.expand_path(path))
183
- try_path = File.join(cwd, 'coverage', 'coverage.json')
196
+ try_path = File.join(Dir.pwd, 'coverage', 'coverage.json')
184
197
  self.simplecov_resultset = try_path if File.exist?(try_path)
185
198
  end
186
199
 
187
200
  def guess_lcov_path
188
- cwd = Pathname.new(File.expand_path(path))
201
+ cwd = Pathname.new(Dir.pwd)
189
202
  try_path = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
190
203
  self.lcov = try_path if File.exist?(try_path)
191
204
  end
@@ -197,7 +210,8 @@ module Undercover
197
210
  self.glob_allow_filters = split_comma_separated_with_braces(comma_separated_globs)
198
211
  end
199
212
 
200
- desc = 'Skip files matching specified glob patterns (comma separated). Empty by default.'
213
+ desc = 'Skip files matching specified glob patterns (comma separated). ' \
214
+ "Defaults to '#{DEFAULT_FILE_EXCLUDE_GLOBS.join(',')}'"
201
215
  parser.on('-x', '--exclude-files globs', desc) do |comma_separated_globs|
202
216
  self.glob_reject_filters = split_comma_separated_with_braces(comma_separated_globs)
203
217
  end
@@ -209,5 +223,12 @@ module Undercover
209
223
  self.formatter = format
210
224
  end
211
225
  end
226
+
227
+ def hint_option(parser)
228
+ desc = 'Hide the one-line hint printed after the text report (or set UNDERCOVER_NO_HINT=1)'
229
+ parser.on('--no-hint', desc) do
230
+ self.no_hint = true
231
+ end
232
+ end
212
233
  end
213
234
  end
@@ -11,19 +11,12 @@ module Undercover
11
11
  def_delegators :node, :first_line, :last_line, :name
12
12
  def_delegators :coverage_adapter, :skipped?, :branch_label
13
13
 
14
- def initialize(node, coverage_adapter, file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
14
+ # @param coverage_index [CoverageIndex] the file's coverage, shared by all of its nodes
15
+ def initialize(node, coverage_adapter, file_path, coverage_index: nil)
15
16
  @node = node
16
17
  @coverage_adapter = coverage_adapter
17
- file_cov = coverage_adapter.coverage(file_path)
18
- @coverage = file_cov.select do |ln, _|
19
- if first_line == last_line
20
- ln == first_line
21
- elsif node.empty_def? || node.is_a?(Imagen::Node::Block)
22
- ln >= first_line && ln <= last_line # rubocop:disable Style/ComparableBetween
23
- else
24
- ln > first_line && ln < last_line
25
- end
26
- end
18
+ coverage_index ||= CoverageIndex.new(coverage_adapter.coverage(file_path))
19
+ @coverage = coverage_index.within(coverage_lines)
27
20
 
28
21
  @file_path = file_path
29
22
  @flagged = false
@@ -37,24 +30,18 @@ module Undercover
37
30
  @flagged
38
31
  end
39
32
 
40
- def uncovered?(line_no) # rubocop:disable Metrics/AbcSize
33
+ def uncovered?(line_no)
41
34
  return false if skipped?(file_path, line_no)
42
35
  return true if coverage.empty?
36
+ return true if uncovered_branch_lines.include?(line_no)
43
37
 
44
- # check branch coverage for line_no
45
- coverage.each do |ln, _block, _branch, cov|
46
- return true if ln == line_no && cov && cov != 'ignored' && cov.zero?
47
- end
48
-
49
- # check line coverage for line_no
50
- line_cov = coverage.select { |cov| cov.size == 2 }.find { |ln, _cov| ln == line_no }
51
- line_cov && line_cov[1].zero?
38
+ line_hits[line_no]&.zero?
52
39
  end
53
40
 
54
41
  # Method `coverage_f` returns the total coverage of this Undercover::Result
55
42
  # as a % value, taking into account missing branches. Line coverage will be counted
56
43
  # as 0 if any branch is untested.
57
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
44
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
58
45
  def coverage_f
59
46
  return 0.0 if coverage.empty?
60
47
 
@@ -75,7 +62,6 @@ module Undercover
75
62
 
76
63
  (lines.values.sum.to_f / lines.keys.size).round(4)
77
64
  end
78
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
79
65
 
80
66
  # TODO: create a formatter interface instead and add some tests.
81
67
  # TODO: re-enable rubocops
@@ -105,7 +91,7 @@ module Undercover
105
91
  Rainbow(formatted_line).darkgray.dark
106
92
  elsif skipped?(file_path, num)
107
93
  Rainbow(formatted_line).darkgray.dark +
108
- Rainbow(' skipped with :nocov:').italic.darkgray.dark
94
+ Rainbow(' skipped').italic.darkgray.dark
109
95
  elsif covered.nil?
110
96
  Rainbow(formatted_line).darkgray.dark +
111
97
  Rainbow(' hits: n/a').italic.darkgray.dark +
@@ -144,6 +130,30 @@ module Undercover
144
130
 
145
131
  private
146
132
 
133
+ def coverage_lines
134
+ return first_line..first_line if first_line == last_line
135
+ return first_line..last_line if covers_own_boundaries?
136
+
137
+ (first_line + 1)..(last_line - 1)
138
+ end
139
+
140
+ def covers_own_boundaries?
141
+ node.empty_def? || node.kind == :block || node.is_a?(ErbNode)
142
+ end
143
+
144
+ def uncovered_branch_lines
145
+ @uncovered_branch_lines ||= coverage.filter_map do |ln, _block, _branch, cov|
146
+ ln if cov && cov != 'ignored' && cov.zero?
147
+ end.to_set
148
+ end
149
+
150
+ # first line tuple per line, as a linear search would find it
151
+ def line_hits
152
+ @line_hits ||= coverage.each_with_object({}) do |cov, hits|
153
+ hits[cov[0]] = cov[1] if cov.size == 2 && !hits.key?(cov[0])
154
+ end
155
+ end
156
+
147
157
  def count_covered_branches(line_number) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
148
158
  branches = coverage.select { |cov| cov.size == 4 && cov[0] == line_number }
149
159
  count_covered = branches.count do |cov|
@@ -1,34 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'simplecov'
4
- require 'simplecov_json_formatter'
4
+ require 'json'
5
+ require 'undercover/version'
6
+
7
+ # SimpleCov ships its own simplecov_json_formatter.rb shim that can shadow the
8
+ # standalone gem on $LOAD_PATH. Require the gem's classes by subpath (which the
9
+ # shim does not define) so the SimpleCovJSONFormatter::* constants are present.
10
+ require 'simplecov_json_formatter/result_hash_formatter'
11
+ require 'simplecov_json_formatter/result_exporter'
5
12
 
6
13
  # Patch ResultExporter to allow setting a custom export_path
7
14
  module SimpleCovJSONFormatter
8
15
  class ResultExporter
9
16
  def export_path
10
- # :nocov:
17
+ # simplecov:disable
11
18
  File.join(SimpleCov.coverage_path, SimpleCov::Formatter::Undercover.output_filename || FILENAME)
12
- # :nocov:
19
+ # simplecov:enable
13
20
  end
14
21
  end
15
22
  end
16
23
 
17
- module SimpleCov
24
+ module SimpleCov # rubocop:disable Style/OneClassPerFile
18
25
  class << self
19
26
  attr_accessor :filter_definitions
20
27
 
21
- alias filtered_uncached filtered
22
-
23
- def filtered(files)
28
+ # Records the filters undercover needs to reproduce SimpleCov's ignore
29
+ # decisions. String/regex filters are serialized declaratively; files
30
+ # dropped by block/array/custom filters (which can't be serialized) are
31
+ # recorded by their project-relative path.
32
+ def track_filtered_out(original_files, filtered_files)
24
33
  @filter_definitions ||= extract_filter_definitions
25
- original_files = files.dup
26
- filtered_uncached(files).tap do |filtered_files|
27
- filtered_file_paths = (original_files.map(&:filename) - filtered_files.map(&:filename))
28
- filtered_file_paths.each do |file|
29
- relative_path = file.delete_prefix("#{SimpleCov.root}/")
30
- @filter_definitions << {file: relative_path} unless covered_by_serializable_filters?(relative_path)
31
- end
34
+ filtered_file_paths = (original_files.map(&:filename) - filtered_files.map(&:filename))
35
+ filtered_file_paths.each do |file|
36
+ relative_path = file.delete_prefix("#{SimpleCov.root}/")
37
+ next if covered_by_serializable_filters?(relative_path)
38
+
39
+ entry = {file: relative_path}
40
+ @filter_definitions << entry unless @filter_definitions.include?(entry)
32
41
  end
33
42
  end
34
43
 
@@ -50,7 +59,7 @@ module SimpleCov
50
59
  end
51
60
 
52
61
  def covered_by_serializable_filters?(relative_path)
53
- @filter_definitions.any? do |filter_def|
62
+ (@filter_definitions || []).any? do |filter_def|
54
63
  if filter_def[:string]
55
64
  relative_path.include?(filter_def[:string])
56
65
  elsif filter_def[:regex]
@@ -61,7 +70,20 @@ module SimpleCov
61
70
  end
62
71
  end
63
72
 
64
- module Undercover
73
+ # SimpleCov applies filters inside the private SimpleCov::Result#apply_filters!,
74
+ # so hook that method to populate filter_definitions as files are dropped.
75
+ module Undercover # rubocop:disable Style/OneClassPerFile
76
+ module SimplecovResultFilterTracking
77
+ def apply_filters!(_filters)
78
+ files_before = @files.to_a
79
+ super
80
+ SimpleCov.track_filtered_out(files_before, @files)
81
+ end
82
+ end
83
+ end
84
+ SimpleCov::Result.prepend(Undercover::SimplecovResultFilterTracking)
85
+
86
+ module Undercover # rubocop:disable Style/OneClassPerFile
65
87
  class ResultHashFormatterWithRoot < SimpleCovJSONFormatter::ResultHashFormatter
66
88
  def format
67
89
  formatted_result[:meta] = {timestamp: @result.created_at.to_i}
@@ -75,7 +97,7 @@ module Undercover
75
97
 
76
98
  def add_undercover_meta_fields
77
99
  formatted_result.tap do |result|
78
- result[:meta].merge!(simplecov_root: SimpleCov.root)
100
+ result[:meta].merge!(simplecov_root: SimpleCov.root, undercover_version: Undercover::VERSION)
79
101
  end
80
102
  end
81
103
 
@@ -93,11 +115,20 @@ module Undercover
93
115
  end
94
116
  end
95
117
 
96
- class UndercoverSimplecovFormatter < SimpleCov::Formatter::JSONFormatter
118
+ class UndercoverSimplecovFormatter
97
119
  class << self
98
120
  attr_accessor :output_filename
99
121
  end
100
122
 
123
+ # Own #format instead of inheriting SimpleCov::Formatter::JSONFormatter's,
124
+ # which bypasses #format_result and always writes coverage.json, dropping
125
+ # undercover's meta and custom filename.
126
+ def format(result)
127
+ result_hash = format_result(result)
128
+ SimpleCovJSONFormatter::ResultExporter.new(result_hash).export
129
+ result_hash
130
+ end
131
+
101
132
  def format_result(result)
102
133
  result_hash_formater = ResultHashFormatterWithRoot.new(result)
103
134
  result_hash_formater.format
@@ -105,7 +136,7 @@ module Undercover
105
136
  end
106
137
  end
107
138
 
108
- module SimpleCov
139
+ module SimpleCov # rubocop:disable Style/OneClassPerFile
109
140
  module Formatter
110
141
  Undercover = ::Undercover::UndercoverSimplecovFormatter
111
142
  end
@@ -1,28 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'undercover/root_to_relative_paths'
3
+ require 'undercover/coverage_root'
4
4
 
5
5
  module Undercover
6
6
  class SimplecovResultAdapter
7
- include RootToRelativePaths
8
-
9
- attr_reader :simplecov_result
7
+ attr_reader :simplecov_result, :only_files, :coverage_root
10
8
 
11
9
  # @param file[File] JSON file supplied by SimpleCov::Formatter::Undercover
12
10
  # @return SimplecovResultAdapter
13
- def self.parse(file, opts = nil)
14
- # :nocov:
11
+ def self.parse(file, opts = nil, only_files: nil)
12
+ # simplecov:disable
15
13
  result_h = JSON.parse(file.read)
16
14
  raise ArgumentError, 'empty SimpleCov' if result_h.empty?
17
15
 
18
- new(result_h, opts)
19
- # :nocov:
16
+ new(result_h, opts, only_files: only_files)
17
+ # simplecov:enable
20
18
  end
21
19
 
22
20
  # @param simplecov_result[SimpleCov::Result]
23
- def initialize(simplecov_result, opts)
21
+ def initialize(simplecov_result, _opts = nil, only_files: nil)
24
22
  @simplecov_result = simplecov_result
25
- @code_dir = opts&.path
23
+ @only_files = only_files
24
+ @coverage_root = CoverageRoot::NONE
25
+ @erb_coverage = (simplecov_result['coverage'] || {}).keys.any? { _1.end_with?('.erb') }
26
+ end
27
+
28
+ # @return Boolean whether the report covers any ERB template, which takes
29
+ # SimpleCov's eval coverage. Recorded before only_files drops unchanged files.
30
+ def erb_coverage?
31
+ @erb_coverage
32
+ end
33
+
34
+ # Filtering waits for the coverage root, since only_files is relative to the repository
35
+ # while coverage keys are relative to SimpleCov.root.
36
+ def coverage_root=(root)
37
+ @coverage_root = root || CoverageRoot::NONE
38
+ keep_only_files!
39
+ end
40
+
41
+ # @return String SimpleCov.root as recorded when the report was written, an absolute
42
+ # path on the machine that ran the tests
43
+ def simplecov_root
44
+ simplecov_result.dig('meta', 'simplecov_root')
45
+ end
46
+
47
+ # @return Array paths relative to SimpleCov.root
48
+ def coverage_keys
49
+ simplecov_result['coverage'].keys
26
50
  end
27
51
 
28
52
  # @param filepath[String]
@@ -69,8 +93,15 @@ module Undercover
69
93
 
70
94
  private
71
95
 
96
+ def keep_only_files!
97
+ return unless only_files
98
+
99
+ wanted = only_files.to_set { |path| CoverageRoot.strip(path, coverage_root) }
100
+ simplecov_result['coverage'].select! { |path, _| wanted.include?(path) }
101
+ end
102
+
72
103
  def find_file(filepath)
73
- simplecov_result['coverage'][fix_relative_filepath(filepath)]
104
+ simplecov_result['coverage'][CoverageRoot.strip(filepath, coverage_root)]
74
105
  end
75
106
  end
76
107
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Undercover
4
- VERSION = '0.8.5'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/undercover.rb CHANGED
@@ -2,26 +2,33 @@
2
2
 
3
3
  $LOAD_PATH << 'lib'
4
4
  require 'json'
5
- require 'imagen'
6
5
  require 'rainbow'
7
- require 'bigdecimal'
8
6
  require 'forwardable'
9
7
  require 'simplecov'
10
8
 
11
9
  require 'undercover/lcov_parser'
10
+ require 'undercover/coverage_index'
12
11
  require 'undercover/result'
13
12
  require 'undercover/cli'
14
13
  require 'undercover/changeset'
15
14
  require 'undercover/formatter'
16
15
  require 'undercover/json_formatter'
17
16
  require 'undercover/options'
17
+ require 'undercover/coverage_root'
18
18
  require 'undercover/filter_set'
19
19
  require 'undercover/simplecov_result_adapter'
20
20
  require 'undercover/version'
21
+ require 'undercover/node'
22
+ require 'undercover/node_visitor'
23
+ require 'undercover/erb_node'
24
+ require 'undercover/erb_visitor'
25
+ require 'undercover/erb_extractor'
26
+ require 'undercover/file_loader'
21
27
 
22
28
  module Undercover
23
29
  class Report
24
30
  extend Forwardable
31
+ include FileLoader
25
32
 
26
33
  def_delegators :changeset, :validate
27
34
 
@@ -30,6 +37,7 @@ module Undercover
30
37
  :coverage_adapter,
31
38
  :results,
32
39
  :code_dir,
40
+ :coverage_root,
33
41
  :filter_set,
34
42
  :max_warnings_limit
35
43
 
@@ -40,41 +48,28 @@ module Undercover
40
48
  # @param coverage_adapter [Undercover::SimplecovResultAdapter|Undercover::LcovParser] pre-parsed coverage adapter
41
49
  def initialize(changeset, opts, coverage_adapter)
42
50
  @coverage_adapter = coverage_adapter
43
-
44
- @code_dir = opts.path
45
51
  @changeset = changeset
46
-
47
- ignored_files = coverage_adapter.ignored_files || []
48
- @filter_set = FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters, ignored_files)
52
+ @code_dir = changeset.repo_workdir
53
+ @coverage_root = resolve_coverage_root
54
+ coverage_adapter.coverage_root = @coverage_root
55
+ @filter_set = build_filter_set(opts)
49
56
  changeset.filter_with(filter_set)
50
57
  @max_warnings_limit = opts.max_warnings_limit
51
58
  @loaded_files = {}
52
59
  @results = {}
53
60
  end
54
61
 
55
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
62
+ # rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
56
63
  def build
57
64
  flag_count = 0
58
65
  changeset.each_changed_line do |filepath, line_no|
59
66
  break if max_warnings_limit && flag_count >= max_warnings_limit
60
67
 
61
- dist_from_line_no = lambda do |res|
62
- return BigDecimal::INFINITY if line_no < res.first_line
63
-
64
- res_lines = res.first_line..res.last_line
65
- return BigDecimal::INFINITY unless res_lines.cover?(line_no)
66
-
67
- line_no - res.first_line
68
- end
69
- dist_from_line_no_sorter = lambda do |res1, res2|
70
- dist_from_line_no[res1] <=> dist_from_line_no[res2]
71
- end
72
-
73
68
  load_and_parse_file(filepath)
74
69
 
75
70
  next unless loaded_files[filepath]
76
71
 
77
- res = loaded_files[filepath].min(&dist_from_line_no_sorter)
72
+ res = result_for_line(filepath, line_no)
78
73
  if res.uncovered?(line_no) && !res.flagged?
79
74
  res.flag
80
75
  flag_count += 1
@@ -84,7 +79,6 @@ module Undercover
84
79
  end
85
80
  self
86
81
  end
87
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
88
82
 
89
83
  def build_warnings
90
84
  warn('Undercover::Report#build_warnings is deprecated! ' \
@@ -100,6 +94,29 @@ module Undercover
100
94
  all_results.select(&:flagged?)
101
95
  end
102
96
 
97
+ # Changed files undercover would have reported on, had --include-files /
98
+ # --exclude-files not rejected them. Everything rejected and nothing reported usually
99
+ # means the globs are written against the wrong root.
100
+ def globbed_out_files
101
+ @globbed_out_files ||= changeset.file_paths.select { |path| filter_set.rejected_by_globs?(path) }
102
+ end
103
+
104
+ # Changed ERB templates left unchecked because the coverage report has no ERB coverage
105
+ # at all, meaning SimpleCov ran without enable_coverage(:eval). Checking them anyway
106
+ # would flag every block at 0%.
107
+ def erb_files_without_coverage
108
+ return [] if coverage_adapter.erb_coverage?
109
+
110
+ @erb_files_without_coverage ||= changeset.file_paths.select do |path|
111
+ path.end_with?(FileLoader::ERB_EXTENSION) && filter_set.include?(path)
112
+ end
113
+ end
114
+
115
+ # Changed ERB templates left unchecked because they could not be parsed
116
+ def erb_parse_failures
117
+ @erb_parse_failures ||= []
118
+ end
119
+
103
120
  def inspect
104
121
  "#<Undercover::Report:#{object_id} results: #{results.size}>"
105
122
  end
@@ -109,21 +126,41 @@ module Undercover
109
126
 
110
127
  attr_reader :loaded_files
111
128
 
112
- # rubocop:disable Metrics/AbcSize
113
- def load_and_parse_file(filepath)
114
- key = filepath.gsub(/^\.\//, '')
115
- return if loaded_files[key]
116
-
117
- root_ast = Imagen::Node::Root.new.build_from_file(
118
- File.join(code_dir, filepath)
119
- )
120
- return if root_ast.children.empty?
129
+ # The innermost result whose lines cover line_no: the one starting closest above it,
130
+ # the first one listed on a tie. Lines outside every result fall back to the first.
131
+ def result_for_line(filepath, line_no)
132
+ @line_results ||= {}
133
+ by_line = @line_results[filepath] ||= index_results_by_line(loaded_files[filepath])
134
+ by_line[line_no] || loaded_files[filepath].first
135
+ end
121
136
 
122
- loaded_files[key] = []
123
- root_ast.find_all(->(node) { !node.is_a?(Imagen::Node::Root) }).each do |imagen_node|
124
- loaded_files[key] << Result.new(imagen_node, coverage_adapter, filepath)
137
+ def index_results_by_line(file_results)
138
+ by_line = {}
139
+ file_results.each do |res|
140
+ (res.first_line..res.last_line).each do |line_no|
141
+ current = by_line[line_no]
142
+ by_line[line_no] = res if current.nil? || res.first_line > current.first_line
143
+ end
125
144
  end
145
+ by_line
146
+ end
147
+
148
+ def build_filter_set(opts)
149
+ FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters, coverage_adapter.ignored_files || [],
150
+ coverage_root: coverage_root)
151
+ end
152
+
153
+ # LCOV reports carry no SimpleCov.root, so fall back to where undercover runs, which
154
+ # is where SimpleCov ran for anyone invoking the CLI by hand. A wrong guess is caught
155
+ # by the covered-file check in CoverageRoot.
156
+ def resolve_coverage_root
157
+ simplecov_root = coverage_adapter.simplecov_root || Dir.pwd
158
+ CoverageRoot.derive(code_dir, simplecov_root, coverage_adapter.coverage_keys, exists: method(:repository_path?))
159
+ end
160
+
161
+ # On disk, or committed at HEAD for files a sparse checkout left out
162
+ def repository_path?(path)
163
+ File.exist?(File.join(code_dir, path)) || changeset.head_path?(path)
126
164
  end
127
- # rubocop:enable Metrics/AbcSize
128
165
  end
129
166
  end
data/undercover.gemspec CHANGED
@@ -25,14 +25,16 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
25
25
  spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
26
26
  spec.require_paths = ['lib']
27
27
 
28
- spec.required_ruby_version = '>= 3.0.0'
28
+ spec.required_ruby_version = '>= 3.2.0'
29
29
 
30
30
  spec.add_dependency 'base64'
31
31
  spec.add_dependency 'benchmark'
32
- spec.add_dependency 'bigdecimal'
33
- spec.add_dependency 'imagen', '>= 0.2.0'
32
+ spec.add_dependency 'erubi', '>= 1.13'
33
+ spec.add_dependency 'prism', '>= 1.8', '< 3'
34
34
  spec.add_dependency 'rainbow', '>= 2.1', '< 4.0'
35
35
  spec.add_dependency 'rugged', '>= 0.27', '< 1.10'
36
- spec.add_dependency 'simplecov'
37
- spec.add_dependency 'simplecov_json_formatter'
36
+ spec.add_dependency 'simplecov', '>= 1.0'
37
+ # Pinned: undercover requires its internal files by path and subclasses
38
+ # ResultHashFormatter, so a file reorganisation would break loading.
39
+ spec.add_dependency 'simplecov_json_formatter', '~> 0.1'
38
40
  end