deep-cover 0.1.1

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +10 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +8 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +127 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/cov +43 -0
  12. data/bin/gemcov +8 -0
  13. data/bin/selfcov +21 -0
  14. data/bin/setup +8 -0
  15. data/bin/testall +88 -0
  16. data/deep_cover.gemspec +44 -0
  17. data/exe/deep-cover +6 -0
  18. data/future_read_me.md +108 -0
  19. data/lib/deep-cover.rb +1 -0
  20. data/lib/deep_cover.rb +11 -0
  21. data/lib/deep_cover/analyser.rb +24 -0
  22. data/lib/deep_cover/analyser/base.rb +51 -0
  23. data/lib/deep_cover/analyser/branch.rb +20 -0
  24. data/lib/deep_cover/analyser/covered_code_source.rb +31 -0
  25. data/lib/deep_cover/analyser/function.rb +12 -0
  26. data/lib/deep_cover/analyser/ignore_uncovered.rb +19 -0
  27. data/lib/deep_cover/analyser/node.rb +11 -0
  28. data/lib/deep_cover/analyser/per_char.rb +20 -0
  29. data/lib/deep_cover/analyser/per_line.rb +23 -0
  30. data/lib/deep_cover/analyser/statement.rb +31 -0
  31. data/lib/deep_cover/analyser/subset.rb +24 -0
  32. data/lib/deep_cover/auto_run.rb +49 -0
  33. data/lib/deep_cover/autoload_tracker.rb +75 -0
  34. data/lib/deep_cover/backports.rb +9 -0
  35. data/lib/deep_cover/base.rb +55 -0
  36. data/lib/deep_cover/builtin_takeover.rb +2 -0
  37. data/lib/deep_cover/cli/debugger.rb +93 -0
  38. data/lib/deep_cover/cli/deep_cover.rb +49 -0
  39. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +105 -0
  40. data/lib/deep_cover/config.rb +52 -0
  41. data/lib/deep_cover/core_ext/autoload_overrides.rb +40 -0
  42. data/lib/deep_cover/core_ext/coverage_replacement.rb +26 -0
  43. data/lib/deep_cover/core_ext/load_overrides.rb +24 -0
  44. data/lib/deep_cover/core_ext/require_overrides.rb +36 -0
  45. data/lib/deep_cover/coverage.rb +198 -0
  46. data/lib/deep_cover/covered_code.rb +138 -0
  47. data/lib/deep_cover/custom_requirer.rb +93 -0
  48. data/lib/deep_cover/node.rb +8 -0
  49. data/lib/deep_cover/node/arguments.rb +50 -0
  50. data/lib/deep_cover/node/assignments.rb +250 -0
  51. data/lib/deep_cover/node/base.rb +99 -0
  52. data/lib/deep_cover/node/begin.rb +25 -0
  53. data/lib/deep_cover/node/block.rb +53 -0
  54. data/lib/deep_cover/node/boolean.rb +22 -0
  55. data/lib/deep_cover/node/branch.rb +28 -0
  56. data/lib/deep_cover/node/case.rb +94 -0
  57. data/lib/deep_cover/node/collections.rb +21 -0
  58. data/lib/deep_cover/node/const.rb +10 -0
  59. data/lib/deep_cover/node/def.rb +38 -0
  60. data/lib/deep_cover/node/empty_body.rb +21 -0
  61. data/lib/deep_cover/node/exceptions.rb +74 -0
  62. data/lib/deep_cover/node/if.rb +36 -0
  63. data/lib/deep_cover/node/keywords.rb +84 -0
  64. data/lib/deep_cover/node/literals.rb +77 -0
  65. data/lib/deep_cover/node/loops.rb +72 -0
  66. data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
  67. data/lib/deep_cover/node/mixin/check_completion.rb +16 -0
  68. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +25 -0
  69. data/lib/deep_cover/node/mixin/executed_after_children.rb +13 -0
  70. data/lib/deep_cover/node/mixin/execution_location.rb +56 -0
  71. data/lib/deep_cover/node/mixin/flow_accounting.rb +63 -0
  72. data/lib/deep_cover/node/mixin/has_child.rb +138 -0
  73. data/lib/deep_cover/node/mixin/has_child_handler.rb +73 -0
  74. data/lib/deep_cover/node/mixin/has_tracker.rb +44 -0
  75. data/lib/deep_cover/node/mixin/is_statement.rb +18 -0
  76. data/lib/deep_cover/node/mixin/rewriting.rb +32 -0
  77. data/lib/deep_cover/node/mixin/wrapper.rb +13 -0
  78. data/lib/deep_cover/node/module.rb +64 -0
  79. data/lib/deep_cover/node/root.rb +18 -0
  80. data/lib/deep_cover/node/send.rb +83 -0
  81. data/lib/deep_cover/node/splat.rb +13 -0
  82. data/lib/deep_cover/node/variables.rb +14 -0
  83. data/lib/deep_cover/parser_ext/range.rb +40 -0
  84. data/lib/deep_cover/reporter.rb +6 -0
  85. data/lib/deep_cover/reporter/istanbul.rb +151 -0
  86. data/lib/deep_cover/tools.rb +18 -0
  87. data/lib/deep_cover/tools/builtin_coverage.rb +50 -0
  88. data/lib/deep_cover/tools/camelize.rb +8 -0
  89. data/lib/deep_cover/tools/dump_covered_code.rb +32 -0
  90. data/lib/deep_cover/tools/execute_sample.rb +23 -0
  91. data/lib/deep_cover/tools/format.rb +16 -0
  92. data/lib/deep_cover/tools/format_char_cover.rb +18 -0
  93. data/lib/deep_cover/tools/format_generated_code.rb +25 -0
  94. data/lib/deep_cover/tools/number_lines.rb +18 -0
  95. data/lib/deep_cover/tools/our_coverage.rb +9 -0
  96. data/lib/deep_cover/tools/require_relative_dir.rb +10 -0
  97. data/lib/deep_cover/tools/silence_warnings.rb +15 -0
  98. data/lib/deep_cover/version.rb +3 -0
  99. metadata +326 -0
