coverband 2.0.3 → 3.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +73 -0
  4. data/.travis.yml +0 -3
  5. data/README.md +3 -3
  6. data/changes.md +65 -49
  7. data/coverband.gemspec +1 -1
  8. data/lib/coverband.rb +9 -6
  9. data/lib/coverband/adapters/base.rb +22 -2
  10. data/lib/coverband/adapters/file_store.rb +11 -11
  11. data/lib/coverband/adapters/redis_store.rb +22 -57
  12. data/lib/coverband/collectors/coverage.rb +57 -53
  13. data/lib/coverband/configuration.rb +6 -14
  14. data/lib/coverband/integrations/background.rb +7 -0
  15. data/lib/coverband/{middleware.rb → integrations/middleware.rb} +1 -3
  16. data/lib/coverband/reporters/base.rb +37 -82
  17. data/lib/coverband/reporters/console_report.rb +3 -0
  18. data/lib/coverband/reporters/simple_cov_report.rb +4 -3
  19. data/lib/coverband/reporters/web.rb +38 -35
  20. data/lib/coverband/utils/s3_report_writer.rb +59 -0
  21. data/lib/coverband/{tasks.rb → utils/tasks.rb} +0 -0
  22. data/lib/coverband/version.rb +1 -1
  23. data/test/benchmarks/benchmark.rake +3 -3
  24. data/test/test_helper.rb +18 -17
  25. data/test/unit/adapters_base_test.rb +29 -0
  26. data/test/unit/adapters_file_store_test.rb +2 -2
  27. data/test/unit/adapters_redis_store_test.rb +14 -51
  28. data/test/unit/collectors_coverage_test.rb +3 -107
  29. data/test/unit/configuration_test.rb +2 -9
  30. data/test/unit/full_stack_test.rb +47 -0
  31. data/test/unit/middleware_test.rb +21 -57
  32. data/test/unit/reports_base_test.rb +12 -71
  33. data/test/unit/reports_console_test.rb +9 -22
  34. data/test/unit/reports_simple_cov_test.rb +3 -37
  35. data/test/unit/reports_web_test.rb +4 -0
  36. data/test/unit/{s3_report_writer_test.rb → utils_s3_report_writer_test.rb} +1 -1
  37. metadata +29 -18
  38. data/lib/coverband/adapters/memory_cache_store.rb +0 -53
  39. data/lib/coverband/collectors/base.rb +0 -126
  40. data/lib/coverband/collectors/trace.rb +0 -122
  41. data/lib/coverband/s3_report_writer.rb +0 -49
  42. data/test/unit/adapters_memory_cache_store_test.rb +0 -66
  43. data/test/unit/collectors_base_test.rb +0 -104
  44. data/test/unit/collectors_trace_test.rb +0 -106
@@ -2,64 +2,81 @@
2
2
 
3
3
  module Coverband
4
4
  module Collectors
5
- class Coverage < Base
6
- def record_coverage
7
- # noop
5
+ ###
6
+ # TODO: look at alternatives to semaphore
7
+ # StandardError seems line be better option
8
+ # coverband previously had RuntimeError here
9
+ # but runtime error can let a large number of error crash this method
10
+ # and this method is currently in a ensure block in middleware and threads
11
+ ###
12
+ class Coverage
13
+ def self.instance
14
+ Thread.current[:coverband_instance] ||= Coverband::Collectors::Coverage.new
8
15
  end
9
16
 
10
- def stop_coverage
11
- # noop
17
+ def reset_instance
18
+ @project_directory = File.expand_path(Coverband.configuration.root)
19
+ @file_line_usage = {}
20
+ @ignored_files = Set.new
21
+ @ignore_patterns = Coverband.configuration.ignore + ['internal:prelude', 'schema.rb']
22
+ @reporting_frequency = Coverband.configuration.reporting_frequency
23
+ @store = Coverband.configuration.store
24
+ @verbose = Coverband.configuration.verbose
25
+ @logger = Coverband.configuration.logger
26
+ @current_thread = Thread.current
27
+ Thread.current[:coverband_instance] = nil
28
+ self
12
29
  end
