deep-cover 0.1.16 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +3 -8
  4. data/.travis.yml +4 -4
  5. data/CHANGELOG.md +10 -0
  6. data/Gemfile +3 -1
  7. data/README.md +9 -4
  8. data/Rakefile +6 -3
  9. data/deep_cover.gemspec +2 -2
  10. data/lib/deep_cover.rb +10 -0
  11. data/lib/deep_cover/analyser.rb +1 -2
  12. data/lib/deep_cover/analyser/base.rb +32 -0
  13. data/lib/deep_cover/analyser/branch.rb +19 -4
  14. data/lib/deep_cover/analyser/node.rb +52 -0
  15. data/lib/deep_cover/analyser/per_char.rb +18 -1
  16. data/lib/deep_cover/analyser/stats.rb +54 -0
  17. data/lib/deep_cover/backports.rb +1 -0
  18. data/lib/deep_cover/base.rb +17 -1
  19. data/lib/deep_cover/builtin_takeover.rb +5 -0
  20. data/lib/deep_cover/cli/deep_cover.rb +2 -1
  21. data/lib/deep_cover/cli/instrumented_clone_reporter.rb +12 -10
  22. data/lib/deep_cover/config.rb +22 -8
  23. data/lib/deep_cover/core_ext/coverage_replacement.rb +22 -18
  24. data/lib/deep_cover/coverage.rb +3 -203
  25. data/lib/deep_cover/coverage/analysis.rb +36 -0
  26. data/lib/deep_cover/coverage/base.rb +91 -0
  27. data/lib/deep_cover/coverage/istanbul.rb +34 -0
  28. data/lib/deep_cover/coverage/persistence.rb +93 -0
  29. data/lib/deep_cover/covered_code.rb +12 -22
  30. data/lib/deep_cover/custom_requirer.rb +6 -2
  31. data/lib/deep_cover/node/base.rb +1 -1
  32. data/lib/deep_cover/node/case.rb +13 -2
  33. data/lib/deep_cover/node/exceptions.rb +2 -2
  34. data/lib/deep_cover/node/if.rb +21 -2
  35. data/lib/deep_cover/node/mixin/flow_accounting.rb +1 -0
  36. data/lib/deep_cover/node/send.rb +9 -2
  37. data/lib/deep_cover/node/short_circuit.rb +10 -0
  38. data/lib/deep_cover/parser_ext/range.rb +4 -4
  39. data/lib/deep_cover/reporter/html.rb +15 -0
  40. data/lib/deep_cover/reporter/html/base.rb +14 -0
  41. data/lib/deep_cover/reporter/html/index.rb +78 -0
  42. data/lib/deep_cover/reporter/html/site.rb +78 -0
  43. data/lib/deep_cover/reporter/html/source.rb +136 -0
  44. data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
  45. data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
  46. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +338 -0
  47. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
  48. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
  49. data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
  50. data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
  51. data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
  52. data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
  53. data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
  54. data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
  55. data/lib/deep_cover/reporter/html/tree.rb +55 -0
  56. data/lib/deep_cover/tools/content_tag.rb +11 -0
  57. data/lib/deep_cover/tools/covered.rb +9 -0
  58. data/lib/deep_cover/tools/merge.rb +16 -0
  59. data/lib/deep_cover/tools/render_template.rb +13 -0
  60. data/lib/deep_cover/tools/transform_keys.rb +9 -0
  61. data/lib/deep_cover/version.rb +1 -1
  62. metadata +33 -7
  63. data/lib/deep_cover/analyser/ignore_uncovered.rb +0 -21
  64. data/lib/deep_cover/analyser/optionally_covered.rb +0 -19
@@ -2,3 +2,8 @@
2
2
 
3
3
  require_relative '../deep_cover'
4
4
  require_relative '../deep_cover/core_ext/coverage_replacement'
