bullet 4.14.0 → 5.0.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +56 -1
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile.mongoid +0 -2
  5. data/Gemfile.mongoid-2.4 +2 -4
  6. data/Gemfile.mongoid-2.5 +2 -4
  7. data/Gemfile.mongoid-2.6 +1 -3
  8. data/Gemfile.mongoid-2.7 +2 -4
  9. data/Gemfile.mongoid-2.8 +2 -4
  10. data/Gemfile.mongoid-3.0 +2 -4
  11. data/Gemfile.mongoid-3.1 +2 -4
  12. data/Gemfile.mongoid-4.0 +1 -3
  13. data/Gemfile.mongoid-5.0 +17 -0
  14. data/Gemfile.rails-3.0 +1 -3
  15. data/Gemfile.rails-3.1 +1 -3
  16. data/Gemfile.rails-3.2 +1 -3
  17. data/Gemfile.rails-4.0 +1 -3
  18. data/Gemfile.rails-4.1 +1 -3
  19. data/Gemfile.rails-4.2 +1 -3
  20. data/Gemfile.rails-5.0 +17 -0
  21. data/README.md +31 -2
  22. data/bullet.gemspec +1 -1
  23. data/lib/bullet/active_record3.rb +65 -35
  24. data/lib/bullet/active_record3x.rb +54 -32
  25. data/lib/bullet/active_record4.rb +62 -30
  26. data/lib/bullet/active_record41.rb +63 -32
  27. data/lib/bullet/active_record42.rb +105 -40
  28. data/lib/bullet/active_record5.rb +217 -0
  29. data/lib/bullet/dependency.rb +16 -0
  30. data/lib/bullet/detector/association.rb +16 -16
  31. data/lib/bullet/detector/counter_cache.rb +13 -13
  32. data/lib/bullet/detector/n_plus_one_query.rb +33 -24
  33. data/lib/bullet/detector/unused_eager_loading.rb +1 -1
  34. data/lib/bullet/ext/object.rb +3 -1
  35. data/lib/bullet/mongoid5x.rb +56 -0
  36. data/lib/bullet/notification/base.rb +1 -1
  37. data/lib/bullet/notification/n_plus_one_query.rb +6 -0
  38. data/lib/bullet/rack.rb +19 -11
  39. data/lib/bullet/version.rb +1 -1
  40. data/lib/bullet.rb +28 -11
  41. data/spec/bullet/detector/counter_cache_spec.rb +8 -8
  42. data/spec/bullet/detector/n_plus_one_query_spec.rb +42 -27
  43. data/spec/bullet/ext/object_spec.rb +6 -0
  44. data/spec/bullet/notification/base_spec.rb +2 -2
  45. data/spec/bullet/notification/n_plus_one_query_spec.rb +3 -3
  46. data/spec/bullet/notification/unused_eager_loading_spec.rb +1 -1
  47. data/spec/bullet/rack_spec.rb +3 -3
  48. data/spec/bullet_spec.rb +47 -0
  49. data/spec/integration/active_record3/association_spec.rb +11 -2
  50. data/spec/integration/active_record4/association_spec.rb +58 -3
  51. data/spec/integration/active_record5/association_spec.rb +715 -0
  52. data/spec/integration/counter_cache_spec.rb +17 -1
  53. data/spec/models/category.rb +4 -1
  54. data/spec/models/comment.rb +2 -0
  55. data/spec/models/post.rb +7 -2
  56. data/spec/models/reply.rb +3 -0
  57. data/spec/models/submission.rb +1 -1
  58. data/spec/spec_helper.rb +3 -2
  59. data/spec/support/mongo_seed.rb +13 -0
  60. data/spec/support/sqlite_seed.rb +14 -6
  61. data/test.sh +1 -0
  62. data/update.sh +15 -0
  63. metadata +16 -7
@@ -20,6 +20,22 @@ module Bullet
20
20
  call_object_associations.add(object.bullet_key, associations)
21
21
  end
22
22
 
