deep-cover-core 0.6.4 → 0.7.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/deep_cover_core.gemspec +1 -0
  3. data/lib/deep_cover.rb +3 -20
  4. data/lib/deep_cover/auto_run.rb +10 -35
  5. data/lib/deep_cover/autoload_tracker.rb +9 -5
  6. data/lib/deep_cover/base.rb +85 -12
  7. data/lib/deep_cover/basics.rb +15 -3
  8. data/lib/deep_cover/config.rb +36 -2
  9. data/lib/deep_cover/config_setter.rb +1 -1
  10. data/lib/deep_cover/core_ext/exec_callbacks.rb +15 -9
  11. data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +1 -1
  12. data/lib/deep_cover/coverage.rb +51 -41
  13. data/lib/deep_cover/covered_code.rb +54 -17
  14. data/lib/deep_cover/custom_requirer.rb +10 -10
  15. data/lib/deep_cover/global_variables.rb +32 -0
  16. data/lib/deep_cover/load.rb +25 -9
  17. data/lib/deep_cover/node/base.rb +6 -6
  18. data/lib/deep_cover/node/begin.rb +1 -2
  19. data/lib/deep_cover/node/block.rb +5 -1
  20. data/lib/deep_cover/node/case.rb +1 -1
  21. data/lib/deep_cover/node/empty_body.rb +1 -5
  22. data/lib/deep_cover/node/if.rb +3 -4
  23. data/lib/deep_cover/node/loops.rb +1 -1
  24. data/lib/deep_cover/node/mixin/flow_accounting.rb +1 -8
  25. data/lib/deep_cover/node/mixin/has_child.rb +3 -12
  26. data/lib/deep_cover/node/mixin/has_child_handler.rb +2 -5
  27. data/lib/deep_cover/node/mixin/has_tracker.rb +3 -7
  28. data/lib/deep_cover/node/module.rb +20 -23
  29. data/lib/deep_cover/node/root.rb +1 -1
  30. data/lib/deep_cover/node/send.rb +4 -5
  31. data/lib/deep_cover/node/short_circuit.rb +1 -1
  32. data/lib/deep_cover/persistence.rb +100 -0
  33. data/lib/deep_cover/problem_with_diagnostic.rb +2 -2
  34. data/lib/deep_cover/setup/clone_mode_entry_template.rb +66 -0
  35. data/lib/deep_cover/setup/deep_cover_auto_run.rb +21 -0
  36. data/lib/deep_cover/setup/deep_cover_config.rb +19 -0
  37. data/lib/deep_cover/setup/deep_cover_without_config.rb +15 -0
  38. data/lib/deep_cover/tools.rb +0 -2
  39. data/lib/deep_cover/tools/after_tests.rb +33 -0
  40. data/lib/deep_cover/tools/looks_like_rails_project.rb +13 -0
  41. data/lib/deep_cover/tools/our_coverage.rb +1 -1
  42. data/lib/deep_cover/tools/scan_match_datas.rb +1 -1
  43. data/lib/deep_cover/version.rb +4 -2
  44. metadata +24 -7
  45. data/lib/deep_cover/coverage/persistence.rb +0 -84
  46. data/lib/deep_cover/tracker_bucket.rb +0 -50
  47. data/lib/deep_cover/tracker_hits_per_path.rb +0 -35
  48. data/lib/deep_cover/tracker_storage.rb +0 -76
  49. data/lib/deep_cover/tracker_storage_per_path.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 786d0c79f687a02eefb1e3c9dc1af8c9b4b25d4c18fa86988d1c1a84d56f80e6
4
- data.tar.gz: 6b3ef23fa825eb7f72673e4a4be0fe3889418c035d0b87c17323c2ceb97f5fba
3
+ metadata.gz: 26469b9b1a015b36a11d432d22d871647ba060fc272c4edd274175d7163dcceb
4
+ data.tar.gz: 51917845e6fdcc9e073e776fc13cade71a82d7f0806a01d36e1b653c52ee8b5f
5
5
  SHA512:
