deep-cover-core 0.6.3.pre

Sign up to get free protection for your applications and to get access to all the features.
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