23
+ # possible_objects keep the class to object relationships
24
+ # that the objects may cause N+1 query.
25
+ # e.g. { Post => ["Post:1", "Post:2"] }
26
+ def possible_objects
27
+ Thread.current[:bullet_possible_objects]
28
+ end
29
+
30
+ # impossible_objects keep the class to objects relationships
31
+ # that the objects may not cause N+1 query.
32
+ # e.g. { Post => ["Post:1", "Post:2"] }
33
+ # if find collection returns only one object, then the object is impossible object,
34
+ # impossible_objects are used to avoid treating 1+1 query to N+1 query.
35
+ def impossible_objects
36
+ Thread.current[:bullet_impossible_objects]
37
+ end
38
+
23
39
  private
24
40
  # object_associations keep the object relationships
25
41
  # that the object has many associations.
@@ -38,22 +54,6 @@ module Bullet
38
54
  Thread.current[:bullet_call_object_associations]
39
55
  end
40
56
 
41
- # possible_objects keep the class to object relationships
42
- # that the objects may cause N+1 query.
43
- # e.g. { Post => ["Post:1", "Post:2"] }
44
- def possible_objects
45
- Thread.current[:bullet_possible_objects]
46
- end
47
-
48
- # impossible_objects keep the class to objects relationships
49
- # that the objects may not cause N+1 query.
50
- # e.g. { Post => ["Post:1", "Post:2"] }
51
- # if find collection returns only one object, then the object is impossible object,
52
- # impossible_objects are used to avoid treating 1+1 query to N+1 query.
53
- def impossible_objects
54
- Thread.current[:bullet_impossible_objects]
55
- end
56
-
57
57
  # inversed_objects keeps object relationships
58
58
  # that association is inversed.
59
59
  # e.g. { "Comment:1" => ["post"] }
@@ -8,7 +8,7 @@ module Bullet
8
8
  return unless object.primary_key_value
9
9
 
10
10
  Bullet.debug("Detector::CounterCache#add_counter_cache", "object: #{object.bullet_key}, associations: #{associations}")
11
- if conditions_met?(object.bullet_key, associations)
11
+ if conditions_met?(object, associations)
12
12
  create_notification object.class.to_s, associations
13
13
  end
14
14
  end
@@ -32,6 +32,18 @@ module Bullet
32
32
  impossible_objects.add object.bullet_key
33
33
  end
34
34
 
35
+ def conditions_met?(object, associations)
36
+ possible_objects.include?(object.bullet_key) && !impossible_objects.include?(object.bullet_key)
37
+ end
38
+
39
+ def possible_objects
40
+ Thread.current[:bullet_counter_possible_objects]
41
+ end
42
+
43
+ def impossible_objects
44
+ Thread.current[:bullet_counter_impossible_objects]
45
+ end
46
+
35
47
  private
36
48
  def create_notification(klazz, associations)
37
49
  notify_associations = Array(associations) - Bullet.get_whitelist_associations(:counter_cache, klazz)
@@ -41,18 +53,6 @@ module Bullet
41
53
  Bullet.notification_collector.add notice
42
54
  end
43
55
  end
44
-
45
- def possible_objects
46
- Thread.current[:bullet_counter_possible_objects]
47
- end
48
-
49
- def impossible_objects
50
- Thread.current[:bullet_counter_impossible_objects]
51
- end
52
-
53
- def conditions_met?(bullet_key, associations)
54
- possible_objects.include?(bullet_key) && !impossible_objects.include?(bullet_key)
55
- end
56
56
  end
57
57
  end
58
58
  end
@@ -15,7 +15,7 @@ module Bullet
15
15
  add_call_object_associations(object, associations)
16
16
 
17
17
  Bullet.debug("Detector::NPlusOneQuery#call_association", "object: #{object.bullet_key}, associations: #{associations}")
18
- if conditions_met?(object.bullet_key, associations)
18
+ if !excluded_stacktrace_path? && conditions_met?(object, associations)
19
19
  Bullet.debug("detect n + 1 query", "object: #{object.bullet_key}, associations: #{associations}")
20
20
  create_notification caller_in_project, object.class.to_s, associations
21
21
  end
@@ -49,6 +49,35 @@ module Bullet
49
49
  inversed_objects.add object.bullet_key, association