6
- metadata.gz: 42806a5cf72180c3af74f5df4d03c067f52cade9d473adb2794db02416496de9aa6100390a4d3a597710378d8dd9bfbef783a65394cf9920adff115af53108cb
7
- data.tar.gz: 20d7363fe8fa8c1195d0eaf98f21f91bbfeb2371383fde3b1bddc28f986b503da7ef397f4d1ac3f61a49790018fc08ae038bf9314edf4f0e77ffab0eafce8283
6
+ metadata.gz: fd179008eefe481fdcd29f5857228fda6cf1b364b027694309ca40e664a548be13ba404d168d3d1f34e7749b7d037f9d70a4cfaa11450615ccec4d7aa615a6c2
7
+ data.tar.gz: bc452e18b3ec005229d4c889c5396cffa2ddadb1daa9cd4132f9e66b126be6d2a305b76d54718e1fcea5c8f548ef4e6e518620a221c8712298bc318f4068b1ea
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  # Support
30
30
  spec.add_runtime_dependency 'backports', '>= 3.11.0'
31
31
  spec.add_runtime_dependency 'binding_of_caller'
32
+ spec.add_runtime_dependency 'term-ansicolor'
32
33
 
33
34
  # Reporters
34
35
  spec.add_runtime_dependency 'terminal-table'
data/lib/deep_cover.rb CHANGED
@@ -1,22 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DeepCover
4
- require_relative 'deep_cover/load'
5
-
6
- load_absolute_basics
7
-
8
- extend Base
9
- extend ConfigSetter
10
- end
11
- DeepCover::GLOBAL_BINDING = binding
12
-
13
- require './.deep_cover.rb' if File.exist?('./.deep_cover.rb')
14
-
15
- if ENV['DEEP_COVER_OPTIONS']
16
- DeepCover.config.set(YAML.load(ENV['DEEP_COVER_OPTIONS']))
17
- end
18
- if %w[1 t true].include?(ENV['DEEP_COVER'])
19
- DeepCover.start
20
- require_relative 'deep_cover/auto_run'
21
- DeepCover::AutoRun.run!('.').report!(**DeepCover.config)
22
- end
3
+ require_relative 'deep_cover/setup/deep_cover_without_config'
4
+ require_relative 'deep_cover/setup/deep_cover_config'
5
+ require_relative 'deep_cover/setup/deep_cover_auto_run'
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'core_ext/exec_callbacks'
4
+ require_relative 'tools/after_tests'
4
5
 
5
6
  module DeepCover
6
7
  module AutoRun
7
8
  class Runner
8
- def initialize(covered_path)
9
- @covered_path = covered_path
10
- @saved = !(DeepCover.respond_to?(:running?) && DeepCover.running?)
9
+ include Tools::AfterTests
10
+ def initialize
11
+ @saved = false
11
12
  end
12
13
 
13
14
  def run!
@@ -23,49 +24,23 @@ module DeepCover
23
24
 
24
25
  private
25
26
 
26
- def saved?
27
- @saved
28
- end
29
-
30
- def coverage
31
- @coverage ||= if saved?
32
- Coverage.load(@covered_path, with_trackers: false)
33
- else
34
- DeepCover.coverage
35
- end
36
- end
37
-
38
27
  def save
28
+ return if @saved
39
29
  require_relative '../deep_cover'
40
- coverage.save(@covered_path) unless saved?
41
- coverage.save_trackers(@covered_path)
30
+ DeepCover.persistence.save_trackers(DeepCover::GlobalVariables.tracker_hits_per_path)
31
+ @saved = true
42
32
  end
43
33
 
44
34
  def report(**options)
35
+ save # Some of the hooks seem to do things in reverse order. Not sure if all of them.
36
+ coverage = Coverage.load
45
37
  coverage.report(**options)
46
38
  end
