coverband 5.2.5 → 5.2.6.rc.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "singleton"
5
+
6
+ module Coverband
7
+ module Collectors
8
+ module I18n
9
+ module KeyRegistry
10
+ def lookup(locale, key, scope = [], options = {})
11
+ separator = options[:separator] || ::I18n.default_separator
12
+ flat_key = ::I18n.normalize_keys(locale, key, scope, separator).join(separator)
13
+ Coverband.configuration.translations_tracker.track_key(flat_key)
14
+
15
+ super
16
+ end
17
+ end
18
+ end
19
+
20
+ ###
21
+ # This class tracks translation usage via I18n::Backend
22
+ ###
23
+ class TranslationTracker
24
+ attr_accessor :target
25
+ attr_reader :logger, :store, :ignore_patterns
26
+
27
+ def initialize(options = {})
28
+ raise NotImplementedError, "#{self.class.name} requires Rails 4 or greater" unless self.class.supported_version?
29
+ raise "Coverband: #{self.class.name} initialized before configuration!" if !Coverband.configured? && ENV["COVERBAND_TEST"] == "test"
30
+
31
+ @ignore_patterns = Coverband.configuration.ignore
32
+ @store = options.fetch(:store) { Coverband.configuration.store }
33
+ @logger = options.fetch(:logger) { Coverband.configuration.logger }
34
+ @target = options.fetch(:target) do
35
+ if defined?(Rails.application)
36
+ # I18n.eager_load!
37
+ # I18n.backend.send(:translations)
38
+ app_translation_keys = []
39
+ app_translation_files = ::I18n.load_path.select { |f| f.match(/config\/locales/) }
40
+ app_translation_files.each do |file|
41
+ app_translation_keys += flatten_hash(YAML.load_file(file)).keys
42
+ end
43
+ app_translation_keys.uniq
44
+ else
45
+ []
46
+ end
47
+ end
48
+
49
+ @one_time_timestamp = false
50
+
51
+ @logged_keys = Set.new
52
+ @keys_to_record = Set.new
53
+ end
54
+
55
+ def logged_keys
56
+ @logged_keys.to_a
57
+ end
58
+
59
+ def keys_to_record
60
+ @keys_to_record.to_a
61
+ end
62
+
63
+ ###
64
+ # This method is called on every translation usage
65
+ ###
66
+ def track_key(key)
67
+ if key
68
+ if newly_seen_key?(key)
69
+ @logged_keys << key
70
+ @keys_to_record << key if track_key?(key)
71
+ end
72
+ end
73
+ end
74
+
75
+ def used_keys
76
+ redis_store.hgetall(tracker_key)
77
+ end
78
+
79
+ def all_keys
80
+ target.uniq
81
+ end
82
+
83
+ def unused_keys(used_keys = nil)
84
+ recently_used_keys = (used_keys || self.used_keys).keys
85
+ all_keys.reject { |k| recently_used_keys.include?(k.to_s) }
86
+ end
87
+
88
+ def as_json
89
+ used_keys = self.used_keys
90
+ {
91
+ unused_keys: unused_keys(used_keys),
92
+ used_keys: used_keys
93
+ }.to_json
94
+ end
95
+
96
+ def tracking_since
97
+ if (tracking_time = redis_store.get(tracker_time_key))
98
+ Time.at(tracking_time.to_i).iso8601
99
+ else
100
+ "N/A"
101
+ end
102
+ end
103
+
104
+ def reset_recordings
105
+ redis_store.del(tracker_key)
106
+ redis_store.del(tracker_time_key)
107
+ end
108
+
109
+ def clear_key!(key)
110
+ return unless key
111
+
112
+ redis_store.hdel(tracker_key, key)
113
+ @logged_keys.delete(key)
114
+ end
115
+
116
+ def save_report
117
+ redis_store.set(tracker_time_key, Time.now.to_i) unless @one_time_timestamp || tracker_time_key_exists?
118
+ @one_time_timestamp = true
119
+ reported_time = Time.now.to_i
120
+ @keys_to_record.to_a.each do |key|
121
+ redis_store.hset(tracker_key, key.to_s, reported_time)
122
+ end
123
+ @keys_to_record.clear
124
+ rescue => e
125
+ # we don't want to raise errors if Coverband can't reach redis.
126
+ # This is a nice to have not a bring the system down
127
+ logger&.error "Coverband: #{self.class.name} failed to store, error #{e.class.name} info #{e.message}"
128
+ end
129
+
130
+ def self.supported_version?
131
+ defined?(Rails) && defined?(Rails::VERSION) && Rails::VERSION::STRING.split(".").first.to_i >= 4
132
+ end
133
+
134
+ protected
135
+
136
+ def newly_seen_key?(key)
137
+ !@logged_keys.include?(key)
138
+ end
139
+
140
+ def track_key?(key, options = {})
141
+ @ignore_patterns.none? { |pattern| key.to_s.include?(pattern) }
142
+ end
143
+
144
+ private
145
+
146
+ def flatten_hash(hash)
147
+ hash.each_with_object({}) do |(k, v), h|
148
+ if v.is_a? Hash
149
+ flatten_hash(v).map do |h_k, h_v|
150
+ h["#{k}.#{h_k}".to_sym] = h_v
151
+ end
152
+ else
153
+ h[k] = v
154
+ end
155
+ end
156
+ end
157
+
158
+ def redis_store
159
+ store.raw_store
160
+ end
161
+
162
+ def tracker_time_key_exists?
163
+ if defined?(redis_store.exists?)
164
+ redis_store.exists?(tracker_time_key)
165
+ else
166
+ redis_store.exists(tracker_time_key)
167
+ end
168
+ end
169
+
170
+ def tracker_key
171
+ "#{class_key}_tracker"
172
+ end
173
+
174
+ def tracker_time_key
175
+ "#{class_key}_tracker_time"
176
+ end
177
+
178
+ def class_key
179
+ @class_key ||= self.class.name.split("::").last
180
+ end
181
+ end
182
+ end
183
+ end
@@ -9,7 +9,8 @@ module Coverband
9
9
  :test_env, :web_enable_clear, :gem_details, :web_debug, :report_on_exit,
