deep-cover 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +5 -5
  2. data/.deep_cover.rb +8 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +15 -1
  5. data/.travis.yml +1 -0
  6. data/README.md +30 -1
  7. data/Rakefile +10 -1
  8. data/bin/cov +1 -1
  9. data/deep_cover.gemspec +4 -5
  10. data/exe/deep-cover +5 -3
  11. data/lib/deep_cover.rb +1 -1
  12. data/lib/deep_cover/analyser/node.rb +1 -1
  13. data/lib/deep_cover/analyser/ruby25_like_branch.rb +209 -0
  14. data/lib/deep_cover/auto_run.rb +19 -19
  15. data/lib/deep_cover/autoload_tracker.rb +181 -44
  16. data/lib/deep_cover/backports.rb +3 -1
  17. data/lib/deep_cover/base.rb +13 -8
  18. data/lib/deep_cover/basics.rb +1 -1
  19. data/lib/deep_cover/cli/debugger.rb +2 -2
  20. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +21 -8
  21. data/lib/deep_cover/cli/runner.rb +126 -0
  22. data/lib/deep_cover/config_setter.rb +1 -0
  23. data/lib/deep_cover/core_ext/autoload_overrides.rb +82 -14
  24. data/lib/deep_cover/core_ext/coverage_replacement.rb +34 -5
  25. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  26. data/lib/deep_cover/core_ext/load_overrides.rb +4 -6
  27. data/lib/deep_cover/core_ext/require_overrides.rb +1 -3
  28. data/lib/deep_cover/coverage.rb +105 -2
  29. data/lib/deep_cover/coverage/analysis.rb +30 -28
  30. data/lib/deep_cover/coverage/persistence.rb +60 -70
  31. data/lib/deep_cover/covered_code.rb +16 -49
  32. data/lib/deep_cover/custom_requirer.rb +112 -51
  33. data/lib/deep_cover/load.rb +10 -6
  34. data/lib/deep_cover/memoize.rb +1 -3
  35. data/lib/deep_cover/module_override.rb +7 -0
  36. data/lib/deep_cover/node/assignments.rb +2 -1
  37. data/lib/deep_cover/node/base.rb +6 -6
  38. data/lib/deep_cover/node/block.rb +10 -8
  39. data/lib/deep_cover/node/case.rb +3 -3
  40. data/lib/deep_cover/node/collections.rb +8 -0
  41. data/lib/deep_cover/node/if.rb +19 -3
  42. data/lib/deep_cover/node/literals.rb +28 -7
  43. data/lib/deep_cover/node/mixin/can_augment_children.rb +4 -4
  44. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +1 -1
  45. data/lib/deep_cover/node/mixin/filters.rb +6 -2
  46. data/lib/deep_cover/node/mixin/has_child.rb +8 -8
  47. data/lib/deep_cover/node/mixin/has_child_handler.rb +3 -3
  48. data/lib/deep_cover/node/mixin/has_tracker.rb +7 -3
  49. data/lib/deep_cover/node/root.rb +1 -1
  50. data/lib/deep_cover/node/send.rb +53 -7
  51. data/lib/deep_cover/node/short_circuit.rb +11 -3
  52. data/lib/deep_cover/parser_ext/range.rb +11 -27
  53. data/lib/deep_cover/problem_with_diagnostic.rb +1 -1
  54. data/lib/deep_cover/reporter.rb +0 -1
  55. data/lib/deep_cover/reporter/base.rb +68 -0
  56. data/lib/deep_cover/reporter/html.rb +1 -1
  57. data/lib/deep_cover/reporter/html/index.rb +4 -8
  58. data/lib/deep_cover/reporter/html/site.rb +10 -18
  59. data/lib/deep_cover/reporter/html/source.rb +3 -3
  60. data/lib/deep_cover/reporter/html/template/source.html.erb +1 -1
  61. data/lib/deep_cover/reporter/istanbul.rb +86 -56
  62. data/lib/deep_cover/reporter/text.rb +5 -13
  63. data/lib/deep_cover/reporter/{util/tree.rb → tree/util.rb} +19 -21
  64. data/lib/deep_cover/tools/blank.rb +25 -0
  65. data/lib/deep_cover/tools/builtin_coverage.rb +8 -8
  66. data/lib/deep_cover/tools/dump_covered_code.rb +2 -9
  67. data/lib/deep_cover/tools/execute_sample.rb +17 -6
  68. data/lib/deep_cover/tools/format_generated_code.rb +1 -1
  69. data/lib/deep_cover/tools/indent_string.rb +26 -0
  70. data/lib/deep_cover/tools/our_coverage.rb +2 -2
  71. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  72. data/lib/deep_cover/tracker_bucket.rb +50 -0
  73. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  74. data/lib/deep_cover/tracker_storage.rb +76 -0
  75. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  76. data/lib/deep_cover/version.rb +1 -1
  77. data/lib/deep_cover_entry.rb +3 -0
  78. metadata +30 -37
  79. data/bin/gemcov +0 -8
  80. data/bin/selfcov +0 -21
  81. data/lib/deep_cover/cli/deep_cover.rb +0 -126
  82. data/lib/deep_cover/coverage/base.rb +0 -81
  83. data/lib/deep_cover/coverage/istanbul.rb +0 -34
  84. 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
