bullet 6.1.1 → 6.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +66 -0
  3. data/CHANGELOG.md +20 -0
  4. data/README.md +10 -7
  5. data/lib/bullet.rb +68 -21
  6. data/lib/bullet/active_record41.rb +1 -0
  7. data/lib/bullet/active_record42.rb +1 -0
  8. data/lib/bullet/active_record52.rb +11 -0
  9. data/lib/bullet/active_record60.rb +11 -0
  10. data/lib/bullet/active_record61.rb +11 -0
  11. data/lib/bullet/bullet_xhr.js +1 -0
  12. data/lib/bullet/detector/base.rb +2 -1
  13. data/lib/bullet/detector/counter_cache.rb +1 -1
  14. data/lib/bullet/detector/n_plus_one_query.rb +3 -3
  15. data/lib/bullet/detector/unused_eager_loading.rb +1 -1
  16. data/lib/bullet/mongoid4x.rb +1 -1
  17. data/lib/bullet/mongoid5x.rb +1 -1
  18. data/lib/bullet/mongoid6x.rb +1 -1
  19. data/lib/bullet/mongoid7x.rb +1 -1
  20. data/lib/bullet/notification.rb +2 -1
  21. data/lib/bullet/rack.rb +5 -3
  22. data/lib/bullet/stack_trace_filter.rb +7 -9
  23. data/lib/bullet/version.rb +1 -1
  24. data/perf/benchmark.rb +4 -1
  25. data/spec/bullet/detector/unused_eager_loading_spec.rb +6 -2
  26. data/spec/bullet/ext/object_spec.rb +1 -1
  27. data/spec/bullet/rack_spec.rb +88 -17
  28. data/spec/bullet_spec.rb +39 -10
  29. data/spec/integration/active_record/association_spec.rb +53 -8
  30. data/spec/integration/counter_cache_spec.rb +3 -3
  31. data/spec/integration/mongoid/association_spec.rb +1 -1
  32. data/spec/models/attachment.rb +5 -0
  33. data/spec/models/deal.rb +5 -0
  34. data/spec/models/folder.rb +2 -1
  35. data/spec/models/group.rb +2 -1
  36. data/spec/models/page.rb +2 -1
  37. data/spec/models/post.rb +2 -0
  38. data/spec/models/submission.rb +1 -0
  39. data/spec/models/user.rb +1 -0
  40. data/spec/models/writer.rb +2 -1
  41. data/spec/spec_helper.rb +0 -2
  42. data/spec/support/mongo_seed.rb +1 -0
  43. data/spec/support/sqlite_seed.rb +20 -0
  44. data/test.sh +1 -0
  45. metadata +8 -4
  46. data/.travis.yml +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d02240600f13580ed16200edd5c39563bec1cb49a46ccbc82c57cff73a46e2f
4
- data.tar.gz: 0f40a4a1fe6157dabec88cb0ecc817168eda794aa1177c41ede1df351695d76a
3
+ metadata.gz: b21c2c4ca3caf6c41961a7182a5c8d37f6b89aa78b8dfb7badfb940f11a23191
4
+ data.tar.gz: 0f432034f9b4cb2fe6c6572481c797aefcfa69373839fbd0985a7efd2a0bbea9
5
5
  SHA512:
6
- metadata.gz: 1d3d3ce81767ce81091ddd7412dd8f341190ffac085477cde3ea04fa1ed696661f84be64d9c1fb6e6a0de276788e56f18aaada213df712cb51e6efb0a577ec37
7
- data.tar.gz: a3633af514a31e0ad6ff317a98616ed2c6c3ce070109288b8514c80277d4098359ed25a60bd5bcc4cd0ebf469f156751fa707f8d410f010bfcb8828e206e08a3
6
+ metadata.gz: d2457987f11f034fa457030cf7161d7508fb26230cf9e88a2571c54a934ad9a1330b48dd3d38eede3033bd7ef6c920e564dcbfa43a15354bf72d14ed799aefe2
7
+ data.tar.gz: ed6690fcb3bd778d6d5a27538a0ee78d298c749be1e445c305b9884448ca13f97e5a62c76ac48229a250041bb09ba609d6b154b12cf42a0f3dca66fbb9c83021
@@ -0,0 +1,66 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: CI
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test_rails_4:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ matrix:
21
+ gemfile: ['Gemfile.rails-4.0', 'Gemfile.rails-4.1', 'Gemfile.rails-4.2']
22
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
23
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
24
+ steps:
25
+ - uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: 2.3
30
+ bundler: 1
31
+ bundler-cache: true
32
+ - name: Run tests
33
+ run: bundle exec rake
34
+ test_rails_5:
35
+ runs-on: ubuntu-latest
36
+ strategy:
37
+ matrix:
38
+ gemfile: ['Gemfile.rails-5.0', 'Gemfile.rails-5.1', 'Gemfile.rails-5.2']
39
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
40
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
41
+ steps:
42
+ - uses: actions/checkout@v2
43
+ - name: Set up Ruby
44
+ uses: ruby/setup-ruby@v1
45
+ with:
46
+ ruby-version: 2.5
47
+ bundler: 1
48
+ bundler-cache: true
49
+ - name: Run tests
50
+ run: bundle exec rake
51
+ test_rails_6:
52
+ runs-on: ubuntu-latest
53
+ strategy:
54
+ matrix:
55
+ gemfile: ['Gemfile.rails-6.0', 'Gemfile.rails-6.1']
56
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
57
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
58
+ steps:
59
+ - uses: actions/checkout@v2
60
+ - name: Set up Ruby
61
+ uses: ruby/setup-ruby@v1
62
+ with:
63
+ ruby-version: 2.7
64
+ bundler-cache: true
65
+ - name: Run tests
66
+ run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## Next Release
2
2
 
