undercover 0.8.4 → 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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +28 -1
- data/Gemfile +3 -1
- data/README.md +149 -9
- data/bin/undercover +1 -9
- data/lib/undercover/ast_branch_annotator.rb +62 -0
- data/lib/undercover/changeset.rb +29 -5
- data/lib/undercover/cli.rb +106 -28
- 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/formatter.rb +18 -1
- data/lib/undercover/json_formatter.rb +84 -0
- data/lib/undercover/lcov_parser.rb +39 -16
- data/lib/undercover/node.rb +78 -0
- data/lib/undercover/node_visitor.rb +86 -0
- data/lib/undercover/options.rb +50 -17
- data/lib/undercover/result.rb +43 -24
- data/lib/undercover/simplecov_formatter.rb +50 -19
- data/lib/undercover/simplecov_result_adapter.rb +49 -11
- data/lib/undercover/version.rb +1 -1
- data/lib/undercover.rb +73 -35
- data/undercover.gemspec +9 -6
- metadata +43 -13
- data/lib/undercover/root_to_relative_paths.rb +0 -24
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Undercover
|
|
6
|
+
# Builds the Node tree of a Prism syntax tree
|
|
7
|
+
class NodeVisitor < Prism::Visitor
|
|
8
|
+
attr_reader :roots
|
|
9
|
+
|
|
10
|
+
def initialize(file_path, source_lines)
|
|
11
|
+
super()
|
|
12
|
+
@file_path = file_path
|
|
13
|
+
@source_lines = source_lines
|
|
14
|
+
@roots = []
|
|
15
|
+
@parents = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def visit_class_node(node)
|
|
19
|
+
add(:class, node.name.to_s, node, node.body) { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_module_node(node)
|
|
23
|
+
add(:module, node.name.to_s, node, node.body) { super }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def visit_def_node(node)
|
|
27
|
+
add(node.receiver ? :class_method : :instance_method, node.name_loc.slice, node, node.body) { super }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_call_node(node)
|
|
31
|
+
visit_block_call(node) { super }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def visit_super_node(node)
|
|
35
|
+
visit_block_call(node) { super }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_forwarding_super_node(node)
|
|
39
|
+
visit_block_call(node) { super }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def visit_lambda_node(node)
|
|
43
|
+
add(:block, block_name(node.parameters), node, node.body) { super }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def visit_block_call(node, &)
|
|
49
|
+
block = node.block
|
|
50
|
+
return yield unless block.is_a?(Prism::BlockNode)
|
|
51
|
+
|
|
52
|
+
add(:block, block_name(block.parameters), node, block.body, &)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add(kind, name, prism_node, body)
|
|
56
|
+
new_node = build(kind, name, prism_node, empty: body.nil?)
|
|
57
|
+
(@parents.last&.children || roots) << new_node
|
|
58
|
+
@parents.push(new_node)
|
|
59
|
+
yield
|
|
60
|
+
@parents.pop
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build(kind, name, prism_node, empty:)
|
|
64
|
+
Node.new(kind, name, prism_node, @file_path, @source_lines, empty:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Lists explicit parameters only: `it` and numbered parameter blocks are a plain "block"
|
|
68
|
+
def block_name(parameters)
|
|
69
|
+
names = parameters.is_a?(Prism::BlockParametersNode) ? parameter_names(parameters) : []
|
|
70
|
+
names.empty? ? 'block' : "block (#{names.join(', ')})"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parameter_names(block_parameters)
|
|
74
|
+
[*ordered_parameters(block_parameters.parameters), *block_parameters.locals].map do |param|
|
|
75
|
+
param.is_a?(Prism::MultiTargetNode) ? param.slice : (param.name if param.respond_to?(:name))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ordered_parameters(params)
|
|
80
|
+
return [] unless params
|
|
81
|
+
|
|
82
|
+
[*params.requireds, *params.optionals, params.rest, *params.posts, *params.keywords, params.keyword_rest,
|
|
83
|
+
params.block].compact.grep_v(Prism::ImplicitRestNode)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/undercover/options.rb
CHANGED
|
@@ -32,32 +32,36 @@ 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
|
+
|
|
37
|
+
FORMATS = %w[text json].freeze
|
|
36
38
|
|
|
37
39
|
attr_accessor :lcov,
|
|
38
40
|
:simplecov_resultset,
|
|
39
41
|
:path,
|
|
40
42
|
:git_dir,
|
|
41
43
|
:compare,
|
|
42
|
-
:
|
|
44
|
+
:commits_only,
|
|
43
45
|
:run_mode,
|
|
44
46
|
:file_scope,
|
|
45
47
|
:glob_allow_filters,
|
|
46
48
|
:glob_reject_filters,
|
|
47
|
-
:max_warnings_limit
|
|
49
|
+
:max_warnings_limit,
|
|
50
|
+
:formatter,
|
|
51
|
+
:no_hint
|
|
48
52
|
|
|
49
53
|
def initialize
|
|
50
54
|
@run_mode = DIFF_TRIGGER_LINE
|
|
51
55
|
@file_scope = FILE_SCOPE_EXTENDED
|
|
52
56
|
# set defaults
|
|
53
|
-
self.path = '.'
|
|
54
57
|
self.git_dir = '.git'
|
|
55
58
|
self.glob_allow_filters = DEFAULT_FILE_INCLUDE_GLOBS
|
|
56
59
|
self.glob_reject_filters = DEFAULT_FILE_EXCLUDE_GLOBS
|
|
57
60
|
self.max_warnings_limit = nil
|
|
61
|
+
self.formatter = 'text'
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
64
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
|
|
61
65
|
def parse(args)
|
|
62
66
|
args = build_opts(args)
|
|
63
67
|
|
|
@@ -70,10 +74,10 @@ module Undercover
|
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
opts.on_tail('--version', 'Show version') do
|
|
73
|
-
# :
|
|
77
|
+
# simplecov:disable
|
|
74
78
|
puts VERSION
|
|
75
79
|
exit
|
|
76
|
-
# :
|
|
80
|
+
# simplecov:enable
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
lcov_path_option(opts)
|
|
@@ -81,16 +85,18 @@ module Undercover
|
|
|
81
85
|
project_path_option(opts)
|
|
82
86
|
git_dir_option(opts)
|
|
83
87
|
compare_option(opts)
|
|
88
|
+
commits_only_option(opts)
|
|
84
89
|
ruby_syntax_option(opts)
|
|
85
90
|
max_warnings_limit_option(opts)
|
|
86
91
|
file_filters(opts)
|
|
92
|
+
format_option(opts)
|
|
93
|
+
hint_option(opts)
|
|
87
94
|
end.parse(args)
|
|
88
95
|
|
|
89
96
|
guess_resultset_path unless simplecov_resultset
|
|
90
97
|
guess_lcov_path unless lcov
|
|
91
98
|
self
|
|
92
99
|
end
|
|
93
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
94
100
|
|
|
95
101
|
private
|
|
96
102
|
|
|
@@ -138,8 +144,12 @@ module Undercover
|
|
|
138
144
|
end
|
|
139
145
|
end
|
|
140
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.
|
|
141
150
|
def project_path_option(parser)
|
|
142
|
-
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')
|
|
143
153
|
self.path = path
|
|
144
154
|
end
|
|
145
155
|
end
|
|
@@ -158,11 +168,20 @@ module Undercover
|
|
|
158
168
|
end
|
|
159
169
|
end
|
|
160
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.
|
|
161
179
|
def ruby_syntax_option(parser)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
166
185
|
end
|
|
167
186
|
end
|
|
168
187
|
|
|
@@ -174,13 +193,12 @@ module Undercover
|
|
|
174
193
|
end
|
|
175
194
|
|
|
176
195
|
def guess_resultset_path
|
|
177
|
-
|
|
178
|
-
try_path = File.join(cwd, 'coverage', 'coverage.json')
|
|
196
|
+
try_path = File.join(Dir.pwd, 'coverage', 'coverage.json')
|
|
179
197
|
self.simplecov_resultset = try_path if File.exist?(try_path)
|
|
180
198
|
end
|
|
181
199
|
|
|
182
200
|
def guess_lcov_path
|
|
183
|
-
cwd = Pathname.new(
|
|
201
|
+
cwd = Pathname.new(Dir.pwd)
|
|
184
202
|
try_path = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
|
|
185
203
|
self.lcov = try_path if File.exist?(try_path)
|
|
186
204
|
end
|
|
@@ -192,10 +210,25 @@ module Undercover
|
|
|
192
210
|
self.glob_allow_filters = split_comma_separated_with_braces(comma_separated_globs)
|
|
193
211
|
end
|
|
194
212
|
|
|
195
|
-
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(',')}'"
|
|
196
215
|
parser.on('-x', '--exclude-files globs', desc) do |comma_separated_globs|
|
|
197
216
|
self.glob_reject_filters = split_comma_separated_with_braces(comma_separated_globs)
|
|
198
217
|
end
|
|
199
218
|
end
|
|
219
|
+
|
|
220
|
+
def format_option(parser)
|
|
221
|
+
desc = "Output format: #{FORMATS.join(', ')} (default: text)"
|
|
222
|
+
parser.on('--format FORMAT', FORMATS, desc) do |format|
|
|
223
|
+
self.formatter = format
|
|
224
|
+
end
|
|
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
|
|
200
233
|
end
|
|
201
234
|
end
|
data/lib/undercover/result.rb
CHANGED
|
@@ -9,21 +9,14 @@ module Undercover
|
|
|
9
9
|
attr_reader :node, :coverage, :coverage_adapter, :file_path
|
|
10
10
|
|
|
11
11
|
def_delegators :node, :first_line, :last_line, :name
|
|
12
|
-
def_delegators :coverage_adapter, :skipped
|
|
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 +
|
|
@@ -123,6 +109,15 @@ module Undercover
|
|
|
123
109
|
end
|
|
124
110
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
125
111
|
|
|
112
|
+
# Returns raw branch coverage entries for this result as structured hashes.
|
|
113
|
+
# Callers that need human-readable descriptions should use AstBranchAnnotator
|
|
114
|
+
# separately (e.g. JsonFormatter).
|
|
115
|
+
def branches
|
|
116
|
+
coverage.select { |cov| cov.size == 4 }.map do |ln, block_no, branch_no, count|
|
|
117
|
+
{line: ln, block: block_no, branch: branch_no, count: count}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
126
121
|
def file_path_with_lines
|
|
127
122
|
"#{file_path}:#{first_line}:#{last_line}"
|
|
128
123
|
end
|
|
@@ -135,6 +130,30 @@ module Undercover
|
|
|
135
130
|
|
|
136
131
|
private
|
|
137
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
|
+
|
|
138
157
|
def count_covered_branches(line_number) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
139
158
|
branches = coverage.select { |cov| cov.size == 4 && cov[0] == line_number }
|
|
140
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]
|
|
@@ -52,6 +76,13 @@ module Undercover
|
|
|
52
76
|
source_file['lines'][line_no - 1] == 'ignored'
|
|
53
77
|
end
|
|
54
78
|
|
|
79
|
+
def branch_label(filepath, branch_no)
|
|
80
|
+
source_file = find_file(filepath)
|
|
81
|
+
return nil unless source_file
|
|
82
|
+
|
|
83
|
+
source_file['branches']&.dig(branch_no - 1, 'type')
|
|
84
|
+
end
|
|
85
|
+
|
|
55
86
|
# unused for now
|
|
56
87
|
def total_coverage; end
|
|
57
88
|
def total_branch_coverage; end
|
|
@@ -62,8 +93,15 @@ module Undercover
|
|
|
62
93
|
|
|
63
94
|
private
|
|
64
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
|
+
|
|
65
103
|
def find_file(filepath)
|
|
66
|
-
simplecov_result['coverage'][
|
|
104
|
+
simplecov_result['coverage'][CoverageRoot.strip(filepath, coverage_root)]
|
|
67
105
|
end
|
|
68
106
|
end
|
|
69
107
|
end
|
data/lib/undercover/version.rb
CHANGED