bullet 6.1.4 → 7.0.5

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +82 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile.rails-7.0 +10 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +32 -26
  7. data/lib/bullet/active_record41.rb +1 -0
  8. data/lib/bullet/active_record42.rb +1 -0
  9. data/lib/bullet/active_record5.rb +10 -8
  10. data/lib/bullet/active_record52.rb +21 -25
  11. data/lib/bullet/active_record60.rb +20 -24
  12. data/lib/bullet/active_record61.rb +20 -24
  13. data/lib/bullet/active_record70.rb +284 -0
  14. data/lib/bullet/bullet_xhr.js +3 -2
  15. data/lib/bullet/dependency.rb +10 -0
  16. data/lib/bullet/detector/association.rb +8 -0
  17. data/lib/bullet/detector/base.rb +2 -1
  18. data/lib/bullet/detector/counter_cache.rb +2 -2
  19. data/lib/bullet/detector/n_plus_one_query.rb +24 -13
  20. data/lib/bullet/detector/unused_eager_loading.rb +3 -3
  21. data/lib/bullet/mongoid7x.rb +34 -19
  22. data/lib/bullet/notification.rb +2 -1
  23. data/lib/bullet/rack.rb +42 -7
  24. data/lib/bullet/registry/call_stack.rb +12 -0
  25. data/lib/bullet/registry.rb +1 -0
  26. data/lib/bullet/stack_trace_filter.rb +14 -10
  27. data/lib/bullet/version.rb +1 -1
  28. data/lib/bullet.rb +28 -24
  29. data/lib/generators/bullet/install_generator.rb +0 -1
  30. data/perf/benchmark.rb +4 -1
  31. data/spec/bullet/detector/n_plus_one_query_spec.rb +1 -33
  32. data/spec/bullet/detector/unused_eager_loading_spec.rb +11 -2
  33. data/spec/bullet/ext/object_spec.rb +1 -1
  34. data/spec/bullet/notification/base_spec.rb +4 -4
  35. data/spec/bullet/rack_spec.rb +50 -18
  36. data/spec/bullet/stack_trace_filter_spec.rb +26 -0
  37. data/spec/bullet_spec.rb +15 -15
  38. data/spec/integration/active_record/association_spec.rb +58 -10
  39. data/spec/integration/counter_cache_spec.rb +4 -4
  40. data/spec/integration/mongoid/association_spec.rb +1 -1
  41. data/spec/models/deal.rb +5 -0
  42. data/spec/models/folder.rb +2 -1
  43. data/spec/models/group.rb +2 -1
  44. data/spec/models/page.rb +2 -1
  45. data/spec/models/post.rb +2 -0
  46. data/spec/models/role.rb +7 -0
  47. data/spec/models/user.rb +1 -0
  48. data/spec/models/writer.rb +2 -1
  49. data/spec/spec_helper.rb +0 -2
  50. data/spec/support/mongo_seed.rb +1 -0
  51. data/spec/support/sqlite_seed.rb +30 -0
  52. data/test.sh +2 -0
  53. metadata +13 -4
  54. data/.travis.yml +0 -33
@@ -5,5 +5,6 @@ module Bullet
5
5
  autoload :Base, 'bullet/registry/base'
6
6
  autoload :Object, 'bullet/registry/object'
7
7
  autoload :Association, 'bullet/registry/association'
8
+ autoload :CallStack, 'bullet/registry/call_stack'
8
9
  end
9
10
  end
@@ -1,18 +1,21 @@
1
1
  # frozen_string_literal: true
2
+ require "bundler"
2
3
 
3
4
  module Bullet
4
5
  module StackTraceFilter
5
6
  VENDOR_PATH = '/vendor'
6
7
  IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
7
8
 
8
- def caller_in_project
9
+ # @param bullet_key[String] - use this to get stored call stack from call_stacks object.
10
+ def caller_in_project(bullet_key = nil)
9
11
  vendor_root = Bullet.app_root + VENDOR_PATH
10
12
  bundler_path = Bundler.bundle_path.to_s
11
- select_caller_locations do |location|
13
+ select_caller_locations(bullet_key) do |location|
12
14
  caller_path = location_as_path(location)
13
15
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
14
- !caller_path.include?(bundler_path) ||
15
- Bullet.stacktrace_includes.any? { |include_pattern| pattern_matches?(location, include_pattern) }
16
+ !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
17
+ pattern_matches?(location, include_pattern)
18
+ }
16
19
  end
17
20
  end