13
30
 
14
- def report_coverage
15
- unless @enabled
16
- @logger.info 'coverage disabled' if @verbose
31
+ def report_coverage(force_report = false)
32
+ return if !ready_to_report? && !force_report
33
+ unless @store
34
+ @logger.debug 'no store set, no-op'
17
35
  return
18
36
  end
19
-
20
- if failed_recently?
21
- @logger.error 'coverage reporting standing-by because of recent failure' if @verbose
22
- return
37
+ new_results = get_new_coverage_results
38
+ add_filtered_files(new_results)
39
+ @store.save_report(files_with_line_usage)
40
+ @file_line_usage.clear
41
+ rescue StandardError => err
42
+ if @verbose
43
+ @logger.error 'coverage failed to store'
44
+ @logger.error "error: #{err.inspect} #{err.message}"
45
+ @logger.error err.backtrace
23
46
  end
47
+ end
24
48
 
25
- new_results = nil
26
- @semaphore.synchronize { new_results = new_coverage(::Coverage.peek_result.dup) }
49
+ protected
50
+
51
+ def track_file?(file)
52
+ @ignore_patterns.none? do |pattern|
53
+ file.include?(pattern)
54
+ end && file.start_with?(@project_directory)
55
+ end
56
+
57
+ private
58
+
59
+ def add_filtered_files(new_results)
27
60
  new_results.each_pair do |file, line_counts|
28
61
  next if @ignored_files.include?(file)
29
62
  next unless track_file?(file)
30
63
  add_file(file, line_counts)
31
64
  end
65
+ end
32
66
 
33
- if @verbose
34
- @logger.debug "coverband file usage: #{file_usage.inspect}"
35
- output_file_line_usage if @verbose == 'debug'
36
- end
37
-
38
- if @store
39
- @store.save_report(files_with_line_usage)
40
- @file_line_usage.clear
41
- elsif @verbose
42
- @logger.debug 'coverage report: '
43
- @logger.debug @file_line_usage.inspect
44
- end
45
- # StandardError might be better option
46
- # coverband previously had RuntimeError here
47
- # but runtime error can let a large number of error crash this method
48
- # and this method is currently in a ensure block in middleware
49
- rescue StandardError => err
50
- failed!
51
- if @verbose
52
- @logger.error 'coverage missing'
53
- @logger.error "error: #{err.inspect} #{err.message}"
54
- @logger.error err.backtrace
55
- end
67
+ def ready_to_report?
68
+ (rand * 100.0) >= (100.0 - @reporting_frequency)
56
69
  end
57
70
 
58
- private
71
+ def get_new_coverage_results
72
+ coverage_results = nil
73
+ @semaphore.synchronize { coverage_results = new_coverage(::Coverage.peek_result.dup) }
74
+ coverage_results
75
+ end
59
76
 
60
77
  def files_with_line_usage
61
78
  @file_line_usage.select do |_file_name, coverage|
62
- coverage.values.any? { |value| value != 0 }
79
+ coverage.any? { |value| value && value.nonzero? }
63
80
  end
64
81
  end
65
82
 
@@ -93,21 +110,8 @@ module Coverband
93
110
  new_results.dup
94
111
  end
95
112
 
96
- # TODO this seems like a dumb conversion for the already good coverage format
97
- # coverage is 0 based other implementation matches line number
98
113
  def add_file(file, line_counts)
99
- @file_line_usage[file] = Hash.new(0) unless @file_line_usage.include?(file)
100
- line_counts.each_with_index do |line_count, index|
101
- @file_line_usage[file][(index + 1)] = line_count if line_count
102
- end
103
- end
104
-
105
- def file_usage
106
- hash = {}
107
- @file_line_usage.each do |file, lines|
108
- hash[file] = lines.values.compact.inject(0, :+)
109
- end
110
- hash.sort_by { |_key, value| value }
114
+ @file_line_usage[file] = line_counts
111
115
  end
112
116
 
113
117
  def initialize
