deep-cover 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.deep_cover.rb +8 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -1
- data/.travis.yml +1 -0
- data/README.md +30 -1
- data/Rakefile +10 -1
- data/bin/cov +1 -1
- data/deep_cover.gemspec +4 -5
- data/exe/deep-cover +5 -3
- data/lib/deep_cover.rb +1 -1
- data/lib/deep_cover/analyser/node.rb +1 -1
- data/lib/deep_cover/analyser/ruby25_like_branch.rb +209 -0
- data/lib/deep_cover/auto_run.rb +19 -19
- data/lib/deep_cover/autoload_tracker.rb +181 -44
- data/lib/deep_cover/backports.rb +3 -1
- data/lib/deep_cover/base.rb +13 -8
- data/lib/deep_cover/basics.rb +1 -1
- data/lib/deep_cover/cli/debugger.rb +2 -2
- data/lib/deep_cover/cli/instrumented_clone_reporter.rb +21 -8
- data/lib/deep_cover/cli/runner.rb +126 -0
- data/lib/deep_cover/config_setter.rb +1 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +82 -14
- data/lib/deep_cover/core_ext/coverage_replacement.rb +34 -5
- data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +4 -6
- data/lib/deep_cover/core_ext/require_overrides.rb +1 -3
- data/lib/deep_cover/coverage.rb +105 -2
- data/lib/deep_cover/coverage/analysis.rb +30 -28
- data/lib/deep_cover/coverage/persistence.rb +60 -70
- data/lib/deep_cover/covered_code.rb +16 -49
- data/lib/deep_cover/custom_requirer.rb +112 -51
- data/lib/deep_cover/load.rb +10 -6
- data/lib/deep_cover/memoize.rb +1 -3
- data/lib/deep_cover/module_override.rb +7 -0
- data/lib/deep_cover/node/assignments.rb +2 -1
- data/lib/deep_cover/node/base.rb +6 -6
- data/lib/deep_cover/node/block.rb +10 -8
- data/lib/deep_cover/node/case.rb +3 -3
- data/lib/deep_cover/node/collections.rb +8 -0
- data/lib/deep_cover/node/if.rb +19 -3
- data/lib/deep_cover/node/literals.rb +28 -7
- data/lib/deep_cover/node/mixin/can_augment_children.rb +4 -4
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +1 -1
- data/lib/deep_cover/node/mixin/filters.rb +6 -2
- data/lib/deep_cover/node/mixin/has_child.rb +8 -8
- data/lib/deep_cover/node/mixin/has_child_handler.rb +3 -3
- data/lib/deep_cover/node/mixin/has_tracker.rb +7 -3
- data/lib/deep_cover/node/root.rb +1 -1
- data/lib/deep_cover/node/send.rb +53 -7
- data/lib/deep_cover/node/short_circuit.rb +11 -3
- data/lib/deep_cover/parser_ext/range.rb +11 -27
- data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
- data/lib/deep_cover/reporter.rb +0 -1
- data/lib/deep_cover/reporter/base.rb +68 -0
- data/lib/deep_cover/reporter/html.rb +1 -1
- data/lib/deep_cover/reporter/html/index.rb +4 -8
- data/lib/deep_cover/reporter/html/site.rb +10 -18
- data/lib/deep_cover/reporter/html/source.rb +3 -3
- data/lib/deep_cover/reporter/html/template/source.html.erb +1 -1
- data/lib/deep_cover/reporter/istanbul.rb +86 -56
- data/lib/deep_cover/reporter/text.rb +5 -13
- data/lib/deep_cover/reporter/{util/tree.rb → tree/util.rb} +19 -21
- data/lib/deep_cover/tools/blank.rb +25 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +8 -8
- data/lib/deep_cover/tools/dump_covered_code.rb +2 -9
- data/lib/deep_cover/tools/execute_sample.rb +17 -6
- data/lib/deep_cover/tools/format_generated_code.rb +1 -1
- data/lib/deep_cover/tools/indent_string.rb +26 -0
- data/lib/deep_cover/tools/our_coverage.rb +2 -2
- data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
- data/lib/deep_cover/tracker_bucket.rb +50 -0
- data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
- data/lib/deep_cover/tracker_storage.rb +76 -0
- data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
- data/lib/deep_cover/version.rb +1 -1
- data/lib/deep_cover_entry.rb +3 -0
- metadata +30 -37
- data/bin/gemcov +0 -8
- data/bin/selfcov +0 -21
- data/lib/deep_cover/cli/deep_cover.rb +0 -126
- data/lib/deep_cover/coverage/base.rb +0 -81
- data/lib/deep_cover/coverage/istanbul.rb +0 -34
- data/lib/deep_cover/tools/transform_keys.rb +0 -9
@@ -2,77 +2,214 @@
|
|
2
2
|
|
3
3
|
require 'weakref'
|
4
4
|
|
5
|
+
# TODO: if a constant is removed, AutoloadEntries should be removed
|
6
|
+
|
5
7
|
module DeepCover
|
6
8
|
class AutoloadTracker
|
7
|
-
|
8
|
-
|
9
|
+
AutoloadEntry = Struct.new(:weak_mod, :name, :target_path, :interceptor_path) do
|
10
|
+
# If the ref is dead, will return nil
|
11
|
+
# If the target is frozen, will warn and return nil
|
12
|
+
# Otherwise return the target
|
13
|
+
def mod_if_available
|
14
|
+
mod = weak_mod.__getobj__
|
15
|
+
if mod.frozen?
|
16
|
+
AutoloadTracker.warn_frozen_module(mod)
|
17
|
+
nil
|
18
|
+
else
|
19
|
+
mod
|
20
|
+
end
|
21
|
+
rescue WeakRef::RefError
|
22
|
+
nil
|
23
|
+
end
|
9
24
|
end
|
10
25
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
path += '.rb' if ext != '.rb'
|
16
|
-
|
17
|
-
pairs = @autoloaded_paths[path] ||= []
|
18
|
-
pairs << [WeakRef.new(const), name]
|
26
|
+
attr_reader :autoloads_by_basename, :interceptor_files_by_path
|
27
|
+
def initialize
|
28
|
+
@autoloads_by_basename = {}
|
29
|
+
@interceptor_files_by_path = {}
|
19
30
|
end
|
20
31
|
|
21
|
-
def
|
22
|
-
|
32
|
+
def autoload_path_for(mod, name, path)
|
33
|
+
interceptor_path = setup_interceptor_for(mod, name, path)
|
23
34
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
pairs
|
35
|
+
if DeepCover.custom_requirer.is_being_required?(path)
|
36
|
+
already_loaded_feature
|
37
|
+
else
|
38
|
+
interceptor_path
|
29
39
|
end
|
30
40
|
end
|
31
41
|
|
32
|
-
def
|
33
|
-
|
42
|
+
def possible_autoload_target?(requested_path)
|
43
|
+
basename = basename_without_extension(requested_path)
|
44
|
+
autoloads = @autoloads_by_basename[basename]
|
45
|
+
autoloads && !autoloads.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
def wrap_require(requested_path, absolute_path_found) # &block
|
49
|
+
entries = entries_for_target(requested_path, absolute_path_found)
|
34
50
|
|
35
51
|
begin
|
36
|
-
|
37
|
-
|
38
|
-
|
52
|
+
entries.each do |entry|
|
53
|
+
mod = entry.mod_if_available
|
54
|
+
next unless mod
|
55
|
+
mod.autoload_without_deep_cover(entry.name, already_loaded_feature)
|
39
56
|
end
|
40
57
|
|
41
58
|
yield
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
59
|
+
ensure
|
60
|
+
entries = entries_for_target(requested_path, absolute_path_found)
|
61
|
+
entries.each do |entry|
|
62
|
+
mod = entry.mod_if_available
|
63
|
+
next unless mod
|
64
|
+
# Putting the autoloads back back since we couldn't complete the require
|
65
|
+
mod.autoload_without_deep_cover(entry.name, entry.interceptor_path)
|
46
66
|
end
|
47
|
-
|
48
|
-
raise
|
49
67
|
end
|
50
68
|
end
|
51
69
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
70
|
+
# This is only used on MRI, so ObjectSpace is alright.
|
71
|
+
def initialize_autoloaded_paths(mods = ObjectSpace.each_object(Module)) # &do_autoload_block
|
72
|
+
mods.each do |mod|
|
73
|
+
# Module's constants are shared with Object. But if you set autoloads directly on Module, they
|
74
|
+
# appear on multiple classes. So just skip, Object will take care of those.
|
75
|
+
next if mod == Module
|
76
|
+
|
77
|
+
if mod.frozen?
|
78
|
+
if mod.constants.any? { |name| mod.autoload?(name) }
|
79
|
+
self.class.warn_frozen_module(mod)
|
59
80
|
end
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
mod.constants.each do |name|
|
85
|
+
path = mod.autoload?(name)
|
86
|
+
next unless path
|
87
|
+
interceptor_path = setup_interceptor_for(mod, name, path)
|
88
|
+
yield mod, name, interceptor_path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# We need to remove the interceptor hooks, otherwise, the problem if manually requiring
|
94
|
+
# something that is autoloaded will cause issues.
|
95
|
+
def remove_interceptors # &do_autoload_block
|
96
|
+
@autoloads_by_basename.each do |basename, entries|
|
97
|
+
entries.each do |entry|
|
98
|
+
mod = entry.mod_if_available
|
99
|
+
next unless mod
|
100
|
+
# Module's constants are shared with Object. But if you set autoloads directly on Module, they
|
101
|
+
# appear on multiple classes. So just skip, Object will take care of those.
|
102
|
+
next if mod == Module
|
103
|
+
yield mod, entry.name, entry.target_path
|
60
104
|
end
|
61
105
|
end
|
106
|
+
|
107
|
+
@autoloaded_paths = {}
|
108
|
+
@interceptor_files_by_path = {}
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
attr_accessor :warned_for_frozen_module
|
113
|
+
end
|
114
|
+
self.warned_for_frozen_module = false
|
115
|
+
|
116
|
+
# Using frozen modules/classes is almost unheard of, but a warning makes things easier if someone does it
|
117
|
+
def self.warn_frozen_module(mod)
|
118
|
+
return if warned_for_frozen_module
|
119
|
+
self.warned_for_frozen_module ||= true
|
120
|
+
warn "There is an autoload on a frozen module/class: #{mod}, DeepCover cannot handle those, failure is probable. " \
|
121
|
+
"This warning won't be displayed again (even for different module/class)"
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def setup_interceptor_for(mod, name, path)
|
127
|
+
interceptor_path = autoload_interceptor_for(path)
|
128
|
+
entry = AutoloadEntry.new(WeakRef.new(mod), name, path, interceptor_path)
|
129
|
+
|
130
|
+
basename = basename_without_extension(path)
|
131
|
+
|
132
|
+
@autoloads_by_basename[basename] ||= []
|
133
|
+
@autoloads_by_basename[basename] << entry
|
134
|
+
interceptor_path
|
62
135
|
end
|
63
136
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
137
|
+
def entries_for_target(requested_path, absolute_path_found)
|
138
|
+
basename = basename_without_extension(requested_path)
|
139
|
+
autoloads = @autoloads_by_basename[basename] || []
|
140
|
+
|
141
|
+
if absolute_path_found
|
142
|
+
autoloads.select { |entry| entry_is_target?(entry, requested_path, absolute_path_found) }
|
143
|
+
elsif requested_path == File.absolute_path(requested_path)
|
144
|
+
[]
|
145
|
+
elsif requested_path.start_with?('./', '../')
|
146
|
+
[]
|
147
|
+
else
|
148
|
+
# We didn't find a path that goes through the $LOAD_PATH
|
149
|
+
# It's possible that RubyGems will actually add the $LOAD_PATH and require an actual file
|
150
|
+
# So we must make a best-guest for possible matches
|
151
|
+
requested_path_to_compare = without_extension(requested_path)
|
152
|
+
autoloads.select { |entry| requested_path_to_compare == without_extension(entry.target_path) }
|
69
153
|
end
|
70
154
|
end
|
71
155
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
156
|
+
def entry_is_target?(entry, requested_path, absolute_path_found)
|
157
|
+
return true if entry.target_path == requested_path
|
158
|
+
target_path_rb = with_rb_extension(entry.target_path)
|
159
|
+
return true if target_path_rb == requested_path
|
160
|
+
|
161
|
+
# Even though this is not efficient, it's safer to resolve entries' target_path each time
|
162
|
+
# instead of storing the result, in case subsequent changes to $LOAD_PATH gives different results
|
163
|
+
entry_absolute_path = DeepCover.custom_requirer.resolve_path(entry.target_path)
|
164
|
+
return true if entry_absolute_path == absolute_path_found
|
165
|
+
false
|
166
|
+
end
|
167
|
+
|
168
|
+
def basename_without_extension(path)
|
169
|
+
without_extension(File.basename(path))
|
170
|
+
end
|
171
|
+
|
172
|
+
def with_rb_extension(path)
|
173
|
+
path += '.rb' unless has_supported_extension?(path)
|
174
|
+
path
|
175
|
+
end
|
176
|
+
|
177
|
+
def without_extension(path)
|
178
|
+
path = path[0...-3] if has_supported_extension?(path)
|
179
|
+
path
|
180
|
+
end
|
181
|
+
|
182
|
+
def has_supported_extension?(path)
|
183
|
+
path.end_with?('.rb', '.so')
|
184
|
+
end
|
185
|
+
|
186
|
+
# It is not possible to simply remove an autoload. So, instead, we must change the
|
187
|
+
# autoload to an already loaded path.
|
188
|
+
# The autoload will be set back to what it was once the require returns. This is
|
189
|
+
# needed in case that required path wasn't the one that fulfilled the autoload, or
|
190
|
+
# if the constant and $LOADED_FEATURES gets removed, since in that situation,
|
191
|
+
# the autoload is supposed to be active again.
|
192
|
+
def already_loaded_feature
|
193
|
+
$LOADED_FEATURES.first
|
194
|
+
end
|
195
|
+
|
196
|
+
def autoload_interceptor_for(path)
|
197
|
+
existing_files = @interceptor_files_by_path[path] || []
|
198
|
+
reusable_file = existing_files.detect { |f| !$LOADED_FEATURES.include?(f.path) }
|
199
|
+
return reusable_file.path if reusable_file
|
200
|
+
|
201
|
+
new_file = Tempfile.new([File.basename(path), '.rb'])
|
202
|
+
# Need to store all the tempfiles so that they are not GCed, which would delete the files themselves.
|
203
|
+
# Keeping them by path allows us to reuse them.
|
204
|
+
@interceptor_files_by_path[path] ||= []
|
205
|
+
@interceptor_files_by_path[path] << new_file
|
206
|
+
new_file.write(<<-RUBY)
|
207
|
+
# Intermediary file for ruby's autoload made by deep-cover
|
208
|
+
require #{path.to_s.inspect}
|
209
|
+
RUBY
|
210
|
+
new_file.close
|
211
|
+
|
212
|
+
new_file.path
|
76
213
|
end
|
77
214
|
end
|
78
215
|
end
|
data/lib/deep_cover/backports.rb
CHANGED
@@ -12,9 +12,11 @@ class Pathname
|
|
12
12
|
def binwrite(*args)
|
13
13
|
File.binwrite(to_path, *args)
|
14
14
|
end unless method_defined? :binwrite
|
15
|
-
end
|
15
|
+
end # nocov
|
16
16
|
require 'backports/2.4.0/false_class/dup'
|
17
17
|
require 'backports/2.4.0/true_class/dup'
|
18
18
|
require 'backports/2.4.0/hash/transform_values'
|
19
19
|
require 'backports/2.4.0/enumerable/sum'
|
20
20
|
require 'backports/2.5.0/hash/slice'
|
21
|
+
require 'backports/2.5.0/hash/transform_keys'
|
22
|
+
require 'backports/2.5.0/kernel/yield_self'
|
data/lib/deep_cover/base.rb
CHANGED
@@ -3,27 +3,32 @@
|
|
3
3
|
module DeepCover
|
4
4
|
module Base
|
5
5
|
def running?
|
6
|
-
@started
|
6
|
+
@started ||= false # rubocop:disable Naming/MemoizedInstanceVariableName [#5648]
|
7
7
|
end
|
8
8
|
|
9
9
|
def start
|
10
|
-
return if
|
10
|
+
return if running?
|
11
11
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
12
|
-
#
|
12
|
+
# Autoloaded files are not supported on jruby. We need to use binding_of_caller
|
13
|
+
# And that appears to be unavailable in jruby.
|
13
14
|
else
|
14
15
|
require_relative 'core_ext/autoload_overrides'
|
15
16
|
AutoloadOverride.active = true
|
16
|
-
autoload_tracker.initialize_autoloaded_paths
|
17
|
+
autoload_tracker.initialize_autoloaded_paths { |mod, name, path| mod.autoload_without_deep_cover(name, path) }
|
17
18
|
end
|
18
19
|
require_relative 'core_ext/require_overrides'
|
19
20
|
RequireOverride.active = true
|
20
21
|
config # actualize configuration
|
22
|
+
@custom_requirer = nil
|
21
23
|
@started = true
|
22
24
|
end
|
23
25
|
|
24
26
|
def stop
|
25
27
|
require_relative 'core_ext/require_overrides'
|
26
|
-
|
28
|
+
if defined? AutoloadOverride
|
29
|
+
AutoloadOverride.active = false
|
30
|
+
autoload_tracker.remove_interceptors { |mod, name, path| mod.autoload_without_deep_cover(name, path) }
|
31
|
+
end
|
27
32
|
RequireOverride.active = false
|
28
33
|
@started = false
|
29
34
|
end
|
@@ -51,16 +56,16 @@ module DeepCover
|
|
51
56
|
def config_changed(what)
|
52
57
|
case what
|
53
58
|
when :paths
|
54
|
-
warn "Changing DeepCover's paths after starting coverage is highly discouraged" if
|
59
|
+
warn "Changing DeepCover's paths after starting coverage is highly discouraged" if running?
|
55
60
|
@custom_requirer = nil
|
56
61
|
when :tracker_global
|
57
|
-
raise NotImplementedError, "Changing DeepCover's tracker global after starting coverage is not supported" if
|
62
|
+
raise NotImplementedError, "Changing DeepCover's tracker global after starting coverage is not supported" if running?
|
58
63
|
@coverage = nil
|
59
64
|
end
|
60
65
|
end
|
61
66
|
|
62
67
|
def reset
|
63
|
-
stop if
|
68
|
+
stop if running?
|
64
69
|
@coverage = @custom_requirer = @autoload_tracker = nil
|
65
70
|
config.reset
|
66
71
|
self
|
data/lib/deep_cover/basics.rb
CHANGED
@@ -59,7 +59,7 @@ module DeepCover
|
|
59
59
|
number_lines(lines, lineno: @lineno)
|
60
60
|
end
|
61
61
|
rescue Exception => e
|
62
|
-
output { "Can't run coverage: #{e.class
|
62
|
+
output { "Can't run coverage: #{e.class}: #{e}\n#{e.backtrace.join("\n")}" }
|
63
63
|
@failed = true
|
64
64
|
end
|
65
65
|
end
|
@@ -100,7 +100,7 @@ module DeepCover
|
|
100
100
|
execute_sample(covered_code)
|
101
101
|
# output { trace_counts } # Keep for low-level debugging purposes
|
102
102
|
rescue Exception => e
|
103
|
-
output { "Can't `execute_sample`:#{e.class
|
103
|
+
output { "Can't `execute_sample`:#{e.class}: #{e}\n#{e.backtrace.join("\n")}" }
|
104
104
|
@failed = true
|
105
105
|
end
|
106
106
|
|
@@ -20,8 +20,13 @@ module DeepCover
|
|
20
20
|
@root_path = @root_path.dirname
|
21
21
|
raise "Can't find Gemfile" unless @root_path.join('Gemfile').exist?
|
22
22
|
end
|
23
|
-
|
24
|
-
|
23
|
+
path = Pathname('~/test_deep_cover').expand_path
|
24
|
+
if path.exist?
|
25
|
+
@dest_root = path.join(@source_path.basename)
|
26
|
+
@dest_root.mkpath
|
27
|
+
else
|
28
|
+
@dest_root = Pathname.new(Dir.mktmpdir('deep_cover_test'))
|
29
|
+
end
|
25
30
|
|
26
31
|
gem_relative_path = @source_path.relative_path_from(@root_path)
|
27
32
|
@main_path = @dest_root.join(gem_relative_path)
|
@@ -107,6 +112,7 @@ module DeepCover
|
|
107
112
|
|
108
113
|
def patch_gemfile
|
109
114
|
gemfile = @dest_root.join('Gemfile')
|
115
|
+
require 'bundler'
|
110
116
|
deps = Bundler::Definition.build(gemfile, nil, nil).dependencies
|
111
117
|
|
112
118
|
return if deps.find { |e| e.name == 'deep-cover' }
|
@@ -137,12 +143,11 @@ module DeepCover
|
|
137
143
|
end
|
138
144
|
|
139
145
|
def cover
|
140
|
-
coverage = Coverage.new
|
146
|
+
coverage = Coverage.new(tracker_global: ::DeepCover.config.tracker_global)
|
141
147
|
each_dir_to_cover do |to_cover|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
coverage: coverage, root_path: @dest_root.to_s,
|
148
|
+
FileUtils.cp_r(to_cover, to_cover.sub_ext('_original'))
|
149
|
+
Tools.dump_covered_code(to_cover,
|
150
|
+
coverage: coverage,
|
146
151
|
dest_path: to_cover)
|
147
152
|
end
|
148
153
|
coverage.save(@dest_root.to_s)
|
@@ -150,7 +155,14 @@ module DeepCover
|
|
150
155
|
|
151
156
|
def process
|
152
157
|
Bundler.with_clean_env do
|
153
|
-
system("cd #{@main_path} && #{@options[:command]}")
|
158
|
+
system({'DISABLE_SPRING' => 'true'}, "cd #{@main_path} && #{@options[:command]}")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def restore
|
163
|
+
each_dir_to_cover do |to_cover|
|
164
|
+
FileUtils.mv(to_cover, to_cover.sub_ext('_instrumented'))
|
165
|
+
FileUtils.mv(to_cover.sub_ext('_original'), to_cover)
|
154
166
|
end
|
155
167
|
end
|
156
168
|
|
@@ -174,6 +186,7 @@ module DeepCover
|
|
174
186
|
patch
|
175
187
|
bundle if @options[:bundle]
|
176
188
|
process
|
189
|
+
restore
|
177
190
|
end
|
178
191
|
report
|
179
192
|
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
require 'slop'
|
5
|
+
require_relative '../../deep-cover'
|
6
|
+
bootstrap
|
7
|
+
|
8
|
+
module CLI
|
9
|
+
module SlopExtension
|
10
|
+
attr_accessor :stopped
|
11
|
+
attr_reader :ignored
|
12
|
+
|
13
|
+
def try_process(*)
|
14
|
+
@ignored ||= 0
|
15
|
+
return if stopped
|
16
|
+
o = super
|
17
|
+
@ignored += 1 unless o
|
18
|
+
o
|
19
|
+
end
|
20
|
+
end
|
21
|
+
::Slop::Parser.prepend SlopExtension
|
22
|
+
|
23
|
+
module Runner
|
24
|
+
extend self
|
25
|
+
|
26
|
+
def show_version
|
27
|
+
require_relative '../version'
|
28
|
+
require 'parser'
|
29
|
+
puts "deep-cover v#{DeepCover::VERSION}; parser v#{Parser::VERSION}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def show_help
|
33
|
+
puts menu
|
34
|
+
end
|
35
|
+
|
36
|
+
class OptionParser < Struct.new(:delegate)
|
37
|
+
def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissing
|
38
|
+
options = args.last
|
39
|
+
if options.is_a?(Hash) && options.has_key?(:default)
|
40
|
+
args[-2] += " [#{options[:default]}]"
|
41
|
+
end
|
42
|
+
delegate.public_send(method, *args, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse
|
47
|
+
Slop.parse do |o|
|
48
|
+
yield OptionParser.new(o)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def menu
|
53
|
+
@menu ||= parse do |o|
|
54
|
+
o.banner = ['usage: deep-cover [options] exec <command ...>',
|
55
|
+
' or deep-cover [options] [path/to/app/or/gem]',
|
56
|
+
].join("\n")
|
57
|
+
o.separator ''
|
58
|
+
o.string '-o', '--output', 'output folder', default: DeepCover.config.output
|
59
|
+
o.string '--reporter', 'reporter', default: DeepCover.config.reporter
|
60
|
+
o.bool '--open', 'open the output coverage', default: CLI_DEFAULTS[:open]
|
61
|
+
|
62
|
+
o.separator 'Coverage options'
|
63
|
+
@ignore_uncovered_map = OPTIONALLY_COVERED.map do |option|
|
64
|
+
default = DeepCover.config.ignore_uncovered.include?(option)
|
65
|
+
o.bool "--ignore-#{dasherize(option)}", '', default: default
|
66
|
+
[:"ignore_#{option}", option]
|
67
|
+
end.to_h
|
68
|
+
|
69
|
+
o.separator "\nWhen not using ’exec’:"
|
70
|
+
o.string '-c', '--command', 'command to run tests', default: CLI_DEFAULTS[:command]
|
71
|
+
o.bool '--bundle', 'run bundle before the tests', default: CLI_DEFAULTS[:bundle]
|
72
|
+
o.bool '--process', 'turn off to only redo the reporting', default: CLI_DEFAULTS[:process]
|
73
|
+
|
74
|
+
o.separator "\nFor testing purposes:"
|
75
|
+
o.bool '--profile', 'use profiler' unless RUBY_PLATFORM == 'java'
|
76
|
+
o.string '-e', '--expression', 'test ruby expression instead of a covering a path'
|
77
|
+
o.bool '-d', '--debug', 'enter debugging after cover'
|
78
|
+
|
79
|
+
o.separator "\nOther available commands:"
|
80
|
+
o.on('--version', 'print the version') do
|
81
|
+
show_version
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
o.boolean('-h', '--help')
|
85
|
+
|
86
|
+
o.boolean('exec', '', help: false) do
|
87
|
+
o.parser.stopped = true if o.parser.ignored == 0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def convert_options(options)
|
93
|
+
iu = options[:ignore_uncovered] = []
|
94
|
+
@ignore_uncovered_map.each do |cli_option, option|
|
95
|
+
iu << option if options.delete(cli_option)
|
96
|
+
end
|
97
|
+
options[:output] = false if ['false', 'f', ''].include?(options[:output])
|
98
|
+
options
|
99
|
+
end
|
100
|
+
|
101
|
+
def go
|
102
|
+
options = convert_options(menu.to_h)
|
103
|
+
if options[:help]
|
104
|
+
show_help
|
105
|
+
elsif options[:expression]
|
106
|
+
require_relative 'debugger'
|
107
|
+
Debugger.new(options[:expression], **options).show
|
108
|
+
elsif menu.parser.stopped
|
109
|
+
require_relative 'exec'
|
110
|
+
Exec.new(menu.arguments, **options).run
|
111
|
+
else
|
112
|
+
require_relative 'instrumented_clone_reporter'
|
113
|
+
path = menu.arguments.first || '.'
|
114
|
+
InstrumentedCloneReporter.new(path, **options).run
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# Poor man's dasherize. 'an_example' => 'an-example'
|
121
|
+
def dasherize(string)
|
122
|
+
string.to_s.tr('_', '-')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|