18
21
 
@@ -48,15 +51,16 @@ module Bullet
48
51
  end
49
52
 
50
53
  def location_as_path(location)
54
+ return location if location.is_a?(String)
55
+
51
56
  IS_RUBY_19 ? location : location.absolute_path.to_s
52
57
  end
53
58
 
54
- def select_caller_locations
55
- if IS_RUBY_19
56
- caller.select { |caller_path| yield caller_path }
57
- else
58
- caller_locations.select { |location| yield location }
59
- end
59
+ def select_caller_locations(bullet_key = nil)
60
+ return caller.select { |caller_path| yield caller_path } if IS_RUBY_19
61
+
62
+ call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations
63
+ call_stack.select { |location| yield location }
60
64
  end
61
65
  end
62
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '6.1.4'
4
+ VERSION = '7.0.5'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -20,13 +20,14 @@ 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|
29
- app.middleware.use Bullet::Rack
26
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware)
27
+ app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
28
+ else
29
+ app.middleware.use Bullet::Rack
30
+ end
30
31
  end
31
32
  end
32
33
  end
@@ -38,10 +39,11 @@ module Bullet
38
39
  :stacktrace_includes,
39
40
  :stacktrace_excludes,
40
41
  :skip_html_injection
41
- attr_reader :whitelist
42
+ attr_reader :safelist
42
43
  attr_accessor :add_footer, :orm_patches_applied, :skip_http_headers
43
44
 
44
- available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
45
+ available_notifiers =
46
+ UniformNotifier::AVAILABLE_NOTIFIERS.select { |notifier| notifier != :raise }.map { |notifier| "#{notifier}=" }
45
47
  available_notifiers_options = { to: UniformNotifier }
46
48
  delegate(*available_notifiers, **available_notifiers_options)
47
49
 
@@ -59,7 +61,7 @@ module Bullet
59
61
  @enable = @n_plus_one_query_enable = @unused_eager_loading_enable = @counter_cache_enable = enable
60
62
 
61
63
  if enable?
62
- reset_whitelist
64
+ reset_safelist
63
65
  unless orm_patches_applied
64
66
  self.orm_patches_applied = true
65
67
  Bullet::Mongoid.enable if mongoid?
@@ -72,8 +74,9 @@ module Bullet
72
74
  !!@enable
73
75
  end
74
76
 
77
+ # Rails.root might be nil if `railties` is a dependency on a project that does not use Rails
75
78
  def app_root
76
- @app_root ||= (defined?(::Rails.root) ? Rails.root.to_s : Dir.pwd).to_s
79
+ @app_root ||= (defined?(::Rails.root) && !::Rails.root.nil? ? Rails.root.to_s : Dir.pwd).to_s
77
80
  end
78
81
 
79
82
  def n_plus_one_query_enable?
@@ -96,29 +99,29 @@ module Bullet
96
99
  @stacktrace_excludes ||= []
97
100
  end
98
101
 
99
- def add_whitelist(options)
100
- reset_whitelist
101
- @whitelist[options[:type]][options[:class_name]] ||= []
102
- @whitelist[options[:type]][options[:class_name]] << options[:association].to_sym
102
+ def add_safelist(options)
103
+ reset_safelist
104
+ @safelist[options[:type]][options[:class_name]] ||= []
105
+ @safelist[options[:type]][options[:class_name]] << options[:association].to_sym
103
106
  end
104
107
 
105
- def delete_whitelist(options)
106
- reset_whitelist
107
- @whitelist[options[:type]][options[:class_name]] ||= []
108
- @whitelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
109
- @whitelist[options[:type]].delete_if { |_key, val| val.empty? }
108
+ def delete_safelist(options)
109
+ reset_safelist
110
+ @safelist[options[:type]][options[:class_name]] ||= []
111
+ @safelist[options[:type]][options[:class_name]].delete(options[:association].to_sym)
112
+ @safelist[options[:type]].delete_if { |_key, val| val.empty? }
110
113
  end
111
114
 
112
- def get_whitelist_associations(type, class_name)
113
- Array(@whitelist[type][class_name])
115
+ def get_safelist_associations(type, class_name)
116
+ Array.wrap(@safelist[type][class_name])
114
117
  end
115
118
 
116
- def reset_whitelist
117
- @whitelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
119
+ def reset_safelist
120
+ @safelist ||= { n_plus_one_query: {}, unused_eager_loading: {}, counter_cache: {} }
118
121
  end
119
122
 
