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.
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