10
10
  :simulate_oneshot_lines_coverage,
11
11
  :view_tracker, :defer_eager_loading_data,
12
- :track_routes, :route_tracker
12
+ :track_routes, :route_tracker,
13
+ :track_translations, :translations_tracker
13
14
  attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
14
15
  :s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
15
16
  :service_test_mode, :process_type, :track_views, :redis_url,
@@ -70,6 +71,8 @@ module Coverband
70
71
  @view_tracker = nil
71
72
  @track_routes = false
72
73
  @route_tracker = nil
74
+ @track_translations = false
75
+ @translations_tracker = nil
73
76
  @web_debug = false
74
77
  @report_on_exit = true
75
78
  @use_oneshot_lines_coverage = ENV["ONESHOT"] || false
@@ -40,6 +40,7 @@ module Coverband
40
40
  Coverband.report_coverage
41
41
  Coverband.configuration.view_tracker&.report_views_tracked
42
42
  Coverband.configuration.route_tracker&.report_routes_tracked
43
+ Coverband.configuration.translations_tracker&.save_report
43
44
  if Coverband.configuration.verbose
44
45
  logger.debug("Coverband: background reporting coverage (#{Coverband.configuration.store.type}). Sleeping #{sleep_seconds}s")
45
46
  end
@@ -42,6 +42,10 @@ module Coverband
42
42
  clear_route_tracking_route
43
43
  when %r{\/clear_route_tracking}
44
44
  clear_route_tracking
45
+ when %r{\/clear_translation_tracking_key}
46
+ clear_route_translation_key
47
+ when %r{\/clear_translation_tracking}
48
+ clear_translation_tracking
45
49
  when %r{\/clear_view_tracking_file}
46
50
  clear_view_tracking_file
47
51
  when %r{\/clear_view_tracking}
