coverband 4.2.0 → 4.2.1.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -9
  3. data/.travis.yml +9 -1
  4. data/Gemfile +2 -1
  5. data/Gemfile.rails4 +1 -0
  6. data/README.md +34 -13
  7. data/Rakefile +10 -2
  8. data/changes.md +25 -10
  9. data/coverband.gemspec +5 -1
  10. data/lib/coverband.rb +30 -18
  11. data/lib/coverband/adapters/base.rb +2 -7
  12. data/lib/coverband/adapters/file_store.rb +1 -1
  13. data/lib/coverband/at_exit.rb +2 -11
  14. data/lib/coverband/collectors/coverage.rb +29 -32
  15. data/lib/coverband/collectors/delta.rb +20 -24
  16. data/lib/coverband/configuration.rb +49 -19
  17. data/lib/coverband/integrations/background.rb +6 -4
  18. data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
  19. data/lib/coverband/integrations/bundler.rb +8 -0
  20. data/lib/coverband/integrations/report_middleware.rb +15 -0
  21. data/lib/coverband/integrations/resque.rb +3 -5
  22. data/lib/coverband/reporters/base.rb +39 -13
  23. data/lib/coverband/reporters/html_report.rb +21 -27
  24. data/lib/coverband/reporters/web.rb +2 -21
  25. data/lib/coverband/utils/file_list.rb +16 -5
  26. data/lib/coverband/utils/file_path_helper.rb +2 -0
  27. data/lib/coverband/utils/html_formatter.rb +25 -8
  28. data/lib/coverband/utils/lines_classifier.rb +5 -0
  29. data/lib/coverband/utils/railtie.rb +11 -12
  30. data/lib/coverband/utils/result.rb +5 -44
  31. data/lib/coverband/utils/results.rb +51 -0
  32. data/lib/coverband/utils/source_file.rb +29 -5
  33. data/lib/coverband/utils/tasks.rb +11 -2
  34. data/lib/coverband/version.rb +1 -1
  35. data/public/application.js +27 -0
  36. data/test/benchmarks/benchmark.rake +10 -20
  37. data/test/coverband/adapters/file_store_test.rb +5 -5
  38. data/test/coverband/adapters/redis_store_test.rb +8 -7
  39. data/test/coverband/at_exit_test.rb +0 -2
  40. data/test/coverband/collectors/coverage_test.rb +57 -9
  41. data/test/coverband/collectors/delta_test.rb +27 -6
  42. data/test/coverband/configuration_test.rb +47 -7
  43. data/test/coverband/coverband_test.rb +0 -1
  44. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  45. data/test/coverband/integrations/background_test.rb +1 -3
  46. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  47. data/test/coverband/integrations/resque_worker_test.rb +4 -3
  48. data/test/coverband/integrations/test_resque_job.rb +3 -1
  49. data/test/coverband/reporters/base_test.rb +4 -4
  50. data/test/coverband/reporters/console_test.rb +1 -2
  51. data/test/coverband/reporters/html_test.rb +79 -16
  52. data/test/coverband/reporters/web_test.rb +0 -10
  53. data/test/coverband/utils/file_groups_test.rb +1 -1
  54. data/test/coverband/utils/file_list_test.rb +5 -5
  55. data/test/coverband/utils/html_formatter_test.rb +45 -0
  56. data/test/coverband/utils/result_test.rb +27 -47
  57. data/test/coverband/utils/results_test.rb +54 -0
  58. data/test/coverband/utils/s3_report_test.rb +2 -0
  59. data/test/coverband/utils/source_file_test.rb +50 -0
  60. data/test/forked/rails_full_stack_test.rb +101 -0
  61. data/test/forked/rails_rake_full_stack_test.rb +32 -0
  62. data/test/integration/full_stack_test.rb +17 -15
  63. data/test/rails4_dummy/Rakefile +6 -0
  64. data/test/rails4_dummy/config/application.rb +8 -9
  65. data/test/rails4_dummy/config/coverband.rb +3 -3
  66. data/test/rails5_dummy/Rakefile +6 -0
  67. data/test/rails5_dummy/config/application.rb +6 -10
  68. data/test/rails5_dummy/config/coverband.rb +2 -2
  69. data/test/rails_test_helper.rb +23 -4
  70. data/test/test_helper.rb +26 -1
  71. data/test/unique_files.rb +6 -5
  72. data/views/file_list.erb +2 -2
  73. data/views/gem_list.erb +10 -1
  74. data/views/layout.erb +10 -3
  75. data/views/source_file.erb +13 -4
  76. data/views/source_file_loader.erb +1 -1
  77. metadata +79 -9
  78. data/test/coverband/integrations/middleware_test.rb +0 -96
  79. data/test/integration/rails_full_stack_test.rb +0 -95
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Resque.after_fork do |job|
3
+ Resque.after_fork do |_job|
4
4
  Coverband.start