50
50
  end
51
51
 
52
+ # decide whether the object.associations is unpreloaded or not.
53
+ def conditions_met?(object, associations)
54
+ possible?(object) && !impossible?(object) && !association?(object, associations)
55
+ end
56
+
57
+ def possible?(object)
58
+ possible_objects.include? object.bullet_key
59
+ end
60
+
61
+ def impossible?(object)
62
+ impossible_objects.include? object.bullet_key
63
+ end
64
+
65
+ # check if object => associations already exists in object_associations.
66
+ def association?(object, associations)
67
+ value = object_associations[object.bullet_key]
68
+ if value
69
+ value.each do |v|
70
+ # associations == v comparision order is important here because
71
+ # v variable might be a squeel node where :== method is redefined,
72
+ # so it does not compare values at all and return unexpected results
73
+ result = v.is_a?(Hash) ? v.key?(associations) : associations == v
74
+ return true if result
75
+ end
76
+ end
77
+
78
+ false
79
+ end
80
+
52
81
  private
53
82
  def create_notification(callers, klazz, associations)
54
83
  notify_associations = Array(associations) - Bullet.get_whitelist_associations(:n_plus_one_query, klazz)
@@ -59,11 +88,6 @@ module Bullet
59
88
  end
60
89
  end
61
90
 
62
- # decide whether the object.associations is unpreloaded or not.
63
- def conditions_met?(bullet_key, associations)
64
- possible?(bullet_key) && !impossible?(bullet_key) && !association?(bullet_key, associations)
65
- end
66
-
67
91
  def caller_in_project
68
92
  app_root = rails? ? Rails.root.to_s : Dir.pwd
69
93
  vendor_root = app_root + "/vendor"
@@ -73,25 +97,10 @@ module Bullet
73
97
  end
74
98
  end
75
99
 
76
- def possible?(bullet_key)
77
- possible_objects.include? bullet_key
78
- end
79
-
80
- def impossible?(bullet_key)
81
- impossible_objects.include? bullet_key
82
- end
83
-
84
- # check if object => associations already exists in object_associations.
85
- def association?(bullet_key, associations)
86
- value = object_associations[bullet_key]
87
- if value
88
- value.each do |v|
89
- result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
90
- return true if result
91
- end
100
+ def excluded_stacktrace_path?
101
+ Bullet.stacktrace_excludes.any? do |excluded_path|
102
+ caller_in_project.any? { |c| c.include?(excluded_path) }
92
103
  end
93
-
94
- return false
95
104
  end
96
105
  end
97
106
  end
@@ -48,7 +48,7 @@ module Bullet
48
48
  end
49
49
 
50
50
  eager_loadings.add(*to_add) if to_add
51
- to_merge.each { |k,val| eager_loadings.merge k, val }
51
+ to_merge.each { |k, val| eager_loadings.merge k, val }
52
52
  to_delete.each { |k| eager_loadings.delete k }
53
53
 
54
54
  eager_loadings.add bullet_keys, associations unless bullet_keys.empty?
@@ -4,7 +4,9 @@ class Object
4
4
  end
5
5
 
6
6
  def primary_key_value
7
- if self.class.respond_to?(:primary_key) && self.class.primary_key
7
+ if self.class.respond_to?(:primary_keys) && self.class.primary_keys
8
+ self.class.primary_keys.map { |primary_key| self.send primary_key }.join(',')
9
+ elsif self.class.respond_to?(:primary_key) && self.class.primary_key
8
10
  self.send self.class.primary_key
9
11
  else
10
12
  self.id
