coverband 4.2.0 → 4.2.4

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +29 -10
  3. data/.travis.yml +16 -8
  4. data/CODE_OF_CONDUCT.md +76 -0
  5. data/Gemfile +3 -1
  6. data/Gemfile.rails4 +1 -0
  7. data/README.md +108 -59
  8. data/Rakefile +19 -3
  9. data/changes.md +85 -10
  10. data/coverband.gemspec +3 -5
  11. data/lib/coverband/adapters/base.rb +64 -45
  12. data/lib/coverband/adapters/file_store.rb +15 -9
  13. data/lib/coverband/adapters/hash_redis_store.rb +190 -0
  14. data/lib/coverband/adapters/redis_store.rb +35 -11
  15. data/lib/coverband/at_exit.rb +2 -11
  16. data/lib/coverband/collectors/coverage.rb +35 -41
  17. data/lib/coverband/collectors/delta.rb +34 -28
  18. data/lib/coverband/collectors/view_tracker.rb +157 -0
  19. data/lib/coverband/configuration.rb +115 -33
  20. data/lib/coverband/integrations/background.rb +12 -4
  21. data/lib/coverband/integrations/{middleware.rb → background_middleware.rb} +2 -6
  22. data/lib/coverband/integrations/bundler.rb +8 -0
  23. data/lib/coverband/integrations/report_middleware.rb +15 -0
  24. data/lib/coverband/integrations/resque.rb +3 -5
  25. data/lib/coverband/reporters/base.rb +40 -16
  26. data/lib/coverband/reporters/console_report.rb +1 -0
  27. data/lib/coverband/reporters/html_report.rb +31 -27
  28. data/lib/coverband/reporters/web.rb +63 -18
  29. data/lib/coverband/utils/absolute_file_converter.rb +47 -0
  30. data/lib/coverband/utils/file_hasher.rb +16 -0
  31. data/lib/coverband/utils/file_list.rb +16 -5
  32. data/lib/coverband/utils/html_formatter.rb +45 -8
  33. data/lib/coverband/utils/lines_classifier.rb +5 -0
  34. data/lib/coverband/utils/railtie.rb +28 -10
  35. data/lib/coverband/utils/relative_file_converter.rb +41 -0
  36. data/lib/coverband/utils/result.rb +6 -39
  37. data/lib/coverband/utils/results.rb +51 -0
  38. data/lib/coverband/utils/s3_report.rb +2 -3
  39. data/lib/coverband/utils/source_file.rb +34 -8
  40. data/lib/coverband/utils/tasks.rb +10 -10
  41. data/lib/coverband/version.rb +5 -1
  42. data/lib/coverband.rb +49 -27
  43. data/lua/install.sh +15 -0
  44. data/lua/lib/persist-coverage.lua +28 -0
  45. data/lua/test/bootstrap.lua +5 -0
  46. data/lua/test/harness.lua +19 -0
  47. data/lua/test/redis-call.lua +55 -0
  48. data/lua/test/test-persist-coverage.lua +132 -0
  49. data/public/application.css +14 -6
  50. data/public/application.js +27 -0
  51. data/test/benchmarks/benchmark.rake +167 -73
  52. data/test/benchmarks/init_rails.rake +10 -0
  53. data/test/coverband/adapters/base_test.rb +73 -42
  54. data/test/coverband/adapters/file_store_test.rb +48 -37
  55. data/test/coverband/adapters/hash_redis_store_test.rb +195 -0
  56. data/test/coverband/adapters/redis_store_test.rb +102 -59
  57. data/test/coverband/at_exit_test.rb +0 -2
  58. data/test/coverband/collectors/coverage_test.rb +71 -13
  59. data/test/coverband/collectors/delta_test.rb +36 -6
  60. data/test/coverband/collectors/view_tracker_test.rb +113 -0
  61. data/test/coverband/configuration_test.rb +47 -7
  62. data/test/coverband/coverband_test.rb +14 -2
  63. data/test/coverband/integrations/background_middleware_test.rb +44 -0
  64. data/test/coverband/integrations/background_test.rb +11 -3
  65. data/test/coverband/integrations/report_middleware_test.rb +44 -0
  66. data/test/coverband/integrations/resque_worker_test.rb +5 -6
  67. data/test/coverband/integrations/test_resque_job.rb +3 -1
  68. data/test/coverband/reporters/base_test.rb +5 -81
  69. data/test/coverband/reporters/console_test.rb +3 -10
  70. data/test/coverband/reporters/html_test.rb +71 -21
  71. data/test/coverband/reporters/web_test.rb +0 -10
  72. data/test/coverband/utils/absolute_file_converter_test.rb +56 -0
  73. data/test/coverband/utils/file_groups_test.rb +11 -5
  74. data/test/coverband/utils/file_hasher_test.rb +29 -0
  75. data/test/coverband/utils/file_list_test.rb +5 -5
  76. data/test/coverband/utils/html_formatter_test.rb +41 -0
  77. data/test/coverband/utils/relative_file_converter_test.rb +39 -0
  78. data/test/coverband/utils/result_test.rb +6 -47
  79. data/test/coverband/utils/results_test.rb +54 -0
  80. data/test/coverband/utils/s3_report_test.rb +2 -0
  81. data/test/coverband/utils/source_file_test.rb +54 -0
  82. data/test/dog.rb.erb +12 -0
  83. data/test/forked/rails_full_stack_test.rb +106 -0
  84. data/test/forked/rails_rake_full_stack_test.rb +58 -0
  85. data/test/integration/full_stack_test.rb +15 -16
  86. data/test/rails4_dummy/Rakefile +6 -0
  87. data/test/rails4_dummy/config/application.rb +8 -9
  88. data/test/rails4_dummy/config/coverband.rb +3 -11
  89. data/test/rails4_dummy/config/coverband_missing_redis.rb +3 -0
  90. data/test/rails5_dummy/Rakefile +6 -0
  91. data/test/rails5_dummy/config/application.rb +6 -10
  92. data/test/rails5_dummy/config/coverband.rb +7 -3
  93. data/test/rails5_dummy/config/coverband_missing_redis.rb +15 -0
  94. data/test/rails_test_helper.rb +22 -5
  95. data/test/test_helper.rb +52 -11
  96. data/test/unique_files.rb +17 -9
  97. data/views/data.erb +1 -0
  98. data/views/file_list.erb +13 -12
  99. data/views/gem_list.erb +10 -1
  100. data/views/layout.erb +6 -18
  101. data/views/nav.erb +36 -0
  102. data/views/source_file.erb +20 -4
  103. data/views/source_file_loader.erb +1 -1
  104. data/views/view_tracker.erb +52 -0
  105. metadata +71 -12
  106. data/lib/coverband/utils/file_path_helper.rb +0 -57
  107. data/test/coverband/integrations/middleware_test.rb +0 -96
  108. data/test/integration/rails_full_stack_test.rb +0 -95
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coverband
4
- class Middleware
4
+ class BackgroundMiddleware
5
5
  def initialize(app)