@@ -65,6 +69,8 @@ module Coverband
65
69
  [200, {"Content-Type" => "text/html"}, [view_tracker]]
66
70
  when %r{\/route_tracker}
67
71
  [200, {"Content-Type" => "text/html"}, [route_tracker]]
72
+ when %r{\/translations_tracker}
73
+ [200, {"Content-Type" => "text/html"}, [translations_tracker]]
68
74
  when %r{\/enriched_debug_data}
69
75
  [200, {"Content-Type" => "text/json"}, [enriched_debug_data]]
70
76
  when %r{\/debug_data}
@@ -109,6 +115,14 @@ module Coverband
109
115
  base_path: base_path).format_route_tracker!
110
116
  end
111
117
 
118
+ def translations_tracker
119
+ notice = "<strong>Notice:</strong> #{Rack::Utils.escape_html(request.params["notice"])}<br/>"
120
+ notice = request.params["notice"] ? notice : ""
121
+ Coverband::Utils::HTMLFormatter.new(nil,
122
+ notice: notice,
123
+ base_path: base_path).format_translations_tracker!
124
+ end
125
+
112
126
  def view_tracker_data
113
127
  Coverband::Collectors::ViewTracker.new(store: Coverband.configuration.store).as_json
114
128
  end
@@ -200,6 +214,29 @@ module Coverband
200
214
  [302, {"Location" => "#{base_path}/route_tracker?notice=#{notice}"}, []]
201
215
  end
202
216
 
217
+ def clear_translation_tracking
218
+ if Coverband.configuration.web_enable_clear
219
+ tracker = Coverband::Collectors::TranslationTracker.new(store: Coverband.configuration.store)
220
+ tracker.reset_recordings
221
+ notice = "translation tracking reset"
222
+ else
223
+ notice = "web_enable_clear isn't enabled in your configuration"
224
+ end
225
+ [302, {"Location" => "#{base_path}/translations_tracker?notice=#{notice}"}, []]
226
+ end
227
+
228
+ def clear_translation_tracking_key
229
+ if Coverband.configuration.web_enable_clear
230
+ tracker = Coverband::Collectors::TranslationTracker.new(store: Coverband.configuration.store)
231
+ key = request.params["key"]
232
+ tracker.clear_key!(key)
233
+ notice = "coverage for route #{key} cleared"
234
+ else
235
+ notice = "web_enable_clear isn't enabled in your configuration"
236
+ end
237
+ [302, {"Location" => "#{base_path}/translations_tracker?notice=#{notice}"}, []]
238
+ end
239
+
203
240
  private
204
241
 
205
242
  # This method should get the root mounted endpoint
@@ -211,7 +248,7 @@ module Coverband
211
248
  # %r{\/.*\/}.match?(request.path) ? request.path.match("\/.*\/")[0] : "/"
212
249
  # ^^ the above is NOT valid Ruby 2.3/2.4 even though rubocop / standard think it is
213
250
  def base_path
214
- request.path =~ %r{\/.*\/} ? request.path.match("\/.*\/")[0] : "/"
251
+ request.path =~ %r{\/.*\/} ? request.path.match("/.*/")[0] : "/"
215
252
  end
216
253
  end
217
254
  end
@@ -16,8 +16,8 @@ module Coverband
16
16
  attr_reader :notice, :base_path
17
17
 
18
18
  def initialize(report, options = {})
19
- @notice = options.fetch(:notice) { nil }
20
- @base_path = options.fetch(:base_path) { "./" }
19
+ @notice = options.fetch(:notice, nil)
20
+ @base_path = options.fetch(:base_path, "./")
21
21
  @coverage_result = Coverband::Utils::Results.new(report) if report
22
22
  end
23
23
 
@@ -41,6 +41,10 @@ module Coverband
41
41
  format_route_tracker
42
42
  end
43
43
 
44
+ def format_translations_tracker!
45
+ format_translations_tracker
46
+ end
47
+
44
48
  def format_source_file!(filename)