@@ -3,9 +3,9 @@
3
3
  module Coverband
4
4
  class Configuration
5
5
  attr_accessor :redis, :root_paths, :root,
6
- :ignore, :additional_files, :percentage, :verbose,
7
- :reporter, :startup_delay, :memory_caching,
8
- :include_gems, :collector, :disable_on_failure_for,
6
+ :ignore, :additional_files, :verbose,
7
+ :reporter, :reporting_frequency,
8
+ :disable_on_failure_for,
9
9
  :redis_namespace, :redis_ttl,
10
10
  :safe_reload_files
11
11
 
@@ -13,24 +13,16 @@ module Coverband
13
13
 
14
14
  def initialize
15
15
  @root = Dir.pwd
16
- @redis = nil
17
16
  @root_paths = []
18
17
  @ignore = []
19
18
  @additional_files = []
20
- @include_gems = false
21
- @percentage = 0.0
19
+ @reporting_frequency = 0.0
22
20
  @verbose = false
23
21
  @reporter = 'scov'
24
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0')
25
- @collector = 'trace'
26
- else
27
- @collector = 'coverage'
28
- end
29
22
  @logger = Logger.new(STDOUT)
30
- @startup_delay = 0
31
- @memory_caching = false
32
23
  @store = nil
33
- @disable_on_failure_for = nil
24
+
25
+ # TODO: should we push these to adapter configs
34
26
  @s3_region = nil
35
27
  @s3_bucket = nil
36
28
  @s3_access_key_id = nil
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ class Background
5
+ # TODO: stub to implement background thread recording
6
+ end
7
+ end
@@ -7,11 +7,9 @@ module Coverband
7
7
  end
8
8
 
9
9
  def call(env)
10
- Coverband::Collectors::Base.instance.configure_sampling
11
- Coverband::Collectors::Base.instance.record_coverage
12
10
  @app.call(env)
13
11
  ensure
14
- Coverband::Collectors::Base.instance.report_coverage
12
+ Coverband::Collectors::Coverage.instance.report_coverage
15
13
  end
16
14
  end
17
15
  end
@@ -2,25 +2,26 @@
2
2
 
3
3
  module Coverband
4
4
  module Reporters
5
+ ###
6
+ # This is the base clase for report generation
7
+ # it helps with filtering, normalization, etc for final reprort generation
8
+ ###
5
9
  class Base
6
- def self.report(store, options = {})
7
- roots = get_roots
8
- additional_coverage_data = options.fetch(:additional_scov_data) { [] }
10
+ def self.report(store, _options = {})
11
+ scov_style_report = get_current_scov_data_imp(store, root_paths)
9
12
 
10
13
  if Coverband.configuration.verbose
11
- Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}"
12
- Coverband.configuration.logger.debug "additional data:\n #{additional_coverage_data}"
13
- end
14
-
15
- scov_style_report = report_scov_with_additional_data(store, additional_coverage_data, roots)
16
-
17
- if Coverband.configuration.verbose
18
- Coverband.configuration.logger.debug "report:\n #{scov_style_report.inspect}"
14
+ msg = "report:\n #{scov_style_report.inspect}"
15
+ Coverband.configuration.logger.debug msg
19
16
  end
20
17
  scov_style_report
21
18
  end
22
19
 
23
- def self.get_roots
20
+ # protected
21
+ # below are all not public API
22
+ # but as they are class methods protected doesn't really do anything
23
+
24
+ def self.root_paths
24
25
  roots = Coverband.configuration.root_paths
25
26
  roots << "#{current_root}/"
26
27
  roots
@@ -30,19 +31,26 @@ module Coverband
30
31
  File.expand_path(Coverband.configuration.root)
31
32
  end
32
33
 
33
- protected
34
-
35
34
  def self.fix_file_names(report_hash, roots)
36
- fixed_report = {} # normalize names across servers
37
- report_hash.each_pair do |key, values|
35
+ if Coverband.configuration.verbose
36
+ Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}"
37
+ end
38
+
39
+ fixed_report = {}
40
+ # normalize names across servers
41
+ report_hash.each_pair do |key, vals|
38
42
  filename = filename_from_key(key, roots)