5
+
6
+ require 'coverage'
7
+ BuiltinCoverage = Coverage
8
+ Object.send(:remove_const, 'Coverage')
9
+ Coverage = DeepCover::CoverageReplacement.dup
@@ -40,11 +40,12 @@ module DeepCover
40
40
  o.separator ''
41
41
  o.string '-o', '--output', 'output folder', default: './coverage'
42
42
  o.string '-c', '--command', 'command to run tests', default: 'bundle exec rake'
43
+ o.string '--reporter', 'reporter', default: 'html'
43
44
  o.bool '--bundle', 'run bundle before the tests', default: true
44
45
  o.bool '--process', 'turn off to only redo the reporting', default: true
45
46
  o.bool '--open', 'open the output coverage', default: false
46
47
  o.separator 'Coverage options'
47
- @ignore_uncovered_map = Analyser.optionally_covered.map do |option|
48
+ @ignore_uncovered_map = Analyser::Node.optionally_covered.map do |option|
48
49
  default = Config::DEFAULTS[:ignore_uncovered].include?(option)
49
50
  o.bool "--ignore-#{Tools.dasherize(option)}", '', default: default
50
51
  [:"ignore_#{option}", option]
@@ -106,23 +106,25 @@ module DeepCover
106
106
 
107
107
  def patch_gemfile
108
108
  gemfile = @dest_root.join('Gemfile')
109
+ deps = Bundler::Definition.build(gemfile, nil, nil).dependencies
110
+
111
+ return if deps.find { |e| e.name == 'deep-cover' }
112
+
109
113
  content = File.read(gemfile)
110
- unless content =~ /gem 'deep-cover'/
111
- puts "Patching Gemfile #{gemfile}"
112
- File.write(gemfile, [
113
- '# This file was modified by DeepCover',
114
- content,
115
- "gem 'deep-cover', path: '#{File.expand_path(__dir__ + '/../../../')}'",
116
- '',
117
- ].join("\n"))
118
- end
114
+ puts "Patching Gemfile #{gemfile}"
115
+ File.write(gemfile, [
116
+ '# This file was modified by DeepCover',
117
+ content,
118
+ "gem 'deep-cover', path: '#{File.expand_path(__dir__ + '/../../../')}'",
119
+ '',
120
+ ].join("\n"))
119
121
  end
120
122
 
121
123
  def patch_rubocop
122
124
  path = @dest_root.join('.rubocop.yml')
123
125
  return unless path.exist?
124
126
  puts 'Patching .rubocop.yml'
125
- config = YAML.safe_load(path.read.gsub(/(?<!\w)lib(?!\w)/, 'lib_original'))
127
+ config = YAML.load(path.read.gsub(/(?<!\w)lib(?!\w)/, 'lib_original'))
126
128
  ((config['AllCops'] ||= {})['Exclude'] ||= []) << 'lib/**/*' << 'app/**/*'
127
129
  path.write("# This file was modified by DeepCover\n" + YAML.dump(config))
128
130
  end
@@ -14,38 +14,52 @@ module DeepCover
14
14
  end
15
15
 
16
16
  def to_hash
17
- copy(@options)
17
+ @options.dup
18
18
  end
19
19
  alias_method :to_h, :to_hash
20
20
 
21
21
  def ignore_uncovered(*keywords)
22
- check_uncovered(keywords)
23
- change(:ignore_uncovered, @options[:ignore_uncovered] | keywords)
22
+ if keywords.empty?
23
+ @options[:ignore_uncovered]
24
+ else
25
+ check_uncovered(keywords)
26
+ change(:ignore_uncovered, @options[:ignore_uncovered] | keywords)
27
+ end
24
28
  end
25
29
 
26
30
  def detect_uncovered(*keywords)
27
- check_uncovered(keywords)
28
- change(:ignore_uncovered, @options[:ignore_uncovered] - keywords)
31
+ if keywords.empty?
32
+ Analyser::Node.optionally_covered - @options[:ignore_uncovered]
33
+ else
34
+ check_uncovered(keywords)
35
+ change(:ignore_uncovered, @options[:ignore_uncovered] - keywords)
36
+ end
29
37
  end
