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