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