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
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bullet::Detector::Counter do
4
+ before(:each) do
5
+ Bullet.start_request
6
+ end
7
+
8
+ after(:each) do
9
+ Bullet.end_request
10
+ end
11
+
12
+ it "should need counter cache with all cities" do
13
+ Country.all.each do |country|
14
+ country.cities.size
15
+ end
16
+ Bullet.collected_counter_cache_notifications.should_not be_empty
17
+ end
18
+
19
+ it "should not need counter cache if already define counter_cache" do
20
+ Person.all.each do |person|
21
+ person.pets.size
22
+ end
23
+ Bullet.collected_counter_cache_notifications.should be_empty
24
+ end
25
+
26
+ it "should not need counter cache with only one object" do
27
+ Country.first.cities.size
28
+ Bullet.collected_counter_cache_notifications.should be_empty
29
+ end
30
+
31
+ it "should not need counter cache with part of cities" do
32
+ Country.all.each do |country|
33
+ country.cities.where(:name => 'first').size
34
+ end
35
+ Bullet.collected_counter_cache_notifications.should be_empty
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ class Address < ActiveRecord::Base
2
+ belongs_to :company
3
+ end
@@ -0,0 +1,3 @@
1
+ class Author < ActiveRecord::Base
2
+ has_many :documents
3
+ end
@@ -0,0 +1,5 @@
1
+ class BaseUser < ActiveRecord::Base
2
+ has_many :comments
3
+ has_many :posts
4
+ belongs_to :newspaper
5
+ end
@@ -0,0 +1,7 @@
1
+ class Category < ActiveRecord::Base
2
+ has_many :posts
3
+ has_many :entries
4
+
5
+ has_many :submissions
6
+ has_many :users
7
+ end
@@ -0,0 +1,3 @@
1
+ class City < ActiveRecord::Base
2
+ belongs_to :country
3
+ end
@@ -0,0 +1,4 @@
1
+ class Client < ActiveRecord::Base
2
+ has_many :relationships
3
+ has_many :firms, :through => :relationships
4
+ end
@@ -0,0 +1,4 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :post
3
+ belongs_to :author, :class_name => "BaseUser"
4
+ end
@@ -0,0 +1,3 @@
1
+ class Company < ActiveRecord::Base
2
+ has_one :address
3
+ end
@@ -0,0 +1,3 @@
1
+ class Contact < ActiveRecord::Base
2
+ has_many :emails
3
+ end
@@ -0,0 +1,3 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :cities
3
+ end
@@ -0,0 +1,4 @@
1
+ class Deal < ActiveRecord::Base
2
+ belongs_to :hotel
3
+ has_one :location, :through => :hotel
4
+ end
@@ -0,0 +1,5 @@
1
+ class Document < ActiveRecord::Base
2
+ has_many :children, :class_name => "Document", :foreign_key => 'parent_id'
3
+ belongs_to :parent, :class_name => "Document", :foreign_key => 'parent_id'
4
+ belongs_to :author
5
+ end
@@ -0,0 +1,3 @@
1
+ class Email < ActiveRecord::Base
2
+ belongs_to :contact
3
+ end
@@ -0,0 +1,3 @@
1
+ class Entry < ActiveRecord::Base
2
+ belongs_to :category
3
+ end
@@ -0,0 +1,4 @@
1
+ class Firm < ActiveRecord::Base
2
+ has_many :relationships
3
+ has_many :clients, :through => :relationships
4
+ end
@@ -0,0 +1,2 @@
1
+ class Folder < Document
2
+ end
@@ -0,0 +1,4 @@
1
+ class Hotel < ActiveRecord::Base
2
+ belongs_to :location
3
+ has_many :deals
4
+ end
@@ -0,0 +1,3 @@
1
+ class Location < ActiveRecord::Base
2
+ has_many :hotels
3
+ end
@@ -0,0 +1,3 @@
1
+ class Newspaper < ActiveRecord::Base
2
+ has_many :writers, :class_name => "BaseUser"
3
+ end
@@ -0,0 +1,2 @@
1
+ class Page < Document
2
+ end
@@ -0,0 +1,3 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :pets
3
+ end
@@ -0,0 +1,3 @@
1
+ class Pet < ActiveRecord::Base
2
+ belongs_to :person, :counter_cache => true
3
+ end
@@ -0,0 +1,11 @@
1
+ class Post < ActiveRecord::Base
2
+ belongs_to :category
3
+ has_many :comments
4
+ belongs_to :writer
5
+
6
+
7
+ scope :preload_posts, lambda { includes(:comments) }
8
+ scope :in_category_name, lambda { |name|
9
+ where(['categories.name = ?', name]).includes(:category)
10
+ }
11
+ end
@@ -0,0 +1,4 @@
1
+ class Relationship < ActiveRecord::Base
2
+ belongs_to :firm
3
+ belongs_to :client
4
+ end
@@ -0,0 +1,3 @@
1
+ class Student < ActiveRecord::Base
2
+ has_and_belongs_to_many :teachers
3
+ end
@@ -0,0 +1,4 @@
1
+ class Submission < ActiveRecord::Base
2
+ belongs_to :category
3
+ belongs_to :user
4
+ end
@@ -0,0 +1,3 @@
1
+ class Teacher < ActiveRecord::Base
2
+ has_and_belongs_to_many :students
3
+ end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ has_one :submission
3
+ belongs_to :category
4
+ end
@@ -0,0 +1,2 @@
1
+ class Writer < BaseUser
2
+ end
@@ -1,5 +1,3 @@
1
- #require 'pry'
2
- require 'rubygems'
3
1
  require 'rspec'
