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