- def initialize(autoloaded_paths = {})
8
- @autoloaded_paths = autoloaded_paths
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
- def add(const, name, path)
12
- ext = File.extname(path)
13
- # We don't care about .so files
14
- return if ext == '.so'
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 pairs_for_absolute_path(absolute_path)
22
- paths = autoloaded_paths_matching_absolute(absolute_path)
32
+ def autoload_path_for(mod, name, path)
33
+ interceptor_path = setup_interceptor_for(mod, name, path)
23
34
 
24
- paths.flat_map do |path|
25
- pairs = @autoloaded_paths[path] || []
26
- pairs = pairs.map { |weak_const, name| [self.class.value_from_weak_ref(weak_const), name] }
27
- pairs.select!(&:first)
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 wrap_require(absolute_path)
33
- pairs = pairs_for_absolute_path(absolute_path)
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
- pairs.each do |const, name|
37
- # Changing the autoload to an already loaded file (this one)
38
- const.autoload_without_deep_cover(name, __FILE__)
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
- rescue Exception
43
- pairs.each do |const, name|
44
- # Changing the autoload to an already loaded file (this one)
45
- const.autoload_without_deep_cover(name, absolute_path)
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
- def initialize_autoloaded_paths
53
- @autoloaded_paths = {}
54
- # This is only used on MRI, so ObjectSpace is alright.
55
- ObjectSpace.each_object(Module) do |mod|
56
- mod.constants.each do |name|
57
- if (path = mod.autoload?(name))
58
- add(mod, name, path)
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
- # We need all the paths of autoloaded_path that match a given absolute_path
65
- # Since this can happen a lot, a cache is made which only chan
66
- def autoloaded_paths_matching_absolute(absolute_path)
67
- @autoloaded_paths.keys.select do |path|
68
- absolute_path == DeepCover.custom_requirer.resolve_path(path)
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
- # A simple if the ref is dead, return nil.
73
- # WTF ruby, why is there no such simple interface ?!
74
- def self.value_from_weak_ref(weak_ref)
75
- WeakRef.class_variable_get(:@@__map)[weak_ref]
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
@@ -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'
@@ -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 @started
10
+ return if running?
11
11
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
12
- # No issues with autoload in jruby, so no need to override it!
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
- AutoloadOverride.active = false if defined? AutoloadOverride
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 @started
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 @started
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 @started
68
+ stop if running?
64
69
  @coverage = @custom_requirer = @autoload_tracker = nil
65
70
  config.reset
66
71
  self
@@ -18,5 +18,5 @@ module DeepCover
18
18
  open: false,
19
19
  }.freeze
20
20
 
21
- OPTIONALLY_COVERED = %i[case_implicit_else default_argument raise trivial_if]
21
+ OPTIONALLY_COVERED = %i[case_implicit_else default_argument raise trivial_if warn]
22
22
  end
@@ -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.name}: #{e}\n#{e.backtrace.join("\n")}" }
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.name}: #{e}\n#{e.backtrace.join("\n")}" }
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
- @dest_root = Pathname('~/test_deep_cover').expand_path
24
- @dest_root = Pathname.new(Dir.mktmpdir('deep_cover_test')) unless @dest_root.exist?
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
- original = to_cover.sub_ext('_original')
143
- FileUtils.cp_r(to_cover, original)
144
- Tools.dump_covered_code(original,
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