120
- def clear_whitelist
121
- @whitelist = nil
123
+ def clear_safelist
124
+ @safelist = nil
122
125
  end
123
126
 
124
127
  def bullet_logger=(active)
@@ -132,7 +135,7 @@ module Bullet
132
135
  end
133
136
 
134
137
  def debug(title, message)
135
- puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] == TRUE
138
+ puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
136
139
  end
137
140
 
138
141
  def start_request
@@ -145,6 +148,7 @@ module Bullet
145
148
  Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
146
149
  Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
147
150
  Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
151
+ Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
148
152
 
149
153
  Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
150
154
  Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
@@ -16,7 +16,6 @@ module Bullet
16
16
  Bullet.alert = true
17
17
  Bullet.bullet_logger = true
18
18
  Bullet.console = true
19
- # Bullet.growl = true
20
19
  Bullet.rails_logger = true
21
20
  Bullet.add_footer = true
22
21
  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) }
@@ -39,7 +39,7 @@ module Bullet
39
39
 
40
40
  it 'should be false if object, association pair is not existed' do
41
41
  NPlusOneQuery.add_object_associations(@post, :association1)
42
- expect(NPlusOneQuery.association?(@post, :associatio2)).to eq false
42
+ expect(NPlusOneQuery.association?(@post, :association2)).to eq false
43
43
  end
44
44
  end
45
45
 
@@ -127,38 +127,6 @@ module Bullet
127
127
  end
128
128
  end
129
129
 
130
- context '.caller_in_project' do
131
- it 'should include only paths that are in the project' do
132
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
133
- not_in_project = OpenStruct.new(absolute_path: '/def/def.rb')
134
-
135
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, not_in_project])
136
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
137
- expect(NPlusOneQuery).to receive(:create_notification).with([in_project], 'Post', :association)
138
- NPlusOneQuery.call_association(@post, :association)
139
- end
140
-
141
- context 'stacktrace_includes' do
142
- before { Bullet.stacktrace_includes = ['def', /xyz/] }
143
- after { Bullet.stacktrace_includes = nil }
144
-
145
- it 'should include paths that are in the stacktrace_include list' do
146
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
147
- included_gems = [OpenStruct.new(absolute_path: '/def/def.rb'), OpenStruct.new(absolute_path: 'xyz/xyz.rb')]
148
- excluded_gem = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
149
-
150
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, *included_gems, excluded_gem])
151
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
152
- expect(NPlusOneQuery).to receive(:create_notification).with(
153
- [in_project, *included_gems],
154
- 'Post',
155
- :association
156
- )
157
- NPlusOneQuery.call_association(@post, :association)
158
- end
159
- end
160
- end
161
-
162
130
  context '.add_possible_objects' do
163
131
  it 'should add possible objects' do
164
132
  NPlusOneQuery.add_possible_objects([@post, @post2])
@@ -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
@@ -61,6 +65,11 @@ module Bullet
61
65
  expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
62
66
  UnusedEagerLoading.check_unused_preload_associations
63
67
  end
68
+
69
+ it 'should create call stack for notification' do
70
+ UnusedEagerLoading.add_object_associations(@post, :association)
71
+ expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
72
+ end
64
73
  end
65
74
 
66
75
  context '.add_eager_loadings' 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
@@ -74,8 +74,8 @@ module Bullet
74
74
  it 'should send full_notice to notifier' do
75
75
  notifier = double
76
76
  allow(subject).to receive(:notifier).and_return(notifier)
77
- allow(subject).to receive(:notification_data).and_return(foo: :bar)
78
- expect(notifier).to receive(:inline_notify).with(foo: :bar)
77
+ allow(subject).to receive(:notification_data).and_return({ foo: :bar })
78
+ expect(notifier).to receive(:inline_notify).with({ foo: :bar })
79
79
  subject.notify_inline
80
80
  end
81
81
  end
@@ -84,8 +84,8 @@ module Bullet
84
84
  it 'should send full_out_of_channel to notifier' do
85
85
  notifier = double
86
86
  allow(subject).to receive(:notifier).and_return(notifier)
87
- allow(subject).to receive(:notification_data).and_return(foo: :bar)
88
- expect(notifier).to receive(:out_of_channel_notify).with(foo: :bar)
87
+ allow(subject).to receive(:notification_data).and_return({ foo: :bar })
88
+ expect(notifier).to receive(:out_of_channel_notify).with({ foo: :bar })
89
89
  subject.notify_out_of_channel
90
90
  end