6
6
  @app = app
7
7
  end
@@ -10,11 +10,7 @@ module Coverband
10
10
  @app.call(env)
11
11
  ensure
12
12
  AtExit.register
13
- if Coverband.configuration.background_reporting_enabled
14
- Background.start
15
- else
16
- Collectors::Coverage.instance.report_coverage
17
- end
13
+ Background.start if Coverband.configuration.background_reporting_enabled
18
14
  end
19
15
  end
20
16
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundlerEagerLoad
4
+ def require(*groups)
5
+ Coverband.eager_loading_coverage { super }
6
+ end
7
+ end
8
+ Bundler.singleton_class.prepend BundlerEagerLoad
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ class ReportMiddleware
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ @app.call(env)
11
+ ensure
12
+ Collectors::Coverage.instance.report_coverage
13
+ end
14
+ end
15
+ end
@@ -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
@@ -8,37 +8,61 @@ module Coverband
8
8
  ###
9
9
  class Base
10
10
  class << self
11
- include Coverband::Utils::FilePathHelper
11
+ DATA_KEY = 'data'
12
+
12
13
  def report(store, _options = {})
13
14
  all_roots = Coverband.configuration.all_root_paths
14
15
  scov_style_report = get_current_scov_data_imp(store, all_roots)
15
16
 
16
- if Coverband.configuration.verbose
17
- msg = "report:\n #{scov_style_report.inspect}"
18
- # Coverband.configuration.logger.debug msg
19
- end
17
+ # These are extremelhy verbose but useful during coverband development, not generally for users
18
+ # Only avaiable by uncommenting this mode is never released
19
+ # if Coverband.configuration.verbose
20
+ # # msg = "report:\n #{scov_style_report.inspect}"
21
+ # # Coverband.configuration.logger.debug msg
22
+ # end
20
23
  scov_style_report
21
24
  end
22
25
 