@@ -0,0 +1,56 @@
1
+ module Bullet
2
+ module Mongoid
3
+ def self.enable
4
+ require 'mongoid'
5
+ ::Mongoid::Contextual::Mongo.class_eval do
6
+ alias_method :origin_first, :first
7
+ alias_method :origin_last, :last
8
+ alias_method :origin_each, :each
9
+ alias_method :origin_eager_load, :eager_load
10
+
11
+ def first
12
+ result = origin_first
13
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
14
+ result
15
+ end
16
+
17
+ def last
18
+ result = origin_last
19
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
20
+ result
21
+ end
22
+
23
+ def each(&block)
24
+ records = view.map{ |doc| ::Mongoid::Factory.from_db(klass, doc) }
25
+ if records.length > 1
26
+ Bullet::Detector::NPlusOneQuery.add_possible_objects(records)
27
+ elsif records.size == 1
28
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(records.first)
29
+ end
30
+ origin_each(&block)
31
+ end
32
+
33
+ def eager_load(docs)
34
+ associations = criteria.inclusions.map(&:name)
35
+ docs.each do |doc|
36
+ Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
37
+ end
38
+ Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
39
+ origin_eager_load(docs)
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Relations::Accessors.class_eval do
44
+ alias_method :origin_get_relation, :get_relation
45
+
46
+ def get_relation(name, metadata, object, reload = false)
47
+ result = origin_get_relation(name, metadata, object, reload)
48
+ if metadata.macro !~ /embed/
49
+ Bullet::Detector::NPlusOneQuery.call_association(self, name)
50
+ end
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -70,7 +70,7 @@ module Bullet
70
70
  end
71
71
 
72
72
  def associations_str
73
- ":include => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
73
+ ":includes => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
74
74
  end
75
75
  end
76
76
  end
@@ -15,6 +15,12 @@ module Bullet
15
15
  "N+1 Query #{@path ? "in #{@path}" : 'detected'}"
16
16
  end
17
17
 
18
+ def notification_data
19
+ super.merge(
20
+ :backtrace => @callers
21
+ )
22
+ end
23
+
18
24
  protected
19
25
  def call_stack_messages
20
26
  (['N+1 Query method call stack'] + @callers).join( "\n " )
data/lib/bullet/rack.rb CHANGED
@@ -10,17 +10,16 @@ module Bullet
10
10
  return @app.call(env) unless Bullet.enable?
11
11
  Bullet.start_request
12
12
  status, headers, response = @app.call(env)
13
- return [status, headers, response] if file?(headers) || sse?(response) || empty?(response)
14
13
 
15
14
  response_body = nil
16
15
  if Bullet.notification?
17
- if status == 200 && !response_body(response).frozen? && html_request?(headers, response)
18
- response_body = response_body(response) << Bullet.gather_inline_notifications
19
- add_footer_note(response_body) if Bullet.add_footer
16
+ if !file?(headers) && !sse?(headers) && !empty?(response) &&
17
+ status == 200 && !response_body(response).frozen? && html_request?(headers, response)
18
+ response_body = response_body(response)
19
+ append_to_html_body(response_body, footer_note) if Bullet.add_footer
20
+ append_to_html_body(response_body, Bullet.gather_inline_notifications)
20
21
  headers['Content-Length'] = response_body.bytesize.to_s
21
22
  end
22
- end
23
- if Bullet.enable? && Bullet.notification?
24
23
  Bullet.perform_out_of_channel_notifications(env)
25
24
  end
26
25
  [status, headers, response_body ? [response_body] : response]
@@ -42,16 +41,25 @@ module Bullet
42
41
  end
43
42
  end
44
43
 
45
- def add_footer_note(response_body)
46
- response_body << "<div #{footer_div_attributes}>" + Bullet.footer_info.uniq.join("<br>") + "</div>"
44
+ def append_to_html_body(response_body, content)
45
+ if response_body.include?('</body>')
46
+ position = response_body.rindex('</body>')
47
+ response_body.insert(position, content)
48
+ else
49
+ response_body << content
50
+ end
51
+ end
52
+
53
+ def footer_note
54
+ "<div #{footer_div_attributes}>" + Bullet.footer_info.uniq.join("<br>") + "</div>"
47
55
  end
48
56
 
49
57
  def file?(headers)
50
58
  headers["Content-Transfer-Encoding"] == "binary"
51
59
  end
52
60
 
53
- def sse?(response)
54
- response.respond_to?(:stream) && response.stream.is_a?(ActionController::Live::Buffer)
61
+ def sse?(headers)
62
+ headers["Content-Type"] == "text/event-stream"
55
63
  end
56
64
 
