bullet 2.2.1 → 2.3.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 (80) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +4 -1
  3. data/Gemfile +3 -2
  4. data/Gemfile.lock +85 -69
  5. data/Guardfile +8 -0
  6. data/lib/bullet.rb +13 -8
  7. data/lib/bullet/action_controller2.rb +4 -4
  8. data/lib/bullet/active_record2.rb +5 -6
  9. data/lib/bullet/active_record3.rb +2 -3
  10. data/lib/bullet/active_record31.rb +6 -8
  11. data/lib/bullet/detector/association.rb +27 -53
  12. data/lib/bullet/detector/counter.rb +34 -29
  13. data/lib/bullet/detector/n_plus_one_query.rb +47 -28
  14. data/lib/bullet/detector/unused_eager_association.rb +31 -30
  15. data/lib/bullet/notification/base.rb +14 -12
  16. data/lib/bullet/notification/n_plus_one_query.rb +6 -10
  17. data/lib/bullet/notification/unused_eager_loading.rb +1 -2
  18. data/lib/bullet/notification_collector.rb +1 -2
  19. data/lib/bullet/rack.rb +6 -3
  20. data/lib/bullet/registry/association.rb +4 -6
  21. data/lib/bullet/registry/base.rb +10 -7
  22. data/lib/bullet/registry/object.rb +6 -6
  23. data/lib/bullet/version.rb +1 -1
  24. data/perf/benchmark.rb +30 -12
  25. data/spec/bullet/detector/association_spec.rb +90 -0
  26. data/spec/bullet/detector/base_spec.rb +14 -0
  27. data/spec/bullet/detector/counter_spec.rb +65 -0
  28. data/spec/bullet/detector/n_plus_one_query_spec.rb +94 -0
  29. data/spec/bullet/detector/unused_eager_association_spec.rb +62 -0
  30. data/spec/bullet/notification/base_spec.rb +67 -0
  31. data/spec/bullet/notification/counter_cache_spec.rb +12 -0
  32. data/spec/bullet/notification/n_plus_one_query_spec.rb +13 -0
  33. data/spec/bullet/notification/unused_eager_loading_spec.rb +12 -0
  34. data/spec/bullet/notification_collector_spec.rb +32 -0
  35. data/spec/bullet/rack_spec.rb +80 -24
  36. data/spec/bullet/registry/association_spec.rb +26 -0
  37. data/spec/bullet/registry/base_spec.rb +44 -0
  38. data/spec/bullet/registry/object_spec.rb +25 -0
  39. data/spec/integration/association_for_chris_spec.rb +37 -0
  40. data/spec/integration/association_for_peschkaj_spec.rb +26 -0
  41. data/spec/{bullet → integration}/association_spec.rb +1 -359
  42. data/spec/integration/counter_spec.rb +37 -0
  43. data/spec/models/address.rb +3 -0
  44. data/spec/models/author.rb +3 -0
  45. data/spec/models/base_user.rb +5 -0
  46. data/spec/models/category.rb +7 -0
  47. data/spec/models/city.rb +3 -0
  48. data/spec/models/client.rb +4 -0
  49. data/spec/models/comment.rb +4 -0
  50. data/spec/models/company.rb +3 -0
  51. data/spec/models/contact.rb +3 -0
  52. data/spec/models/country.rb +3 -0
  53. data/spec/models/deal.rb +4 -0
  54. data/spec/models/document.rb +5 -0
  55. data/spec/models/email.rb +3 -0
  56. data/spec/models/entry.rb +3 -0
  57. data/spec/models/firm.rb +4 -0
  58. data/spec/models/folder.rb +2 -0
  59. data/spec/models/hotel.rb +4 -0
  60. data/spec/models/location.rb +3 -0
  61. data/spec/models/newspaper.rb +3 -0
  62. data/spec/models/page.rb +2 -0
  63. data/spec/models/person.rb +3 -0
  64. data/spec/models/pet.rb +3 -0
  65. data/spec/models/post.rb +11 -0
  66. data/spec/models/relationship.rb +4 -0
  67. data/spec/models/student.rb +3 -0
  68. data/spec/models/submission.rb +4 -0
  69. data/spec/models/teacher.rb +3 -0
  70. data/spec/models/user.rb +4 -0
  71. data/spec/models/writer.rb +2 -0
  72. data/spec/spec_helper.rb +16 -106
  73. data/spec/support/bullet_ext.rb +55 -0
  74. data/spec/support/rack_double.rb +55 -0
  75. data/spec/support/seed.rb +256 -0
  76. metadata +104 -14
  77. data/.watchr +0 -24
  78. data/spec/bullet/association_for_chris_spec.rb +0 -96
  79. data/spec/bullet/association_for_peschkaj_spec.rb +0 -86
  80. data/spec/bullet/counter_spec.rb +0 -136
