deep-cover-core 0.6.3.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rspec_all +3 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +11 -0
- data/deep_cover_core.gemspec +46 -0
- data/lib/deep-cover.rb +3 -0
- data/lib/deep_cover/analyser/base.rb +104 -0
- data/lib/deep_cover/analyser/branch.rb +41 -0
- data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
- data/lib/deep_cover/analyser/function.rb +14 -0
- data/lib/deep_cover/analyser/node.rb +54 -0
- data/lib/deep_cover/analyser/per_char.rb +38 -0
- data/lib/deep_cover/analyser/per_line.rb +41 -0
- data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
- data/lib/deep_cover/analyser/statement.rb +33 -0
- data/lib/deep_cover/analyser/stats.rb +54 -0
- data/lib/deep_cover/analyser/subset.rb +27 -0
- data/lib/deep_cover/analyser.rb +23 -0
- data/lib/deep_cover/auto_run.rb +71 -0
- data/lib/deep_cover/autoload_tracker.rb +215 -0
- data/lib/deep_cover/backports.rb +22 -0
- data/lib/deep_cover/base.rb +117 -0
- data/lib/deep_cover/basics.rb +22 -0
- data/lib/deep_cover/builtin_takeover.rb +10 -0
- data/lib/deep_cover/config.rb +104 -0
- data/lib/deep_cover/config_setter.rb +33 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
- data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
- data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
- data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
- data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
- data/lib/deep_cover/coverage/analysis.rb +42 -0
- data/lib/deep_cover/coverage/persistence.rb +84 -0
- data/lib/deep_cover/coverage.rb +125 -0
- data/lib/deep_cover/covered_code.rb +145 -0
- data/lib/deep_cover/custom_requirer.rb +187 -0
- data/lib/deep_cover/flag_comment_associator.rb +68 -0
- data/lib/deep_cover/load.rb +66 -0
- data/lib/deep_cover/memoize.rb +48 -0
- data/lib/deep_cover/module_override.rb +39 -0
- data/lib/deep_cover/node/arguments.rb +51 -0
- data/lib/deep_cover/node/assignments.rb +273 -0
- data/lib/deep_cover/node/base.rb +155 -0
- data/lib/deep_cover/node/begin.rb +27 -0
- data/lib/deep_cover/node/block.rb +61 -0
- data/lib/deep_cover/node/branch.rb +32 -0
- data/lib/deep_cover/node/case.rb +113 -0
- data/lib/deep_cover/node/collections.rb +31 -0
- data/lib/deep_cover/node/const.rb +12 -0
- data/lib/deep_cover/node/def.rb +40 -0
- data/lib/deep_cover/node/empty_body.rb +32 -0
- data/lib/deep_cover/node/exceptions.rb +79 -0
- data/lib/deep_cover/node/if.rb +73 -0
- data/lib/deep_cover/node/keywords.rb +86 -0
- data/lib/deep_cover/node/literals.rb +100 -0
- data/lib/deep_cover/node/loops.rb +74 -0
- data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
- data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
- data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
- data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
- data/lib/deep_cover/node/mixin/filters.rb +47 -0
- data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
- data/lib/deep_cover/node/mixin/has_child.rb +145 -0
- data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
- data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
- data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
- data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
- data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
- data/lib/deep_cover/node/module.rb +66 -0
- data/lib/deep_cover/node/root.rb +20 -0
- data/lib/deep_cover/node/send.rb +161 -0
- data/lib/deep_cover/node/short_circuit.rb +42 -0
- data/lib/deep_cover/node/splat.rb +15 -0
- data/lib/deep_cover/node/variables.rb +16 -0
- data/lib/deep_cover/node.rb +23 -0
- data/lib/deep_cover/parser_ext/range.rb +21 -0
- data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
- data/lib/deep_cover/reporter/base.rb +68 -0
- data/lib/deep_cover/reporter/html/base.rb +14 -0
- data/lib/deep_cover/reporter/html/index.rb +59 -0
- data/lib/deep_cover/reporter/html/site.rb +68 -0
- data/lib/deep_cover/reporter/html/source.rb +136 -0
- data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css +291 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
- data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
- data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
- data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
- data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
- data/lib/deep_cover/reporter/html.rb +15 -0
- data/lib/deep_cover/reporter/istanbul.rb +184 -0
- data/lib/deep_cover/reporter/text.rb +58 -0
- data/lib/deep_cover/reporter/tree/util.rb +86 -0
- data/lib/deep_cover/reporter.rb +10 -0
- data/lib/deep_cover/tools/blank.rb +25 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
- data/lib/deep_cover/tools/camelize.rb +13 -0
- data/lib/deep_cover/tools/content_tag.rb +11 -0
- data/lib/deep_cover/tools/covered.rb +9 -0
- data/lib/deep_cover/tools/execute_sample.rb +34 -0
- data/lib/deep_cover/tools/format.rb +18 -0
- data/lib/deep_cover/tools/format_char_cover.rb +19 -0
- data/lib/deep_cover/tools/format_generated_code.rb +27 -0
- data/lib/deep_cover/tools/indent_string.rb +26 -0
- data/lib/deep_cover/tools/merge.rb +16 -0
- data/lib/deep_cover/tools/number_lines.rb +22 -0
- data/lib/deep_cover/tools/our_coverage.rb +11 -0
- data/lib/deep_cover/tools/profiling.rb +68 -0
- data/lib/deep_cover/tools/render_template.rb +13 -0
- data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
- data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
- data/lib/deep_cover/tools/silence_warnings.rb +18 -0
- data/lib/deep_cover/tools/slice.rb +9 -0
- data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
- data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
- data/lib/deep_cover/tools.rb +22 -0
- data/lib/deep_cover/tracker_bucket.rb +50 -0
- data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
- data/lib/deep_cover/tracker_storage.rb +76 -0
- data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
- data/lib/deep_cover/version.rb +5 -0
- data/lib/deep_cover.rb +22 -0
- 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
|