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,49 @@
1
+ require 'deep_cover'
2
+ require 'pry'
3
+
4
+ module DeepCover
5
+ module AutoRun
6
+ extend self
7
+
8
+ def detect
9
+ @covered_path = File.expand_path('./lib')
10
+ Coverage.saved? @covered_path
11
+ end
12
+
13
+ def load
14
+ @coverage = Coverage.load(@covered_path)
15
+ end
16
+
17
+ def save
18
+ @coverage.save_trackers(@covered_path)
19
+ end
20
+
21
+ def after_tests
22
+ use_at_exit = true
23
+ if defined?(Minitest)
24
+ puts "Registering with Minitest"
25
+ use_at_exit = false
26
+ Minitest.after_run { yield }
27
+ end
28
+ if defined?(Rspec)
29
+ use_at_exit = false
30
+ puts "Registering with Rspec"
31
+ RSpec.configure do |config|
32
+ config.after(:suite) { yield }
33
+ end
34
+ end
35
+ if use_at_exit
36
+ puts "Using at_exit"
37
+ at_exit { yield }
38
+ end
39
+ end
40
+
41
+ def run!
42
+ detect
43
+ load
44
+ after_tests { save }
45
+ end
46
+
47
+ run!
48
+ end
49
+ end
@@ -0,0 +1,75 @@
1
+ require 'weakref'
2
+
3
+ module DeepCover
4
+ class AutoloadTracker
5
+ def initialize(autoloaded_paths = {})
6
+ @autoloaded_paths = autoloaded_paths
7
+ end
8
+
9
+ def add(const, name, path)
10
+ ext = File.extname(path)
11
+ # We don't care about .so files
12
+ return if ext == '.so'
13
+ path = path + '.rb' if ext != '.rb'
14
+
15
+ pairs = @autoloaded_paths[path] ||= []
16
+ pairs << [WeakRef.new(const), name]
17
+ end
18
+
19
+ def pairs_for_absolute_path(absolute_path)
20
+ paths = autoloaded_paths_matching_absolute(absolute_path)
21
+
22
+ paths.flat_map do |path|
23
+ pairs = @autoloaded_paths[path] || []
24
+ pairs = pairs.map{|weak_const, name| [self.class.value_from_weak_ref(weak_const), name] }
25
+ pairs.select!(&:first)
26
+ pairs
27
+ end
28
+ end
29
+
30
+ def wrap_require(absolute_path)
31
+ pairs = pairs_for_absolute_path(absolute_path)
32
+
33
+ begin
34
+ pairs.each do |const, name|
35
+ # Changing the autoload to an already loaded file (this one)
36
+ const.autoload_without_coverage(name, __FILE__)
37
+ end
38
+
39
+ yield
40
+ rescue Exception
41
+ pairs.each do |const, name|
42
+ # Changing the autoload to an already loaded file (this one)
43
+ const.autoload_without_coverage(name, absolute_path)
44
+ end
45
+
46
+ raise
47
+ end
48
+ end
49
+
50
+ def initialize_autoloaded_paths
51
+ @autoloaded_paths = {}
52
+ ObjectSpace.each_object(Module) do |mod|
53
+ mod.constants.each do |name|
54
+ if path = mod.autoload?(name)
55
+ add(mod, name, path)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # We need all the paths of autoloaded_path that match a given absolute_path
62
+ # Since this can happen a lot, a cache is made which only chan
63
+ def autoloaded_paths_matching_absolute(absolute_path)
64
+ @autoloaded_paths.keys.select do |path|
65
+ absolute_path == DeepCover.custom_requirer.resolve_path(path)
66
+ end
67
+ end
68
+
69
+ # A simple if the ref is dead, return nil.
70
+ # WTF ruby, why is there no such simple interface ?!
71
+ def self.value_from_weak_ref(weak_ref)
72
+ WeakRef.class_variable_get(:@@__map)[weak_ref]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,9 @@
1
+ # We use a few features newer than our target of Ruby 2.0+:
2
+ class Module
3
+ public :prepend # Public in Ruby 2.1+.
4
+ end
5
+ require 'backports/2.1.0/module/include'
6
+ require 'backports/2.1.0/enumerable/to_h'
7
+ require 'backports/2.4.0/false_class/dup'
8
+ require 'backports/2.4.0/true_class/dup'
9
+ require 'backports/2.4.0/hash/transform_values'
@@ -0,0 +1,55 @@
1
+ module DeepCover
2
+ module Base
3
+ def start
4
+ return if @started
5
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
6
+ # No issues with autoload in jruby, so no need to override it!
7
+ else
8
+ require_relative 'core_ext/autoload_overrides'
9
+ autoload_tracker.initialize_autoloaded_paths
10
+ end
11
+ require_relative 'core_ext/require_overrides'
12
+ @started = true
13
+ end
14
+
15
+ def stop
16
+ # TODO
17
+ end
18
+
19
+ def line_coverage(filename)
20
+ coverage.line_coverage(handle_relative_filename(filename), **@config)
21
+ end
22
+
23
+ def covered_code(filename)
24
+ coverage.covered_code(handle_relative_filename(filename))
25
+ end
26
+
27
+ def cover
28
+ start
29
+ yield
30
+ ensure
31
+ stop
32
+ end
33
+
34
+ def coverage
35
+ @coverage ||= Coverage.new
36
+ end
37
+
38
+ def custom_requirer
39
+ @custom_requirer ||= CustomRequirer.new
40
+ end
41
+
42
+ def autoload_tracker
43
+ @autoload_tracker ||= AutoloadTracker.new
44
+ end
45
+
46
+ def handle_relative_filename(filename)
47
+ unless Pathname.new(filename).absolute?
48
+ relative_to = File.dirname(caller[1].partition(/\.rb:\d/).first)
49
+ filename = File.absolute_path(filename, relative_to)
50
+ end
51
+ filename += '.rb' unless filename =~ /\.rb$/
52
+ filename
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'deep_cover'
2
+ require_relative 'deep_cover/core_ext/coverage_replacement'
@@ -0,0 +1,93 @@
1
+ require 'term/ansicolor'
2
+
3
+ module DeepCover
4
+ module CLI
5
+ class Debugger
6
+ include Tools
7
+
8
+ module ColorAST
9
+ def fancy_type
10
+ color = case
11
+ when !executable?
12
+ :faint
13
+ when !was_executed?
14
+ :red
15
+ when flow_interrupt_count > 0
16
+ :yellow
17
+ else
18
+ :green
19
+ end
20
+ Term::ANSIColor.send(color, super)
21
+ end
22
+ end
23
+
24
+ def initialize(source, filename: '(source)', lineno: 1, pry: false)
25
+ @source = source
26
+ @filename = filename
27
+ @lineno = lineno
28
+ @pry = pry
29
+ end
30
+
31
+ def show
32
+ show_line_coverage
33
+ show_instrumented_code
34
+ show_ast
35
+ show_char_coverage
36
+ pry if @pry
37
+ finish
38
+ end
39
+
40
+ def show_line_coverage
41
+ puts "Line Coverage: Builtin | DeepCover | DeepCover Strict:\n"
42
+ begin
43
+ builtin_line_coverage = builtin_coverage(@source, @filename, @lineno)
44
+ our_line_coverage = our_coverage(@source, @filename, @lineno)
45
+ our_strict_line_coverage = our_coverage(@source, @filename, @lineno, allow_partial: false)
46
+ lines = format(builtin_line_coverage, our_line_coverage, our_strict_line_coverage, source: @source)
47
+ puts number_lines(lines, lineno: @lineno)
48
+ rescue Exception => e
49
+ puts "Can't run coverage: #{e.class.name}: #{e}\n#{e.backtrace.join("\n")}"
50
+ @failed = true
51
+ end
52
+ end
53
+
54
+ def show_instrumented_code
55
+ puts "\nInstrumented code:\n"
56
+ puts format_generated_code(covered_code)
57
+ end
58
+
59
+ def show_ast
60
+ puts "\nParsed code:\n"
61
+ begin
62
+ execute_sample(covered_code)
63
+ rescue Exception => e
64
+ puts "Can't `execute_sample`:#{e.class.name}: #{e}\n#{e.backtrace.join("\n")}"
65
+ @failed = true
66
+ end
67
+
68
+ Node.prepend ColorAST
69
+ puts covered_code.covered_ast
70
+ end
71
+
72
+ def show_char_coverage
73
+ puts "\nChar coverage:\n"
74
+
75
+ puts format_char_cover(covered_code, show_whitespace: !!ENV['W'])
76
+ end
77
+
78
+ def pry
79
+ a = covered_code.covered_ast
80
+ b = a.children.first
81
+ binding.pry
82
+ end
83
+
84
+ def finish
85
+ exit(!@failed)
86
+ end
87
+
88
+ def covered_code
89
+ @covered_code ||= CoveredCode.new(source: @source, path: @filename, lineno: @lineno)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,49 @@
1
+ module DeepCover
2
+ require 'bundler/setup'
3
+ require 'slop'
4
+ require 'deep_cover'
5
+ require_relative_dir '.'
6
+
7
+ module CLI
8
+ module DeepCover
9
+ extend self
10
+
11
+ def show_version
12
+ puts "deep-cover v#{DeepCover::VERSION}; parser v#{Parser::Version}"
13
+ end
14
+
15
+ def show_help
16
+ puts options
17
+ end
18
+
19
+ def options
20
+ @options ||= Slop.parse do |o|
21
+ o.banner = "usage: deep-cover [options] [path/to/app/or/gem]"
22
+ o.separator ''
23
+ o.string '-o', '--output', 'output folder', default: './coverage'
24
+ o.string '-c', '--command', 'command to run tests', default: 'rake'
25
+
26
+ o.separator ''
27
+ o.separator 'For testing purposes:'
28
+ o.string '-e', '--expression', 'test ruby expression instead of a covering a path'
29
+ o.bool '-d', '--debug', 'enter debugging after cover'
30
+
31
+ o.separator ''
32
+ o.separator 'Other available commands:'
33
+ o.on('--version', 'print the version') { version; exit }
34
+ o.on('-h', '--help') { help; exit }
35
+ end
36
+ end
37
+
38
+ def go
39
+ if options[:expression]
40
+ Debugger.new(options[:expression], pry: options[:debug]).show
41
+ elsif (path = options.arguments.first)
42
+ InstrumentedCloneReporter.new(path, **options).run
43
+ else
44
+ show_help
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,105 @@
1
+ require 'yaml'
2
+ require 'tmpdir'
3
+
4
+ module DeepCover
5
+ module CLI
6
+ class InstrumentedCloneReporter
7
+ include Tools
8
+ attr_reader :dest_path
9
+
10
+ def initialize(gem_path, command: 'rake', **options)
11
+ @command = command
12
+ @options = options
13
+ @root_path = File.expand_path(gem_path)
14
+ if File.exist?(File.join(@root_path, 'Gemfile'))
15
+ @gem_relative_path = '' # Typical case
16
+ else
17
+ # E.g. rails/activesupport
18
+ @gem_relative_path = File.basename(@root_path)
19
+ @root_path = File.dirname(@root_path)
20
+ raise "Can't find Gemfile" unless File.exist?(File.join(@root_path, 'Gemfile'))
21
+ end
22
+ @dest_root = File.expand_path('~/test_deep_cover')
23
+ @dest_root = Dir.mktmpdir("deep_cover_test") unless Dir.exist?(@dest_root)
24
+ `rm -rf #{@dest_root}/* #{@dest_root}/.*`
25
+ @dest_path = File.expand_path(File.join(@dest_root, @gem_relative_path))
26
+ end
27
+
28
+ def copy
29
+ @copy ||= `cp -r #{@root_path}/* #{@dest_root} && cp #{@root_path}/.* #{@dest_root}`
30
+ end
31
+
32
+ def patch_ruby_file(ruby_file)
33
+ content = File.read(ruby_file)
34
+ # Insert our code after leading comments:
35
+ content.sub!(/^((#.*\n+)*)/, '\1require "deep_cover/auto_run";')
36
+ File.write(ruby_file, content)
37
+ end
38
+
39
+ def patch_main_ruby_files
40
+ main = File.join(dest_path, 'lib/*.rb')
41
+ Dir.glob(main).each do |main|
42
+ puts "Patching #{main}"
43
+ patch_ruby_file(main)
44
+ end
45
+ end
46
+
47
+ def patch_gemfile
48
+ gemfile = File.expand_path(File.join(dest_path, 'Gemfile'))
49
+ gemfile = File.expand_path(File.join(dest_path, '..', 'Gemfile')) unless File.exist?(gemfile)
50
+ content = File.read(gemfile)
51
+ unless content =~ /gem 'deep-cover'/
52
+ puts "Patching Gemfile"
53
+ File.write(gemfile, [
54
+ "# This file was modified by DeepCover",
55
+ content,
56
+ "gem 'deep-cover', path: '#{File.expand_path(__dir__ + '/../../../')}'",
57
+ '',
58
+ ].join("\n"))
59
+ end
60
+ Bundler.with_clean_env do
61
+ `cd #{dest_path} && bundle`
62
+ end
63
+ end
64
+
65
+ def patch_rubocop
66
+ path = File.expand_path(File.join(dest_path, '.rubocop.yml'))
67
+ return unless File.exists?(path)
68
+ puts "Patching .rubocop.yml"
69
+ config = YAML.load(File.read(path).gsub(/(?<!\w)lib(?!\w)/, 'lib_original'))
70
+ ((config['AllCops'] ||= {})['Exclude'] ||= []) << 'lib/**/*'
71
+ File.write(path, "# This file was modified by DeepCover\n" + YAML.dump(config))
72
+ end
73
+
74
+ def patch
75
+ patch_gemfile
76
+ patch_rubocop
77
+ patch_main_ruby_files
78
+ end
79
+
80
+ def cover
81
+ `cp -R #{dest_path}/lib #{dest_path}/lib_original`
82
+ @covered_path = Tools.dump_covered_code(File.join(dest_path, 'lib_original'), File.join(dest_path, 'lib'))
83
+ end
84
+
85
+ def process
86
+ Bundler.with_clean_env do
87
+ system("cd #{dest_path} && #{@command}", out: $stdout, err: :out)
88
+ end
89
+ end
90
+
91
+ def report
92
+ coverage = Coverage.load @covered_path
93
+ puts coverage.report(dir: @covered_path, **@options)
94
+ end
95
+
96
+ def run
97
+ copy
98
+ cover
99
+ patch
100
+ process
101
+ report
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,52 @@
1
+ module DeepCover
2
+ class Config
3
+ DEFAULTS = {
4
+ ignore_uncovered: [],
5
+ paths: %w[./app ./lib],
6
+ allow_partial: false,
7
+ }
8
+
9
+ def initialize
10
+ @options = copy(DEFAULTS)
11
+ end
12
+
13
+ def to_hash
14
+ copy(@options)
15
+ end
16
+ alias_method :to_h, :to_hash
17
+
18
+ def ignore_uncovered(*keywords)
19
+ @options[:ignore_uncovered] -= keywords
20
+ self
21
+ end
22
+
23
+ def detect_uncovered(*keywords)
24
+ @options[:ignore_uncovered] += keywords
25
+ self
26
+ end
27
+
28
+ def paths(paths)
29
+ @options[:paths] = paths
30
+ self
31
+ end
32
+
33
+ private
34
+ def copy(h)
35
+ h.dup.transform_values(&:dup)
36
+ end
37
+
38
+ module Setter
39
+ def configure(&block)
40
+ @config ||= Config.new
41
+
42
+ raise "Must provide a block" unless block
43
+ case block.arity
44
+ when 0
45
+ @config.instance_eval(&block)
46
+ when 1
47
+ block.call(@config)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end