47
-
48
- def after_tests
49
- use_at_exit = true
50
- if defined?(Minitest)
51
- use_at_exit = false
52
- Minitest.after_run { yield }
53
- end
54
- if defined?(Rspec)
55
- use_at_exit = false
56
- RSpec.configure do |config|
57
- config.after(:suite) { yield }
58
- end
59
- end
60
- if use_at_exit
61
- at_exit { yield }
62
- end
63
- end
64
39
  end
65
40
 
66
41
  def self.run!(covered_path)
67
42
  @runners ||= {}
68
- @runners[covered_path] ||= Runner.new(covered_path).run!
43
+ @runners[covered_path] ||= Runner.new.run!
69
44
  end
70
45
  end
71
46
  end
@@ -170,17 +170,21 @@ module DeepCover
170
170
  end
171
171
 
172
172
  def with_rb_extension(path)
173
- path += '.rb' unless has_supported_extension?(path)
173
+ path += '.rb' unless find_requirable_extension(path)
174
174
  path
175
175
  end
176
176
 
177
177
  def without_extension(path)
178
- path = path[0...-3] if has_supported_extension?(path)
179
- path
178
+ if (ext = find_requirable_extension(path))
179
+ path[0...-ext.length]
180
+ else
181
+ path
182
+ end
180
183
  end
181
184
 
182
- def has_supported_extension?(path)
183
- path.end_with?('.rb', '.so')
185
+ private def find_requirable_extension(path)
186
+ ext = File.extname(path)
187
+ REQUIRABLE_EXTENSIONS[ext] && ext
184
188
  end
185
189
 
186
190
  # It is not possible to simply remove an autoload. So, instead, we must change the
@@ -27,7 +27,7 @@ module DeepCover
27
27
  end
28
28
 
29
29
  config # actualize configuration
30
- @lookup_paths = nil
30
+ @lookup_globs = @all_tracked_file_paths = nil
31
31
  @started = true
32
32
  end
33
33
 
@@ -41,6 +41,10 @@ module DeepCover
41
41
  @started = false
42
42
  end
43
43
 
44
+ def delete_trackers
45
+ persistence.delete_trackers
46
+ end
47
+
44
48
  def line_coverage(filename)
45
49
  coverage.line_coverage(handle_relative_filename(filename), **config.to_h)
46
50
  end
@@ -65,7 +69,7 @@ module DeepCover
65
69
  case what
66
70
  when :paths
67
71
  warn "Changing DeepCover's paths after starting coverage is highly discouraged" if running?
68
- @lookup_paths = nil
72
+ @lookup_globs = @all_tracked_file_paths = nil
69
73
  when :tracker_global
70
74
  raise NotImplementedError, "Changing DeepCover's tracker global after starting coverage is not supported" if running?
71
75
  @coverage = nil
@@ -74,25 +78,90 @@ module DeepCover
74
78
 
75
79
  def reset
76
80
  stop if running?
77
- @coverage = @custom_requirer = @autoload_tracker = @lookup_paths = nil
81
+ @coverage = @custom_requirer = @autoload_tracker = @lookup_globs = @all_tracked_file_paths = nil
78
82
  config.reset
79
83
  self
80
84
  end
81
85
 
82
86
  def coverage
83
- @coverage ||= Coverage.new(tracker_global: config.tracker_global)
87
+ @coverage ||= Coverage.new
88
+ end
89
+
90
+ def lookup_globs
91
+ return @lookup_globs if @lookup_globs
92
+ paths = Array(config.paths || :auto_detect).dup
93
+ paths.concat(auto_detected_paths) if paths.delete(:auto_detect)
94
+
95
+ paths = paths.map { |p| File.expand_path(p) }
96
+ paths = ['/'] if paths.include?('/')
97
+ globs = paths.map! do |path|
98
+ if File.directory?(path)
99
+ # File.join is needed to avoid //**/*.rb
100
+ File.join(path, '**/*.rb')
101
+ else
102
+ # Either a single file's path, a glob, or a path that doesn't exists
103
+ path
104
+ end
105
+ end
106
+ @lookup_globs = globs
84
107
  end