30
38
 
31
39
  def paths(paths = nil)
32
40
  if paths
33
- change(:paths, Array(paths).dup.freeze)
41
+ change(:paths, Array(paths).dup)
34
42
  else
35
43
  @options[:paths]
36
44
  end
37
45
  end
38
46
 
47
+ def reset
48
+ DEFAULTS.each do |key, value|
49
+ change(key, value)
50
+ end
51
+ end
52
+
39
53
  private
40
54
 
41
55
  def check_uncovered(keywords)
42
- unknown = keywords - Analyser.optionally_covered
56
+ unknown = keywords - Analyser::Node.optionally_covered
43
57
  raise ArgumentError, "unknown options: #{unknown.join(', ')}" unless unknown.empty?
44
58
  end
45
59
 
46
60
  def change(option, value)
47
61
  if @options[option] != value
48
- @options[option] = value
62
+ @options[option] = value.freeze
49
63
  @notify.config_changed(option) if @notify.respond_to? :config_changed
50
64
  end
51
65
  self
@@ -2,27 +2,31 @@
2
2
 
3
3
  # This is a complete replacement for the builtin Coverage module of Ruby
4
4
 
5
- require 'coverage'
6
- BuiltinCoverage = Coverage
7
- Object.send(:remove_const, 'Coverage')
5
+ module DeepCover
6
+ module CoverageReplacement
7
+ class << self
8
+ def running?
9
+ DeepCover.running?
10
+ end
8
11
 
9
- module Coverage
10
- def self.start
11
- @started = true
12
- DeepCover.start
13
- DeepCover.coverage.reset
14
- end
12
+ def start
13
+ return if running?
14
+ DeepCover.start
15
+ nil
16
+ end
15
17
 
16
- def self.result
17
- raise 'coverage measurement is not enabled' unless @started
18
- @started = false
19
- self.peek
20
- end
18
+ def result
19
+ r = peek_result
20
+ DeepCover.stop
21
+ r
22
+ end
21
23
 
22
- def self.peek
23
- results = DeepCover.coverage.covered_codes.map do |filename, covered_code|
24
- [filename, covered_code.line_coverage(allow_partial: false)]
24
+ def peek_result
25
+ raise 'coverage measurement is not enabled' unless running?
26
+ DeepCover.coverage.covered_codes.map do |covered_code|
27
+ [covered_code.path, covered_code.line_coverage(allow_partial: false)]
28
+ end.to_h
29
+ end
25
30
  end
26
- Hash[results]
27
31
  end
28
32
  end
@@ -1,208 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeepCover
4
- require 'parser'
5
- silence_warnings do
6
- require 'parser/current'
7
- end
8
4
  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, **options)