4
2
  require 'rspec/autorun'
5
3
  require 'rails'
@@ -19,116 +17,28 @@ require 'bullet'
19
17
  Bullet.enable = true
20
18
  ActiveRecord::Migration.verbose = false
21
19
 
22
- module Bullet
23
- def self.collected_notifications_of_class( notification_class )
24
- Bullet.notification_collector.collection.select do |notification|
25
- notification.is_a? notification_class
26
- end
27
- end
28
-
29
- def self.collected_counter_cache_notifications
30
- collected_notifications_of_class Bullet::Notification::CounterCache
31
- end
32
-
33
- def self.collected_n_plus_one_query_notifications
34
- collected_notifications_of_class Bullet::Notification::NPlusOneQuery
35
- end
36
-
37
- def self.collected_unused_eager_association_notifications
38
- collected_notifications_of_class Bullet::Notification::UnusedEagerLoading
39
- end
40
- end
41
-
42
- module Bullet
43
- module Detector
44
- class Association
45
- class <<self
46
- # returns true if all associations are preloaded
47
- def completely_preloading_associations?
48
- Bullet.collected_n_plus_one_query_notifications.empty?
49
- end
50
-
51
- def has_unused_preload_associations?
52
- Bullet.collected_unused_eager_association_notifications.present?
53
- end
54
-
55
- # returns true if a given object has a specific association
56
- def creating_object_association_for?(object, association)
57
- object_associations[object].present? && object_associations[object].include?(association)
58
- end
59
-
60
- # returns true if a given class includes the specific unpreloaded association
61
- def detecting_unpreloaded_association_for?(klass, association)
62
- for_class_and_assoc = Bullet.collected_n_plus_one_query_notifications.select do |notification|
63
- notification.base_class == klass and
64
- notification.associations.include?( association )
65
- end
66
- for_class_and_assoc.present?
67
- end
68
-
69
- # returns true if the given class includes the specific unused preloaded association
70
- def unused_preload_associations_for?(klass, association)
71
- for_class_and_assoc = Bullet.collected_unused_eager_association_notifications.select do |notification|
72
- notification.base_class == klass and
73
- notification.associations.include?( association )
74
- end
75
- for_class_and_assoc.present?
76
- end
77
- end
78
- end
79
- end
80
- end
20
+ MODELS = File.join(File.dirname(__FILE__), "models")
21
+ $LOAD_PATH.unshift(MODELS)
81
22
 
82
- class AppDouble
83
- def call env
84
- env = @env
85
- [ status, headers, response ]
86
- end
87
-
88
- def status= status
89
- @status = status
90
- end
91
-
92
- def headers= headers
93
- @headers = headers
94
- end
95
-
96
- def headers
97
- @headers ||= {}
98
- @headers
99
- end
100
-
101
- def response= response
102
- @response = response
103
- end
104
-
105
- private
106
- def status
107
- @status || 200
108
- end
109
-
110
- def response
111
- @response || ResponseDouble.new
112
- end
23
+ # Autoload every model for the test suite that sits in spec/models.
24
+ Dir[ File.join(MODELS, "*.rb") ].sort.each do |file|
25
+ name = File.basename(file, ".rb")
26
+ autoload name.camelize.to_sym, name
113
27
  end