85
108
 
86
- def lookup_paths
87
- return @lookup_paths if @lookup_paths
88
- lookup_paths = config.paths || Dir.getwd
89
- lookup_paths = Array(lookup_paths).map { |p| File.expand_path(p) }
90
- lookup_paths = ['/'] if lookup_paths.include?('/')
91
- @lookup_paths = lookup_paths
109
+ # Auto detects path that we want to cover. This is used when :auto_detect is in the config.paths.
110
+ # If the results aren't what you expect, then specify the paths yourself.
111
+ # We want this to work for most project's struture:
112
+ # * Single gems: just a gem directly in the top-level
113
+ # * Multi gems: contains multiple gems, each in a dir of the top-level dir (the rails gem does that)
114
+ # * Hybrid gems: a gem in the top-level dir and one in sub-dirs (the deep-cover gem does that)
115
+ # * Rails application
116
+ #
117
+ # For gems and Rails application, normally, everything to check coverage for is in lib/, and app/.
118
+ # For other projects, we go for every directories except test/ spec/ bin/ exe/.
119
+ #
120
+ # If the current dir has a .gemspec file, we consider it a "root".
121
+ # In addition, if any sub-dir of the current dir (not recursive) has a .gemspec file, we also consider them as "roots"
122
+ # If the current dir looks like a Rails application, add it as a "root"
123
+ # For each "roots", the "tracked dirs" will be "#{root}/app" and "#{root}/lib"
124
+ #
125
+ # If no "tracked dirs" exist, fallback to everything in current directory except each of test/ spec/ bin/ exe/.
126
+ def auto_detected_paths
127
+ require_relative 'tools/looks_like_rails_project'
128
+
129
+ gemspec_paths = Dir['./*.gemspec'] + Dir['./*/*.gemspec']
130
+ root_paths = gemspec_paths.map!(&File.method(:dirname))
131
+ root_paths.uniq!
132
+ root_paths << '.' if !root_paths.include?('.') && Tools::LooksLikeRailsProject.looks_like_rails_project?('.')
133
+
134
+ tracked_paths = root_paths.flat_map { |p| [File.join(p, 'app'), File.join(p, 'lib')] }
135
+ tracked_paths.select! { |p| File.exist?(p) }
136
+
137
+ if tracked_paths.empty?
138
+ # So track every sub-dirs except a couple
139
+ # The final '/' in Dir[] makes it return directories only, but they will also end with a '/'
140
+ # So need to include that last '/' in the substracted paths.
141
+ # TODO: We probably want a cleaner way of filtering out some directories. But for now, that's what we got.
142
+ tracked_paths = Dir['./*/'] - %w(./autotest/ ./features/ ./spec/ ./test/ ./bin/ ./exe/)
143
+ # And track every ruby files in the top-level
144
+ tracked_paths << './*.rb'
145
+ end
146
+ tracked_paths
147
+ # path expansion is done in #lookup_globs
148
+ end
149
+
150
+ def tracked_file_path?(path)
151
+ # The flags are to make fnmatch match the same things as Dir.glob... This doesn't seem to be documented anywhere
152
+ # EXTGLOB: allow matching {lib,app} as either lib or app
153
+ # PATHNAME: Makes wildcard match not match /, and make /**/ (and pattern starting with **/) be any number of nested directory
154
+ lookup_globs.any? { |glob| File.fnmatch?(glob, path, File::FNM_EXTGLOB | File::FNM_PATHNAME) }
92
155
  end
93
156
 