91
91
  end
@@ -31,12 +31,6 @@ module Bullet
31
31
  response = double(body: '<html><head></head><body></body></html>')
32
32
  expect(middleware).not_to be_html_request(headers, response)
33
33
  end
34
-
35
- it "should be false if response body doesn't contain html tag" do
36
- headers = { 'Content-Type' => 'text/html' }
37
- response = double(body: '<div>Partial</div>')
38
- expect(middleware).not_to be_html_request(headers, response)
39
- end
40
34
  end
41
35
 
42
36
  context 'empty?' do
@@ -54,6 +48,11 @@ module Bullet
54
48
  response = double(body: '')
55
49
  expect(middleware).to be_empty(response)
56
50
  end
51
+
52
+ it 'should be true if no response body' do
53
+ response = double
54
+ expect(middleware).to be_empty(response)
55
+ end
57
56
  end
58
57
 
59
58
  context '#call' do
@@ -105,9 +104,10 @@ module Bullet
105
104
 
106
105
  it 'should change response body for html safe string if add_footer is true' do
107
106
  expect(Bullet).to receive(:add_footer).exactly(3).times.and_return(true)
108
- app.response = Support::ResponseDouble.new.tap do |response|
109
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
110
- end
107
+ app.response =
108
+ Support::ResponseDouble.new.tap do |response|
109
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
110
+ end
111
111
  _, headers, response = middleware.call('Content-Type' => 'text/html')
112
112
 
113
113
  expect(headers['Content-Length']).to eq((73 + middleware.send(:footer_note).length).to_s)
@@ -117,7 +117,7 @@ module Bullet
117
117
  it 'should add the footer-text header for non-html requests when add_footer is true' do
118
118
  allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
119
119
  allow(Bullet).to receive(:footer_info).and_return(['footer text'])
120
- app.headers = {'Content-Type' => 'application/json'}
120
+ app.headers = { 'Content-Type' => 'application/json' }
121
121
  _, headers, _response = middleware.call({})
122
122
  expect(headers).to include('X-bullet-footer-text' => '["footer text"]')
123
123
  end
@@ -129,11 +129,29 @@ module Bullet
129
129
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
130
130
  end
131
131
 
132
+ it 'should include CSP nonce in inline script if console_enabled and a CSP is applied' do
133
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
134
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
135
+ allow(middleware).to receive(:xhr_script).and_call_original
136
+
137
+ nonce = '+t9/wTlgG6xbHxXYUaDNzQ=='
138
+ app.headers = {
139
+ 'Content-Type' => 'text/html',
140
+ 'Content-Security-Policy' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'"
141
+ }
142
+
143
+ _, headers, response = middleware.call('Content-Type' => 'text/html')
144
+
145
+ size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length
146
+ expect(headers['Content-Length']).to eq(size.to_s)
147
+ end
148
+
132
149
  it 'should change response body for html safe string if console_enabled is true' do
133
150
  expect(Bullet).to receive(:console_enabled?).and_return(true)
134
- app.response = Support::ResponseDouble.new.tap do |response|
135
- response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
136
- end
151
+ app.response =
152
+ Support::ResponseDouble.new.tap do |response|
153
+ response.body = ActiveSupport::SafeBuffer.new('<html><head></head><body></body></html>')
154
+ end
137
155
  _, headers, response = middleware.call('Content-Type' => 'text/html')
138
156
  expect(headers['Content-Length']).to eq('56')
139
157
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
@@ -142,7 +160,7 @@ module Bullet
142
160
  it 'should add headers for non-html requests when console_enabled is true' do
143
161
  allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
144
162
  allow(Bullet).to receive(:text_notifications).and_return(['text notifications'])
145
- app.headers = {'Content-Type' => 'application/json'}
163
+ app.headers = { 'Content-Type' => 'application/json' }
146
164
  _, headers, _response = middleware.call({})
147
165
  expect(headers).to include('X-bullet-console-text' => '["text notifications"]')
148
166
  end
@@ -155,13 +173,13 @@ module Bullet
155
173
  end
156
174
 
157
175
  it "shouldn't add headers unnecessarily" do
158
- app.headers = {'Content-Type' => 'application/json'}
176
+ app.headers = { 'Content-Type' => 'application/json' }
159
177
  _, headers, _response = middleware.call({})
160
178
  expect(headers).not_to include('X-bullet-footer-text')
161
179
  expect(headers).not_to include('X-bullet-console-text')
162
180
  end
163
181
 