5
5
  Coverband.runtime_coverage!
6
- # no reason to miss coverage on a first resque job
7
- Coverband::Collectors::Delta.set_default_results
8
6
  end
9
7
 
10
8
  Resque.before_first_fork do
11
9
  Coverband.eager_loading_coverage!
12
10
  Coverband.configuration.background_reporting_enabled = false
13
11
  Coverband::Background.stop
14
- Coverband::Collectors::Coverage.instance.report_coverage(true)
12
+ Coverband.report_coverage
15
13
  end
16
14
 
17
15
  module Coverband
@@ -19,7 +17,7 @@ module Coverband
19
17
  def perform
20
18
  super
21
19
  ensure
22
- Coverband.report_coverage(true)
20
+ Coverband.report_coverage
23
21
  end
24
22
  end
25
23
  end
@@ -13,19 +13,45 @@ module Coverband
13
13
  all_roots = Coverband.configuration.all_root_paths
14
14
  scov_style_report = get_current_scov_data_imp(store, all_roots)
15
15
 
16
- if Coverband.configuration.verbose
17
- msg = "report:\n #{scov_style_report.inspect}"
18
- # Coverband.configuration.logger.debug msg
19
- end
16
+ # These are extremelhy verbose but useful during coverband development, not generally for users
17
+ # if Coverband.configuration.verbose
18
+ # # msg = "report:\n #{scov_style_report.inspect}"
19
+ # # Coverband.configuration.logger.debug msg
20
+ # end
20
21
  scov_style_report
21
22
  end
22
23
 
24
+ ###
25
+ # Add back files that exist in the project but have no Coverage
26
+ # This makes it easy to find and delete files with no references
27
+ ###
28
+ def fix_reports(reports)
29
+ # list all files, even if not tracked by Coverband (0% coverage)
30
+ file_patterns = ["#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"]
31
+ if Coverband.configuration.track_gems
32
+ file_patterns.concat(Bundler.definition.specs.reject { |spec| spec.name == 'coverband' }.map(&:full_require_paths)
33
+ .flatten.map { |path| "#{path}/**/*.{rb}" })
34
+ end
35
+ filtered_report_files = {}
36
+
37
+ reports.each_pair do |report_name, report_data|
38
+ filtered_report_files[report_name] = {}
39
+ report_files = Coverband::Utils::Result.add_not_loaded_files(report_data, file_patterns)
40
+
41
+ # apply coverband filters
42
+ report_files.each_pair do |file, data|
43
+ next if Coverband.configuration.ignore.any? { |i| file.match(i) }
44
+
45
+ filtered_report_files[report_name][file] = data
46
+ end
47
+ end
48
+ filtered_report_files
49
+ end
50
+
23
51
  protected
24
52
 
25
53
  def fix_file_names(report_hash, roots)
26
- if Coverband.configuration.verbose
27
- Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}"
28
- end
54
+ Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}" if Coverband.configuration.verbose
29
55
 
30
56
  # normalize names across servers
31
57
  report_hash.each_with_object({}) do |(name, report), fixed_report|
@@ -33,12 +59,12 @@ module Coverband
33
59
  report.each_pair do |key, vals|
34
60
  filename = relative_path_to_full(key, roots)
35
61
  fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename]['data'] && vals['data']
36
- merged_data = merge_arrays(fixed_report[name][filename]['data'], vals['data'])
37
- vals['data'] = merged_data
38
- vals
39
- else
40
- vals
41
- end
62
+ merged_data = merge_arrays(fixed_report[name][filename]['data'], vals['data'])
63
+ vals['data'] = merged_data
64
+ vals
65
+ else
66
+ vals
67
+ end
42
68
  end
43
69
  end
44
70
  end
@@ -3,13 +3,13 @@
3
3
  module Coverband
4
4
  module Reporters
5
5
  class HTMLReport < Base
6
- attr_accessor :filtered_report_files, :open_report, :html, :notice,
6
+ attr_accessor :filtered_report_files, :open_report, :static, :notice,
7
7
  :base_path, :filename
8
8
 
9
9
  def initialize(store, options = {})
10
10
  coverband_reports = Coverband::Reporters::Base.report(store, options)
11
11
  self.open_report = options.fetch(:open_report) { true }