57
65
  def html_request?(headers, response)
@@ -69,7 +77,7 @@ module Bullet
69
77
  private
70
78
  def footer_div_attributes
71
79
  <<EOF
72
- data-is-bullet-footer style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
80
+ data-is-bullet-footer ondblclick="this.parentNode.removeChild(this);" style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
73
81
  -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
74
82
  -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
75
83
  padding: 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Bullet
3
- VERSION = "4.14.0"
3
+ VERSION = "5.0.0"
4
4
  end
data/lib/bullet.rb CHANGED
@@ -16,6 +16,9 @@ module Bullet
16
16
  autoload :Registry, 'bullet/registry'
17
17
  autoload :NotificationCollector, 'bullet/notification_collector'
18
18
 
19
+ BULLET_DEBUG = 'BULLET_DEBUG'.freeze
20
+ TRUE = 'true'.freeze
21
+
19
22
  if defined? Rails::Railtie
20
23
  class BulletRailtie < Rails::Railtie
21
24
  initializer "bullet.configure_rails_initialization" do |app|
@@ -25,11 +28,13 @@ module Bullet
25
28
  end
26
29
 
27
30
  class << self
28
- attr_writer :enable, :n_plus_one_query_enable, :unused_eager_loading_enable, :counter_cache_enable, :stacktrace_includes
31
+ attr_writer :enable, :n_plus_one_query_enable, :unused_eager_loading_enable, :counter_cache_enable, :stacktrace_includes, :stacktrace_excludes
29
32
  attr_reader :notification_collector, :whitelist
30
33
  attr_accessor :add_footer, :orm_pathches_applied
31
34
 
32
- delegate :alert=, :console=, :growl=, :rails_logger=, :xmpp=, :airbrake=, :bugsnag=, :to => UniformNotifier
35
+ available_notifiers = UniformNotifier::AVAILABLE_NOTIFIERS.map { |notifier| "#{notifier}=" }
36
+ available_notifiers << { :to => UniformNotifier }
37
+ delegate *available_notifiers
33
38
 
34
39
  def raise=(should_raise)
35
40
  UniformNotifier.raise=(should_raise ? Notification::UnoptimizedQueryError : false)
@@ -71,7 +76,12 @@ module Bullet
71
76
  @stacktrace_includes || []
72
77
  end
73
78
 
79
+ def stacktrace_excludes
80
+ @stacktrace_excludes || []
81
+ end
82
+
74
83
  def add_whitelist(options)
84
+ reset_whitelist
75
85
  @whitelist[options[:type]][options[:class_name].classify] ||= []
76
86
  @whitelist[options[:type]][options[:class_name].classify] << options[:association].to_sym
77
87
  end
@@ -81,7 +91,11 @@ module Bullet
81
91
  end
82
92
 
83
93
  def reset_whitelist
84
- @whitelist = {:n_plus_one_query => {}, :unused_eager_loading => {}, :counter_cache => {}}
94
+ @whitelist ||= {:n_plus_one_query => {}, :unused_eager_loading => {}, :counter_cache => {}}
95
+ end
96
+
97
+ def clear_whitelist
98
+ @whitelist = nil
85
99
  end
86
100
 
87
101
  def bullet_logger=(active)
@@ -96,7 +110,7 @@ module Bullet
96
110
  end
97
111
 
98
112
  def debug(title, message)
99
- puts "[Bullet][#{title}] #{message}" if ENV['BULLET_DEBUG'] == 'true'
113
+ puts "[Bullet][#{title}] #{message}" if ENV[BULLET_DEBUG] == TRUE
100
114
  end
101
115
 
102
116
  def start_request
@@ -130,7 +144,7 @@ module Bullet
130
144
  end
131
145
 
132
146
  def start?
133
- Thread.current[:bullet_start]
147
+ enable? && Thread.current[:bullet_start]
134
148
  end
135
149
 
136
150
  def notification_collector
@@ -160,7 +174,7 @@ module Bullet
160
174
 
161
175
  def footer_info
162
176
  info = []
163
- for_each_active_notifier_with_notification do |notification|
177
+ notification_collector.collection.each do |notification|
164
178
  info << notification.short_notice
