deep-cover-core 0.6.3.pre

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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rspec_all +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Gemfile +11 -0
  6. data/deep_cover_core.gemspec +46 -0
  7. data/lib/deep-cover.rb +3 -0
  8. data/lib/deep_cover/analyser/base.rb +104 -0
  9. data/lib/deep_cover/analyser/branch.rb +41 -0
  10. data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
  11. data/lib/deep_cover/analyser/function.rb +14 -0
  12. data/lib/deep_cover/analyser/node.rb +54 -0
  13. data/lib/deep_cover/analyser/per_char.rb +38 -0
  14. data/lib/deep_cover/analyser/per_line.rb +41 -0
  15. data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
  16. data/lib/deep_cover/analyser/statement.rb +33 -0
  17. data/lib/deep_cover/analyser/stats.rb +54 -0
  18. data/lib/deep_cover/analyser/subset.rb +27 -0
  19. data/lib/deep_cover/analyser.rb +23 -0
  20. data/lib/deep_cover/auto_run.rb +71 -0
  21. data/lib/deep_cover/autoload_tracker.rb +215 -0
  22. data/lib/deep_cover/backports.rb +22 -0
  23. data/lib/deep_cover/base.rb +117 -0
  24. data/lib/deep_cover/basics.rb +22 -0
  25. data/lib/deep_cover/builtin_takeover.rb +10 -0
  26. data/lib/deep_cover/config.rb +104 -0
  27. data/lib/deep_cover/config_setter.rb +33 -0
  28. data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
  29. data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
  30. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  31. data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
  32. data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
  33. data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
  34. data/lib/deep_cover/coverage/analysis.rb +42 -0
  35. data/lib/deep_cover/coverage/persistence.rb +84 -0
  36. data/lib/deep_cover/coverage.rb +125 -0
  37. data/lib/deep_cover/covered_code.rb +145 -0
  38. data/lib/deep_cover/custom_requirer.rb +187 -0
  39. data/lib/deep_cover/flag_comment_associator.rb +68 -0
  40. data/lib/deep_cover/load.rb +66 -0
  41. data/lib/deep_cover/memoize.rb +48 -0
  42. data/lib/deep_cover/module_override.rb +39 -0
  43. data/lib/deep_cover/node/arguments.rb +51 -0
  44. data/lib/deep_cover/node/assignments.rb +273 -0
  45. data/lib/deep_cover/node/base.rb +155 -0
  46. data/lib/deep_cover/node/begin.rb +27 -0
  47. data/lib/deep_cover/node/block.rb +61 -0
  48. data/lib/deep_cover/node/branch.rb +32 -0
  49. data/lib/deep_cover/node/case.rb +113 -0
  50. data/lib/deep_cover/node/collections.rb +31 -0
  51. data/lib/deep_cover/node/const.rb +12 -0
  52. data/lib/deep_cover/node/def.rb +40 -0
  53. data/lib/deep_cover/node/empty_body.rb +32 -0
  54. data/lib/deep_cover/node/exceptions.rb +79 -0
  55. data/lib/deep_cover/node/if.rb +73 -0
  56. data/lib/deep_cover/node/keywords.rb +86 -0
  57. data/lib/deep_cover/node/literals.rb +100 -0
  58. data/lib/deep_cover/node/loops.rb +74 -0
  59. data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
  60. data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
  61. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
  62. data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
  63. data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
  64. data/lib/deep_cover/node/mixin/filters.rb +47 -0
  65. data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
  66. data/lib/deep_cover/node/mixin/has_child.rb +145 -0
  67. data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
  68. data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
  69. data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
  70. data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
  71. data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
  72. data/lib/deep_cover/node/module.rb +66 -0
  73. data/lib/deep_cover/node/root.rb +20 -0
  74. data/lib/deep_cover/node/send.rb +161 -0
  75. data/lib/deep_cover/node/short_circuit.rb +42 -0
  76. data/lib/deep_cover/node/splat.rb +15 -0
  77. data/lib/deep_cover/node/variables.rb +16 -0
  78. data/lib/deep_cover/node.rb +23 -0
  79. data/lib/deep_cover/parser_ext/range.rb +21 -0
  80. data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
  81. data/lib/deep_cover/reporter/base.rb +68 -0
  82. data/lib/deep_cover/reporter/html/base.rb +14 -0
  83. data/lib/deep_cover/reporter/html/index.rb +59 -0
  84. data/lib/deep_cover/reporter/html/site.rb +68 -0
  85. data/lib/deep_cover/reporter/html/source.rb +136 -0
  86. data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
  87. data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
  88. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css +291 -0
  89. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -0
  90. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
  91. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
  92. data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
  93. data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
  94. data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
  95. data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
  96. data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
  97. data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
  98. data/lib/deep_cover/reporter/html.rb +15 -0
  99. data/lib/deep_cover/reporter/istanbul.rb +184 -0
  100. data/lib/deep_cover/reporter/text.rb +58 -0
  101. data/lib/deep_cover/reporter/tree/util.rb +86 -0
  102. data/lib/deep_cover/reporter.rb +10 -0
  103. data/lib/deep_cover/tools/blank.rb +25 -0
  104. data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
  105. data/lib/deep_cover/tools/camelize.rb +13 -0
  106. data/lib/deep_cover/tools/content_tag.rb +11 -0
  107. data/lib/deep_cover/tools/covered.rb +9 -0
  108. data/lib/deep_cover/tools/execute_sample.rb +34 -0
  109. data/lib/deep_cover/tools/format.rb +18 -0
  110. data/lib/deep_cover/tools/format_char_cover.rb +19 -0
  111. data/lib/deep_cover/tools/format_generated_code.rb +27 -0
  112. data/lib/deep_cover/tools/indent_string.rb +26 -0
  113. data/lib/deep_cover/tools/merge.rb +16 -0
  114. data/lib/deep_cover/tools/number_lines.rb +22 -0
  115. data/lib/deep_cover/tools/our_coverage.rb +11 -0
  116. data/lib/deep_cover/tools/profiling.rb +68 -0
  117. data/lib/deep_cover/tools/render_template.rb +13 -0
  118. data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
  119. data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
  120. data/lib/deep_cover/tools/silence_warnings.rb +18 -0
  121. data/lib/deep_cover/tools/slice.rb +9 -0
  122. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  123. data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
  124. data/lib/deep_cover/tools.rb +22 -0
  125. data/lib/deep_cover/tracker_bucket.rb +50 -0
  126. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  127. data/lib/deep_cover/tracker_storage.rb +76 -0
  128. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  129. data/lib/deep_cover/version.rb +5 -0
  130. data/lib/deep_cover.rb +22 -0
  131. metadata +329 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Analyser::StatsBase