164
- context "when skip_http_headers is enabled" do
182
+ context 'when skip_http_headers is enabled' do
165
183
  before do
166
184
  allow(Bullet).to receive(:skip_http_headers).and_return(true)
167
185
  end
@@ -183,14 +201,14 @@ module Bullet
183
201
 
184
202
  it 'should not add the footer-text header for non-html requests when add_footer is true' do
185
203
  allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
186
- app.headers = {'Content-Type' => 'application/json'}
204
+ app.headers = { 'Content-Type' => 'application/json' }
187
205
  _, headers, _response = middleware.call({})
188
206
  expect(headers).not_to include('X-bullet-footer-text')
189
207
  end
190
208
 
191
209
  it 'should not add headers for non-html requests when console_enabled is true' do
192
210
  allow(Bullet).to receive(:console_enabled?).at_least(:once).and_return(true)
193
- app.headers = {'Content-Type' => 'application/json'}
211
+ app.headers = { 'Content-Type' => 'application/json' }
194
212
  _, headers, _response = middleware.call({})
195
213
  expect(headers).not_to include('X-bullet-console-text')
196
214
  end
@@ -259,6 +277,20 @@ module Bullet
259
277
  expect(middleware.response_body(response)).to eq body_string
260
278
  end
261
279
  end
280
+
281
+ begin
282
+ require 'rack/files'
283
+
284
+ context 'when `response` is a Rack::Files::Iterator' do
285
+ let(:response) { instance_double(::Rack::Files::Iterator) }
286
+ before { allow(response).to receive(:is_a?).with(::Rack::Files::Iterator) { true } }
287
+
288
+ it 'should return nil' do
289
+ expect(middleware.response_body(response)).to be_nil
290
+ end
291
+ end
292
+ rescue LoadError
293
+ end
262
294
  end
263
295
  end
264
296
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Bullet
6
+ RSpec.describe StackTraceFilter do
7
+ let(:dummy_class) { Class.new { extend StackTraceFilter } }
8
+ let(:root_path) { Dir.pwd }
9
+ let(:bundler_path) { Bundler.bundle_path }
10
+
11
+ describe '#caller_in_project' do
12
+ it 'gets the caller in the project' do
13
+ expect(dummy_class).to receive(:call_stacks).and_return({
14
+ 'Post:1' => [
15
+ File.join(root_path, 'lib/bullet.rb'),
16
+ File.join(root_path, 'vendor/uniform_notifier.rb'),
17
+ File.join(bundler_path, 'rack.rb')
18
+ ]
19
+ })
20
+ expect(dummy_class.caller_in_project('Post:1')).to eq([
21
+ File.join(root_path, 'lib/bullet.rb')
22
+ ])
23
+ end
24
+ end
25
+ end
26
+ end
data/spec/bullet_spec.rb CHANGED
@@ -74,31 +74,31 @@ describe Bullet, focused: true do
74
74
  end
75
75
  end
76
76
 
77
- describe '#add_whitelist' do
77
+ describe '#add_safelist' do
78
78
  context "for 'special' class names" do
79
- it 'is added to the whitelist successfully' do
80
- 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
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
82
  end
83
83
  end
84
84
  end
85
85
 
86
- describe '#delete_whitelist' do
86
+ describe '#delete_safelist' do
87
87
  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)
90
- Bullet.delete_whitelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
91
- expect(Bullet.whitelist[:n_plus_one_query]).to eq({})
88
+ it 'is deleted from the safelist successfully' do
89
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
90
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
91
+ expect(Bullet.safelist[:n_plus_one_query]).to eq({})
92
92
  end
93
93
  end
94
94
 
95
95
  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)
99
- 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
96
+ it 'is deleted from the safelist successfully' do
97
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :department)
98
+ Bullet.add_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
99
+ Bullet.delete_safelist(type: :n_plus_one_query, class_name: 'Klass', association: :team)
100
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to include :department
101
+ expect(Bullet.get_safelist_associations(:n_plus_one_query, 'Klass')).to_not include :team
102
102
  end
103
103
  end
104
104
  end
@@ -58,6 +58,19 @@ if active_record?
58
58
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
59
59
  end
60
60
 
61
+ it 'should detect non preload comment => post with inverse_of from a query' do
62
+ Post.first.comments.find_each do |comment|
63
+ comment.name
64
+ comment.post.name
65
+ end
66
+
67
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
68
+ expect(Post.first.comments.count).not_to eq(0)
69
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
70
+
71
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
72
+ end
73
+
61
74
  it 'should detect non preload post => comments with empty?' do