@@ -0,0 +1,40 @@
1
+ # We need to override autoload, because MRI has special behaviors associated with it
2
+ # that we can't reuse, hence we need to do workarounds.
3
+ #
4
+ # Basically, when trying to use a constant set to be autoloaded in an optionnal way, like:
5
+ # * module A; ...; end
6
+ # * A ||= 1
7
+ # When autoloading the file, the above won't work and will raise a "uninitialized constant A"
8
+ # because ruby doesn't understand that custom require is currently requiring the correct file.
9
+ #
10
+ # Our solution is to track autoloads ourself, and when requiring a path that has autoloads,
11
+ # we remove the autoloads from the constants first.
12
+
13
+ require 'binding_of_caller'
14
+
15
+ class << Kernel
16
+ alias_method :autoload_without_coverage, :autoload
17
+ def autoload(name, path)
18
+ mod = binding.of_caller(1).eval('Module.nesting').first || Object
19
+ DeepCover.autoload_tracker.add(mod, name, path)
20
+ mod.autoload_without_coverage(name, path)
21
+ end
22
+ end
23
+
24
+ module Kernel
25
+ alias_method :autoload_without_coverage, :autoload
26
+ def autoload(name, path)
27
+ mod = binding.of_caller(1).eval('Module.nesting').first || Object
28
+ DeepCover.autoload_tracker.add(mod, name, path)
29
+ mod.autoload_without_coverage(name, path)
30
+ end
31
+ end
32
+
33
+
34
+ class Module
35
+ alias_method :autoload_without_coverage, :autoload
36
+ def autoload(name, path)
37
+ DeepCover.autoload_tracker.add(self, name, path)
38
+ autoload_without_coverage(name, path)
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ # This is a complete replacement for the builtin Coverage module of Ruby
2
+
3
+ require 'coverage'
4
+ BuiltinCoverage = Coverage
5
+ Object.send(:remove_const, 'Coverage')
6
+
7
+ module Coverage
8
+ def self.start
9
+ @started = true
10
+ DeepCover.start
11
+ DeepCover.coverage.reset
12
+ end
13
+
14
+ def self.result
15
+ raise 'coverage measurement is not enabled' unless @started
16
+ @started = false
17
+ self.peek
18
+ end
19
+
20
+ def self.peek
21
+ results = DeepCover.coverage.covered_codes.map do |filename, covered_code|
22
+ [filename, covered_code.line_coverage(allow_partial: false)]
23
+ end
24
+ Hash[results]
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # These are the monkeypatches to replace the default #load in order
2
+ # to instrument the code before it gets run.
3
+ # For now, this is not used, and may never be. The tracking and reporting for things can might be
4
+ # loaded multiple times can be complex and is beyond the current scope of the project.
5
+
6
+ class << Kernel
7
+ alias_method :load_without_coverage, :load
8
+ def load(path, wrap = false)
9
+ return load_without_coverage(path, wrap) if wrap
10
+
11
+ result = DeepCover.custom_requirer.load(path)
12
+ if [:not_found, :cover_failed, :not_supported].include?(result)
13
+ load_without_coverage(path)
14
+ else
15
+ result
16
+ end
17
+ end
18
+ end
19
+
20
+ module Kernel
21
+ def load(path, wrap = false)
22
+ Kernel.require(path, wrap)
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ # These are the monkeypatches to replace the default #require and
2
+ # #require_relative in order to instrument the code before it gets run.
3
+
4
+ class << Kernel
5
+ alias_method :require_without_coverage, :require
6
+ def require(path)
7
+ result = DeepCover.custom_requirer.require(path)
8
+ if [:not_found, :cover_failed, :not_supported].include?(result)
9
+ require_without_coverage(path)
10
+ else
11
+ result
12
+ end
13
+ end
14
+
15
+ def require_relative(path)
16
+ base = caller(1..1).first[/[^:]+/]
17
+ raise LoadError, "cannot infer basepath" unless base
18
+ base = File.dirname(base)
19
+
20
+ require(File.absolute_path(path, base))
21
+ end
22
+ end
23
+
24
+ module Kernel
25
+ def require(path)
26
+ Kernel.require(path)
27
+ end
28
+
29
+ def require_relative(path)
30
+ base = caller(1..1).first[/[^:]+/]
31
+ raise LoadError, "cannot infer basepath" unless base
32
+ base = File.dirname(base)
33
+
34
+ require(File.absolute_path(path, base))
35
+ end
36
+ end
@@ -0,0 +1,198 @@
1
+ module DeepCover
2
+ require 'parser'
3
+ silence_warnings do
4
+ require 'parser/current'
5
+ end
6
+ require 'pry'
7
+ require 'pathname'
8
+ require_relative 'covered_code'
9
+ require 'securerandom'
10
+
11
+ # A collection of CoveredCode
12
+ class Coverage
13
+ include Enumerable
14
+
15
+ def initialize(**options)
16
+ @covered_codes = {}
17
+ @options = options
18
+ end
19
+
20
+ def covered_codes
21
+ @covered_codes.dup
22
+ end
23
+
24
+ def reset
25
+ @covered_codes = {}
26
+ end
27
+
28
+ def line_coverage(filename, **options)
29
+ covered_code(filename).line_coverage(**options)
30
+ end
31
+
32
+ def covered_code(path)
33
+ raise 'path must be an absolute path' unless Pathname.new(path).absolute?
34
+ @covered_codes[path] ||= CoveredCode.new(path: path, **@options)
35
+ end
36
+
37
+ def each
38
+ return to_enum unless block_given?
39
+ @covered_codes.each{|_path, covered_code| yield covered_code}
40
+ self
41
+ end
42
+
43
+ def to_istanbul(**options)
44
+ map do |covered_code|
45
+ next {} unless covered_code.has_executed?
46
+ covered_code.to_istanbul(**options)
47
+ end.inject(:merge)
48
+ end
49
+
50
+ def output_istanbul(dir: '.', name: ".nyc_output", **options)
51
+ path = Pathname.new(dir).expand_path.join(name)
52
+ path.mkpath
53
+ path.each_child(&:delete)
54
+ path.join('deep_cover.json').write(JSON.pretty_generate(to_istanbul(**options)))
55
+ path
56
+ end
57
+
58
+ def report_istanbul(output: nil, **options)
59
+ dir = output_istanbul(**options).dirname
60
+ if output
61
+ output = File.expand_path(output)
62
+ html = "--reporter=html --report-dir='#{output}' && open '#{output}/index.html'"
63
+ end
64
+ `cd #{dir} && nyc report --reporter=text #{html}`
65
+ end
66
+
67
+ def basic_report
68
+ missing = map do |covered_code|
69
+ if covered_code.has_executed?
70
+ missed = covered_code.line_coverage.each_with_index.map do |line_cov, line_index|
71
+ line_index + 1 if line_cov == 0
72
+ end.compact
73
+ else
74
+ missed = ['all']
75
+ end
76
+ [covered_code.buffer.name, missed] unless missed.empty?
77
+ end.compact.to_h
78
+ missing.map do |path, lines|
79
+ "#{File.basename(path)}: #{lines.join(', ')}"
80
+ end.join("\n")
81
+ end
82
+
83
+ def report(**options)
84
+ if Reporter::Istanbul.available?
85
+ report_istanbul(**options)
86
+ else
87
+ warn "nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`"
88
+ basic_report
89
+ end
90
+ end
91
+
92
+ def self.load(dest_path, dirname = 'deep_cover')
93
+ Persistence.new(dest_path, dirname).load
94
+ end
95
+
96
+ def self.saved?(dest_path, dirname = 'deep_cover')
97
+ Persistence.new(dest_path, dirname).saved?
98
+ end
99
+
100
+ def save(dest_path, dirname = 'deep_cover')
101
+ Persistence.new(dest_path, dirname).save(self)
102
+ self
103
+ end
104
+
105
+ def save_trackers(dest_path, dirname = 'deep_cover')
106
+ Persistence.new(dest_path, dirname).save_trackers(tracker_global)
107
+ self
108
+ end
109
+
110
+ def tracker_global
111
+ @options[:tracker_global]
112
+ end
113
+
114
+ class Persistence
115
+ BASENAME = 'coverage.dc'
116
+ TRACKER_TEMPLATE = 'trackers%{unique}.dct'
117
+
118
+ attr_reader :dir_path
119
+ def initialize(dest_path, dirname)
120
+ @dir_path = Pathname(dest_path).join(dirname).expand_path
121
+ end
122
+
123
+ def load
124
+ saved?
125
+ load_trackers
126
+ load_coverage
127
+ end
128
+
129
+ def save(coverage)
130
+ create_if_needed
131
+ delete_trackers
132
+ save_coverage(coverage)
133
+ end
134
+
135
+ def save_trackers(global)
136
+ saved?
137
+ basename = TRACKER_TEMPLATE % {unique: SecureRandom.urlsafe_base64}
138
+ dir_path.join(basename).binwrite(Marshal.dump({
139
+ version: DeepCover::VERSION,
140
+ global: global,
141
+ trackers: eval(global),
142
+ }))
143
+ end
144
+
145
+ def saved?
146
+ raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
147
+ self
148
+ end
149
+
150
+ private
151
+
152
+ def create_if_needed
153
+ dir_path.mkpath
154
+ end
155
+
156
+ def save_coverage(coverage)
157
+ dir_path.join(BASENAME).binwrite(Marshal.dump({
158
+ version: DeepCover::VERSION,
159
+ coverage: coverage,
160
+ }))
161
+ end
162
+
163
+ def load_coverage
164
+ Marshal.load(dir_path.join(BASENAME).binread).tap do |version: raise, coverage: raise|
165
+ raise "dump version mismatch: #{deep_cover}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
166
+ return coverage
167
+ end
168
+ end
169
+
170
+ def load_trackers
171
+ tracker_files.each do |full_path|
172
+ Marshal.load(full_path.binread).tap do |version: raise, global: raise, trackers: raise|
173
+ raise "dump version mismatch: #{deep_cover}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
174
+ merge_trackers(eval("#{global} ||= {}"), trackers)
175
+ end
176
+ end
177
+ end
178
+
179
+ def merge_trackers(hash, to_merge)
180
+ hash.merge!(to_merge) do |_key, current, to_add|
181
+ unless current.size == 0 || current.size == to_add.size
182
+ warn "Merging trackers of different sizes: #{current.size} vs #{to_add.size}"
183
+ end
184
+ to_add.zip(current).map{|a, b| a+b}
185
+ end
186
+ end
187
+
188
+ def tracker_files
189
+ basename = TRACKER_TEMPLATE % { unique: '*' }
190
+ Pathname.glob(dir_path.join(basename))
191
+ end
192
+
193
+ def delete_trackers
194
+ tracker_files.each(&:delete)
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,138 @@
1
+ module DeepCover
2
+ class CoveredCode
3
+ attr_accessor :covered_source, :buffer, :tracker_global, :local_var
4
+ @@counter = 0
5
+ @@globals = Hash.new{|h, global| h[global] = eval("#{global} ||= {}") }
6
+
7
+ def initialize(path: nil, source: nil, lineno: nil, tracker_global: '$_cov', local_var: '_temp')
8
+ raise "Must provide either path or source" unless path || source
9
+
10
+ @buffer = ::Parser::Source::Buffer.new(path)
11
+ if source
12
+ @buffer.source = source
13
+ else
14
+ @buffer.read
15
+ end
16
+ @lineno = lineno
17
+ @tracker_count = 0
18
+ @tracker_global = tracker_global
19
+ @local_var = local_var
20
+ @covered_source = instrument_source
21
+ end
22
+
23
+ def path
24
+ @buffer.name || "(source: '#{@buffer.source[0..20]}...')"
25
+ end
26
+
27
+ def name
28
+ @buffer.name ? File.basename(@buffer.name) : "(source)"
29
+ end
30
+
31
+ def nb_lines
32
+ @nb_lines ||= begin
33
+ lines = buffer.source_lines
34
+ if lines.size == 0
35
+ 0
36
+ else
37
+ lines.size - (lines.last.empty? ? 1 : 0)
38
+ end
39
+ end
40
+ end
41
+
42
+ def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
43
+ return if has_executed?
44
+ global[nb] = Array.new(@tracker_count, 0)
45
+ eval(@covered_source, binding, @buffer.name || '<raw_code>', @lineno || 1)
46
+ self
47
+ end
48
+
49
+ def cover
50
+ must_have_executed
51
+ global[nb]
52
+ end
53
+
54
+ def line_coverage(**options)
55
+ must_have_executed
56
+ Analyser::PerLine.new(self, **options).results
57
+ end
58
+
59
+ def to_istanbul(**options)
60
+ must_have_executed
61
+ Reporter::Istanbul.new(self, **options).convert
62
+ end
63
+
64
+ def char_cover(**options)
65
+ must_have_executed
66
+ Analyser::PerChar.new(self, **options).results
67
+ end
68
+
69
+ def nb
70
+ @nb ||= (@@counter += 1)
71
+ end
72
+
73
+ # Returns a range of tracker ids
74
+ def allocate_trackers(nb_needed)
75
+ prev = @tracker_count
76
+ @tracker_count += nb_needed
77
+ prev...@tracker_count
78
+ end
79
+
80
+ def tracker_source(tracker_id)
81
+ "#{tracker_global}[#{nb}][#{tracker_id}]+=1"
82
+ end
83
+
84
+ def trackers_setup_source
85
+ "(#{tracker_global}||={})[#{nb}]||=Array.new(#{@tracker_count},0)"
86
+ end
87
+
88
+ def tracker_hits(tracker_id)
89
+ cover.fetch(tracker_id)
90
+ end
91
+
92
+ def covered_ast
93
+ root.main
94
+ end
95
+
96
+ def root
97
+ @root ||= begin
98
+ ast = Parser::CurrentRuby.new.parse(@buffer)
99
+ Node::Root.new(ast, self)
100
+ end
101
+ end
102
+
103
+ def each_node(*args, &block)
104
+ covered_ast.each_node(*args, &block)
105
+ end
106
+
107
+ def instrument_source
108
+ rewriter = ::Parser::Source::Rewriter.new(@buffer)
109
+ covered_ast.each_node do |node|
110
+ prefix, suffix = node.rewrite_prefix_suffix
111
+ unless prefix.empty?
112
+ expression = node.expression
113
+ prefix = yield prefix, node, expression.begin, :prefix if block_given?
114
+ rewriter.insert_before_multi expression, prefix rescue binding.pry
115
+ end
116
+ unless suffix.empty?
117
+ expression = node.expression
118
+ suffix = yield suffix, node, expression.end, :suffix if block_given?
119
+ rewriter.insert_after_multi expression, suffix
120
+ end
121
+ end
122
+ rewriter.process
123
+ end
124
+
125
+ def has_executed?
126
+ global[nb] != nil
127
+ end
128
+
129
+ protected
130
+ def global
131
+ @@globals[tracker_global]
132
+ end
133
+
134
+ def must_have_executed
135
+ raise "cover for #{buffer.name} not available, file wasn't executed" unless has_executed?
136
+ end
137
+ end
138
+ end