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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +15 -0
- data/.github/workflows/ruby.yml +2 -2
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +20 -1
- data/Gemfile +3 -1
- data/README.md +92 -9
- data/bin/undercover +1 -9
- data/lib/undercover/ast_branch_annotator.rb +44 -32
- data/lib/undercover/changeset.rb +29 -5
- data/lib/undercover/cli.rb +96 -17
- data/lib/undercover/coverage_index.rb +19 -0
- data/lib/undercover/coverage_root.rb +73 -0
- data/lib/undercover/erb_extractor.rb +32 -0
- data/lib/undercover/erb_node.rb +20 -0
- data/lib/undercover/erb_visitor.rb +42 -0
- data/lib/undercover/file_loader.rb +51 -0
- data/lib/undercover/filter_set.rb +34 -9
- data/lib/undercover/json_formatter.rb +1 -1
- data/lib/undercover/lcov_parser.rb +35 -16
- data/lib/undercover/node.rb +78 -0
- data/lib/undercover/node_visitor.rb +86 -0
- data/lib/undercover/options.rb +38 -17
- data/lib/undercover/result.rb +33 -23
- data/lib/undercover/simplecov_formatter.rb +50 -19
- data/lib/undercover/simplecov_result_adapter.rb +42 -11
- data/lib/undercover/version.rb +1 -1
- data/lib/undercover.rb +72 -35
- data/undercover.gemspec +7 -5
- metadata +29 -15
- data/lib/undercover/root_to_relative_paths.rb +0 -24
data/lib/undercover/options.rb
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
# :
|
|
77
|
+
# simplecov:disable
|
|
78
78
|
puts VERSION
|
|
79
79
|
exit
|
|
80
|
-
# :
|
|
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', '
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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(
|
|
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).
|
|
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
|
data/lib/undercover/result.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
@coverage =
|
|
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)
|
|
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
|
-
|
|
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
|
|
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 '
|
|
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
|
-
# :
|
|
17
|
+
# simplecov:disable
|
|
11
18
|
File.join(SimpleCov.coverage_path, SimpleCov::Formatter::Undercover.output_filename || FILENAME)
|
|
12
|
-
# :
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
3
|
+
require 'undercover/coverage_root'
|
|
4
4
|
|
|
5
5
|
module Undercover
|
|
6
6
|
class SimplecovResultAdapter
|
|
7
|
-
|
|
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
|
-
# :
|
|
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
|
-
# :
|
|
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,
|
|
21
|
+
def initialize(simplecov_result, _opts = nil, only_files: nil)
|
|
24
22
|
@simplecov_result = simplecov_result
|
|
25
|
-
@
|
|
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'][
|
|
104
|
+
simplecov_result['coverage'][CoverageRoot.strip(filepath, coverage_root)]
|
|
74
105
|
end
|
|
75
106
|
end
|
|
76
107
|
end
|
data/lib/undercover/version.rb
CHANGED
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
|
-
|
|
48
|
-
|
|
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 =
|
|
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
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
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 '
|
|
33
|
-
spec.add_dependency '
|
|
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
|
-
|
|
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
|