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