3
+ ## 6.1.5 (08/16/2021)
4
+
5
+ * Rename whitelist to safelist
6
+ * Fix onload called twice
7
+ * Support Rack::Files::Iterator responses
8
+ * Ensure HABTM associations are not incorrectly labeled n+1
9
+
10
+ ## 6.1.4 (02/26/2021)
11
+
12
+ * Added an option to stop adding HTTP headers to API requests
13
+
14
+ ## 6.1.3 (01/21/2021)
15
+
16
+ * Consider ThroughAssociation at SingularAssociation like CollectionAssociation
17
+ * Add xhr_script only when add_footer is enabled
18
+
19
+ ## 6.1.2 (12/12/2020)
20
+
21
+ * Revert "Make whitelist thread safe"
22
+
3
23
  ## 6.1.1 (12/12/2020)
4
24
 
5
25
  * Add support Rails 6.1
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Bullet
2
2
 
3
+ ![Main workflow](https://github.com/flyerhzm/bullet/actions/workflows/main.yml/badge.svg)
3
4
  [![Gem Version](https://badge.fury.io/rb/bullet.svg)](http://badge.fury.io/rb/bullet)
4
- [![Build Status](https://secure.travis-ci.org/flyerhzm/bullet.svg)](http://travis-ci.org/flyerhzm/bullet)
5
5
  [![AwesomeCode Status for flyerhzm/bullet](https://awesomecode.io/projects/6755235b-e2c1-459e-bf92-b8b13d0c0472/status)](https://awesomecode.io/repos/flyerhzm/bullet)
6
6
  [![Coderwall Endorse](http://api.coderwall.com/flyerhzm/endorsecount.png)](http://coderwall.com/flyerhzm)
7
7
 
@@ -67,6 +67,7 @@ config.after_initialize do
67
67
  Bullet.rails_logger = true
68
68
  Bullet.honeybadger = true
69
69
  Bullet.bugsnag = true
70
+ Bullet.appsignal = true
70
71
  Bullet.airbrake = true
71
72
  Bullet.rollbar = true
72
73
  Bullet.add_footer = true
@@ -90,10 +91,12 @@ The code above will enable all of the Bullet notification systems:
90
91
  * `Bullet.honeybadger`: add notifications to Honeybadger
91
92
  * `Bullet.bugsnag`: add notifications to bugsnag
92
93
  * `Bullet.airbrake`: add notifications to airbrake
94
+ * `Bullet.appsignal`: add notifications to AppSignal
93
95
  * `Bullet.rollbar`: add notifications to rollbar
94
96
  * `Bullet.sentry`: add notifications to sentry
95
97
  * `Bullet.add_footer`: adds the details in the bottom left corner of the page. Double click the footer or use close button to hide footer.
96
- * `Bullet.skip_html_injection`: prevents Bullet from injecting XHR into the returned HTML. This must be false for receiving alerts or console logging.
98
+ * `Bullet.skip_html_injection`: prevents Bullet from injecting code into the returned HTML. This must be false for receiving alerts, showing the footer or console logging.
99
+ * `Bullet.skip_http_headers`: don't add headers to API requests, and remove the javascript that relies on them. Note that this prevents bullet from logging warnings to the browser console or updating the footer.
97
100
  * `Bullet.stacktrace_includes`: include paths with any of these substrings in the stack trace, even if they are not in your main app
98
101
  * `Bullet.stacktrace_excludes`: ignore paths with any of these substrings in the stack trace, even if they are not in your main app.
99
102
  Each item can be a string (match substring), a regex, or an array where the first item is a path to match, and the second
@@ -118,15 +121,15 @@ Bullet.unused_eager_loading_enable = false
118
121
  Bullet.counter_cache_enable = false
119
122
  ```
120
123
 
121
- ## Whitelist
124
+ ## Safe list
122
125
 
123
126
  Sometimes Bullet may notify you of query problems you don't care to fix, or
124
- which come from outside your code. You can whitelist these to ignore them:
127
+ which come from outside your code. You can add them to a safe list to ignore them:
125
128
 
126
129
  ```ruby
127
- Bullet.add_whitelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
128
- Bullet.add_whitelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments
129
- Bullet.add_whitelist :type => :counter_cache, :class_name => "Country", :association => :cities
130
+ Bullet.add_safelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
131
+ Bullet.add_safelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments
132
+ Bullet.add_safelist :type => :counter_cache, :class_name => "Country", :association => :cities
130
133
  ```
131
134
 
132
135
  If you want to skip bullet in some specific controller actions, you can
data/lib/bullet.rb CHANGED
@@ -20,9 +20,6 @@ module Bullet
20
20
  autoload :Registry, 'bullet/registry'
21
21
  autoload :NotificationCollector, 'bullet/notification_collector'
22
22
 
23
- BULLET_DEBUG = 'BULLET_DEBUG'
24
- TRUE = 'true'
25
-
26
23
  if defined?(Rails::Railtie)
27
24
  class BulletRailtie < Rails::Railtie
28
25
  initializer 'bullet.configure_rails_initialization' do |app|
@@ -38,9 +35,11 @@ module Bullet
38
35
  :stacktrace_includes,
39
36
  :stacktrace_excludes,
40
37
  :skip_html_injection
41
- attr_accessor :add_footer, :orm_patches_applied
38
+ attr_reader :safelist
39
+ attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers
42
40
 
43
- available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.map { |notifier| "#{notifier}=" }
41
+ available_notifiers =
42
+ UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
44
43
  available_notifiers_options = { to: UniformNotifier }
45
44
  delegate(*available_notifiers, **available_notifiers_options)
46
45
 
@@ -58,7 +57,7 @@ module Bullet
58
57
  @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
59
58
 
60
59
  if enable?
61
- reset_whitelist
60
+ reset_safelist
62
61
  unless orm_patches_applied
63
62
  self.orm_patches_applied = true
64
63
  Bullet::Mongoid.enable if mongoid?
@@ -71,8 +70,9 @@ module Bullet
71
70
  !!@enable
72
71
  end
73
72
 
73
+ # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
74
74
  def app_root
75
- (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
75
+ @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
76
76
  end
77
77
 
78
78
  def n_plus_one_query_enable?
@@ -88,36 +88,81 @@ module Bullet
88
88
  end
89
89
 
90
90
  def stacktrace_includes
91
- @stacktrace_includes || []
91
+ @stacktrace_includes ||= []
92
92
  end
93
93
 
94
94
  def stacktrace_excludes
95
- @stacktrace_excludes || []
95
+ @stacktrace_excludes ||= []
96
+ end
97
+
98
+ def add_safelist(options)
99
+ reset_safelist
100
+ @safelist[options[:type]][options[:class_name]] ||= []
101
+ @safelist[options[:type]][options[:class_name]] << options[:association].to_sym
102
+ end
103
+
104
+ def delete_safelist(options)
105
+ reset_safelist
106
+ @safelist[options[:type]][options[:class_name]] ||= []
107
+ @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
108
+ @safelist[options[:type]].delete_if { |_key, val| val.empty? }
109
+ end
110
+
111
+ def get_safelist_associations(type, class_name)
112
+ Array(@safelist[type][class_name])
113
+ end
114
+
115
+ def reset_safelist
116
+ @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
117
+ end
118
+
119
+ def clear_safelist
120
+ @safelist = nil
96
121
  end
97
122
 
98
123
  def add_whitelist(options)
99
- reset_whitelist
100
- Thread.current[:whitelist][options[:type]][options[:class_name]] ||= []
101
- Thread.current[:whitelist][options[:type]][options[:class_name]] << options[:association].to_sym
124
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
125
+ add_whitelist is deprecated in favor of add_safelist. It will be removed from the next major release.
126
+ WARN
127
+ )
128
+
129
+ add_safelist(options)
102
130
  end
103
131
 
104
132
  def delete_whitelist(options)
105
- reset_whitelist
106
- Thread.current[:whitelist][options[:type]][options[:class_name]] ||= []
107
- Thread.current[:whitelist][options[:type]][options[:class_name]].delete(options[:association].to_sym)
108
- Thread.current[:whitelist][options[:type]].delete_if { |_key, val| val.empty? }
133
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
134
+ delete_whitelist is deprecated in favor of delete_safelist. It will be removed from the next major release.
135
+ WARN
136
+ )
137
+
138
+ delete_safelist(options)
109
139
  end
110
140
 
111
141
  def get_whitelist_associations(type, class_name)
112
- Array(Thread.current[:whitelist][type][class_name])
142
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
143
+ get_whitelist_associations is deprecated in favor of get_safelist_associations. It will be removed from the next major release.
144
+ WARN
145
+ )
146
+
147
+ get_safelist_associations(type, class_name)
113
148
  end
114
149
 
115
150
  def reset_whitelist
116
- Thread.current[:whitelist] ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
151
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
152
+ reset_whitelist is deprecated in favor of reset_safelist. It will be removed from the next major release.
153
+ WARN
154
+ )
155
+
156
+ reset_safelist
117
157
  end
118
158
 
119
159
  def clear_whitelist
120
- Thread.current[:whitelist] = nil
160
+ ActiveSupport::Deprecation.warn(<<~WARN.strip
161
+ clear_whitelist is deprecated in favor of clear_safelist. It will be removed from the next major release.
162
+ WARN
163
+ )
164
+
165
+ clear_safelist
121
166
  end
122
167
 
123
168
  def bullet_logger=(active)
@@ -131,7 +176,7 @@ module Bullet
131
176
  end
132
177
 
133
178
  def debug(title, message)
134
- puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] == TRUE
179
+ puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
135
180
  end
136
181
 
137
182
  def start_request
@@ -240,7 +285,9 @@ module Bullet
240
285
  end
241
286
 
242
287
  def inject_into_page?
243
- !@skip_html_injection && (console_enabled? || add_footer)
288
+ return false if defined?(@skip_html_injection) && @skip_html_injection
289
+
290
+ console_enabled? || add_footer
244
291
  end
245
292
 
246
293
  private
@@ -30,6 +30,7 @@ module Bullet
30
30
 
31
31
  ::ActiveRecord::Relation.class_eval do
32
32
  alias_method :origin_to_a, :to_a
33
+
33
34
  # if select a collection of objects, then these objects have possible to cause N+1 query.
34
35
  # if select only one object, then the only one object has impossible to cause N+1 query.
35
36
  def to_a
@@ -52,6 +52,7 @@ module Bullet
52
52
 
53
53
  ::ActiveRecord::Relation.class_eval do
54
54
  alias_method :origin_to_a, :to_a
55
+
55
56
  # if select a collection of objects, then these objects have possible to cause N+1 query.
56
57
  # if select only one object, then the only one object has impossible to cause N+1 query.
57
58
  def to_a
@@ -202,6 +202,17 @@ module Bullet
202
202
 
203
203
  if Bullet.start?
204
204
  if owner.class.name !~ /^HABTM_/ && !@inversed
205
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
206
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
207
+ association = owner.association reflection.through_reflection.name
208
+ Array(association.target).each do |through_record|
209
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
210
+ end
211
+
212
+ if reflection.through_reflection != through_reflection
213
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
214
+ end
215
+ end
205
216
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
206
217
 
207
218
  if Bullet::Detector::NPlusOneQuery.impossible?(owner)
@@ -229,6 +229,17 @@ module Bullet
229
229
 
230
230
  if Bullet.start?
231
231
  if owner.class.name !~ /^HABTM_/ && !@inversed
232
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
233
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
234
+ association = owner.association(reflection.through_reflection.name)
235
+ Array(association.target).each do |through_record|
236
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
237
+ end
238
+
239
+ if reflection.through_reflection != through_reflection
240
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
241
+ end
242
+ end
232
243
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
233
244
 
234
245
  if Bullet::Detector::NPlusOneQuery.impossible?(owner)
@@ -229,6 +229,17 @@ module Bullet
229
229
 
230
230
  if Bullet.start?
231
231
  if owner.class.name !~ /^HABTM_/ && !@inversed
232
+ if is_a? ::ActiveRecord::Associations::ThroughAssociation
233
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
234
+ association = owner.association(reflection.through_reflection.name)
235
+ Array(association.target).each do |through_record|
236
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
237
+ end
238
+
239
+ if reflection.through_reflection != through_reflection
240
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
241
+ end
242
+ end
232
243
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
233
244
 
234
245
  if Bullet::Detector::NPlusOneQuery.impossible?(owner)
@@ -20,6 +20,7 @@
20
20
  if (this.onload) {
21
21
  this._storedOnload = this.onload;
22
22
  }
23
+ this.onload = null
23
24
  this.addEventListener("load", bulletXHROnload);
24
25
  return Reflect.apply(oldSend, this, arguments);
25
26
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base; end
5
+ class Base
6
+ end
6
7
  end
7
8
  end
@@ -54,7 +54,7 @@ module Bullet
54
54
  private
55
55
 
56
56
  def create_notification(klazz, associations)
57
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:counter_cache, klazz)
57
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
58
58
 
59
59
  if notify_associations.present?
60
60
  notice = Bullet::Notification::CounterCache.new klazz, notify_associations
@@ -35,6 +35,7 @@ module Bullet
35
35
 
36
36
  objects = Array(object_or_objects)
37
37
  return if objects.map(&:bullet_primary_key_value).compact.empty?
38
+ return if objects.all? { |obj| obj.class.name =~ /^HABTM_/ }
38
39
 
39
40
  Bullet.debug(
40
41
  'Detector::NPlusOneQuery#add_possible_objects',
@@ -84,8 +85,7 @@ module Bullet
84
85
  # associations == v comparison order is important here because
85
86
  # v variable might be a squeel node where :== method is redefined,
86
87
  # so it does not compare values at all and return unexpected results
87
- result =
88
- v.is_a?(Hash) ? v.key?(associations) : associations == v
88
+ result = v.is_a?(Hash) ? v.key?(associations) : associations == v
89
89
  return true if result
90
90
  end
91
91
 
@@ -95,7 +95,7 @@ module Bullet
95
95
  private
96
96
 
97
97
  def create_notification(callers, klazz, associations)
98
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
98
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
99
99
 
100
100
  if notify_associations.present?
101
101
  notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
@@ -65,7 +65,7 @@ module Bullet
65
65
  private
66
66
 
67
67
  def create_notification(callers, klazz, associations)
68
- notify_associations = Array(associations) - Bullet.get_whitelist_associations(:unused_eager_loading, klazz)
68
+ notify_associations = Array(associations) - Bullet.get_safelist_associations(:unused_eager_loading, klazz)
69
69
 
70
70
  if notify_associations.present?
71
71
  notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -23,7 +23,7 @@ module Bullet
23
23
  end
24
24
 
25
25
  def each(&block)
26
- return to_enum unless block_given?
26
+ return to_enum unless block
27
27
 
28
28
  records = []
29
29
  origin_each { |record| records << record }
@@ -7,6 +7,7 @@ module Bullet
7
7
  autoload :NPlusOneQuery, 'bullet/notification/n_plus_one_query'
8
8
  autoload :CounterCache, 'bullet/notification/counter_cache'
9
9
 
10
- class UnoptimizedQueryError < StandardError; end
10
+ class UnoptimizedQueryError < StandardError
11
+ end
11
12
  end
12
13
  end
data/lib/bullet/rack.rb CHANGED
@@ -22,9 +22,11 @@ module Bullet
22
22
  response_body = response_body(response)
23
23
  response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
24
  response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
- response_body = append_to_html_body(response_body, xhr_script)
25
+ if Bullet.add_footer && !Bullet.skip_http_headers
26
+ response_body = append_to_html_body(response_body, xhr_script)
27
+ end
26
28
  headers['Content-Length'] = response_body.bytesize.to_s
27
- else
29
+ elsif !Bullet.skip_http_headers
28
30
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
29
31
  set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
30
32
  end
@@ -82,7 +84,7 @@ module Bullet
82
84
  def response_body(response)
83
85
  if response.respond_to?(:body)
84
86
  Array === response.body ? response.body.first : response.body
85
- else
87
+ elsif response.respond_to?(:first)
86
88
  response.first
87
89
  end
88
90
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
+ require "bundler"
2
3
 
3
4
  module Bullet
4
5
  module StackTraceFilter
5
6
  VENDOR_PATH = '/vendor'
7
+ IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
6
8
 
7
9
  def caller_in_project
8
10
  vendor_root = Bullet.app_root + VENDOR_PATH
@@ -10,8 +12,9 @@ module Bullet
10
12
  select_caller_locations do |location|
11
13
  caller_path = location_as_path(location)
12
14
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
13
- !caller_path.include?(bundler_path) ||
14
- Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
15
+ !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
16
+ pattern_matches?(location, include_pattern)
17
+ }
15
18
  end
16
19
  end
17
20
 
@@ -47,20 +50,15 @@ module Bullet
47
50
  end
48
51
 
49
52
  def location_as_path(location)
50
- ruby_19? ? location : location.absolute_path.to_s
53
+ IS_RUBY_19 ? location : location.absolute_path.to_s
51
54
  end
52
55
 
53
56
  def select_caller_locations
54
- if ruby_19?
57
+ if IS_RUBY_19
55
58
  caller.select { |caller_path| yield caller_path }
56
59
  else
57
60
  caller_locations.select { |location| yield location }
58
61
  end
59
62
  end
60
-
61
- def ruby_19?
62
- @ruby_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') if @ruby_19.nil?
63
- @ruby_19
64
- end
65
63
  end
66
64
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '6.1.1'
4
+ VERSION = '6.1.5'
5
5
  end
data/perf/benchmark.rb CHANGED
@@ -30,7 +30,10 @@ end
30
30
 
31
31
  # create database bullet_benchmark;
32
32
  ActiveRecord::Base.establish_connection(
33
- adapter: 'mysql2', database: 'bullet_benchmark', server: '/tmp/mysql.socket', username: 'root'
33
+ adapter: 'mysql2',
34
+ database: 'bullet_benchmark',
35
+ server: '/tmp/mysql.socket',
36
+ username: 'root'
34
37
  )
35
38
 
36
39
  ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
@@ -19,7 +19,9 @@ module Bullet
19
19
  it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do
20
20
  UnusedEagerLoading.add_eager_loadings([@post], :association)
21
21
  UnusedEagerLoading.add_call_object_associations(@post, :association)
22
- expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq([:association])
22
+ expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
23
+ [:association]
24
+ )
23
25
  end
24
26
 
25
27
  it 'should not get call associations if not exist in call_object_associations' do
@@ -30,7 +32,9 @@ module Bullet
30
32
 
31
33
  context '.diff_object_associations' do
32
34
  it 'should return associations not exist in call_association' do
33
- expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq([:association])
35
+ expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
36
+ [:association]
37
+ )
34
38
  end
35
39
 
36
40
  it 'should return empty if associations exist in call_association' do
@@ -10,7 +10,7 @@ describe Object do
10
10
  end
11
11
 
12
12
  if mongoid?
13
- it 'should return class with namesapce and id composition' do
13
+ it 'should return class with namespace and id composition' do
14
14
  post = Mongoid::Post.first
15
15
  expect(post.bullet_key).to eq("Mongoid::Post:#{post.id}")
16
16
  end
@@ -69,7 +69,6 @@ module Bullet
69
69
  expect(Bullet).to receive(:notification?).and_return(true)
70
70
  expect(Bullet).to receive(:console_enabled?).and_return(true)
71
71
  expect(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
72
- expect(middleware).to receive(:xhr_script).and_return('')
73
72
  expect(Bullet).to receive(:perform_out_of_channel_notifications)
74
73
  _, headers, response = middleware.call('Content-Type' => 'text/html')
75
74
  expect(headers['Content-Length']).to eq('56')
@@ -84,37 +83,44 @@ module Bullet
84
83
  allow(Bullet).to receive(:console_enabled?).and_return(true)
85
84
  expect(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
86
85
  _, headers, response = middleware.call('Content-Type' => 'text/html')
87
- expect(headers['Content-Length']).to eq((58 + middleware.send(:xhr_script).length).to_s)
86
+ expect(headers['Content-Length']).to eq('58')
88
87
  end
89
88
 
90
89
  context 'with injection notifiers' do
91
90
  before do
92
91
  expect(Bullet).to receive(:notification?).and_return(true)
93
92
  allow(Bullet).to receive(:gather_inline_notifications).and_return('<bullet></bullet>')
94
- allow(middleware).to receive(:xhr_script).and_return('')
93
+ allow(middleware).to receive(:xhr_script).and_return('<script></script>')
95
94
  allow(middleware).to receive(:footer_note).and_return('footer')
96
95
  expect(Bullet).to receive(:perform_out_of_channel_notifications)
97
96
  end
98
97
 
99
98
  it 'should change response body if add_footer is true' do
100
- expect(Bullet).to receive(:add_footer).twice.and_return(true)
99
+ expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
101
100
  _, headers, response = middleware.call('Content-Type' => 'text/html')
102
101
 
103
- expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
104
- expect(response.first).to start_with('<html><head></head><body>')
105
- expect(response.first).to include('<bullet></bullet><')
102
+ expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
103
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet><script></script></body></html>])
106
104
  end
107
105
 
108
106
  it 'should change response body for html safe string if add_footer is true' do
109
- expect(Bullet).to receive(:add_footer).twice.and_return(true)
110
- app.response = Support::ResponseDouble.new.tap do |response|
111
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
112
- end
107
+ expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
108
+ app.response =
109
+ Support::ResponseDouble.new.tap do |response|
110
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
111
+ end
113
112
  _, headers, response = middleware.call('Content-Type' => 'text/html')
114
113
 
115
- expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
116
- expect(response.first).to start_with('<html><head></head><body>')
117
- expect(response.first).to include('<bullet></bullet><')
114
+ expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
115
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet><script></script></body></html>])
116
+ end
117
+
118
+ it 'should add the footer-text header for non-html requests when add_footer is true' do
119
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
120
+ allow(Bullet).to receive(:footer_info).and_return(['footer text'])
121
+ app.headers = { 'Content-Type' => 'application/json' }
122
+ _, headers, _response = middleware.call({})
123
+ expect(headers).to include('X-bullet-footer-text' => '["footer text"]')
118
124
  end
119
125
 
120
126
  it 'should change response body if console_enabled is true' do
@@ -126,20 +132,71 @@ module Bullet
126
132
 
127
133
  it 'should change response body for html safe string if console_enabled is true' do
128
134
  expect(Bullet).to receive(:console_enabled?).and_return(true)
129
- app.response = Support::ResponseDouble.new.tap do |response|
130
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
131
- end
135
+ app.response =
136
+ Support::ResponseDouble.new.tap do |response|
137
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
138
+ end
132
139
  _, headers, response = middleware.call('Content-Type' => 'text/html')
133
140
  expect(headers['Content-Length']).to eq('56')
134
141
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
135
142
  end
136
143
 
144
+ it 'should add headers for non-html requests when console_enabled is true' do
145
+ allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
146
+ allow(Bullet).to receive(:text_notifications).and_return(['text notifications'])
147
+ app.headers = { 'Content-Type' => 'application/json' }
148
+ _, headers, _response = middleware.call({})
149
+ expect(headers).to include('X-bullet-console-text' => '["text notifications"]')
150
+ end
151
+
137
152
  it "shouldn't change response body unnecessarily" do
138
153
  expected_response = Support::ResponseDouble.new 'Actual body'
139
154
  app.response = expected_response
140
155
  _, _, response = middleware.call({})
141
156
  expect(response).to eq(expected_response)
142
157
  end
158
+
159
+ it "shouldn't add headers unnecessarily" do
160
+ app.headers = { 'Content-Type' => 'application/json' }
161
+ _, headers, _response = middleware.call({})
162
+ expect(headers).not_to include('X-bullet-footer-text')
163
+ expect(headers).not_to include('X-bullet-console-text')
164
+ end
165
+
166
+ context 'when skip_http_headers is enabled' do
167
+ before do
168
+ allow(Bullet).to receive(:skip_http_headers).and_return(true)
169
+ end
170
+
171
+ it 'should include the footer but not the xhr script tag if add_footer is true' do
172
+ expect(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
173
+ _, headers, response = middleware.call({})
174
+
175
+ expect(headers['Content-Length']).to eq((56 + middleware.send(:footer_note).length).to_s)
176
+ expect(response).to eq(%w[<html><head></head><body>footer<bullet></bullet></body></html>])
177
+ end
178
+
179
+ it 'should not include the xhr script tag if console_enabled is true' do
180
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
181
+ _, headers, response = middleware.call({})
182
+ expect(headers['Content-Length']).to eq('56')
183
+ expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
184
+ end
185
+
186
+ it 'should not add the footer-text header for non-html requests when add_footer is true' do
187
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
188
+ app.headers = { 'Content-Type' => 'application/json' }
189
+ _, headers, _response = middleware.call({})
190
+ expect(headers).not_to include('X-bullet-footer-text')
191
+ end
192
+
193
+ it 'should not add headers for non-html requests when console_enabled is true' do
194
+ allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
195
+ app.headers = { 'Content-Type' => 'application/json' }
196
+ _, headers, _response = middleware.call({})
197
+ expect(headers).not_to include('X-bullet-console-text')
198
+ end
199
+ end
143
200
  end
144
201
 
145
202
  context 'when skip_html_injection is enabled' do
@@ -204,6 +261,20 @@ module Bullet
204
261
  expect(middleware.response_body(response)).to eq body_string
205
262
  end
206
263
  end
264
+
265
+ begin
266
+ require 'rack/files'
267
+
268
+ context 'when `response` is a Rack::Files::Iterator' do
269
+ let(:response) { instance_double(::Rack::Files::Iterator) }
270
+ before { allow(response).to receive(:is_a?).with(::Rack::Files::Iterator) { true } }
271
+
272
+ it 'should return nil' do
273
+ expect(middleware.response_body(response)).to be_nil
274
+ end
275
+ end
276
+ rescue LoadError
277
+ end
207
278
  end
208
279
  end
209
280
  end
data/spec/bullet_spec.rb CHANGED
@@ -74,31 +74,60 @@ describe Bullet, focused: true do
74
74
  end
75
75
  end
76
76
 
77
+ describe '#add_safelist' do
78
+ context "for 'special' class names" do
79
+ it 'is added to the safelist successfully' do
80
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
81
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
82
+ end
83
+ end
84
+ end
85
+
77
86
  describe '#add_whitelist' do
78
87
  context "for 'special' class names" do
79
- it 'is added to the whitelist successfully' do
88
+ it 'is added to the safelist successfully' do
80
89
  Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
81
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
90
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
91
+ end
92
+ end
93
+ end
94
+
95
+ describe '#delete_safelist' do
96
+ context "for 'special' class names" do
97
+ it 'is deleted from the safelist successfully' do
98
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
99
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
100
+ expect(Bullet.safelist[:n_plus_one_query]).to eq({})
101
+ end
102
+ end
103
+
104
+ context 'when exists multiple definitions' do
105
+ it 'is deleted from the safelist successfully' do
106
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
107
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
108
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
109
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
110
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
82
111
  end
83
112
  end
84
113
  end
85
114
 
86
115
  describe '#delete_whitelist' do
87
116
  context "for 'special' class names" do
88
- it 'is deleted from the whitelist successfully' do
89
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
117
+ it 'is deleted from the safelist successfully' do
118
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
90
119
  Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
91
- expect(Thread.current[:whitelist][:n_plus_one_query]).to eq({})
120
+ expect(Bullet.safelist[:n_plus_one_query]).to eq({})
92
121
  end
93
122
  end
94
123
 
95
124
  context 'when exists multiple definitions' do
96
- it 'is deleted from the whitelist successfully' do
97
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
98
- Bullet.add_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
125
+ it 'is deleted from the safelist successfully' do
126
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
127
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
99
128
  Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
100
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to include :department
101
- expect(Bullet.get_whitelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
129
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
130
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
102
131
  end
103
132
  end
104
133
  end
@@ -129,7 +129,7 @@ if active_record?
129
129
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
130
130
  end
131
131
 
132
- it 'should detect unused preload with post => commnets, no category => posts' do
132
+ it 'should detect unused preload with post => comments, no category => posts' do
133
133
  Category.includes(posts: :comments).each { |category| category.posts.map(&:name) }
134
134
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
135
135
  expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments)
@@ -202,7 +202,7 @@ if active_record?
202
202
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
203
203
  end
204
204
 
205
- it 'should detect preload with post => commnets' do
205
+ it 'should detect preload with post => comments' do
206
206
  Post.first.comments.map(&:name)
207
207
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
208
208
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
@@ -401,6 +401,15 @@ if active_record?
401
401
  end
402
402
 
403
403
  describe Bullet::Detector::Association, 'has_and_belongs_to_many' do
404
+ context 'posts <=> deals' do
405
+ it 'should detect preload associations with join tables that have identifier' do
406
+ Post.includes(:deals).each { |post| post.deals.map(&:name) }
407
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
408
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
409
+
410
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
411
+ end
412
+ end
404
413
  context 'students <=> teachers' do
405
414
  it 'should detect non preload associations' do
406
415
  Student.all.each { |student| student.teachers.map(&:name) }
@@ -561,6 +570,42 @@ if active_record?
561
570
  end
562
571
  end
563
572
 
573
+ describe Bullet::Detector::Association, 'has_one :through' do
574
+ context 'user => attachment' do
575
+ it 'should detect non preload associations' do
576
+ User.all.each { |user| user.submission_attachment.file_name }
577
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
578
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
579
+
580
+ expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(User, :submission_attachment)
581
+ end
582
+
583
+ it 'should detect preload associations' do
584
+ User.includes(:submission_attachment).each { |user| user.submission_attachment.file_name }
585
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
586
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
587
+
588
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
589
+ end
590
+
591
+ it 'should not detect preload associations' do
592
+ User.all.map(&:name)
593
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
594
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
595
+
596
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
597
+ end
598
+
599
+ it 'should detect unused preload associations' do
600
+ User.includes(:submission_attachment).map(&:name)
601
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
602
+ expect(Bullet::Detector::Association).to be_unused_preload_associations_for(User, :submission_attachment)
603
+
604
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
605
+ end
606
+ end
607
+ end
608
+
564
609
  describe Bullet::Detector::Association, 'call one association that in possible objects' do
565
610
  it 'should not detect preload association' do
566
611
  Post.all
@@ -693,9 +738,9 @@ if active_record?
693
738
  end
694
739
  end
695
740
 
696
- context 'whitelist n plus one query' do
697
- before { Bullet.add_whitelist type: :n_plus_one_query, class_name: 'Post', association: :comments }
698
- after { Bullet.clear_whitelist }
741
+ context 'add n plus one query to safelist' do
742
+ before { Bullet.add_safelist type: :n_plus_one_query, class_name: 'Post', association: :comments }
743
+ after { Bullet.clear_safelist }
699
744
 
700
745
  it 'should not detect n plus one query' do
701
746
  Post.all.each { |post| post.comments.map(&:name) }
@@ -714,9 +759,9 @@ if active_record?
714
759
  end
715
760
  end
716
761
 
717
- context 'whitelist unused eager loading' do
718
- before { Bullet.add_whitelist type: :unused_eager_loading, class_name: 'Post', association: :comments }
719
- after { Bullet.clear_whitelist }
762
+ context 'add unused eager loading to safelist' do
763
+ before { Bullet.add_safelist type: :unused_eager_loading, class_name: 'Post', association: :comments }
764
+ after { Bullet.clear_safelist }
720
765
 
721
766
  it 'should not detect unused eager loading' do
722
767
  Post.includes(:comments).map(&:name)
@@ -55,9 +55,9 @@ if !mongoid? && active_record?
55
55
  end
56
56
  end
57
57
 
58
- context 'whitelist' do
59
- before { Bullet.add_whitelist type: :counter_cache, class_name: 'Country', association: :cities }
60
- after { Bullet.clear_whitelist }
58
+ context 'safelist' do
59
+ before { Bullet.add_safelist type: :counter_cache, class_name: 'Country', association: :cities }
60
+ after { Bullet.clear_safelist }
61
61
 
62
62
  it 'should not detect counter cache' do
63
63
  Country.all.each { |country| country.cities.size }
@@ -118,7 +118,7 @@ if mongoid?
118
118
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
119
119
  end
120
120
 
121
- it 'should detect preload with post => commnets' do
121
+ it 'should detect preload with post => comments' do
122
122
  Mongoid::Post.first.comments.map(&:name)
123
123
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
124
124
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Attachment < ActiveRecord::Base
4
+ belongs_to :submission
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Deal < ActiveRecord::Base
4
+ has_and_belongs_to_many :posts
5
+ end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Folder < Document; end
3
+ class Folder < Document
4
+ end
data/spec/models/group.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Group < ActiveRecord::Base; end
3
+ class Group < ActiveRecord::Base
4
+ end
data/spec/models/page.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Page < Document; end
3
+ class Page < Document
4
+ end
data/spec/models/post.rb CHANGED
@@ -4,6 +4,7 @@ class Post < ActiveRecord::Base
4
4
  belongs_to :category, inverse_of: :posts
5
5
  belongs_to :writer
6
6
  has_many :comments, inverse_of: :post
7
+ has_and_belongs_to_many :deals
7
8
 
8
9
  validates :category, presence: true
9
10
 
@@ -21,6 +22,7 @@ class Post < ActiveRecord::Base
21
22
  next unless trigger_after_save
22
23
 
23
24
  temp_comment = Comment.new(post: self)
25
+
24
26
  # this triggers self to be "possible", even though it's
25
27
  # not saved yet
26
28
  temp_comment.post
@@ -3,4 +3,5 @@
3
3
  class Submission < ActiveRecord::Base
4
4
  belongs_to :user
5
5
  has_many :replies
6
+ has_one :attachment
6
7
  end
data/spec/models/user.rb CHANGED
@@ -2,5 +2,6 @@
2
2
 
3
3
  class User < ActiveRecord::Base
4
4
  has_one :submission
5
+ has_one :submission_attachment, through: :submission, source: :attachment, class_name: 'Attachment'
5
6
  belongs_to :category
6
7
  end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Writer < BaseUser; end
3
+ class Writer < BaseUser
4
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,12 +4,10 @@ require 'rspec'
4
4
  begin
5
5
  require 'active_record'
6
6
  rescue LoadError
7
-
8
7
  end
9
8
  begin
10
9
  require 'mongoid'
11
10
  rescue LoadError
12
-
13
11
  end
14
12
 
15
13
  module Rails
@@ -45,6 +45,7 @@ module Support
45
45
  Mongoid.configure do |config|
46
46
  config.load_configuration(clients: { default: { database: 'bullet', hosts: %w[localhost:27017] } })
47
47
  end
48
+
48
49
  # Increase the level from DEBUG in order to avoid excessive logging to the screen
49
50
  Mongo::Logger.logger.level = Logger::WARN
50
51
  end
@@ -21,6 +21,13 @@ module Support
21
21
  post2 = category2.posts.create(name: 'second', writer: writer2)
22
22
  post3 = category2.posts.create(name: 'third', writer: writer2)
23
23
 
24
+ deal1 = Deal.new(name: 'Deal 1')
25
+ deal1.posts << post1
26
+ deal1.posts << post2
27
+ deal2 = Deal.new(name: 'Deal 2')
28
+ post1.deals << deal1
29
+ post1.deals << deal2
30
+
24
31
  comment1 = post1.comments.create(name: 'first', author: writer1)
25
32
  comment2 = post1.comments.create(name: 'first2', author: writer1)
26
33
  comment3 = post1.comments.create(name: 'first3', author: writer1)
@@ -95,6 +102,9 @@ module Support
95
102
  submission1.replies.create(name: 'reply2')
96
103
  submission2.replies.create(name: 'reply3')
97
104
  submission2.replies.create(name: 'reply4')
105
+
106
+ submission1.create_attachment(file_name: 'submission1 file')
107
+ submission2.create_attachment(file_name: 'submission2 file')
98
108
  end
99
109
 
100
110
  def setup_db
@@ -153,6 +163,11 @@ module Support
153
163
  t.column :hotel_id, :integer
154
164
  end
155
165
 
166
+ create_table :deals_posts do |t|
167
+ t.column :deal_id, :integer
168
+ t.column :post_id, :integer
169
+ end
170
+
156
171
  create_table :documents do |t|
157
172
  t.string :name
158
173
  t.string :type
@@ -240,6 +255,11 @@ module Support
240
255
  t.column :name, :string
241
256
  t.column :category_id, :integer
242
257
  end
258
+
259
+ create_table :attachments do |t|
260
+ t.column :file_name, :string
261
+ t.column :submission_id, :integer
262
+ end
243
263
  end
244
264
  end
245
265
  end
data/test.sh CHANGED
@@ -1,5 +1,6 @@
1
1
  #bundle update rails && bundle exec rspec spec
2
2
  #BUNDLE_GEMFILE=Gemfile.mongoid bundle update mongoid && BUNDLE_GEMFILE=Gemfile.mongoid bundle exec rspec spec
3
+ BUNDLE_GEMFILE=Gemfile.rails-6.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-6.1 bundle exec rspec spec
3
4
  BUNDLE_GEMFILE=Gemfile.rails-6.0 bundle && BUNDLE_GEMFILE=Gemfile.rails-6.0 bundle exec rspec spec
4
5
  BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle && BUNDLE_GEMFILE=Gemfile.rails-5.2 bundle exec rspec spec
5
6
  BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle && BUNDLE_GEMFILE=Gemfile.rails-5.1 bundle exec rspec spec
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.1
4
+ version: 6.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-12 00:00:00.000000000 Z
11
+ date: 2021-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -45,9 +45,9 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/workflows/main.yml"
48
49
  - ".gitignore"
49
50
  - ".rspec"
50
- - ".travis.yml"
51
51
  - CHANGELOG.md
52
52
  - Gemfile
53
53
  - Gemfile.mongoid
@@ -129,6 +129,7 @@ files:
129
129
  - spec/integration/counter_cache_spec.rb
130
130
  - spec/integration/mongoid/association_spec.rb
131
131
  - spec/models/address.rb
132
+ - spec/models/attachment.rb
132
133
  - spec/models/author.rb
133
134
  - spec/models/base_user.rb
134
135
  - spec/models/category.rb
@@ -137,6 +138,7 @@ files:
137
138
  - spec/models/comment.rb
138
139
  - spec/models/company.rb
139
140
  - spec/models/country.rb
141
+ - spec/models/deal.rb
140
142
  - spec/models/document.rb
141
143
  - spec/models/entry.rb
142
144
  - spec/models/firm.rb
@@ -190,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
192
  - !ruby/object:Gem::Version
191
193
  version: 1.3.6
192
194
  requirements: []
193
- rubygems_version: 3.1.4
195
+ rubygems_version: 3.2.22
194
196
  signing_key:
195
197
  specification_version: 4
196
198
  summary: help to kill N+1 queries and unused eager loading.
@@ -216,6 +218,7 @@ test_files:
216
218
  - spec/integration/counter_cache_spec.rb
217
219
  - spec/integration/mongoid/association_spec.rb
218
220
  - spec/models/address.rb
221
+ - spec/models/attachment.rb
219
222
  - spec/models/author.rb
220
223
  - spec/models/base_user.rb
221
224
  - spec/models/category.rb
@@ -224,6 +227,7 @@ test_files:
224
227
  - spec/models/comment.rb
225
228
  - spec/models/company.rb
226
229
  - spec/models/country.rb
230
+ - spec/models/deal.rb
227
231
  - spec/models/document.rb
228
232
  - spec/models/entry.rb
229
233
  - spec/models/firm.rb
data/.travis.yml DELETED
@@ -1,33 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.3.0
4
- - 2.6.0
5
- gemfile:
6
- - Gemfile.rails-6.0
7
- - Gemfile.rails-5.2
8
- - Gemfile.rails-5.1
9
- - Gemfile.rails-5.0
10
- - Gemfile.rails-4.2
11
- - Gemfile.rails-4.1
12
- - Gemfile.rails-4.0
13
- matrix:
14
- exclude:
15
- - rvm: 2.3.0
16
- gemfile: Gemfile.rails-6.0
17
- - rvm: 2.6.0
18
- gemfile: Gemfile.rails-5.2
19
- - rvm: 2.6.0
20
- gemfile: Gemfile.rails-5.1
21
- - rvm: 2.6.0
22
- gemfile: Gemfile.rails-5.0
23
- - rvm: 2.6.0
24
- gemfile: Gemfile.rails-4.2
25
- - rvm: 2.6.0
26
- gemfile: Gemfile.rails-4.1
27
- - rvm: 2.6.0
28
- gemfile: Gemfile.rails-4.0
29
- env:
30
- - DB=sqlite
31
- before_install:
32
- - "find /home/travis/.rvm/rubies -wholename '*default/bundler-*.gemspec' -delete"
33
- - gem install bundler -v '< 2'