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
@@ -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