165
179
  end
166
180
  info
@@ -176,14 +190,17 @@ module Bullet
176
190
  end
177
191
 
178
192
  def profile
179
- Bullet.start_request if Bullet.enable?
193
+ if Bullet.enable?
194
+ begin
195
+ Bullet.start_request
180
196
 
181
- yield
197
+ yield
182
198
 
183
- if Bullet.enable? && Bullet.notification?
184
- Bullet.perform_out_of_channel_notifications
199
+ Bullet.perform_out_of_channel_notifications if Bullet.notification?
200
+ ensure
201
+ Bullet.end_request
202
+ end
185
203
  end
186
- Bullet.end_request if Bullet.enable?
187
204
  end
188
205
 
189
206
  private
@@ -10,13 +10,13 @@ module Bullet
10
10
 
11
11
  context ".add_counter_cache" do
12
12
  it "should create notification if conditions met" do
13
- expect(CounterCache).to receive(:conditions_met?).with(@post1.bullet_key, [:comments]).and_return(true)
13
+ expect(CounterCache).to receive(:conditions_met?).with(@post1, [:comments]).and_return(true)
14
14
  expect(CounterCache).to receive(:create_notification).with("Post", [:comments])
15
15
  CounterCache.add_counter_cache(@post1, [:comments])
16
16
  end
17
17
 
18
18
  it "should not create notification if conditions not met" do
19
- expect(CounterCache).to receive(:conditions_met?).with(@post1.bullet_key, [:comments]).and_return(false)
19
+ expect(CounterCache).to receive(:conditions_met?).with(@post1, [:comments]).and_return(false)
20
20
  expect(CounterCache).to receive(:create_notification).never
21
21
  CounterCache.add_counter_cache(@post1, [:comments])
22
22
  end
@@ -25,30 +25,30 @@ module Bullet
25
25
  context ".add_possible_objects" do
26
26
  it "should add possible objects" do
27
27
  CounterCache.add_possible_objects([@post1, @post2])
28
- expect(CounterCache.send(:possible_objects)).to be_include(@post1.bullet_key)
29
- expect(CounterCache.send(:possible_objects)).to be_include(@post2.bullet_key)
28
+ expect(CounterCache.possible_objects).to be_include(@post1.bullet_key)
29
+ expect(CounterCache.possible_objects).to be_include(@post2.bullet_key)
30
30
  end
31
31
 
32
32
  it "should add impossible object" do
33
33
  CounterCache.add_impossible_object(@post1)
34
- expect(CounterCache.send(:impossible_objects)).to be_include(@post1.bullet_key)
34
+ expect(CounterCache.impossible_objects).to be_include(@post1.bullet_key)
35
35
  end
36
36
  end
37
37
 
38
38
  context ".conditions_met?" do
39
39
  it "should be true when object is possible, not impossible" do
40
40
  CounterCache.add_possible_objects(@post1)
41
- expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq true
41
+ expect(CounterCache.conditions_met?(@post1, :associations)).to eq true
42
42
  end
43
43
 
44
44
  it "should be false when object is not possible" do
45
- expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq false
45
+ expect(CounterCache.conditions_met?(@post1, :associations)).to eq false
46
46
  end
47
47
 
48
48
  it "should be true when object is possible, and impossible" do
49
49
  CounterCache.add_possible_objects(@post1)
50
50
  CounterCache.add_impossible_object(@post1)
51
- expect(CounterCache.send(:conditions_met?, @post1.bullet_key, :associations)).to eq false
51
+ expect(CounterCache.conditions_met?(@post1, :associations)).to eq false
52
52
  end
53
53
  end
54
54
  end
@@ -18,73 +18,88 @@ module Bullet
18
18
  context ".possible?" do
19
19
  it "should be true if possible_objects contain" do
20
20
  NPlusOneQuery.add_possible_objects(@post)
21
- expect(NPlusOneQuery.send(:possible?, @post.bullet_key)).to eq true
21
+ expect(NPlusOneQuery.possible?(@post)).to eq true
22
22
  end
23
23
  end
24
24
 