45
49
  source_file = @coverage_result.file_from_path_with_type(filename)
46
50
 
@@ -65,6 +69,10 @@ module Coverband
65
69
  template("route_tracker").result(binding)
66
70
  end
67
71
 
72
+ def format_translations_tracker
73
+ template("translations_tracker").result(binding)
74
+ end
75
+
68
76
  def format(result)
69
77
  Dir[File.join(File.dirname(__FILE__), "../../../public/*")].each do |path|
70
78
  FileUtils.cp_r(path, asset_output_path)
@@ -109,7 +117,7 @@ module Coverband
109
117
  end
110
118
 
111
119
  def button(url, title, opts = {})
112
- delete = opts.fetch(:delete) { false }
120
+ delete = opts.fetch(:delete, false)
113
121
  button_css = delete ? "coveraband-button del" : "coveraband-button"
114
122
  button = "<form action='#{url}' class='coverband-admin-form' method='post'>"
115
123
  button += "<button class='#{button_css}' type='submit'>#{title}</button>"
@@ -48,6 +48,13 @@ module Coverband
48
48
  end
49
49
  end
50
50
 
51
+ if Coverband.configuration.track_translations
52
+ Coverband.configuration.translations_tracker = Coverband::Collectors::TranslationTracker.new
53
+
54
+ # plugin to i18n
55
+ I18n::Backend::Simple.send :include, Coverband::Collectors::I18n::KeyRegistry
56
+ end
57
+
51
58
  if Coverband.configuration.track_views
52
59
  COVERBAND_VIEW_TRACKER = if Coverband.coverband_service?
53
60
  Coverband::Collectors::ViewTrackerService.new
@@ -5,5 +5,5 @@
5
5
  # use format "4.2.1.rc.1" ~> 4.2.1.rc to prerelease versions like v4.2.1.rc.2 and v4.2.1.rc.3
6
6
  ###
7
7
  module Coverband
8
- VERSION = "5.2.5"
8
+ VERSION = "5.2.6.rc.1"
9
9
  end
data/lib/coverband.rb CHANGED
@@ -19,6 +19,7 @@ require "coverband/collectors/coverage"
19
19
  require "coverband/collectors/view_tracker"
20
20
  require "coverband/collectors/view_tracker_service"
21
21
  require "coverband/collectors/route_tracker"
22
+ require "coverband/collectors/translation_tracker"
22
23
  require "coverband/reporters/base"
23
24
  require "coverband/reporters/console_report"