@@ -1,42 +1,47 @@
1
1
  module Bullet
2
2
  module Detector
3
3
  class Counter < Base
4
- def self.clear
5
- @@possible_objects = nil
6
- @@impossible_objects = nil
7
- end
4
+ class <<self
5
+ def clear
6
+ @@possible_objects = nil
7
+ @@impossible_objects = nil
8
+ end
8
9
 
9
- def self.add_counter_cache(object, associations)
10
- if conditions_met?( object, associations )
11
- create_notification object.class, associations
10
+ def add_counter_cache(object, associations)
11
+ if conditions_met?(object.ar_key, associations)
12
+ create_notification object.class.to_s, associations
13
+ end
12
14
  end
13
- end
14
15
 
15
- def self.add_possible_objects(objects)
16
- possible_objects.add objects
17
- end
16
+ def add_possible_objects(object_or_objects)
17
+ if object_or_objects.is_a? Array
18
+ object_or_objects.each { |object| possible_objects.add object.ar_key }
19
+ else
20
+ possible_objects.add object_or_objects.ar_key
21
+ end
22
+ end
18
23
 
19
- def self.add_impossible_object(object)
20
- impossible_objects.add object
21
- end
22
-
23
- private
24
- def self.create_notification( klazz, associations )
25
- notice = Bullet::Notification::CounterCache.new klazz, associations
26
- Bullet.notification_collector.add notice
27
- end
24
+ def add_impossible_object(object)
25
+ impossible_objects.add object.ar_key
26
+ end
28
27
 
29
- def self.possible_objects
30
- @@possible_objects ||= Bullet::Registry::Object.new
31
- end
28
+ private
29
+ def create_notification(klazz, associations)
30
+ notice = Bullet::Notification::CounterCache.new klazz, associations
31
+ Bullet.notification_collector.add notice
32
+ end
32
33
 
33
- def self.impossible_objects
34
- @@impossible_objects ||= Bullet::Registry::Object.new
35
- end
34
+ def possible_objects
35
+ @@possible_objects ||= Bullet::Registry::Object.new
36
+ end
37
+
38
+ def impossible_objects
39
+ @@impossible_objects ||= Bullet::Registry::Object.new
40
+ end
36
41
 
37
- def self.conditions_met?( object, associations )
38
- possible_objects.contains?( object ) and
39
- !impossible_objects.contains?( object )
42
+ def conditions_met?(object_ar_key, associations)
43
+ possible_objects.include?(object_ar_key) && !impossible_objects.include?(object_ar_key)
44
+ end
40
45
  end
41
46
  end
42
47
  end
@@ -1,38 +1,57 @@
1
1
  module Bullet
2
2
  module Detector
3
3
  class NPlusOneQuery < Association
4
- # executed when object.assocations is called.
5
- # first, it keeps this method call for object.association.
6
- # then, it checks if this associations call is unpreload.
7
- # if it is, keeps this unpreload associations and caller.
8
- def self.call_association(object, associations)
9
- @@checked = true
10
- add_call_object_associations(object, associations)
11
-
12
- if conditions_met?(object, associations)
13
- caller_in_project
14
- create_notification object.class, associations
4
+ class <<self
5
+ # executed when object.assocations is called.
6
+ # first, it keeps this method call for object.association.
7
+ # then, it checks if this associations call is unpreload.
8
+ # if it is, keeps this unpreload associations and caller.
9
+ def call_association(object, associations)
10
+ @@checked = true
11
+ add_call_object_associations(object, associations)
12
+
13
+ if conditions_met?(object.ar_key, associations)
14
+ create_notification caller_in_project, object.class.to_s, associations
15
+ end
15
16
  end