5
+ DECIMALS = 2
6
+ include Memoize
7
+ memoize :to_h, :total
8
+
9
+ VALUES = %i[executed not_executed not_executable ignored].freeze # All are exclusive
10
+
11
+ attr_reader(*VALUES)
12
+
13
+ def to_h
14
+ VALUES.map { |val| [val, public_send(val)] }.to_h
15
+ end
16
+
17
+ def initialize(executed: 0, not_executed: 0, not_executable: 0, ignored: 0)
18
+ @executed = executed
19
+ @not_executed = not_executed
20
+ @not_executable = not_executable
21
+ @ignored = ignored
22
+ freeze
23
+ end
24
+
25
+ def +(other)
26
+ self.class.new(to_h.merge(other.to_h) { |k, a, b| a + b })
27
+ end
28
+
29
+ def total
30
+ to_h.values.inject(:+)
31
+ end
32
+
33
+ def with(**values)
34
+ self.class.new(to_h.merge(values))
35
+ end
36
+
37
+ def potentially_executable
38
+ total - not_executable
39
+ end
40
+
41
+ def percent_covered
42
+ return 100 if potentially_executable == 0
43
+ (100 * (1 - not_executed.fdiv(potentially_executable))).round(DECIMALS)
44
+ end
45
+ end
46
+
47
+ class Analyser::Stats < Analyser::StatsBase
48
+ memoize :percent
49
+
50
+ def percent
51
+ Analyser::StatsBase.new(to_h.transform_values { |v| (100 * v).fdiv(total).round(DECIMALS) })
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ # A module to create a subset from a criteria called `in_subset?`
5
+ # Including classes can refine it, or specify SUBSET_CLASSES
6
+ module Analyser::Subset
7
+ def node_children(node)
8
+ find_children(node)
9
+ end
10
+
11
+ private
12
+
13
+ def find_children(from, parent = from)
14
+ @source.node_children(from).flat_map do |node|
15
+ if in_subset?(node, parent)
16
+ [node]
17
+ else
18
+ find_children(node, parent)
19
+ end
20
+ end
21
+ end
22
+
23
+ def in_subset?(node, _parent)
24
+ self.class::SUBSET_CLASSES.any? { |klass| node.is_a?(klass) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ bootstrap
5
+
6
+ # An analyser works on a subset of the original Node AST.
7
+ # The Root node is always considered part of the subset.
8
+ # One can iterate this subset with `each_node`, or ask
9
+ # the analyser for information about a node's children
10
+ # (i.e. with respect to this subset), or runs for any node
11
+ # in this subset.
12
+
13
+ # An analyser can summarize information with `results`.
14
+ # While CoveredCodeSource is based on a CoveredCode, all
15
+ # other analysers are based on another source analyser.
16
+
17
+ class Analyser
18
+ end
19
+
20
+ require_relative_dir 'analyser'
21
+
22
+ Analyser.include Analyser::Base
23
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core_ext/exec_callbacks'
4
+
5
+ module DeepCover
6
+ module AutoRun
7
+ class Runner
8
+ def initialize(covered_path)
9
+ @covered_path = covered_path
10
+ @saved = !(DeepCover.respond_to?(:running?) && DeepCover.running?)
11
+ end
12
+
13
+ def run!
14
+ after_tests { save }
15
+ ExecCallbacks.before_exec { save }
16
+ self
17
+ end
18
+
19
+ def report!(**options)
20
+ after_tests { puts report(**options) }
21
+ self
22
+ end
23
+
24
+ private
25
+
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
+ def save
39
+ require_relative '../deep_cover'
40
+ coverage.save(@covered_path) unless saved?
41
+ coverage.save_trackers(@covered_path)
42
+ end
43
+
44
+ def report(**options)
45
+ coverage.report(**options)
46
+ 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
+ end
65
+
66
+ def self.run!(covered_path)
67
+ @runners ||= {}
68
+ @runners[covered_path] ||= Runner.new(covered_path).run!
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weakref'
4
+
5
+ # TODO: if a constant is removed, AutoloadEntries should be removed
6
+
7
+ module DeepCover
8
+ class AutoloadTracker
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
24
+ end
25
+
26
+ attr_reader :autoloads_by_basename, :interceptor_files_by_path
27
+ def initialize
28
+ @autoloads_by_basename = {}
29
+ @interceptor_files_by_path = {}
30
+ end
31
+
32
+ def autoload_path_for(mod, name, path)
33
+ interceptor_path = setup_interceptor_for(mod, name, path)
34
+
35
+ if DeepCover.custom_requirer.is_being_required?(path)
36
+ already_loaded_feature
37
+ else
38
+ interceptor_path
39
+ end
40
+ end
41
+
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)
50
+
51
+ begin
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)
56
+ end
57
+
58
+ yield
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)
66
+ end
67
+ end
68
+ end
69
+
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)
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
104
+ end
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
135
+ end
136
+
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) }
153
+ end
154
+ end
155
+
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
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # We use a few features newer than our target of Ruby 2.1+:
4
+ class Module
5
+ public :define_method
6
+ end
7
+ require 'pathname'
8
+ class Pathname
9
+ def write(*args)
10
+ File.write(to_path, *args)
11
+ end unless method_defined? :write
12
+ def binwrite(*args)
13
+ File.binwrite(to_path, *args)
14
+ end unless method_defined? :binwrite
15
+ end # nocov
16
+ require 'backports/2.4.0/false_class/dup'
17
+ require 'backports/2.4.0/true_class/dup'
18
+ require 'backports/2.4.0/hash/transform_values'
19
+ require 'backports/2.4.0/enumerable/sum'
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'
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module Base
5
+ def running?
6
+ @started ||= false # rubocop:disable Naming/MemoizedInstanceVariableName [#5648]
7
+ end
8
+
9
+ def start
10
+ return if running?
11
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
12
+ # Autoload is not supported in JRuby. We currently need to use binding_of_caller
13
+ # and that is not available in JRuby. An extension may be able to replace this requirement.
14
+ # require_relative 'core_ext/autoload_overrides'
15
+ # AutoloadOverride.active = true
16
+ require_relative 'core_ext/load_overrides'
17
+ require_relative 'core_ext/require_overrides'
18
+ LoadOverride.active = RequireOverride.active = true
19
+ elsif RUBY_VERSION >= '2.3.0'
20
+ require_relative 'core_ext/instruction_sequence_load_iseq'
21
+ else
22
+ require_relative 'core_ext/autoload_overrides'
23
+ require_relative 'core_ext/load_overrides'
24
+ require_relative 'core_ext/require_overrides'
25
+ AutoloadOverride.active = LoadOverride.active = RequireOverride.active = true
26
+ autoload_tracker.initialize_autoloaded_paths { |mod, name, path| mod.autoload_without_deep_cover(name, path) }
27
+ end
28
+
29
+ config # actualize configuration
30
+ @lookup_paths = nil
31
+ @started = true
32
+ end
33
+
34
+ def stop
35
+ if defined? AutoloadOverride
36
+ AutoloadOverride.active = false
37
+ autoload_tracker.remove_interceptors { |mod, name, path| mod.autoload_without_deep_cover(name, path) }
38
+ end
39
+ RequireOverride.active = false if defined? RequireOverride
40
+
41
+ @started = false
42
+ end
43
+
44
+ def line_coverage(filename)
45
+ coverage.line_coverage(handle_relative_filename(filename), **config.to_h)
46
+ end
47
+
48
+ def covered_code(filename)
49
+ coverage.covered_code(handle_relative_filename(filename))
50
+ end
51
+
52
+ def cover(paths: nil)
53
+ if paths
54
+ prev = config.paths
55
+ config.paths(paths)
56
+ end
57
+ start
58
+ yield
59
+ ensure
60
+ stop
61
+ config.paths(prev) if paths
62
+ end
63
+
64
+ def config_changed(what)
65
+ case what
66
+ when :paths
67
+ warn "Changing DeepCover's paths after starting coverage is highly discouraged" if running?
68
+ @lookup_paths = nil
69
+ when :tracker_global
70
+ raise NotImplementedError, "Changing DeepCover's tracker global after starting coverage is not supported" if running?
71
+ @coverage = nil
72
+ end
73
+ end
74
+
75
+ def reset
76
+ stop if running?
77
+ @coverage = @custom_requirer = @autoload_tracker = @lookup_paths = nil
78
+ config.reset
79
+ self
80
+ end
81
+
82
+ def coverage
83
+ @coverage ||= Coverage.new(tracker_global: config.tracker_global)
84
+ end
85
+
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
92
+ end
93
+
94
+ def within_lookup_paths?(path)
95
+ lookup_paths.any? { |lookup_path| path.start_with?(lookup_path) }
96
+ end
97
+
98
+ def custom_requirer
99
+ @custom_requirer ||= CustomRequirer.new
100
+ end
101
+
102
+ def autoload_tracker
103
+ @autoload_tracker ||= AutoloadTracker.new
104
+ end
105
+
106
+ private
107
+
108
+ def handle_relative_filename(filename)
109
+ unless Pathname.new(filename).absolute?
110
+ relative_to = File.dirname(caller(2..2).first.partition(/\.rb:\d/).first)
111
+ filename = File.absolute_path(filename, relative_to)
112
+ end
113
+ filename += '.rb' unless filename =~ /\.rb$/
114
+ filename
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Basic constants without any dependencies are here
4
+ module DeepCover
5
+ DEFAULTS = {
6
+ ignore_uncovered: [].freeze,
7
+ paths: %w[./app ./lib].freeze,
8
+ allow_partial: false,
9
+ tracker_global: '$_cov',
10
+ reporter: :html,
11
+ output: './coverage',
12
+ }.freeze
13
+
14
+ CLI_DEFAULTS = {
15
+ command: 'bundle exec rake',
16
+ bundle: true,
17
+ process: true,
18
+ open: false,
19
+ }.freeze
20
+
21
+ OPTIONALLY_COVERED = %i[case_implicit_else default_argument raise trivial_if warn]
22
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../deep_cover'
4
+ require_relative 'coverage'
5
+ require_relative 'core_ext/coverage_replacement'
6
+
7
+ require 'coverage'
8
+ BuiltinCoverage = Coverage
9
+ Object.send(:remove_const, 'Coverage')
10
+ Coverage = DeepCover::CoverageReplacement.dup
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Config
5
+ def initialize(notify = nil)
6
+ @notify = notify
7
+ @options = DEFAULTS.dup
8
+ end
9
+
10
+ def to_hash
11
+ @options.dup
12
+ end
13
+ alias_method :to_h, :to_hash
14
+
15
+ def ignore_uncovered(*keywords, &block)
16
+ if block
17
+ raise ArgumentError, "wrong number of arguments (given #{keywords.size}, expected 0..1)" if keywords.size > 1
18
+ keywords << Node.unique_filter if keywords.empty?
19
+ Node.create_filter(keywords.first, &block)
20
+ end
21
+ if keywords.empty?
22
+ @options[:ignore_uncovered]
23
+ else
24
+ keywords = check_uncovered(keywords)
25
+ change(:ignore_uncovered, @options[:ignore_uncovered] | keywords)
26
+ end
27
+ end
28
+
29
+ def detect_uncovered(*keywords)
30
+ raise ArgumentError, 'No block is accepted' if block_given?
31
+ if keywords.empty?
32
+ OPTIONALLY_COVERED - @options[:ignore_uncovered]
33
+ else
34
+ keywords = check_uncovered(keywords)
35
+ change(:ignore_uncovered, @options[:ignore_uncovered] - keywords)
36
+ end
37
+ end
38
+
39
+ def paths(paths = nil)
40
+ if paths
41
+ change(:paths, Array(paths).dup)
42
+ else
43
+ @options[:paths]
44
+ end
45
+ end
46
+
47
+ def tracker_global(tracker_global = nil)
48
+ if tracker_global
49
+ change(:tracker_global, tracker_global)
50
+ else
51
+ @options[:tracker_global]
52
+ end
53
+ end
54
+
55
+ def reporter(reporter = nil)
56
+ if reporter
57
+ change(:reporter, reporter)
58
+ else
59
+ @options[:reporter]
60
+ end
61
+ end
62
+
63
+ def output(path_or_false = nil)
64
+ if path_or_false != nil
65
+ change(:output, path_or_false)
66
+ else
67
+ @options[:output]
68
+ end
69
+ end
70
+
71
+ def reset
72
+ DEFAULTS.each do |key, value|
73
+ change(key, value)
74
+ end
75
+ self
76
+ end
77
+
78
+ def set(**options)
79
+ @options[:ignore_uncovered] = [] if options.has_key?(:ignore_uncovered)
80
+ options.each do |key, value|
81
+ next if key == :allow_partial
82
+ public_send key, value
83
+ end
84
+ self
85
+ end
86
+
87
+ private
88
+
89
+ def check_uncovered(keywords)
90
+ keywords = keywords.first if keywords.size == 1 && keywords.first.is_a?(Array)
91
+ unknown = keywords - OPTIONALLY_COVERED
92
+ raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
93
+ keywords
94
+ end
95
+
96
+ def change(option, value)
97
+ if @options[option] != value
98
+ @options[option] = value.freeze
99
+ @notify.config_changed(option) if @notify.respond_to? :config_changed
100
+ end
101
+ self
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ module ConfigSetter
5
+ def config_queue
6
+ @config_queue ||= []
7
+ end
8
+
9
+ def config(notify = self)
10
+ raise ArgumentError, 'config does not accept an argument. Did you mean `configure`?' if block_given?
11
+ @config ||= Config.new(notify)
12
+ config_queue.each { |block| configure(&block) }
13
+ config_queue.clear
14
+ @config
15
+ end
16
+
17
+ def configure(&block)
18
+ raise 'Must provide a block' unless block
19
+ @config ||= nil # avoid warning
20
+ if @config == nil
21
+ config_queue << block
22
+ else
23
+ case block.arity
24
+ when 0
25
+ @config.instance_eval(&block)
26
+ when 1
27
+ block.call(@config)
28
+ end
29
+ end
30
+ self
31
+ end
32
+ end
33
+ end