94
- def within_lookup_paths?(path)
95
- lookup_paths.any? { |lookup_path| path.start_with?(lookup_path) }
157
+ def all_tracked_file_paths
158
+ return @all_tracked_file_paths.dup if @all_tracked_file_paths
159
+ paths_found = Dir[*lookup_globs]
160
+ paths_found.select! { |path| path.end_with?('.rb') }
161
+ paths_found.select! { |path| File.file?(path) }
162
+ paths_found.uniq!
163
+ @all_tracked_file_paths = paths_found
164
+ @all_tracked_file_paths.dup
96
165
  end
97
166
 
98
167
  def custom_requirer
@@ -103,6 +172,10 @@ module DeepCover
103
172
  @autoload_tracker ||= AutoloadTracker.new
104
173
  end
105
174
 
175
+ def persistence
176
+ @persistence ||= Persistence.new(config.cache_directory)
177
+ end
178
+
106
179
  private
107
180
 
108
181
  def handle_relative_filename(filename)
@@ -4,19 +4,31 @@
4
4
  module DeepCover
5
5
  DEFAULTS = {
6
6
  ignore_uncovered: [].freeze,
7
- paths: %w[./app ./lib].freeze,
7
+ paths: [:auto_detect].freeze,
8
8
  allow_partial: false,
9
9
  tracker_global: '$_cov',
10
10
  reporter: :html,
11
11
  output: './coverage',
12
+ cache_directory: './deep_cover',
12
13
  }.freeze
13
14
 
14
15
  CLI_DEFAULTS = {
15
- command: 'bundle exec rake',
16
- bundle: true,
16
+ command: %w(bundle exec rake),
17
17
  process: true,
18
18
  open: false,
19
19
  }.freeze
20
20
 
21
21
  OPTIONALLY_COVERED = %i[case_implicit_else default_argument raise trivial_if warn]
22
+
23
+ REQUIRABLE_EXTENSIONS = {
24
+ '.rb' => :ruby,
25
+ ".#{RbConfig::CONFIG['DLEXT']}" => :native_extension,
26
+ }
27
+ unless (RbConfig::CONFIG['DLEXT2'] || '').empty?
28
+ REQUIRABLE_EXTENSIONS[".#{RbConfig::CONFIG['DLEXT2']}"] = :native_extension
29
+ end
30
+ REQUIRABLE_EXTENSIONS.freeze
31
+ REQUIRABLE_EXTENSION_KEYS = REQUIRABLE_EXTENSIONS.keys.freeze
32
+
33
+ CORE_GEM_LIB_DIRECTORY = File.expand_path(__dir__ + '/..')
22
34
  end
@@ -3,8 +3,10 @@
3
3
  module DeepCover
4
4
  class Config
5
5
  def initialize(notify = nil)
6
+ @notify = nil
7
+ @options = {ignore_uncovered: []}
8
+ set(**DEFAULTS)
6
9
  @notify = notify
7
- @options = DEFAULTS.dup
8
10
  end
9
11
 
10
12
  def to_hash
@@ -12,6 +14,23 @@ module DeepCover
12
14
  end
13
15
  alias_method :to_h, :to_hash
14
16
 
17
+ def to_hash_for_serialize
18
+ hash = to_hash
19
+ # TODO: (Max) I don't like mixup of configs being partly on DeepCover and Config like that...
20
+ hash[:paths] = DeepCover.lookup_globs
21
+ hash[:output] = hash[:output] ? File.expand_path(hash[:output]) : hash[:output]
22
+ hash[:cache_directory] = File.expand_path(hash[:cache_directory])
23
+ hash
24
+ end
25
+
26
+ def load_hash_for_serialize(hash)
27
+ @options.merge!(hash)
28
+ hash.each_key { |option| @notify.config_changed(option) } if @notify
29
+ # This was already transformed, it should all be absolute paths / globs, avoid doing it for nothing by setting it right away
30
+ # TODO: (Max) I don't like mixup of configs being partly on DeepCover and Config like that...
31
+ DeepCover.instance_variable_set(:@lookup_globs, hash[:paths])
32
+ end
33
+
15
34
  def ignore_uncovered(*keywords, &block)
16
35
  if block