39
- fixed_report[filename] = values
43
+ if fixed_report.key?(filename)
44
+ fixed_report[filename] = merge_arrays(fixed_report[filename], vals)
45
+ else
46
+ fixed_report[filename] = vals
47
+ end
40
48
  end
41
49
  fixed_report
42
50
  end
43
51
 
44
- # > merge_arrays([0,0,1,0,1],[nil,0,1,0,0])
45
- # [0,0,1,0,1]
52
+ # > merge_arrays([nil,0,0,1,0,1],[nil,nil,0,1,0,0])
53
+ # > [nil,0,0,1,0,1]
46
54
  def self.merge_arrays(first, second)
47
55
  merged = []
48
56
  longest = first.length > second.length ? first : second
@@ -56,20 +64,6 @@ module Coverband
56
64
  merged
57
65
  end
58
66
 
59
- # > merge_existing_coverage({"file.rb" => [0,1,2,nil,nil,nil]}, {"file.rb" => [0,1,2,nil,0,1,2]})
60
- # expects = {"file.rb" => [0,2,4,nil,0,1,2]}
61
- def self.merge_existing_coverage(scov_style_report, existing_coverage)
62
- existing_coverage.each_pair do |file_key, existing_lines|
63
- next if Coverband.configuration.ignore.any? { |i| file_key.match(i) }
64
- scov_style_report[file_key] = if current_line_hits = scov_style_report[file_key]
65
- merge_arrays(current_line_hits, existing_lines)
66
- else
67
- existing_lines
68
- end
69
- end
70
- scov_style_report
71
- end
72
-
73
67
  def self.filename_from_key(key, roots)
74
68
  filename = key
75
69
  roots.each do |root|
@@ -82,64 +76,25 @@ module Coverband
82
76
  filename
83
77
  end
84
78
 
85
- # > line_hash(store, 'hearno/script/tester.rb', ['/app/', '/Users/danmayer/projects/hearno/'])
86
- # {"/Users/danmayer/projects/hearno/script/tester.rb"=>[1, nil, 1, 2, nil, nil, nil]}
87
- def self.line_hash(store, key, roots)
88
- filename = filename_from_key(key, roots)
89
- if File.exist?(filename)
90
- count = File.foreach(filename).inject(0) { |c, _line| c + 1 }
91
- line_array = Array.new(count, nil)
92
-
93
- lines_hit = store.covered_lines_for_file(key)
94
- if lines_hit.is_a?(Array)
95
- line_array.each_with_index { |_, index| line_array[index] = 1 if lines_hit.include?((index + 1)) }
96
- else
97
- line_array.each_with_index { |_, index| line_array[index] = (line_array[index].to_i + lines_hit[(index + 1).to_s].to_i) if lines_hit.keys.include?((index + 1).to_s) }
98
- end
99
- { filename => line_array }
100
- else
101
- Coverband.configuration.logger.info "file #{filename} not found in project"
102
- nil
103
- end
104
- end
105
-
79
+ ###
80
+ # why do we need to merge covered files data?
81
+ # basically because paths on machines or deployed hosts could be different, so
82
+ # two different keys could point to the same filename or `line_key`
83
+ # this logic should be pushed to base report
84
+ # TODO: think we are filtering based on ignore while sending to the store
85
+ # and as we pull it out here
86
+ ###
106
87
  def self.get_current_scov_data_imp(store, roots)
107
88
  scov_style_report = {}
108
-
109
- ###
110
- # why do we need to merge covered files data?
111
- # basically because paths on machines or deployed hosts could be different, so
112
- # two different keys could point to the same filename or `line_key`
113
- # this logic should be pushed to base report
114
- ###
115
- store.covered_files.each do |key|
89
+ store.coverage.each_pair do |key, line_data|
116
90
  next if Coverband.configuration.ignore.any? { |i| key.match(i) }
117
- line_data = line_hash(store, key, roots)
118
-
119
91
  next unless line_data
