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,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Autoload is quite difficult to hook into to do what we need to do.
|
4
|
+
#
|
5
|
+
# We create a temporary file that gets set as path for autoloads. The file then does a require of the
|
6
|
+
# autoloaded file. We also keep track of autoloaded constants and files and change the autoload's target when
|
7
|
+
# those files get required (or when we guess they are being).
|
8
|
+
#
|
9
|
+
# Doing it this way solves:
|
10
|
+
#
|
11
|
+
# * When autoload is triggered, it doesn't always call `require`. In Ruby 2.1 and 2.2, it just loads the
|
12
|
+
# file somehow without using require, this means we can't instrument autoloaded files. Using an intercept
|
13
|
+
# file means that the intercept is not covered (we don't care) and then the manual require will
|
14
|
+
# allow us to instrument the real target.
|
15
|
+
#
|
16
|
+
# * When autoload is triggered, there are special states setup internal to ruby to allow constants to
|
17
|
+
# be used conditionally. If the target file is not actually required (such as when using our custom requirer),
|
18
|
+
# then the state is not correct and we can't do simple things such as `A ||= 1` or `module A; ...; end`.
|
19
|
+
# This problem was happening when all we did was use the custom require that does get called for Ruby 2.3+.
|
20
|
+
#
|
21
|
+
# To solve the issues, we keep track of all the autoloads, and when we detect that a file is being autoloaded,
|
22
|
+
# we change the state so that ruby thinks the file was already loaded.
|
23
|
+
#
|
24
|
+
# * An issue with the interceptor files is that if some code manually requires a file that is the target of
|
25
|
+
# autoloading, ruby would not disable the autoload behavior, and would end up trying to autoload once the constant
|
26
|
+
# is reached.
|
27
|
+
#
|
28
|
+
# To solve this, every require, we check if it is for a file that is is meant to autoload a constant, and if so,
|
29
|
+
# we remove that autoload.
|
30
|
+
#
|
31
|
+
# * To make matter more complicated, when loading gems without bundle's setup, the $LOAD_PATH is filled by RubyGems
|
32
|
+
# as part of a monkey-patch of Kernel#require. Because of this, we can't always find the file that will be required,
|
33
|
+
# even though a file will indeed be required.
|
34
|
+
#
|
35
|
+
# However, if nothing is done, the autoload can cause problems on that built-in require (as explained in a previous
|
36
|
+
# point). So when we don't find the target, we check if it's a require that will go through the $LOAD_PATH,
|
37
|
+
# meaning that require's parameter is not an absolute path, and isn't relative to the current directory (so doesn't
|
38
|
+
# start with './' or '../').
|
39
|
+
#
|
40
|
+
# When a require does go through $LOAD_PATH, we do a best effort to deactivate the autoloads that seem to match
|
41
|
+
# to match this require by comparing the required path (as it is received) to all the autoload path we have, and
|
42
|
+
# deactivating those that match. This is only an issue when there is an autoload made which will load a different
|
43
|
+
# gem. This is pretty rare (but deep-cover does it...)
|
44
|
+
#
|
45
|
+
# * All of this changing autoloads means that for modules/classes that are frozen, we can't handle the situation, since
|
46
|
+
# we can't change the autoload stuff.
|
47
|
+
#
|
48
|
+
# We don't resolve this problem. However, we could work around it by always calling the real require for these
|
49
|
+
# files (which means we wouldn't cover them), and by not creating interceptor files for the autoloads. Since we
|
50
|
+
# can't know when the #autoload call is made, if the constant will be frozen later on, we would have to instead
|
51
|
+
# monkey-patch #freeze on modules and classes to remove the interceptor file before things get frozen.
|
52
|
+
#
|
53
|
+
# * Kernel#autoload uses the caller's `Module.nesting` instead of using self.
|
54
|
+
# This means that if we intercept the autoload, then we cannot call the original Kernel#autoload because it will
|
55
|
+
# now use our Module instead if our caller's. The only reliable solution we've found to this is to use binding_of_caller
|
56
|
+
# to get the correct object to call autoload on.
|
57
|
+
#
|
58
|
+
# (This is not a problem with Module#autoload and Module.autoload)
|
59
|
+
#
|
60
|
+
# A possible solution to investigate is to make a simple C extension, to do the work of our monkey-patch, this way,
|
61
|
+
# the check for the caller doesn't find our callstack
|
62
|
+
#
|
63
|
+
# Some situations where Module.nesting of the caller is different from self in Kernel#autoload:
|
64
|
+
# * When in the top-level: (self: main) vs (Module.nesting: nil, which we default to Object)
|
65
|
+
# * When called from a method defined on a module that is included:
|
66
|
+
# module A
|
67
|
+
# def begin_autoload
|
68
|
+
# # `Kernel.autoload` would have the same result
|
69
|
+
# autoload :A1, 'hello'
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
# class B
|
73
|
+
# include A
|
74
|
+
# end
|
75
|
+
# Calling `B.new.begin_autoload` is equivalent to:
|
76
|
+
# A.autoload :A1, 'hello'
|
77
|
+
# NOT this:
|
78
|
+
# B.autoload :A1, 'hello'
|
79
|
+
#
|
80
|
+
require 'binding_of_caller'
|
81
|
+
require 'tempfile'
|
82
|
+
|
83
|
+
module DeepCover
|
84
|
+
load_all
|
85
|
+
|
86
|
+
module KernelAutoloadOverride
|
87
|
+
def autoload(name, path)
|
88
|
+
mod = binding.of_caller(1).eval('Module.nesting').first || Object
|
89
|
+
autoload_path = DeepCover.autoload_tracker.autoload_path_for(mod, name, path)
|
90
|
+
mod.autoload_without_deep_cover(name, autoload_path)
|
91
|
+
end
|
92
|
+
|
93
|
+
extend ModuleOverride
|
94
|
+
override ::Kernel, ::Kernel.singleton_class
|
95
|
+
end
|
96
|
+
|
97
|
+
module ModuleAutoloadOverride
|
98
|
+
def autoload(name, path)
|
99
|
+
autoload_path = DeepCover.autoload_tracker.autoload_path_for(self, name, path)
|
100
|
+
autoload_without_deep_cover(name, autoload_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
extend ModuleOverride
|
104
|
+
override Module
|
105
|
+
end
|
106
|
+
|
107
|
+
module AutoloadOverride
|
108
|
+
def self.active=(flag)
|
109
|
+
KernelAutoloadOverride.active = ModuleAutoloadOverride.active = flag
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This is a complete replacement for the builtin Coverage module of Ruby
|
4
|
+
|
5
|
+
module DeepCover
|
6
|
+
module CoverageReplacement
|
7
|
+
OLD_COVERAGE_SENTINEL = Object.new
|
8
|
+
ALL_COVERAGES = {lines: true, branches: true, methods: true}.freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def running?
|
12
|
+
DeepCover.running?
|
13
|
+
end
|
14
|
+
|
15
|
+
def start(targets = OLD_COVERAGE_SENTINEL)
|
16
|
+
if targets == OLD_COVERAGE_SENTINEL
|
17
|
+
# Do nothing
|
18
|
+
elsif targets == :all
|
19
|
+
targets = ALL_COVERAGES
|
20
|
+
else
|
21
|
+
targets = targets.to_hash.slice(*ALL_COVERAGES.keys).select { |_, v| v }
|
22
|
+
targets = targets.map { |k, v| [k, !!v] }.to_h
|
23
|
+
raise 'no measuring target is specified' if targets.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
if DeepCover.running?
|
27
|
+
raise 'cannot change the measuring target during coverage measurement' if @started_args != targets
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
@started_args = targets
|
32
|
+
|
33
|
+
DeepCover.start
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def result
|
38
|
+
r = peek_result
|
39
|
+
DeepCover.stop
|
40
|
+
r
|
41
|
+
end
|
42
|
+
|
43
|
+
def peek_result
|
44
|
+
raise 'coverage measurement is not enabled' unless running?
|
45
|
+
if @started_args == OLD_COVERAGE_SENTINEL
|
46
|
+
DeepCover.coverage.covered_codes.map do |covered_code|
|
47
|
+
[covered_code.path.to_s, covered_code.line_coverage(allow_partial: false)]
|
48
|
+
end.to_h
|
49
|
+
else
|
50
|
+
DeepCover.coverage.covered_codes.map do |covered_code|
|
51
|
+
cov = {}
|
52
|
+
cov[:branches] = DeepCover::Analyser::Ruby25LikeBranch.new(covered_code).results if @started_args[:branches]
|
53
|
+
cov[:lines] = covered_code.line_coverage(allow_partial: false) if @started_args[:lines]
|
54
|
+
cov[:methods] = {} if @started_args[:methods]
|
55
|
+
[covered_code.path.to_s, cov]
|
56
|
+
end.to_h
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../module_override'
|
4
|
+
|
5
|
+
# Adds a functionality to add callbacks before an `exec`
|
6
|
+
|
7
|
+
module DeepCover
|
8
|
+
module ExecCallbacks
|
9
|
+
class << self
|
10
|
+
attr_reader :callbacks
|
11
|
+
|
12
|
+
def before_exec(&block)
|
13
|
+
self.active = true
|
14
|
+
(@callbacks ||= []) << block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def exec(*args)
|
19
|
+
ExecCallbacks.callbacks.each(&:call)
|
20
|
+
exec_without_deep_cover(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
extend ModuleOverride
|
24
|
+
override ::Kernel, ::Kernel.singleton_class
|
25
|
+
self.active = true
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
load_all
|
5
|
+
|
6
|
+
module InstructionSequenceLoadIseq
|
7
|
+
def load_iseq(path)
|
8
|
+
compiled = InstructionSequenceLoadIseq.load_iseq_logic(path)
|
9
|
+
|
10
|
+
return compiled if compiled
|
11
|
+
|
12
|
+
# By default there is no super, but if bootsnap is there, and things are in the right order,
|
13
|
+
# we could possibly fallback to it as usual to keep the perf gain. Same for other possible
|
14
|
+
# tools using #load_iseq
|
15
|
+
super if defined?(super)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.load_iseq_logic(path)
|
19
|
+
return unless DeepCover.running?
|
20
|
+
return unless DeepCover.within_lookup_paths?(path)
|
21
|
+
|
22
|
+
covered_code = DeepCover.coverage.covered_code_or_warn(path)
|
23
|
+
return unless covered_code
|
24
|
+
|
25
|
+
covered_code.compile_or_warn
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << RubyVM::InstructionSequence
|
31
|
+
prepend DeepCover::InstructionSequenceLoadIseq
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These are the monkeypatches to replace the default #load in order
|
4
|
+
# to instrument the code before it gets run.
|
5
|
+
# For now, this is not used, and may never be. The tracking and reporting for things can might be
|
6
|
+
# loaded multiple times can be complex and is beyond the current scope of the project.
|
7
|
+
|
8
|
+
module DeepCover
|
9
|
+
module LoadOverride
|
10
|
+
def load(path, wrap = false)
|
11
|
+
return load_without_deep_cover(path, wrap) if wrap
|
12
|
+
|
13
|
+
DeepCover.custom_requirer.load(path) { load_without_deep_cover(path) }
|
14
|
+
end
|
15
|
+
|
16
|
+
extend ModuleOverride
|
17
|
+
override ::Kernel, ::Kernel.singleton_class
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These are the monkeypatches to replace the default #require and
|
4
|
+
# #require_relative in order to instrument the code before it gets run.
|
5
|
+
# Kernel.require and Kernel#require must both have their version because
|
6
|
+
# each can have been already overwritten individually. (Rubygems only
|
7
|
+
# overrides Kernel#require)
|
8
|
+
|
9
|
+
module DeepCover
|
10
|
+
load_all
|
11
|
+
|
12
|
+
module RequireOverride
|
13
|
+
def require(path)
|
14
|
+
DeepCover.custom_requirer.require(path) { require_without_deep_cover(path) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def require_relative(path)
|
18
|
+
base = caller(1..1).first[/[^:]+/]
|
19
|
+
raise LoadError, 'cannot infer basepath' unless base
|
20
|
+
base = File.dirname(base)
|
21
|
+
|
22
|
+
require(File.absolute_path(path, base))
|
23
|
+
end
|
24
|
+
|
25
|
+
extend ModuleOverride
|
26
|
+
override ::Kernel, ::Kernel.singleton_class
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
class Coverage
|
5
|
+
class Analysis < Struct.new(:covered_codes, :options)
|
6
|
+
include Memoize
|
7
|
+
memoize :analyser_map, :stat_map
|
8
|
+
|
9
|
+
def analyser_map
|
10
|
+
covered_codes.map do |covered_code|
|
11
|
+
[covered_code, compute_analysers(covered_code)]
|
12
|
+
end.to_h
|
13
|
+
end
|
14
|
+
|
15
|
+
def stat_map
|
16
|
+
analyser_map.transform_values { |a| a.transform_values(&:stats) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def overall
|
20
|
+
return 100 if stat_map.empty?
|
21
|
+
node, branch = Tools.merge(*stat_map.values, :+).values_at(:node, :branch)
|
22
|
+
(node + branch).percent_covered
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.template
|
26
|
+
{node: Analyser::Node, per_char: Analyser::PerChar, branch: Analyser::Branch}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def compute_analysers(covered_code)
|
32
|
+
base = Analyser::Node.new(covered_code, **options)
|
33
|
+
{node: base}.merge!(
|
34
|
+
{
|
35
|
+
per_char: Analyser::PerChar,
|
36
|
+
branch: Analyser::Branch,
|
37
|
+
}.transform_values { |klass| klass.new(base, **options) }
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
require 'securerandom'
|
5
|
+
class Coverage
|
6
|
+
class Persistence
|
7
|
+
BASENAME = 'coverage.dc'
|
8
|
+
TRACKER_TEMPLATE = 'trackers%{unique}.dct'
|
9
|
+
|
10
|
+
attr_reader :dir_path
|
11
|
+
def initialize(dest_path, dirname)
|
12
|
+
@dir_path = Pathname(dest_path).join(dirname).expand_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(with_trackers: true)
|
16
|
+
saved?
|
17
|
+
cov = load_coverage
|
18
|
+
cov.tracker_storage_per_path.tracker_hits_per_path = load_trackers if with_trackers
|
19
|
+
cov
|
20
|
+
end
|
21
|
+
|
22
|
+
def save(coverage)
|
23
|
+
create_if_needed
|
24
|
+
delete_trackers
|
25
|
+
save_coverage(coverage)
|
26
|
+
end
|
27
|
+
|
28
|
+
def save_trackers(tracker_hits_per_path)
|
29
|
+
saved?
|
30
|
+
basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
|
31
|
+
dir_path.join(basename).binwrite(Marshal.dump(
|
32
|
+
version: DeepCover::VERSION,
|
33
|
+
tracker_hits_per_path: tracker_hits_per_path,
|
34
|
+
))
|
35
|
+
end
|
36
|
+
|
37
|
+
def saved?
|
38
|
+
raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def create_if_needed
|
45
|
+
dir_path.mkpath
|
46
|
+
end
|
47
|
+
|
48
|
+
def save_coverage(coverage)
|
49
|
+
dir_path.join(BASENAME).binwrite(Marshal.dump(
|
50
|
+
version: DeepCover::VERSION,
|
51
|
+
coverage: coverage,
|
52
|
+
))
|
53
|
+
end
|
54
|
+
|
55
|
+
# rubocop:disable Security/MarshalLoad
|
56
|
+
def load_coverage
|
57
|
+
Marshal.load(dir_path.join(BASENAME).binread).tap do |version:, coverage:|
|
58
|
+
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
59
|
+
return coverage
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns a TrackerHitsPerPath
|
64
|
+
def load_trackers
|
65
|
+
tracker_files.map do |full_path|
|
66
|
+
Marshal.load(full_path.binread).yield_self do |version:, tracker_hits_per_path:|
|
67
|
+
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
68
|
+
tracker_hits_per_path
|
69
|
+
end
|
70
|
+
end.inject(:merge!)
|
71
|
+
end
|
72
|
+
# rubocop:enable Security/MarshalLoad
|
73
|
+
|
74
|
+
def tracker_files
|
75
|
+
basename = format(TRACKER_TEMPLATE, unique: '*')
|
76
|
+
Pathname.glob(dir_path.join(basename))
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete_trackers
|
80
|
+
tracker_files.each(&:delete)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
|
6
|
+
require_relative_dir 'coverage'
|
7
|
+
|
8
|
+
# A collection of CoveredCode
|
9
|
+
class Coverage
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :tracker_storage_per_path
|
13
|
+
|
14
|
+
def initialize(**options)
|
15
|
+
@covered_code_index = {}
|
16
|
+
@options = options
|
17
|
+
@tracker_storage_per_path = TrackerStoragePerPath.new(TrackerBucket[tracker_global])
|
18
|
+
end
|
19
|
+
|
20
|
+
def covered_codes
|
21
|
+
each.to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def line_coverage(filename, **options)
|
25
|
+
covered_code(filename).line_coverage(**options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def covered_code?(path)
|
29
|
+
@covered_code_index.include?(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def covered_code(path, **options)
|
33
|
+
raise 'path must be an absolute path' unless Pathname.new(path).absolute?
|
34
|
+
@covered_code_index[path] ||= CoveredCode.new(path: path,
|
35
|
+
tracker_storage: @tracker_storage_per_path[path],
|
36
|
+
**options,
|
37
|
+
**@options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def covered_code_or_warn(path, **options)
|
41
|
+
covered_code(path, **options)
|
42
|
+
rescue Parser::SyntaxError => e
|
43
|
+
if e.message =~ /contains escape sequences incompatible with UTF-8/
|
44
|
+
warn "Can't cover #{path} because of incompatible encoding (see issue #9)"
|
45
|
+
else
|
46
|
+
warn "The file #{path} can't be instrumented"
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def each
|
53
|
+
return to_enum unless block_given?
|
54
|
+
@tracker_storage_per_path.each_key do |path|
|
55
|
+
begin
|
56
|
+
cov_code = covered_code(path)
|
57
|
+
rescue Parser::SyntaxError
|
58
|
+
next
|
59
|
+
end
|
60
|
+
yield cov_code
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def report(**options)
|
66
|
+
case (reporter = options.fetch(:reporter, DEFAULTS[:reporter]).to_sym)
|
67
|
+
when :html
|
68
|
+
msg = if (output = options.fetch(:output, DEFAULTS[:output]))
|
69
|
+
Reporter::HTML.report(self, **options)
|
70
|
+
"HTML generated: open #{output}/index.html"
|
71
|
+
else
|
72
|
+
'No HTML generated'
|
73
|
+
end
|
74
|
+
Reporter::Text.report(self, **options) + "\n\n" + msg
|
75
|
+
when :istanbul
|
76
|
+
if Reporter::Istanbul.available?
|
77
|
+
Reporter::Istanbul.report(self, **options)
|
78
|
+
else
|
79
|
+
warn 'nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`'
|
80
|
+
end
|
81
|
+
when :text
|
82
|
+
Reporter::Text.report(self, **options)
|
83
|
+
else
|
84
|
+
raise ArgumentError, "Unknown reporter: #{reporter}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
|
89
|
+
Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.saved?(dest_path, dirname = 'deep_cover')
|
93
|
+
Persistence.new(dest_path, dirname).saved?
|
94
|
+
end
|
95
|
+
|
96
|
+
def save(dest_path, dirname = 'deep_cover')
|
97
|
+
Persistence.new(dest_path, dirname).save(self)
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def save_trackers(dest_path, dirname = 'deep_cover')
|
102
|
+
Persistence.new(dest_path, dirname).save_trackers(@tracker_storage_per_path.tracker_hits_per_path)
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def tracker_global
|
107
|
+
@options.fetch(:tracker_global, DEFAULTS[:tracker_global])
|
108
|
+
end
|
109
|
+
|
110
|
+
def analysis(**options)
|
111
|
+
Analysis.new(covered_codes, options)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def marshal_dump
|
117
|
+
{options: @options, tracker_storage_per_path: @tracker_storage_per_path}
|
118
|
+
end
|
119
|
+
|
120
|
+
def marshal_load(options:, tracker_storage_per_path:)
|
121
|
+
initialize(**options)
|
122
|
+
@tracker_storage_per_path = tracker_storage_per_path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
load_parser
|
6
|
+
|
7
|
+
class CoveredCode
|
8
|
+
attr_accessor :covered_source, :buffer, :tracker_storage, :local_var, :path
|
9
|
+
|
10
|
+
def initialize(
|
11
|
+
path: nil,
|
12
|
+
source: nil,
|
13
|
+
lineno: 1,
|
14
|
+
tracker_global: DEFAULTS[:tracker_global],
|
15
|
+
tracker_storage: TrackerBucket[tracker_global].create_storage,
|
16
|
+
local_var: '_temp'
|
17
|
+
)
|
18
|
+
raise 'Must provide either path or source' unless path || source
|
19
|
+
|
20
|
+
@path = path &&= Pathname(path)
|
21
|
+
@buffer = Parser::Source::Buffer.new('', lineno)
|
22
|
+
@buffer.source = source || path.read
|
23
|
+
@tracker_count = 0
|
24
|
+
@tracker_storage = tracker_storage
|
25
|
+
@local_var = local_var
|
26
|
+
@covered_source = instrument_source
|
27
|
+
end
|
28
|
+
|
29
|
+
def lineno
|
30
|
+
@buffer.first_line
|
31
|
+
end
|
32
|
+
|
33
|
+
def nb_lines
|
34
|
+
lines = buffer.source_lines
|
35
|
+
if lines.empty?
|
36
|
+
0
|
37
|
+
else
|
38
|
+
lines.size - (lines.last.empty? ? 1 : 0)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
|
43
|
+
eval(@covered_source, binding, (@path || '<raw_code>').to_s, lineno) # rubocop:disable Security/Eval
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute_code_or_warn(*args)
|
48
|
+
warn_instead_of_syntax_error do
|
49
|
+
execute_code(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def cover
|
54
|
+
global[nb] ||= Array.new(@tracker_count, 0)
|
55
|
+
end
|
56
|
+
|
57
|
+
def line_coverage(**options)
|
58
|
+
Analyser::PerLine.new(self, **options).results
|
59
|
+
end
|
60
|
+
|
61
|
+
def char_cover(**options)
|
62
|
+
Analyser::PerChar.new(self, **options).results
|
63
|
+
end
|
64
|
+
|
65
|
+
def tracker_hits(tracker_id)
|
66
|
+
cover.fetch(tracker_id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def covered_ast
|
70
|
+
root.main
|
71
|
+
end
|
72
|
+
|
73
|
+
def comments
|
74
|
+
root
|
75
|
+
@comments
|
76
|
+
end
|
77
|
+
|
78
|
+
def root
|
79
|
+
@root ||= begin
|
80
|
+
ast, @comments = parser.parse_with_comments(@buffer)
|
81
|
+
Node::Root.new(ast, self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_node(*args, &block)
|
86
|
+
covered_ast.each_node(*args, &block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def instrument_source
|
90
|
+
rewriter = Parser::Source::TreeRewriter.new(@buffer)
|
91
|
+
covered_ast.each_node(:postorder) do |node|
|
92
|
+
node.rewriting_rules.each do |range, rule|
|
93
|
+
prefix, _node, suffix = rule.partition('%{node}')
|
94
|
+
prefix = yield prefix, node, range.begin, :prefix if block_given? && !prefix.empty?
|
95
|
+
suffix = yield suffix, node, range.end, :suffix if block_given? && !suffix.empty?
|
96
|
+
rewriter.wrap(range, prefix, suffix)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
rewriter.process
|
100
|
+
end
|
101
|
+
|
102
|
+
def compile
|
103
|
+
RubyVM::InstructionSequence.compile(covered_source, path.to_s, path.to_s)
|
104
|
+
end
|
105
|
+
|
106
|
+
def compile_or_warn
|
107
|
+
warn_instead_of_syntax_error do
|
108
|
+
compile
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def freeze
|
113
|
+
unless frozen? # Guard against reentrance
|
114
|
+
super
|
115
|
+
root.each_node(&:freeze)
|
116
|
+
end
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def inspect
|
121
|
+
%{#<DeepCover::CoveredCode "#{path}">}
|
122
|
+
end
|
123
|
+
alias_method :to_s, :inspect
|
124
|
+
|
125
|
+
def warn_instead_of_syntax_error # &block
|
126
|
+
yield
|
127
|
+
rescue ::SyntaxError => e
|
128
|
+
warn Tools.strip_heredoc(<<-MSG)
|
129
|
+
DeepCover is getting confused with the file #{path} and it won't be instrumented.
|
130
|
+
Please report this error and provide the source code around the following lines:
|
131
|
+
#{e}
|
132
|
+
MSG
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def parser
|
139
|
+
Parser::CurrentRuby.new.tap do |parser|
|
140
|
+
parser.diagnostics.all_errors_are_fatal = true
|
141
|
+
parser.diagnostics.ignore_warnings = true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|