17
36
  raise ArgumentError, "wrong number of arguments (given #{keywords.size}, expected 0..1)" if keywords.size > 1
@@ -68,6 +87,22 @@ module DeepCover
68
87
  end
69
88
  end
70
89
 
90
+ def cache_directory(cache_directory = nil)
91
+ if cache_directory
92
+ change(:cache_directory, cache_directory)
93
+ else
94
+ File.expand_path(@options[:cache_directory])
95
+ end
96
+ end
97
+
98
+ def allow_partial(allow_partial = nil)
99
+ if allow_partial != nil
100
+ change(:allow_partial, allow_partial)
101
+ else
102
+ @options[:allow_partial]
103
+ end
104
+ end
105
+
71
106
  def reset
72
107
  DEFAULTS.each do |key, value|
73
108
  change(key, value)
@@ -78,7 +113,6 @@ module DeepCover
78
113
  def set(**options)
79
114
  @options[:ignore_uncovered] = [] if options.has_key?(:ignore_uncovered)
80
115
  options.each do |key, value|
81
- next if key == :allow_partial
82
116
  public_send key, value
83
117
  end
84
118
  self
@@ -7,7 +7,7 @@ module DeepCover
7
7
  end
8
8
 
9
9
  def config(notify = self)
10
- raise ArgumentError, 'config does not accept an argument. Did you mean `configure`?' if block_given?
10
+ raise ArgumentError, 'config does not accept a block. Did you mean `configure`?' if block_given?
11
11
  @config ||= Config.new(notify)
12
12
  config_queue.each { |block| configure(&block) }
13
13
  config_queue.clear
@@ -1,27 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../module_override'
3
+ # This file is used by projects cloned with clone mode. As such, special care must be taken to
4
+ # be compatible with any projects.
5
+ # THERE MUST NOT BE ANY USE/REQUIRE OF DEPENDENCIES OF DeepCover HERE
6
+ # See deep-cover/core_gem/lib/deep_cover/setup/clone_mode_entry_template.rb for explanation of
7
+ # clone mode and of this top_level_module stuff.
8
+ top_level_module = Thread.current['_deep_cover_top_level_module'] || Object # rubocop:disable Lint/UselessAssignment
4
9
 
5
10
  # Adds a functionality to add callbacks before an `exec`
6
11
 
7
- module DeepCover
12
+ module top_level_module::DeepCover # rubocop:disable Naming/ClassAndModuleCamelCase
8
13
  module ExecCallbacks
9
14
  class << self
10
15
  attr_reader :callbacks
11
16
 
12
17
  def before_exec(&block)
13
- self.active = true
14
18
  (@callbacks ||= []) << block
15
19
  end
16
20
  end
21
+ end
17
22
 
18
- def exec(*args)
23
+ # We use #object_id of DeepCover to avoid possible overwrite between clone-mode and non-clone-mode
24
+ original_exec_name = :"exec_without_deep_cover_#{self.object_id}"
25
+ [::Kernel, ::Kernel.singleton_class].each do |mod|
26
+ mod.send(:alias_method, original_exec_name, :exec)
27
+ mod.send(:define_method, :exec) do |*args|
19
28
  ExecCallbacks.callbacks.each(&:call)
20
- exec_without_deep_cover(*args)
29
+ send(original_exec_name, *args)
21
30
  end
22
-
23
- extend ModuleOverride
24
- override ::Kernel, ::Kernel.singleton_class
25
- self.active = true
26
31
  end
32
+ ::Kernel.send :private, original_exec_name
27
33
  end
@@ -17,7 +17,7 @@ module DeepCover
17
17
 
18
18
  def self.load_iseq_logic(path)
19
19
  return unless DeepCover.running?
20
- return unless DeepCover.within_lookup_paths?(path)
20
+ return unless DeepCover.tracked_file_path?(path)
21
21
 
22
22
  covered_code = DeepCover.coverage.covered_code_or_warn(path)
23
23
  return unless covered_code