coverband 2.0.3 → 3.0.0.alpha

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