24
25
  require "coverband/integrations/background"
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../../test_helper", File.dirname(__FILE__))
4
+ require "ostruct"
5
+
6
+ class TranslationTrackerTest < Minitest::Test
7
+ # NOTE: using struct vs open struct as open struct has a special keyword method that overshadows the method value on Ruby 2.x
8
+ Payload = Struct.new(:path, :method)
9
+
10
+ def tracker_key
11
+ Coverband::Collectors::TranslationTracker.expects(:supported_version?).at_least_once.returns(true)
12
+ Coverband::Collectors::TranslationTracker.new.send(:tracker_key)
13
+ end
14
+
15
+ def tracker_time_key
16
+ Coverband::Collectors::TranslationTracker.expects(:supported_version?).at_least_once.returns(true)
17
+ Coverband::Collectors::TranslationTracker.new.send(:tracker_time_key)
18
+ end
19
+
20
+ def setup
21
+ super
22
+ fake_store.raw_store.del(tracker_key)
23
+ end
24
+
25
+ test "init correctly" do
26
+ Coverband::Collectors::TranslationTracker.expects(:supported_version?).returns(true)
27
+ tracker = Coverband::Collectors::TranslationTracker.new(store: fake_store, roots: "dir")
28
+ assert_equal nil, tracker.target.first
29
+ assert !tracker.store.nil?
30
+ assert_equal [], tracker.target
31
+ assert_equal [], tracker.logged_keys
32
+ end
33
+
34
+ test "track standard translation keys" do
35
+ store = fake_store
36
+ translation_key = "en.views.pagination.truncate"
37
+ store.raw_store.expects(:hset).with(tracker_key, translation_key, anything)
38
+ tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir")
39
+
40
+ tracker.track_key(translation_key.to_sym)
41
+ tracker.save_report
42
+ assert_equal [translation_key.to_sym], tracker.logged_keys
43
+ end
44
+
45
+ test "report used_keys" do
46
+ store = fake_store
47
+ translation_key = "en.views.pagination.truncate"
48
+ tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir")
49
+ tracker.track_key(:"en.views.pagination.truncate")
50
+ tracker.save_report
51
+ assert_equal [translation_key], tracker.used_keys.keys
52
+ end
53
+
54
+ test "report unused_keys" do
55
+ store = fake_store
56
+ app_keys = [
57
+ "en.views.pagination.truncate",
58
+ "en.views.pagination.next"
59
+ ]
60
+ tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir", target: app_keys)
61
+ tracker.track_key(:"en.views.pagination.truncate")
62
+ tracker.save_report
63
+ assert_equal [app_keys.last], tracker.unused_keys
64
+ end
65
+
66
+ test "reset store" do
67
+ store = fake_store
68
+ store.raw_store.expects(:del).with(tracker_key)
69
+ store.raw_store.expects(:del).with(tracker_time_key)
70
+ tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir")
71
+ tracker.track_key(:"en.views.pagination.truncate")
72
+ tracker.reset_recordings
73
+ end
74
+
75
+ test "clear_key" do
76
+ store = fake_store
77
+ translation_key = "en.views.pagination.truncate"
78
+ tracker = Coverband::Collectors::TranslationTracker.new(store: store, roots: "dir")
79
+ tracker.track_key(translation_key.to_sym)
80
+ tracker.save_report
81
+ assert_equal [translation_key.to_s], tracker.used_keys.keys
82
+ tracker.clear_key!(translation_key.to_s)
83
+ assert_equal [], tracker.store.raw_store.hgetall(tracker_key).keys
84
+ end
85
+
86
+ protected
87
+
88
+ def fake_store
89
+ @fake_store ||= Coverband::Adapters::RedisStore.new(Coverband::Test.redis, redis_namespace: "coverband_test")
90
+ end
91
+ end
data/views/nav.erb CHANGED
@@ -33,9 +33,14 @@
33
33
  <a href='<%= base_path %>view_tracker'>Views Tracker</a>
34
34
  </li>
35
35
  <% end %>
36
- <% if Coverband.configuration.track_routes %>
36
+ <% if Coverband.configuration.track_routes %>
37
37
  <li class='<%= nav_options[:active_link] == 'route_tracker' ? 'active' : '' %>'>
38
38
  <a href='<%= base_path %>route_tracker'>Route Tracker</a>
39
39
  </li>
40
40
  <% end %>