26
+ ###
27
+ # Add back files that exist in the project but have no Coverage
28
+ # This makes it easy to find and delete files with no references
29
+ ###
30
+ def fix_reports(reports)
31
+ # list all files, even if not tracked by Coverband (0% coverage)
32
+ tracked_glob = Coverband.configuration.tracked_search_paths
33
+ filtered_report_files = {}
34
+
35
+ reports.each_pair do |report_name, report_data|
36
+ filtered_report_files[report_name] = {}
37
+ report_files = Coverband::Utils::Result.add_not_loaded_files(report_data, tracked_glob)
38
+
39
+ # apply coverband filters
40
+ report_files.each_pair do |file, data|
41
+ next if Coverband.configuration.ignore.any? { |i| file.match(i) }
42
+
43
+ filtered_report_files[report_name][file] = data
44
+ end
45
+ end
46
+ filtered_report_files
47
+ end
48
+
23
49
  protected
24
50
 
25
51
  def fix_file_names(report_hash, roots)
26
- if Coverband.configuration.verbose
27
- Coverband.configuration.logger.info "fixing root: #{roots.join(', ')}"
28
- end
52
+ Coverband.configuration.logger.debug "fixing root: #{roots.join(', ')}" if Coverband.configuration.verbose
29
53
 
30
54
  # normalize names across servers
31
55
  report_hash.each_with_object({}) do |(name, report), fixed_report|
32
56
  fixed_report[name] = {}
33
57
  report.each_pair do |key, vals|
34
- filename = relative_path_to_full(key, roots)
35
- 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
58
+ filename = Coverband::Utils::AbsoluteFileConverter.convert(key)
59
+ fixed_report[name][filename] = if fixed_report[name].key?(filename) && fixed_report[name][filename][DATA_KEY] && vals[DATA_KEY]
60
+ merged_data = merge_arrays(fixed_report[name][filename][DATA_KEY], vals[DATA_KEY])
61
+ vals[DATA_KEY] = merged_data
62
+ vals
63
+ else
64
+ vals
65
+ end
42
66
  end
43
67
  end
44
68
  end
@@ -10,6 +10,7 @@ module Coverband
10
10
  scov_style_report = super(store, options)
11
11
 
12
12
  scov_style_report.each_pair do |file, usage|
13
+ # TODO: In Coverband 5 deprecate none hash format
13
14
  if usage.is_a?(Hash)
14
15
  Coverband.configuration.logger.info "#{file}: #{usage['data']}"
15
16
  else
@@ -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,44 @@ 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
 
35
+ def report_data
36
+ report_dynamic_data
37
+ end
38
+
44
39
  private
45
40
 
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 = {}
41
+ def static?
42
+ static
43
+ end
44
+
45
+ def report_static_site
46
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files).format_static_html!
47
+ if open_report
48
+ `open #{Coverband.configuration.root}/coverage/index.html`
49
+ else
50
+ Coverband.configuration.logger.info 'report is ready and viewable: open coverage/index.html'
51
+ end
50
52
 
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)
53
+ Coverband::Utils::S3Report.instance.persist! if Coverband.configuration.s3_bucket
54
+ end
54
55
 
55
- # apply coverband filters
56
- report_files.each_pair do |file, data|
57
- next if Coverband.configuration.ignore.any? { |i| file.match(i) }
56
+ def report_dynamic_html
57
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files,
58
+ base_path: base_path,
59
+ notice: notice).format_dynamic_html!
60
+ end
58
61
 
59
- filtered_report_files[report_name][file] = data
60
- end
61
- end
62
+ def report_dynamic_data
63
+ Coverband::Utils::HTMLFormatter.new(filtered_report_files,
64
+ base_path: base_path,
65
+ notice: notice).format_dynamic_data!
62
66
  end
63
67
  end
64
68
  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
@@ -17,19 +18,30 @@ module Coverband
17
18
  urls: [/.*\.css/, /.*\.js/, /.*\.gif/, /.*\.png/])
18
19
  end
19
20
 
21
+ def check_auth
22
+ return true unless Coverband.configuration.password
23
+
24
+ auth_header = request.get_header('HTTP_AUTHORIZATION')
25
+ return unless auth_header
26
+
27
+ Coverband.configuration.password == Base64.decode64(auth_header.split[1]).split(':')[1]
28
+ end
29
+
20
30
  def call(env)
21
31
  @request = Rack::Request.new(env)
22
32
 
33
+ return [401, { 'www-authenticate' => 'Basic realm=""' }, ['']] unless check_auth
34
+
23
35
  if request.post?
24
36
  case request.path_info
37
+ when %r{\/clear_view_tracking_file}
38
+ clear_view_tracking_file
39
+ when %r{\/clear_view_tracking}
40
+ clear_view_tracking
25
41
  when %r{\/clear_file}
26
42
  clear_file
27
43
  when %r{\/clear}