33
- raise 'path must be an absolute path' unless Pathname.new(path).absolute?
34
- @covered_codes[path] ||= CoveredCode.new(path: path, **options, **@options)
35
- end
36
-
37
- def each
38
- return to_enum unless block_given?
39
- @covered_codes.each_value { |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
- unless [nil, '', 'false'].include? output
61
- output = File.expand_path(output)
62
- html = "--reporter=html --report-dir='#{output}'"
63
- if options[:open]
64
- html += " && open '#{output}/index.html'"
65
- else
66
- msg = "\nHTML coverage written to: '#{output}/index.html'"
67
- end
68
- end
69
- `cd #{dir} && nyc report --reporter=text #{html}` + msg.to_s
70
- end
71
-
72
- def basic_report
73
- missing = map do |covered_code|
74
- if covered_code.has_executed?
75
- missed = covered_code.line_coverage.each_with_index.map do |line_cov, line_index|
76
- line_index + 1 if line_cov == 0
77
- end.compact
78
- else
79
- missed = ['all']
80
- end
81
- [covered_code.buffer.name, missed] unless missed.empty?
82
- end.compact.to_h
83
- missing.map do |path, lines|
84
- "#{File.basename(path)}: #{lines.join(', ')}"
85
- end.join("\n")
86
- end
87
-
88
- def report(**options)
89
- if Reporter::Istanbul.available?
90
- report_istanbul(**options)
91
- else
92
- warn 'nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`'
93
- basic_report
94
- end
95
- end
96
-
97
- def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
98
- Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
99
- end
100
-
101
- def self.saved?(dest_path, dirname = 'deep_cover')
102
- Persistence.new(dest_path, dirname).saved?
103
- end
104
-
105
- def save(dest_path, dirname = 'deep_cover')
106
- Persistence.new(dest_path, dirname).save(self)
107
- self
108
- end
109
-
110
- def save_trackers(dest_path, dirname = 'deep_cover')
111
- Persistence.new(dest_path, dirname).save_trackers(tracker_global)
112
- self
113
- end
114
-
115
- def tracker_global
116
- @options.fetch(:tracker_global, CoveredCode::DEFAULT_TRACKER_GLOBAL)
117
- end
118
-
119
- class Persistence
120
- # rubocop:disable Security/MarshalLoad
121
- BASENAME = 'coverage.dc'
122
- TRACKER_TEMPLATE = 'trackers%{unique}.dct'
123
-
124
- attr_reader :dir_path
125
- def initialize(dest_path, dirname)
126
- @dir_path = Pathname(dest_path).join(dirname).expand_path
127
- end
128
-
129
- def load(with_trackers: true)
130
- saved?
131
- load_trackers if with_trackers
132
- load_coverage
133
- end
134
-
135
- def save(coverage)
136
- create_if_needed
137
- delete_trackers
138
- save_coverage(coverage)
139
- end
140
-
141
- def save_trackers(global)
142
- saved?
143
- trackers = eval(global) # rubocop:disable Security/Eval
144
- # Some testing involves more than one process, some of which don't run any of our covered code.
145
- # Don't save anything if that's the case
146
- return if trackers.nil?
147
- basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
148
- dir_path.join(basename).binwrite(Marshal.dump(
149
- version: DeepCover::VERSION,
150
- global: global,
151
- trackers: trackers,
152
- ))
153
- end
154
-
155
- def saved?
156
- raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
157
- self
158
- end
159
-
160
- private
161
-
162
- def create_if_needed
163
- dir_path.mkpath
164
- end
165
-
166
- def save_coverage(coverage)
167
- dir_path.join(BASENAME).binwrite(Marshal.dump(
168
- version: DeepCover::VERSION,
169
- coverage: coverage,
170
- ))
171
- end
172
-
173
- def load_coverage
174
- Marshal.load(dir_path.join(BASENAME).binread).tap do |version: raise, coverage: raise|
175
- raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
176
- return coverage
177
- end
178
- end
179
-
180
- def load_trackers
181
- tracker_files.each do |full_path|
182
- Marshal.load(full_path.binread).tap do |version: raise, global: raise, trackers: raise|
183
- raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
184
- merge_trackers(eval("#{global} ||= {}"), trackers) # rubocop:disable Security/Eval
185
- end
186
- end
187
- end
188
-
189
- def merge_trackers(hash, to_merge)
190
- hash.merge!(to_merge) do |_key, current, to_add|
191
- unless current.empty? || current.size == to_add.size
192
- warn "Merging trackers of different sizes: #{current.size} vs #{to_add.size}"
193
- end
194
- to_add.zip(current).map { |a, b| a + b }
195
- end
196
- end
197
-
198
- def tracker_files
199
- basename = format(TRACKER_TEMPLATE, unique: '*')
200
- Pathname.glob(dir_path.join(basename))
201
- end
202
-
203
- def delete_trackers
204
- tracker_files.each(&:delete)
205
- end
206
- end
207
- end
5
+ Coverage = Class.new
6
+ require_relative_dir 'coverage'
7
+ Coverage.include Coverage::Istanbul
208
8
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ class Coverage::Analysis < Struct.new(:covered_codes, :options)
5
+ include Memoize
6
+ memoize :analyser_map, :stat_map
7
+
8
+ def analyser_map
9
+ covered_codes.map do |covered_code|
10
+ [covered_code, compute_analysers(covered_code)]
11
+ end.to_h
12
+ end
13
+
14
+ def stat_map
15
+ analyser_map.transform_values { |a| a.transform_values(&:stats) }
16
+ end
17
+
18
+ private
19
+
20
+ def compute_analysers(covered_code)
21
+ base = Analyser::Node.new(covered_code, **options)
22
+ {node: base}.merge!(
23
+ {
24
+ per_char: Analyser::PerChar,
25
+ branch: Analyser::Branch,
26
+ }.transform_values { |klass| klass.new(base, **options) }
27
+ )
28
+ end
29
+ end
30
+
31
+ class Coverage
32
+ def analysis(**options)
33
+ Analysis.new(covered_codes, options)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeepCover
4
+ # A collection of CoveredCode
5
+ class Coverage
6
+ include Enumerable
7
+
8
+ def initialize(**options)
9
+ @covered_code_index = {}
10
+ @options = options
11
+ end
12
+
13
+ def covered_codes
14
+ @covered_code_index.values
15
+ end
16
+
17
+ def reset
18
+ @covered_code_index = {}
19
+ end
20
+
21
+ def line_coverage(filename, **options)
22
+ covered_code(filename).line_coverage(**options)
23
+ end
24
+
25
+ def covered_code(path, **options)
26
+ raise 'path must be an absolute path' unless Pathname.new(path).absolute?
27
+ @covered_code_index[path] ||= CoveredCode.new(path: path, **options, **@options)
28
+ end
29
+
30
+ def each
31
+ return to_enum unless block_given?
32
+ @covered_code_index.each_value { |covered_code| yield covered_code }
33
+ self
34
+ end
35
+
36
+ def report(**options)
37
+ case (reporter = options.fetch(:reporter, :html).to_sym)
38
+ when :html
39
+ Reporter::HTML.report(self, **options)
40
+ when :istanbul
41
+ if Reporter::Istanbul.available?
42
+ report_istanbul(**options)
43
+ else
44
+ warn 'nyc not available. Please install `nyc` using `yarn global add nyc` or `npm i nyc -g`'
45
+ end
46
+ when :text
47
+ basic_report
48
+ else
49
+ raise ArgumentError, "Unknown reporter: #{reporter}"
50
+ end
51
+ end
52
+
53
+ def basic_report
54
+ missing = map do |covered_code|
55
+ if covered_code.has_executed?
56
+ missed = covered_code.line_coverage.each_with_index.map do |line_cov, line_index|
57
+ line_index + 1 if line_cov == 0
58
+ end.compact
59
+ else
60
+ missed = ['all']
61
+ end
62
+ [covered_code.buffer.name, missed] unless missed.empty?
63
+ end.compact.to_h
64
+ missing.map do |path, lines|
65
+ "#{File.basename(path)}: #{lines.join(', ')}"
66
+ end.join("\n")
67
+ end
68
+
69
+ def self.load(dest_path, dirname = 'deep_cover', with_trackers: true)
70
+ Persistence.new(dest_path, dirname).load(with_trackers: with_trackers)
71
+ end
72
+
73
+ def self.saved?(dest_path, dirname = 'deep_cover')
74
+ Persistence.new(dest_path, dirname).saved?
75
+ end
76
+
77
+ def save(dest_path, dirname = 'deep_cover')
78
+ Persistence.new(dest_path, dirname).save(self)
79
+ self
80
+ end
81
+
82
+ def save_trackers(dest_path, dirname = 'deep_cover')
83
+ Persistence.new(dest_path, dirname).save_trackers(tracker_global)
84
+ self
85
+ end
86
+
87
+ def tracker_global
88
+ @options.fetch(:tracker_global, CoveredCode::DEFAULT_TRACKER_GLOBAL)
89
+ end
90
+ end
91
+ end