12
- self.html = options.fetch(:html) { false }
12
+ self.static = options.fetch(:static) { true }
13
13
  # TODO: refactor notice out to top level of web only
14
14
  self.notice = options.fetch(:notice) { nil }
15
15
  self.base_path = options.fetch(:base_path) { nil }
@@ -25,40 +25,34 @@ module Coverband
25
25
  end
26
26
 
27
27
  def report
28
- if html
29
- Coverband::Utils::HTMLFormatter.new(filtered_report_files,
30
- base_path: base_path,
31
- notice: notice).format_html!
28
+ if static?
29
+ report_static_site
32
30
  else
33
- Coverband::Utils::HTMLFormatter.new(filtered_report_files).format!
34
- if open_report
35
- `open #{Coverband.configuration.root}/coverage/index.html`
36
- else
37
- Coverband.configuration.logger.info 'report is ready and viewable: open coverage/index.html'
38
- end
39
-
40
- Coverband::Utils::S3Report.instance.persist! if Coverband.configuration.s3_bucket
31
+ report_dynamic_html
41
32
  end
42
33
  end
43
34
 
44
35
  private
45
36
 
46
- def self.fix_reports(reports)
47
- # list all files, even if not tracked by Coverband (0% coverage)
48
- tracked_glob = "#{Coverband.configuration.current_root}/{app,lib,config}/**/*.{rb}"
49
- filtered_report_files = {}
37
+ def static?
38
+ static
39
+ end
50
40
 
51
- reports.each_pair do |report_name, report_data|
52
- filtered_report_files[report_name] = {}
53
- report_files = Coverband::Utils::Result.add_not_loaded_files(report_data, tracked_glob)
41
+ def report_static_site
42
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files).format_static_html!
43
+ if open_report
44
+ `open #{Coverband.configuration.root}/coverage/index.html`
45
+ else
46
+ Coverband.configuration.logger.info 'report is ready and viewable: open coverage/index.html'
47
+ end
54
48
 
55
- # apply coverband filters
56
- report_files.each_pair do |file, data|
57
- next if Coverband.configuration.ignore.any? { |i| file.match(i) }
49
+ Coverband::Utils::S3Report.instance.persist! if Coverband.configuration.s3_bucket
50
+ end
58
51
 
59
- filtered_report_files[report_name][file] = data
60
- end
61
- end
52
+ def report_dynamic_html
53
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files,
54
+ base_path: base_path,
55
+ notice: notice).format_dynamic_html!
62
56
  end
63
57
  end
64
58
  end
@@ -3,6 +3,7 @@
3
3
  begin
4
4
  require 'rack'
5
5
  rescue LoadError
6
+ puts 'error loading Coverband web reporter as Rack is not available'
6
7
  end
7
8
 
8
9
  module Coverband
@@ -26,10 +27,6 @@ module Coverband
26
27
  clear_file
27
28
  when %r{\/clear}
28
29
  clear
29
- when %r{\/collect_coverage}
30
- collect_coverage
31
- when %r{\/reload_files}
32
- reload_files
33
30
  else
34
31
  [404, { 'Content-Type' => 'text/html' }, ['404 error!']]
35
32
  end
@@ -55,7 +52,7 @@ module Coverband
55
52
  notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>"
56
53
  notice = request.params['notice'] ? notice : ''
57
54
  Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
58
- html: true,
55
+ static: false,
59
56
  base_path: base_path,
60
57
  notice: notice,
61
58
  open_report: false).report
@@ -77,12 +74,6 @@ module Coverband
77
74
  open_report: false).file_details
78
75
  end
79
76
 
