bullet 2.2.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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