deep-cover 0.1.16 → 0.2.0

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