16
- end
17
17
 
18
- private
19
- def self.create_notification(klazz, associations)
20
- notice = Bullet::Notification::NPlusOneQuery.new( callers, klazz, associations )
21
- Bullet.notification_collector.add( notice )
22
- end
18
+ private
19
+ def create_notification(callers, klazz, associations)
20
+ notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, associations)
21
+ Bullet.notification_collector.add(notice)
22
+ end
23
23
 
24
- # decide whether the object.associations is unpreloaded or not.
25
- def self.conditions_met?(object, associations)
26
- possible?(object) and
27
- !impossible?(object) and
28
- !association?(object, associations)
29
- end
24
+ # decide whether the object.associations is unpreloaded or not.
25
+ def conditions_met?(object_ar_key, associations)
26
+ possible?(object_ar_key) && !impossible?(object_ar_key) && !association?(object_ar_key, associations)
27
+ end
28
+
29
+ def caller_in_project
30
+ rails_root = Rails.root.to_s
31
+ vendor_root = rails_root + "/vendor"
32
+ caller.select { |c| c.include?(rails_root) && !c.include?(vendor_root) }
33
+ end
34
+
35
+ def possible?(object_ar_key)
36
+ possible_objects.include? object_ar_key
37
+ end
38
+
39
+ def impossible?(object_ar_key)
40
+ impossible_objects.include? object_ar_key
41
+ end
42
+
43
+ # check if object => associations already exists in object_associations.
44
+ def association?(object_ar_key, associations)
45
+ value = object_associations[object_ar_key]
46
+ if value
47
+ value.each do |v|
48
+ result = v.is_a?(Hash) ? v.has_key?(associations) : v == associations
49
+ return true if result
50
+ end
51
+ end
30
52
 