25
25
  context ".impossible?" do
26
26
  it "should be true if impossible_objects contain" do
27
27
  NPlusOneQuery.add_impossible_object(@post)
28
- expect(NPlusOneQuery.send(:impossible?, @post.bullet_key)).to eq true
28
+ expect(NPlusOneQuery.impossible?(@post)).to eq true
29
29
  end
30
30
  end
31
31
 
32
32
  context ".association?" do
33
33
  it "should be true if object, associations pair is already existed" do
34
34
  NPlusOneQuery.add_object_associations(@post, :association)
35
- expect(NPlusOneQuery.send(:association?, @post.bullet_key, :association)).to eq true
35
+ expect(NPlusOneQuery.association?(@post, :association)).to eq true
36
36
  end
37
37
 
38
38
  it "should be false if object, association pair is not existed" do
39
39
  NPlusOneQuery.add_object_associations(@post, :association1)
40
- expect(NPlusOneQuery.send(:association?, @post.bullet_key, :associatio2)).to eq false
40
+ expect(NPlusOneQuery.association?(@post, :associatio2)).to eq false
41
41
  end
42
42
  end
43
43
 
44
44
  context ".conditions_met?" do
45
45
  it "should be true if object is possible, not impossible and object, associations pair is not already existed" do
46
- allow(NPlusOneQuery).to receive(:possible?).with(@post.bullet_key).and_return(true)
47
- allow(NPlusOneQuery).to receive(:impossible?).with(@post.bullet_key).and_return(false)
48
- allow(NPlusOneQuery).to receive(:association?).with(@post.bullet_key, :associations).and_return(false)
49
- expect(NPlusOneQuery.send(:conditions_met?, @post.bullet_key, :associations)).to eq true
46
+ allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
47
+ allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
48
+ allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
49
+ expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq true
50
50
  end
51
51
 
52
52
  it "should be false if object is not possible, not impossible and object, associations pair is not already existed" do
53
- allow(NPlusOneQuery).to receive(:possible?).with(@post.bullet_key).and_return(false)
54
- allow(NPlusOneQuery).to receive(:impossible?).with(@post.bullet_key).and_return(false)
55
- allow(NPlusOneQuery).to receive(:association?).with(@post.bullet_key, :associations).and_return(false)
56
- expect(NPlusOneQuery.send(:conditions_met?, @post.bullet_key, :associations)).to eq false
53
+ allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(false)
54
+ allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
55
+ allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
56
+ expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
57
57
  end
58
58
 
59
59
  it "should be false if object is possible, but impossible and object, associations pair is not already existed" do
60
- allow(NPlusOneQuery).to receive(:possible?).with(@post.bullet_key).and_return(true)
61
- allow(NPlusOneQuery).to receive(:impossible?).with(@post.bullet_key).and_return(true)
62
- allow(NPlusOneQuery).to receive(:association?).with(@post.bullet_key, :associations).and_return(false)
63
- expect(NPlusOneQuery.send(:conditions_met?, @post.bullet_key, :associations)).to eq false
60
+ allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
61
+ allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(true)
62
+ allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
63
+ expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
64
64
  end
65
65
 
66
66
  it "should be false if object is possible, not impossible and object, associations pair is already existed" do
67
- allow(NPlusOneQuery).to receive(:possible?).with(@post.bullet_key).and_return(true)
68
- allow(NPlusOneQuery).to receive(:impossible?).with(@post.bullet_key).and_return(false)
69
- allow(NPlusOneQuery).to receive(:association?).with(@post.bullet_key, :associations).and_return(true)
70
- expect(NPlusOneQuery.send(:conditions_met?, @post.bullet_key, :associations)).to eq false
67
+ allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
68
+ allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
69
+ allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(true)
70
+ expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
71
71
  end
72
72
  end
73
73
 
74
74
  context ".call_association" do
75
75
  it "should create notification if conditions met" do
76
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post.bullet_key, :association).and_return(true)
76
+ expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
77
77
  expect(NPlusOneQuery).to receive(:caller_in_project).and_return(["caller"])