62
75
  Post.all.each { |post| post.comments.empty? }
63
76
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
@@ -129,7 +142,7 @@ if active_record?
129
142
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
130
143
  end
131
144
 
132
- it 'should detect unused preload with post => commnets, no category => posts' do
145
+ it 'should detect unused preload with post => comments, no category => posts' do
133
146
  Category.includes(posts: :comments).each { |category| category.posts.map(&:name) }
134
147
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
135
148
  expect(Bullet::Detector::Association).to be_unused_preload_associations_for(Post, :comments)
@@ -202,7 +215,7 @@ if active_record?
202
215
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
203
216
  end
204
217
 
205
- it 'should detect preload with post => commnets' do
218
+ it 'should detect preload with post => comments' do
206
219
  Post.first.comments.map(&:name)
207
220
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
208
221
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
@@ -401,6 +414,15 @@ if active_record?
401
414
  end
402
415
 
403
416
  describe Bullet::Detector::Association, 'has_and_belongs_to_many' do
417
+ context 'posts <=> deals' do
418
+ it 'should detect preload associations with join tables that have identifier' do
419
+ Post.includes(:deals).each { |post| post.deals.map(&:name) }
420
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
421
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
422
+
423
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
424
+ end
425
+ end
404
426
  context 'students <=> teachers' do
405
427
  it 'should detect non preload associations' do
406
428
  Student.all.each { |student| student.teachers.map(&:name) }
@@ -442,6 +464,16 @@ if active_record?
442
464
  expect(Bullet::Detector::Association).to be_detecting_unpreloaded_association_for(Student, :teachers)
443
465
  end
444
466
  end
467
+
468
+ context 'user => roles' do
469
+ it 'should detect preload associations' do
470
+ User.first.roles.includes(:resource).each { |role| role.resource }
471
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
472
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
473
+
474
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
475
+ end
476
+ end
445
477
  end
446
478
 
447
479
  describe Bullet::Detector::Association, 'has_many :through' do
@@ -455,7 +487,15 @@ if active_record?
455
487
  end
456
488
 
457
489
  it 'should detect preload associations' do
458
- Firm.includes(:clients).each { |firm| firm.clients.map(&:name) }
490
+ Firm.preload(:clients).each { |firm| firm.clients.map(&:name) }
491
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
492
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
493
+
494
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
495
+ end
496
+
497
+ it 'should detect eager load association' do
498
+ Firm.eager_load(:clients).each { |firm| firm.clients.map(&:name) }
459
499
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
460
500
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
461
501
 
@@ -489,7 +529,15 @@ if active_record?
489
529
  end
490
530
 
491
531
  it 'should detect preload associations' do
492
- Firm.includes(:groups).each { |firm| firm.groups.map(&:name) }
532
+ Firm.preload(:groups).each { |firm| firm.groups.map(&:name) }
533
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
534
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
535
+
536
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
537
+ end
538
+
539
+ it 'should detect eager load associations' do
540
+ Firm.eager_load(:groups).each { |firm| firm.groups.map(&:name) }
493
541
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
494
542
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
495
543
 
@@ -729,9 +777,9 @@ if active_record?
729
777
  end
730
778
  end
731
779
 
732
- context 'whitelist n plus one query' do
733
- before { Bullet.add_whitelist type: :n_plus_one_query, class_name: 'Post', association: :comments }
734
- after { Bullet.clear_whitelist }
780
+ context 'add n plus one query to safelist' do
781
+ before { Bullet.add_safelist type: :n_plus_one_query, class_name: 'Post', association: :comments }
782
+ after { Bullet.clear_safelist }
735
783
 
736
784
  it 'should not detect n plus one query' do
737
785
  Post.all.each { |post| post.comments.map(&:name) }
@@ -750,9 +798,9 @@ if active_record?
750
798
  end
751
799
  end
752
800
 
753
- context 'whitelist unused eager loading' do
754
- before { Bullet.add_whitelist type: :unused_eager_loading, class_name: 'Post', association: :comments }
755
- after { Bullet.clear_whitelist }
801
+ context 'add unused eager loading to safelist' do
802
+ before { Bullet.add_safelist type: :unused_eager_loading, class_name: 'Post', association: :comments }
803
+ after { Bullet.clear_safelist }
756
804
 
757
805
  it 'should not detect unused eager loading' do
758
806
  Post.includes(:comments).map(&:name)