31
- def self.caller_in_project
32
- vender_root ||= File.join(Rails.root, 'vendor')
33
- callers << caller.select { |c| c =~ /#{Rails.root}/ }.
34
- reject { |c| c =~ /#{vender_root}/ }
35
- callers.uniq!
53
+ return false
54
+ end
36
55
  end
37
56
  end
38
57
  end
@@ -1,40 +1,41 @@
1
1
  module Bullet
2
2
  module Detector
3
3
  class UnusedEagerAssociation < Association
4
- # check if there are unused preload associations.
5
- # for each object => association
6
- # get related_objects from eager_loadings associated with object and associations
7
- # get call_object_association from associations of call_object_associations whose object is in related_objects
8
- # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
9
- def self.check_unused_preload_associations
10
- @@checked = true
11
- object_associations.each do |object, association|
12
- object_association_diff = diff_object_association object, association
13
- next if object_association_diff.empty?
4
+ class <<self
5
+ # check if there are unused preload associations.
6
+ # get related_objects from eager_loadings associated with object and associations
7
+ # get call_object_association from associations of call_object_associations whose object is in related_objects
8
+ # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
9
+ def check_unused_preload_associations
10
+ @@checked = true
11
+ object_associations.each do |object_ar_key, association|
12
+ object_association_diff = diff_object_association object_ar_key, association
13
+ next if object_association_diff.empty?
14
14
 
15
- create_notification object.class, object_association_diff
15
+ create_notification object_ar_key.split(":").first, object_association_diff
16
+ end
16
17
  end
17
- end
18
-
19
- protected
20
- def self.create_notification(klazz, associations)
21
- notice = Bullet::Notification::UnusedEagerLoading.new( klazz, associations )
22
- Bullet.notification_collector.add( notice )
23
- end
24
18
 
25
- def self.call_object_association( object, association )
26
- all = Set.new
27
- eager_loadings.similarly_associated( object, association ).each do |related_object|
28
- coa = call_object_associations[related_object]
29
- next if coa.nil?
30
- all.merge coa
31
- end
32
- all.to_a
33
- end
19
+ private
20
+ def create_notification(klazz, associations)
21
+ notice = Bullet::Notification::UnusedEagerLoading.new(klazz, associations)
22
+ Bullet.notification_collector.add(notice)
23
+ end
24
+
25
+ def call_associations(object_ar_key, association)
26
+ all = Set.new
27
+ eager_loadings.similarly_associated(object_ar_key, association).each do |related_object_ar_key|
28
+ coa = call_object_associations[related_object_ar_key]
29
+ next if coa.nil?
30
+ all.merge coa
31
+ end
32
+ all.to_a
33
+ end
34
34
 
35
- def self.diff_object_association( object, association )
36
- potential_objects = association - call_object_association( object, association )
37
- potential_objects.reject {|a| a.is_a?( Hash ) }
35
+ def diff_object_association(object_ar_key, association)
36
+ potential_objects = association - call_associations(object_ar_key, association)
37
+ potential_objects.reject { |a| a.is_a?(Hash) }
38
+ end
38
39
  end
39
40
  end
40
41
  end
@@ -4,18 +4,20 @@ module Bullet
4
4
  attr_accessor :notifier, :url
5
5
  attr_reader :base_class, :associations, :path
6
6
 
7
- def initialize( base_class, associations, path = nil )
7
+ def initialize(base_class, association_or_associations, path = nil)
8
8
  @base_class = base_class
9
- @associations = associations.is_a?( Array ) ? associations : [ associations ]
9
+ @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
10
10
  @path = path
11
11
  end
12
12
 
13
13
  def title
14
+ raise NoMethodError.new("no method title defined")
14
15
  end
15
16
 
16
17
  def body
18
+ raise NoMethodError.new("no method body defined")
17
19
  end
18
-
20
+
19
21
  def whoami
20
22
  "user: " << `whoami`.chomp
21
23
  end
@@ -33,14 +35,14 @@ module Bullet
33
35
  end
34
36
 
35
37
  def notify_inline
36
- self.notifier.inline_notify( self.full_notice )
38
+ self.notifier.inline_notify(self.full_notice)
37
39
  end
38
40
 
39
41
  def notify_out_of_channel
40
- self.notifier.out_of_channel_notify( self.full_notice )
42
+ self.notifier.out_of_channel_notify(self.full_notice)
41
43
  end
42
44
 
43
- def eql?( other )
45
+ def eql?(other)
44
46
  klazz_associations_str == other.klazz_associations_str
45
47
  end
46
48
 
@@ -49,13 +51,13 @@ module Bullet
49
51
  end
50
52
 
51
53
  protected
52
- def klazz_associations_str
53
- " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
54
- end
54
+ def klazz_associations_str
55
+ " #{@base_class} => [#{@associations.map(&:inspect).join(', ')}]"
56
+ end
55
57
 
56
- def associations_str
57
- ":include => #{@associations.map{|a| a.to_s.to_sym unless a.is_a? Hash}.inspect}"
58
- end
58
+ def associations_str
59
+ ":include => #{@associations.map{ |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
60
+ end
59
61
  end
60
62
  end
61
63
  end
@@ -1,8 +1,8 @@
1
1
  module Bullet
2
2
  module Notification
3
3
  class NPlusOneQuery < Base
4
- def initialize( callers, base_class, associations, path = nil )
5
- super( base_class, associations, path )
4
+ def initialize(callers, base_class, associations, path = nil)
5
+ super(base_class, associations, path)
6
6
 
7
7
  @callers = callers
8
8
  end
@@ -10,7 +10,7 @@ module Bullet
10
10
  def body_with_caller
11
11
  "#{body}\n#{call_stack_messages}"
12
12
  end
13
-
13
+
14
14
  def body
15
15
  "#{klazz_associations_str}\n Add to your finder: #{associations_str}"
16
16
  end
@@ -20,13 +20,9 @@ module Bullet
20
20
  end
21
21
 
22
22
  protected
23
- def call_stack_messages
24
- @callers.collect do |c|
25
- [ 'N+1 Query method call stack',
26
- c.collect {|line| " #{line}"} ].flatten
27
- end.join( "\n" )
28
- end
29
-
23
+ def call_stack_messages
24
+ @callers.unshift('N+1 Query method call stack').join( "\n " )
25
+ end
30
26
  end
31
27
  end
32
28
  end
@@ -6,9 +6,8 @@ module Bullet
6
6
  end
7
7
 
8
8
  def title
9
- "Unused Eager Loading #{@path ? "in #{@path}" : 'detected'}"
9
+ "Unused Eager Loading #{@path ? "in #{@path}" : 'detected'}"
10
10
  end
11
-
12
11
  end
13
12
  end
14
13
  end
@@ -12,14 +12,13 @@ module Bullet
12
12
  @collection = Set.new
13
13
  end
14
14
 
15
- def add( value )
15
+ def add(value)
16
16
  @collection << value
17
17
  end
18
18
 
19
19
  def notifications_present?
20
20
  !@collection.empty?
21
21
  end
22
-
23
22
  end
24
23
  end
25
24
 
@@ -13,7 +13,7 @@ module Bullet
13
13
 
14
14
  response_body = nil
15
15
  if Bullet.notification?
16
- if status == 200 and !response.body.frozen? and check_html?(headers, response)
16
+ if status == 200 && !response.body.frozen? && html_request?(headers, response)
17
17
  response_body = response.body << Bullet.gather_inline_notifications
18
18
  headers['Content-Length'] = response_body.length.to_s
19
19
  end
@@ -31,8 +31,11 @@ module Bullet
31
31
  !response.body.is_a?(String) || response.body.empty?
32
32
  end
33
33
 
34
- def check_html?(headers, response)
35
- headers['Content-Type'] and headers['Content-Type'].include? 'text/html' and response.body =~ %r{<html.*</html>}m
34
+ def html_request?(headers, response)
35
+ headers['Content-Type'] &&
36
+ headers['Content-Type'].include?('text/html') &&
37
+ response.body.include?("<html>") &&
38
+ response.body.include?("</html>")
36
39
  end
37
40
 
38
41
  def no_browser_cache(headers)
@@ -1,14 +1,12 @@
1
1
  module Bullet
2
2
  module Registry
3
3
  class Association < Base
4
- def merge( base, associations )
5
- @registry.merge!( { base => associations } )
4
+ def merge(base, associations)
5
+ @registry.merge!(base => associations)
6
6
  end
7
7
 
8
- def similarly_associated( base, associations )
9
- @registry.select do |key, value|
10
- key.include?( base ) and value == associations
11
- end.collect( &:first ).flatten
8
+ def similarly_associated(base, associations)
9
+ @registry.select { |key, value| key.include?(base) && value == associations }.collect(&:first).flatten
12
10
  end
13
11
  end
14
12
  end
@@ -11,19 +11,19 @@ module Bullet
11
11
  @registry[key]
12
12
  end
13
13
 
14
- def each( &block )
15
- @registry.each( &block )
14
+ def each(&block)
15
+ @registry.each(&block)
16
16
  end
17
17
 
18
- def delete( base )
19
- @registry.delete( base )
18
+ def delete(base)
19
+ @registry.delete(base)
20
20
  end
21
21
 
22
- def select( *args, &block )
23
- @registry.select( *args, &block )
22
+ def select(*args, &block)
23
+ @registry.select(*args, &block)
24
24
  end
25
25
 
26
- def add( key, value )
26
+ def add(key, value)
27
27
  @registry[key] ||= Set.new
28
28
  if value.is_a? Array
29
29
  @registry[key] += value
@@ -32,6 +32,9 @@ module Bullet
32
32
  end
33
33
  end
34
34
 
35
+ def include?(key, value)
36
+ @registry[key] && @registry[key].include?(value)
37
+ end
35
38
  end
36
39
  end
37
40
  end
@@ -1,14 +1,14 @@
1
1
  module Bullet
2
2
  module Registry
3
3
  class Object < Base
4
- def add( object_or_objects )
5
- klazz = object_or_objects.is_a?( Array ) ? object_or_objects.first.class :
6
- object_or_objects.class
7
- super( klazz, object_or_objects )
4
+ def add(object_ar_key)
5
+ klazz = object_ar_key.split(":").first
6
+ super(klazz, object_ar_key)
8
7
  end
9
8
 
10
- def contains?( object )
11
- @registry[object.class] and @registry[object.class].include?( object )
9
+ def include?(object_ar_key)
10
+ klazz = object_ar_key.split(":").first
11
+ super(klazz, object_ar_key)
12
12
  end
13
13
  end
14
14
  end