114
28
 
115
- class ResponseDouble
116
- def initialize actual_body = nil
117
- @actual_body = actual_body
118
- end
119
-
120
- def body
121
- @body ||= "Hello world!"
122
- end
29
+ SUPPORT = File.join(File.dirname(__FILE__), "support")
30
+ Dir[ File.join(SUPPORT, "*.rb") ].sort.each { |file| require file }
123
31
 
124
- def body= body
125
- @body = body
32
+ RSpec.configure do |config|
33
+ config.before(:suite) do
34
+ Support::Seed.setup_db
35
+ Support::Seed.seed_db
126
36
  end
127
37
 
128
- def each
129
- yield body
38
+ config.after(:suite) do
39
+ Support::Seed.teardown_db
130
40
  end
131
41
 
132
- def close
133
- end
42
+ config.filter_run :focus => true
43
+ config.run_all_when_everything_filtered = true
134
44
  end
@@ -0,0 +1,55 @@
1
+ module Bullet
2
+ def self.collected_notifications_of_class(notification_class)
3
+ Bullet.notification_collector.collection.select do |notification|
4
+ notification.is_a? notification_class
5
+ end
6
+ end
7
+
8
+ def self.collected_counter_cache_notifications
9
+ collected_notifications_of_class Bullet::Notification::CounterCache
10
+ end
11
+
12
+ def self.collected_n_plus_one_query_notifications
13
+ collected_notifications_of_class Bullet::Notification::NPlusOneQuery
14
+ end
15
+
16
+ def self.collected_unused_eager_association_notifications
17
+ collected_notifications_of_class Bullet::Notification::UnusedEagerLoading
18
+ end
19
+ end
20
+
21
+ module Bullet
22
+ module Detector
23
+ class Association
24
+ class <<self
25
+ # returns true if all associations are preloaded
26
+ def completely_preloading_associations?
27
+ Bullet.collected_n_plus_one_query_notifications.empty?
28
+ end
29
+
30
+ def has_unused_preload_associations?
31
+ Bullet.collected_unused_eager_association_notifications.present?
32
+ end
33
+
34
+ # returns true if a given object has a specific association
35
+ def creating_object_association_for?(object, association)
36
+ object_associations[object.ar_key].present? && object_associations[object.ar_key].include?(association)
37
+ end
38
+
39
+ # returns true if a given class includes the specific unpreloaded association
40
+ def detecting_unpreloaded_association_for?(klass, association)
41
+ Bullet.collected_n_plus_one_query_notifications.select { |notification|
42
+ notification.base_class == klass.to_s && notification.associations.include?(association)
43
+ }.present?
44
+ end
45
+
46
+ # returns true if the given class includes the specific unused preloaded association
47
+ def unused_preload_associations_for?(klass, association)
48
+ Bullet.collected_unused_eager_association_notifications.select { |notification|
49
+ notification.base_class == klass.to_s && notification.associations.include?(association)
50
+ }.present?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ module Support
2
+ class AppDouble
3
+ def call env
4
+ env = @env
5
+ [ status, headers, response ]
6
+ end
7
+
8
+ def status= status
9
+ @status = status
10
+ end
11
+
12
+ def headers= headers
13
+ @headers = headers
14
+ end
15
+
16
+ def headers
17
+ @headers ||= {"Content-Type" => "text/html"}
18
+ @headers
19
+ end
20
+
21
+ def response= response
22
+ @response = response
23
+ end
24
+
25
+ private
26
+ def status
27
+ @status || 200
28
+ end
29
+
30
+ def response
31
+ @response || ResponseDouble.new
32
+ end
33
+ end
34
+
35
+ class ResponseDouble
36
+ def initialize actual_body = nil
37
+ @actual_body = actual_body
38
+ end
39
+
40
+ def body
41
+ @body ||= "<html><head></head><body></body></html>"
42
+ end
43
+
44
+ def body= body
45
+ @body = body
46
+ end
47
+
48
+ def each
49
+ yield body
50
+ end
51
+
52
+ def close
53
+ end
54
+ end
55
+ end