28
44
  clear
29
- when %r{\/collect_coverage}
30
- collect_coverage
31
- when %r{\/reload_files}
32
- reload_files
33
45
  else
34
46
  [404, { 'Content-Type' => 'text/html' }, ['404 error!']]
35
47
  end
@@ -39,6 +51,12 @@ module Coverband
39
51
  @static.call(env)
40
52
  when %r{\/settings}
41
53
  [200, { 'Content-Type' => 'text/html' }, [settings]]
54
+ when %r{\/view_tracker_data}
55
+ [200, { 'Content-Type' => 'text/json' }, [view_tracker_data]]
56
+ when %r{\/view_tracker}
57
+ [200, { 'Content-Type' => 'text/html' }, [view_tracker]]
58
+ when %r{\/enriched_debug_data}
59
+ [200, { 'Content-Type' => 'text/json' }, [enriched_debug_data]]
42
60
  when %r{\/debug_data}
43
61
  [200, { 'Content-Type' => 'text/json' }, [debug_data]]
44
62
  when %r{\/load_file_details}
@@ -55,7 +73,7 @@ module Coverband
55
73
  notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>"
56
74
  notice = request.params['notice'] ? notice : ''
57
75
  Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
58
- html: true,
76
+ static: false,
59
77
  base_path: base_path,
60
78
  notice: notice,
61
79
  open_report: false).report
@@ -65,10 +83,30 @@ module Coverband
65
83
  Coverband::Utils::HTMLFormatter.new(nil, base_path: base_path).format_settings!
66
84
  end
67
85
 
86
+ def view_tracker
87
+ notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params['notice'])}<br/>"
88
+ notice = request.params['notice'] ? notice : ''
89
+ Coverband::Utils::HTMLFormatter.new(nil,
90
+ notice: notice,
91
+ base_path: base_path).format_view_tracker!
92
+ end
93
+
94
+ def view_tracker_data
95
+ Coverband::Collectors::ViewTracker.new(store: Coverband.configuration.store).as_json
96
+ end
97
+
68
98
  def debug_data
69
99
  Coverband.configuration.store.get_coverage_report.to_json
70
100
  end
71
101
 
102
+ def enriched_debug_data
103
+ Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
104
+ static: false,
105
+ base_path: base_path,
106
+ notice: '',
107
+ open_report: false).report_data
108
+ end
109
+
72
110
  def load_file_details
73
111
  filename = request.params['filename']
74
112
  Coverband::Reporters::HTMLReport.new(Coverband.configuration.store,
@@ -77,12 +115,6 @@ module Coverband
77
115
  open_report: false).file_details
78
116
  end
79
117
 
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
118
  def clear
87
119
  if Coverband.configuration.web_enable_clear
88
120
  Coverband.configuration.store.clear!