120
- line_key = line_data.keys.first
121
- previous_line_hash = scov_style_report[line_key]
122
-
123
- if previous_line_hash
124
- line_data[line_key] = merge_arrays(line_data[line_key], previous_line_hash)
125
- end
126
-
127
- scov_style_report.merge!(line_data)
92
+ scov_style_report[key] = line_data
128
93
  end
129
94
 
130
95
  scov_style_report = fix_file_names(scov_style_report, roots)
131
96
  scov_style_report
132
97
  end
133
-
134
- def self.report_scov_with_additional_data(store, additional_scov_data, roots)
135
- scov_style_report = get_current_scov_data_imp(store, roots)
136
-
137
- additional_scov_data.each do |data|
138
- scov_style_report = merge_existing_coverage(scov_style_report, data)
139
- end
140
-
141
- scov_style_report
142
- end
143
98
  end
144
99
  end
145
100
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Coverband
4
4
  module Reporters
5
+ ###
6
+ # Console Report allows for simple reporting via the command line.
7
+ ###
5
8
  class ConsoleReport < Base
6
9
  def self.report(store, options = {})
7
10
  scov_style_report = super(store, options)
@@ -7,12 +7,12 @@ module Coverband
7
7
  begin
8
8
  require 'simplecov'
9
9
  rescue StandardError
10
- Coverband.configuration.logger.error 'coverband requires simplecov in order to generate a report, when configured for the scov report style.'
10
+ msg = 'coverband requires simplecov to generate a report, when config.reporter=scov'
11
+ Coverband.configuration.logger.error msg
11
12
  return
12
13
  end
13
14
 
14
15
  scov_style_report = super(store, options)
15
-
16
16
  open_report = options.fetch(:open_report) { true }
17
17
 
18
18
  # set root to show files if user has simplecov profiles
@@ -43,7 +43,8 @@ module Coverband
43
43
  access_key_id: Coverband.configuration.s3_access_key_id,
44
44
  secret_access_key: Coverband.configuration.s3_secret_access_key
45
45
  }
46
- S3ReportWriter.new(Coverband.configuration.s3_bucket, s3_writer_options).persist! if Coverband.configuration.s3_bucket
46
+ Coverband::Utils::S3ReportWriter.new(Coverband.configuration.s3_bucket,
47
+ s3_writer_options).persist! if Coverband.configuration.s3_bucket
47
48
  end
48
49
  end
49
50
  end
@@ -9,10 +9,10 @@ module Coverband
9
9
  attr_reader :request
10
10
 
11
11
  def initialize
12
+ full_path = Gem::Specification.find_by_name('simplecov-html').full_gem_path
12
13
  @static = Rack::Static.new(self,
13
- root: File.expand_path('public', Gem::Specification.find_by_name('simplecov-html').full_gem_path),
14
- urls: [/.*\.css/, /.*\.js/, /.*\.gif/, /.*\.png/]
15
- )
14
+ root: File.expand_path('public', full_path),
15
+ urls: [/.*\.css/, /.*\.js/, /.*\.gif/, /.*\.png/])
16
16
  end
17
17
 
18
18
  def call(env)
@@ -20,60 +20,63 @@ module Coverband
20
20
 
21
21
  if request.post?
22
22
  case request.path_info
23
- when /\/collect_update_and_view/
23
+ when %r{\/collect_update_and_view}
24
24
  collect_update_and_view
25
- when /\/clear/
25
+ when %r{\/clear}
26
26
  clear
27
- when /\/update_report/
27
+ when %r{\/update_report}
28
28
  update_report
29
- when /\/collect_coverage/
29
+ when %r{\/collect_coverage}
30
30
  collect_coverage
31
- when /\/reload_files/
31
+ when %r{\/reload_files}
32
32
  reload_files
33
33
  else
34
- [404, {'Content-Type' => 'text/html'}, ['404 error!']]
34
+ [404, { 'Content-Type' => 'text/html' }, ['404 error!']]
35
35
  end
36
36
  else
37
37
  case request.path_info
38
38
  when /.*\.(css|js|gif|png)/