41
+ <% if Coverband.configuration.track_translations %>
42
+ <li class='<%= nav_options[:active_link] == 'translations_tracker' ? 'active' : '' %>'>
43
+ <a href='<%= base_path %>translations_tracker'>Translations Tracker</a>
44
+ </li>
45
+ <% end %>
41
46
  </ul>
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns='http://www.w3.org/1999/xhtml'>
3
+ <head>
4
+ <title>Coverband Info: <%= Coverband::VERSION %></title>
5
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6
+ <script src='<%= assets_path('dependencies.js') %>' type='text/javascript'></script>
7
+ <script src='<%= assets_path('application.js') %>' type='text/javascript'></script>
8
+ <link href='<%= assets_path('application.css') %>' media='screen, projection, print' rel='stylesheet' type='text/css'>
9
+ <link rel="icon" type="image/png" href="<%= assets_path('favicon.png') %>" />
10
+ </head>
11
+
12
+ <body>
13
+ <div id="wrapper" style="">
14
+ <%= display_nav(active_link: 'translations_tracker') %>
15
+ <div id="content">
16
+ <% tracker = Coverband::Collectors::TranslationTracker.new(store: Coverband.configuration.store) %>
17
+ <h4>
18
+ <% if Coverband.configuration.web_enable_clear %>
19
+ <%= button("#{base_path}clear_translation_tracking", 'reset translation tracker', delete: true) %>
20
+ <% end %>
21
+ </h4>
22
+ <h2>Unused Translations: (<%= tracker.unused_keys.length %>)</h2>
23
+ <p>These Translations have not been rendered since recording started at <%= tracker.tracking_since %></p>
24
+ <ul>
25
+ <% tracker.unused_keys.each do |key| %>
26
+ <li class="unused-keys"><%= key %></li>
27
+ <% end %>
28
+ </ul>
29
+
30
+ <h2>Used Translations: (<%= tracker.used_keys.length %>)</h2>
31
+ <p>These Translations have been rendered at least once</p>
32
+ <ul>
33
+ <% tracker.used_keys.each_pair do |key, time_at| %>
34
+ <li class="used-keys">
35
+ <%= key %>
36
+ <span class="last_seen_at">last activity recorded <%= Time.at(time_at.to_i)%></span>
37
+ <% if Coverband.configuration.web_enable_clear %>
38
+ <%= button("#{base_path}clear_translations_tracking_key?key=#{key}", 'reset tracked key', delete: true) %>
39
+ <% end %>
40
+ </li>
41
+ <% end %>
42
+ </ul>
43
+ </div>
44
+ <div id="footer">
45
+ Generated by <a href="http://github.com/danmayer/coverband">Coverband</a> v<%= Coverband::VERSION %>
46
+ </div>
47
+ </div>
48
+ </body>
49
+ </html>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coverband
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.5
4
+ version: 5.2.6.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Mayer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-12-21 00:00:00.000000000 Z
12
+ date: 2022-12-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: benchmark-ips
@@ -302,6 +302,7 @@ files:
302
302
  - lib/coverband/collectors/coverage.rb
303
303
  - lib/coverband/collectors/delta.rb
304
304
  - lib/coverband/collectors/route_tracker.rb
305
+ - lib/coverband/collectors/translation_tracker.rb
305
306
  - lib/coverband/collectors/view_tracker.rb
306
307
  - lib/coverband/collectors/view_tracker_service.rb
307
308
  - lib/coverband/configuration.rb
@@ -381,6 +382,7 @@ files:
381
382
  - test/coverband/collectors/coverage_test.rb
382
383
  - test/coverband/collectors/delta_test.rb
383
384
  - test/coverband/collectors/route_tracker_test.rb
385
+ - test/coverband/collectors/translation_tracker_test.rb
384
386
  - test/coverband/collectors/view_tracker_test.rb
385
387
  - test/coverband/configuration_test.rb
386
388
  - test/coverband/coverband_test.rb
@@ -494,6 +496,7 @@ files:
494
496
  - views/settings.erb
495
497
  - views/source_file.erb
496
498
  - views/source_file_loader.erb
499
+ - views/translations_tracker.erb
497
500
  - views/view_tracker.erb
498
501
  homepage: https://github.com/danmayer/coverband
499
502
  licenses:
@@ -515,9 +518,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
515
518
  version: '2.3'
516
519
  required_rubygems_version: !ruby/object:Gem::Requirement
517
520
  requirements:
518
- - - ">="
521
+ - - ">"
519
522
  - !ruby/object:Gem::Version
520
- version: '0'
523
+ version: 1.3.1
521
524
  requirements: []
522
525
  rubygems_version: 3.2.32
523
526
  signing_key:
@@ -541,6 +544,7 @@ test_files:
541
544
  - test/coverband/collectors/coverage_test.rb
542
545
  - test/coverband/collectors/delta_test.rb
543
546
  - test/coverband/collectors/route_tracker_test.rb
547
+ - test/coverband/collectors/translation_tracker_test.rb
544
548
  - test/coverband/collectors/view_tracker_test.rb
545
549
  - test/coverband/configuration_test.rb
546
550
  - test/coverband/coverband_test.rb