@@ -104,14 +136,27 @@ module Coverband
104
136
  [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
105
137
  end
106
138
 
107
- def reload_files
108
- Coverband.configuration&.safe_reload_files&.each do |safe_file|
109
- load safe_file
139
+ def clear_view_tracking
140
+ if Coverband.configuration.web_enable_clear
141
+ tracker = Coverband::Collectors::ViewTracker.new(store: Coverband.configuration.store)
142
+ tracker.reset_recordings
143
+ notice = 'view tracking reset'
144
+ else
145
+ notice = 'web_enable_clear isnt enabled in your configuration'
110
146
  end
111
- # force reload
112
- Coverband.configure
113
- notice = 'coverband files reloaded'
114
- [301, { 'Location' => "#{base_path}?notice=#{notice}" }, []]
147
+ [301, { 'Location' => "#{base_path}/view_tracker?notice=#{notice}" }, []]
148
+ end
149
+
150
+ def clear_view_tracking_file
151
+ if Coverband.configuration.web_enable_clear
152
+ tracker = Coverband::Collectors::ViewTracker.new(store: Coverband.configuration.store)
153
+ filename = request.params['filename']
154
+ tracker.clear_file!(filename)
155
+ notice = "coverage for file #{filename} cleared"
156
+ else
157
+ notice = 'web_enable_clear isnt enabled in your configuration'
158
+ end
159
+ [301, { 'Location' => "#{base_path}/view_tracker?notice=#{notice}" }, []]
115
160
  end
116
161
 
117
162
  private
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Utils
5
+ class AbsoluteFileConverter
6
+ def initialize(roots)
7
+ @cache = {}
8
+ @roots = roots.map { |root| "#{File.expand_path(root)}/" }
9
+ end
10
+
11
+ def self.instance
12
+ @instance ||= new(Coverband.configuration.all_root_paths)
13
+ end
14
+
15
+ def self.reset
16
+ @instance = nil
17
+ end
18
+
19
+ def self.convert(relative_path)
20
+ instance.convert(relative_path)
21
+ end
22
+
23
+ def convert(relative_path)
24
+ @cache[relative_path] ||= begin
25
+ relative_filename = relative_path
26
+ local_filename = relative_filename
27
+ @roots.each do |root|
28
+ relative_filename = relative_filename.sub(/^#{root}/, './')
29
+ # once we have a relative path break out of the loop
30
+ break if relative_filename.start_with? './'
31
+ end
32
+ # the filename for our reports is expected to be a full path.
33
+ # roots.last should be roots << current_root}/
34
+ # a fully expanded path of config.root
35
+ # filename = filename.gsub('./', roots.last)
36
+ # above only works for app files
37
+ # we need to rethink some of this logic
38
+ # gems aren't at project root and can have multiple locations
39
+ local_root = @roots.find do |root|
40
+ File.exist?(relative_filename.gsub('./', root))
41
+ end
42
+ local_root ? relative_filename.gsub('./', local_root) : local_filename
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coverband
4
+ module Utils
5
+ class FileHasher
6
+ @cache = {}
7
+
8
+ def self.hash(file, path_converter: AbsoluteFileConverter.instance)
9
+ @cache[file] ||= begin
10
+ file = path_converter.convert(file)
11
+ Digest::MD5.file(file).hexdigest if File.exist?(file)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -21,22 +21,34 @@ 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
 
32
+ def format_dynamic_data!
33
+ format_data(@coverage_result)
34
+ end
35
+
32
36
  def format_settings!
33
37
  format_settings
34
38
  end
35
39
 
40
+ def format_view_tracker!
41
+ format_view_tracker
42
+ end
43
+
36
44
  def format_source_file!(filename)
37
45
  source_file = @coverage_result.file_from_path_with_type(filename)
38
-
39
- formatted_source_file(@coverage_result, source_file)
46
+
47
+ if source_file
48
+ formatted_source_file(@coverage_result, source_file)
49
+ else
50
+ 'File No Longer Available'
51
+ end
40
52
  end
41
53
 
42
54
  private
@@ -45,6 +57,10 @@ module Coverband
45
57
  template('settings').result(binding)
46
58
  end
47
59
 
60
+ def format_view_tracker
61
+ template('view_tracker').result(binding)
62
+ end
63
+
48
64
  def format(result)
49
65
  Dir[File.join(File.dirname(__FILE__), '../../../public/*')].each do |path|
50
66
  FileUtils.cp_r(path, asset_output_path)
@@ -59,6 +75,10 @@ module Coverband
59
75
  template('layout').result(binding)
60
76
  end
61
77
 
78
+ def format_data(result)
79
+ template('data').result(binding)
80
+ end
81
+
62
82
  # Returns the an erb instance for the template of given name
63
83
  def template(name)
64
84
  ERB.new(File.read(File.join(File.dirname(__FILE__), '../../../views/', "#{name}.erb")))
@@ -70,16 +90,25 @@ module Coverband
70
90
 
71
91
  def asset_output_path
72
92
  return @asset_output_path if defined?(@asset_output_path) && @asset_output_path
93
+
73
94
  @asset_output_path = File.join(output_path)
74
95
  FileUtils.mkdir_p(@asset_output_path)
75
96
  @asset_output_path
76
97
  end
77
98
 
99
+ def served_html?
100
+ !static_html?
101
+ end
102
+
103
+ def static_html?
104
+ base_path.nil?
105
+ end
106
+
78
107
  def assets_path(name)
79
- if base_path
80
- File.join(base_path, name)
81
- else
108
+ if static_html?
82
109
  File.join(name)
110
+ else
111
+ File.join(base_path, name)
83
112
  end
84
113
  end
85
114
 
@@ -91,6 +120,10 @@ module Coverband
91
120
  button + '</form>'
92
121
  end
93
122
 
123
+ def display_nav(nav_options = {})
124
+ template('nav').result(binding)
125
+ end
126
+
94
127
  # Returns the html for the given source_file
95
128
  def formatted_source_file(result, source_file)
96
129
  template('source_file').result(binding)
@@ -157,7 +190,11 @@ module Coverband
157
190
  end
158
191
 
159
192
  def timeago(time)
160
- "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
193
+ if time
194
+ "<abbr class=\"timeago\" title=\"#{time.iso8601}\">#{time.iso8601}</abbr>"
195
+ else
196
+ 'Not Available'
197
+ end
161
198
  end
162
199
 
163
200
  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
  #