deep-cover 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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