39
39
  @static.call(env)
40
- when /\/show/
41
- [200, {'Content-Type' => 'text/html'}, [show]]
42
- when /\//
43
- [200, {'Content-Type' => 'text/html'}, [index]]
40
+ when %r{\/show}
41
+ [200, { 'Content-Type' => 'text/html' }, [show]]
42
+ when %r{\/}
43
+ [200, { 'Content-Type' => 'text/html' }, [index]]
44
44
  else
45
- [404, {'Content-Type' => 'text/html'}, ['404 error!']]
45
+ [404, { 'Content-Type' => 'text/html' }, ['404 error!']]
46
46
  end
47
47
  end
48
48
  end
49
49
 
50
- # todo move to file or template
50
+ # TODO: move to file or template
51
51
  def index
52
- html = "<html>"
53
- html += "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>" if request.params['notice']
54
- html += "<ul>"
55
- html += "<li><a href='#{base_path}'>Coverband Web Admin Index</a></li>"
56
- html += "<li>#{button("#{base_path}collect_update_and_view",'collect data, update report, & view')}</li>"
57
- html += "<li><a href='#{base_path}show'>view coverage report</a></li>"
58
- html += "<li>#{button("#{base_path}collect_coverage",'update coverage data (collect coverage)')}</li>"
59
- html += "<li>#{button("#{base_path}update_report",'update coverage report (rebuild report)')}</li>"
60
- html += "<li>#{button("#{base_path}clear",'clear coverage report')}</li>"
61
- html += "<li>#{button("#{base_path}reload_files",'reload Coverband files')}</li>"
62
- html += "</ul>"
63
- html += "<br/>"
64
- html += "version: #{Coverband::VERSION}<br/>"
65
- html += "<a href='https://github.com/danmayer/coverband'>Coverband</a>"
66
- html += "</html>"
67
- html
52
+ notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>"
53
+ notice = request.params['notice'] ? notice : ''
54
+ %(
55
+ <html>
56
+ #{notice}
57
+ <ul>
58
+ <li><a href='#{base_path}'>Coverband Web Admin Index</a></li>
59
+ <li>#{button("#{base_path}collect_update_and_view", 'collect data, update report, & view')}</li>
60
+ <li><a href='#{base_path}show'>view coverage report</a></li>
61
+ <li>#{button("#{base_path}collect_coverage", 'update coverage data (collect coverage)')}</li>
62
+ <li>#{button("#{base_path}update_report", 'update coverage report (rebuild report)')}</li>
63
+ <li>#{button("#{base_path}clear", 'clear coverage report')}</li>
64
+ <li>#{button("#{base_path}reload_files", 'reload Coverband files')}</li>
65
+ </ul>
66
+ <br/>
67
+ version: #{Coverband::VERSION}<br/>
68
+ <a href='https://github.com/danmayer/coverband'>Coverband</a>
69
+ </html>
70
+ )
68
71
  end
69
72
 
70
73
  def show
71
74
  html = s3.get_object(bucket: Coverband.configuration.s3_bucket, key: 'coverband/index.html').body.read
72
- # hack the static HTML assets to link to the path where this was mounted
75
+ # HACK: the static HTML assets to link to the path where this was mounted
73
76
  html = html.gsub("src='", "src='#{base_path}")
74
77
  html = html.gsub("href='", "href='#{base_path}")
75
- html = html.gsub("loading.gif", "#{base_path}loading.gif")
76
- html = html.gsub("/images/", "#{base_path}images/")
78
+ html = html.gsub('loading.gif', "#{base_path}loading.gif")
79
+ html = html.gsub('/images/', "#{base_path}images/")
77
80
  html
78
81
  end
79
82
 
@@ -90,7 +93,7 @@ module Coverband
90
93
  end
91
94
 
92
95
  def collect_coverage
93
- Coverband::Collectors::Base.instance.report_coverage
96
+ Coverband::Collectors::Coverage.instance.report_coverage
94
97
  notice = 'coverband coverage collected'
95
98
  [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
96
99
  end