80
- def collect_coverage
81
- Coverband.report_coverage(true)
82
- notice = 'coverband coverage collected'
83
- [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
84
- end
85
-
86
77
  def clear
87
78
  if Coverband.configuration.web_enable_clear
88
79
  Coverband.configuration.store.clear!
@@ -104,16 +95,6 @@ module Coverband
104
95
  [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
105
96
  end
106
97
 
107
- def reload_files
108
- Coverband.configuration&.safe_reload_files&.each do |safe_file|
109
- load safe_file
110
- end
111
- # force reload
112
- Coverband.configure
113
- notice = 'coverband files reloaded'
114
- [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
115
- end
116
-
117
98
  private
118
99
 
119
100
  # This method should get the root mounted endpoint
@@ -13,24 +13,28 @@ module Coverband
13
13
  # Returns the count of lines that have coverage
14
14
  def covered_lines
15
15
  return 0.0 if empty?
16
+
16
17
  map { |f| f.covered_lines.count }.inject(:+)
17
18
  end
18
19
 
19
20
  # Returns the count of lines that have been missed
20
21
  def missed_lines
21
22
  return 0.0 if empty?
23
+
22
24
  map { |f| f.missed_lines.count }.inject(:+)
23
25
  end
24
26
 
25
27
  # Returns the count of lines that are not relevant for coverage
26
28
  def never_lines
27
29
  return 0.0 if empty?
30
+
28
31
  map { |f| f.never_lines.count }.inject(:+)
29
32
  end
30
33
 
31
34
  # Returns the count of skipped lines
32
35
  def skipped_lines
33
36
  return 0.0 if empty?
37
+
34
38
  map { |f| f.skipped_lines.count }.inject(:+)
35
39
  end
36
40
 
@@ -40,11 +44,6 @@ module Coverband
40
44
  map(&:covered_percent)
41
45
  end
42
46
 
43
- # Finds the least covered file and returns that file's name
44
- def least_covered_file
45
- sort_by(&:covered_percent).first.filename
46
- end
47
-
48
47
  # Returns the overall amount of relevant lines of code across all files in this list
49
48
  def lines_of_code
50
49
  covered_lines + missed_lines
@@ -54,15 +53,27 @@ module Coverband
54
53
  # @return [Float]
55
54
  def covered_percent
56
55
  return 100.0 if empty? || lines_of_code.zero?
56
+
57
57
  Float(covered_lines * 100.0 / lines_of_code)
58
58
  end
59
59
 
60
+ # Computes the coverage based upon lines covered and lines missed, formatted
61
+ # @return [Float]
62
+ def formatted_covered_percent
63
+ covered_percent.round(2)
64
+ end
65
+
60
66
  # Computes the strength (hits / line) based upon lines covered and lines missed
61
67
  # @return [Float]
62
68
  def covered_strength
63
69
  return 0.0 if empty? || lines_of_code.zero?
70
+
64
71
  Float(map { |f| f.covered_strength * f.lines_of_code }.inject(:+) / lines_of_code)
65
72
  end
73
+
74
+ def first_seen_at
75
+ map(&:first_updated_at).reject { |el| el.is_a?(String) }.min
76
+ end
66
77
  end
67
78
  end
68
79
  end
@@ -6,6 +6,8 @@
6
6
  module Coverband
7
7
  module Utils
8
8
  module FilePathHelper
9
+ module_function
10
+
9
11
  ###
10
12
  # Takes a full path and converts to a relative path
11
13
  ###
@@ -21,11 +21,11 @@ module Coverband
21
21
  @coverage_result = Coverband::Utils::Results.new(report) if report
22
22
  end
23
23
 
24
- def format!
24
+ def format_static_html!
25
25
  format(@coverage_result)
26
26
  end
27
27
 
28
- def format_html!
28
+ def format_dynamic_html!
29
29
  format_html(@coverage_result)
30
30
  end
31
31
 
@@ -35,8 +35,12 @@ module Coverband
35
35
 
36
36
  def format_source_file!(filename)
37
37
  source_file = @coverage_result.file_from_path_with_type(filename)
38
-
39
- formatted_source_file(@coverage_result, source_file)
38
+
39
+ if source_file
40
+ formatted_source_file(@coverage_result, source_file)
41
+ else
42
+ 'File No Longer Available'
43
+ end
40
44
  end
41
45
 
42
46
  private
@@ -70,16 +74,25 @@ module Coverband
70
74
 
71
75
  def asset_output_path
72
76
  return @asset_output_path if defined?(@asset_output_path) && @asset_output_path
77
+
73
78
  @asset_output_path = File.join(output_path)
74
79
  FileUtils.mkdir_p(@asset_output_path)
75
80
  @asset_output_path
76
81
  end
77
82
 
83
+ def served_html?
84
+ !static_html?
85
+ end
86
+
87
+ def static_html?
88
+ base_path.nil?
89
+ end
90
+
78
91
  def assets_path(name)
79
- if base_path
80
- File.join(base_path, name)
81
- else
92
+ if static_html?
82
93
  File.join(name)
94
+ else
95
+ File.join(base_path, name)
83
96
  end
84
97
  end
85
98
 
@@ -157,7 +170,11 @@ module Coverband
157
170
  end
158
171
 
159
172
  def timeago(time)
160
- "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
173
+ if time
174
+ "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
175
+ else
176
+ 'Not Available'
177
+ end
161
178
  end
162
179
 
163
180
  def shortened_filename(source_file)
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ####
4
+ #
5
+ # NOTE: with Ruby 2.6.0 and beyond we can replace this classifier with
6
+ # ::Coverage.line_stub
7
+ # https://ruby-doc.org/stdlib-2.6.1/libdoc/coverage/rdoc/Coverage.html#method-c-line_stub
8
+ #
4
9
  # Thanks for all the help SimpleCov https://github.com/colszowka/simplecov-html
5
10
  # initial version pulled into Coverband from Simplecov 12/04/2018
6
11
  #
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Coverband.eager_loading_coverage!
4
3
  module Coverband
5
- class Railtie < Rails::Railtie
6
- initializer 'coverband.configure' do |app|
7
- app.middleware.use Coverband::Middleware
8
- end
9
-
10
- config.after_initialize do
11
- Coverband.report_coverage(true)
12
- Coverband.configuration.logger&.debug('Coverband: reported after_initialize')
4
+ module RailsEagerLoad
5
+ def eager_load!
6
+ Coverband.eager_loading_coverage!
7
+ super
8
+ ensure
9
+ Coverband.report_coverage
13
10
  Coverband.runtime_coverage!
14
11
  end
12
+ end
13
+ Rails::Engine.prepend(RailsEagerLoad)
15
14
 
16
- config.before_initialize do
17
- Coverband.configuration.logger&.debug('Coverband: set to eager_loading')
18
- Coverband.eager_loading_coverage!
15
+ class Railtie < Rails::Railtie
16
+ initializer 'coverband.configure' do |app|
17
+ app.middleware.use Coverband::BackgroundMiddleware
19
18
  end
20
19
 
21
20
  rake_tasks do
@@ -22,11 +22,8 @@ module Coverband
22
22
  alias source_files files
23
23
  # Explicitly set the Time this result has been created
24
24
  attr_writer :created_at
25
- # Explicitly set the command name that was used for this coverage result.
26
- # Defaults to Coverband.command_name
27
- attr_writer :command_name
28
25
 
29
- def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines
26
+ def_delegators :files, :covered_percent, :covered_percentages, :covered_strength, :covered_lines, :missed_lines
30
27
  def_delegator :files, :lines_of_code, :total_lines
31
28
 
32
29
  # Initialize a new Coverband::Result from given Coverage.result (a Hash of filenames each containing an array of
@@ -37,7 +34,6 @@ module Coverband
37
34
  @files = Coverband::Utils::FileList.new(@original_result.map do |filename, coverage|
38
35
  Coverband::Utils::SourceFile.new(filename, coverage) if File.file?(filename)
39
36
  end.compact.sort_by(&:short_name))
40
- filter!
41
37
  end
42
38
 
43
39
  # Returns all filenames for source files contained in this result
@@ -55,51 +51,16 @@ module Coverband
55
51
  @created_at ||= Time.now
56
52
  end
57
53
 
58
- # The command name that launched this result.
59
- # Delegated to Coverband.command_name if not set manually
60
- def command_name
61
- @command_name ||= 'Coverband'
62
- end
63
-
64
- # Returns a hash representation of this Result that can be used for marshalling it into JSON
65
- def to_hash
66
- { command_name => { 'coverage' => coverage, 'timestamp' => created_at.to_i } }
67
- end
68
-
69
- # Loads a Coverband::Result#to_hash dump
70
- def self.from_hash(hash)
71
- command_name, data = hash.first
72
- result = new(data['coverage'])
73
- result.command_name = command_name
74
- result.created_at = Time.at(data['timestamp'])
75
- result
76
- end
77
-
78
54
  # Finds files that were to be tracked but were not loaded and initializes
79
55
  # the line-by-line coverage to zero (if relevant) or nil (comments / whitespace etc).
80
- def self.add_not_loaded_files(result, tracked_files)
81
- if tracked_files
82
- result = result.dup
83
- Dir[tracked_files].each do |file|
56
+ def self.add_not_loaded_files(result, file_patterns)
57
+ file_patterns.each_with_object(result.dup) do |file_pattern, results_dup|
58
+ Dir[file_pattern].each do |file|
84
59
  absolute = File.expand_path(file)
85
60
 
86
- result[absolute] ||= Coverband::Utils::LinesClassifier.new.classify(File.foreach(absolute))
61
+ results_dup[absolute] ||= Coverband::Utils::LinesClassifier.new.classify(File.foreach(absolute))
87
62
  end
88
63
  end
89
-
90
- result
91
- end
92
-
93
- private
94
-
95
- def coverage
96
- keys = original_result.keys & filenames
97
- Hash[keys.zip(original_result.values_at(*keys))]
98
- end
99
-
100
- # Applies all configured Coverband filters on this result's source files
101
- def filter!
102
- @files = files
103
64
  end
104
65
  end
105
66
  end