78
78
  expect(NPlusOneQuery).to receive(:create_notification).with(["caller"], "Post", :association)
79
79
  NPlusOneQuery.call_association(@post, :association)
80
80
  end
81
81
 
82
82
  it "should not create notification if conditions not met" do
83
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post.bullet_key, :association).and_return(false)
83
+ expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(false)
84
84
  expect(NPlusOneQuery).not_to receive(:caller_in_project!)
85
85
  expect(NPlusOneQuery).not_to receive(:create_notification).with("Post", :association)
86
86
  NPlusOneQuery.call_association(@post, :association)
87
87
  end
88
+
89
+ context "stacktrace_excludes" do
90
+ before { Bullet.stacktrace_excludes = [ 'def' ] }
91
+ after { Bullet.stacktrace_excludes = nil }
92
+
93
+ it "should not create notification when stacktrace contains paths that are in the exclude list" do
94
+ in_project = File.join(Dir.pwd, 'abc', 'abc.rb')
95
+ included_path = '/ghi/ghi.rb'
96
+ excluded_path = '/def/def.rb'
97
+
98
+ expect(NPlusOneQuery).to receive(:caller).and_return([in_project, included_path, excluded_path])
99
+ expect(NPlusOneQuery).to_not receive(:create_notification)
100
+ NPlusOneQuery.call_association(@post, :association)
101
+ end
102
+ end
88
103
  end
89
104
 
90
105
  context ".caller_in_project" do
@@ -93,7 +108,7 @@ module Bullet
93
108
  not_in_project = '/def/def.rb'
94
109
 
95
110
  expect(NPlusOneQuery).to receive(:caller).and_return([in_project, not_in_project])
96
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post.bullet_key, :association).and_return(true)
111
+ expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
97
112
  expect(NPlusOneQuery).to receive(:create_notification).with([in_project], "Post", :association)
98
113
  NPlusOneQuery.call_association(@post, :association)
99
114
  end
@@ -108,7 +123,7 @@ module Bullet
108
123
  excluded_gem = '/ghi/ghi.rb'
109
124
 
110
125
  expect(NPlusOneQuery).to receive(:caller).and_return([in_project, included_gem, excluded_gem])
111
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post.bullet_key, :association).and_return(true)
126
+ expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
112
127
  expect(NPlusOneQuery).to receive(:create_notification).with([in_project, included_gem], "Post", :association)
113
128
  NPlusOneQuery.call_association(@post, :association)
114
129
  end
@@ -118,8 +133,8 @@ module Bullet
118
133
  context ".add_possible_objects" do
119
134
  it "should add possible objects" do
120
135
  NPlusOneQuery.add_possible_objects([@post, @post2])
121
- expect(NPlusOneQuery.send(:possible_objects)).to be_include(@post.bullet_key)
122
- expect(NPlusOneQuery.send(:possible_objects)).to be_include(@post2.bullet_key)
136
+ expect(NPlusOneQuery.possible_objects).to be_include(@post.bullet_key)
137
+ expect(NPlusOneQuery.possible_objects).to be_include(@post2.bullet_key)
123
138
  end
124
139
 
125
140
  it "should not raise error if object is nil" do
@@ -130,7 +145,7 @@ module Bullet
130
145
  context ".add_impossible_object" do
131
146
  it "should add impossible object" do
132
147
  NPlusOneQuery.add_impossible_object(@post)
133
- expect(NPlusOneQuery.send(:impossible_objects)).to be_include(@post.bullet_key)
148
+ expect(NPlusOneQuery.impossible_objects).to be_include(@post.bullet_key)
134
149
  end
135
150
  end
136
151
  end
@@ -27,5 +27,11 @@ describe Object do
27
27
  expect(post.primary_key_value).to eq(post.name)
28
28
  Post.primary_key = 'id'
29
29
  end
30
+
31
+ it "should return value for multiple primary keys" do
32
+ post = Post.first
33
+ allow(Post).to receive(:primary_keys).and_return([:category_id, :writer_id])
34
+ expect(post.primary_key_value).to eq("#{post.category_id},#{post.writer_id}")
35
+ end
30
36
  end
31
37
  end