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,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
  module Bullet
3
- VERSION = "2.2.1"
3
+ VERSION = "2.3.0"
4
4
  end
5
5
 
@@ -83,16 +83,18 @@ PerfTools::CpuProfiler.start(ARGV[0]|| "benchmark_profile") if defined? PerfTool
83
83
 
84
84
  Benchmark.bm(70) do |bm|
85
85
  bm.report("Querying & Iterating #{posts_size} Posts with #{comments_size} Comments and #{users_size} Users") do
86
- Bullet.start_request
87
- Post.select("SQL_NO_CACHE *").includes(:user, :comments => :user).each do |p|
88
- p.title
89
- p.user.name
90
- p.comments.each do |c|
91
- c.body
92
- c.user.name
86
+ 10.times do
87
+ Bullet.start_request
88
+ Post.select("SQL_NO_CACHE *").includes(:user, :comments => :user).each do |p|
89
+ p.title
90
+ p.user.name
91
+ p.comments.each do |c|
92
+ c.body
93
+ c.user.name
94
+ end
93
95
  end
96
+ Bullet.end_request
94
97
  end
95
- Bullet.end_request
96
98
  end
97
99
  end
98
100
 
@@ -100,7 +102,23 @@ PerfTools::CpuProfiler.stop if defined? PerfTools
100
102
  puts "End benchmarking..."
101
103
 
102
104
 
103
- # 2.0.1
104
- # user system total real
105
- # Querying & Iterating 100 Posts with 10000 Comments and 100 Users 2.290000 0.050000 2.340000 ( 2.366174)
106
-
105
+ # Run benchmark with bundler
106
+ #
107
+ # bundle exec ruby perf/benchmark.rb
108
+ #
109
+ # bullet 2.3.0 with rails 3.2.2
110
+ # user system total real
111
+ # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 16.460000 0.190000 16.650000 ( 16.968246)
112
+ #
113
+ # bullet 2.3.0 with rails 3.1.4
114
+ # user system total real
115
+ # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 14.600000 0.130000 14.730000 ( 14.937590)
116
+ #
117
+ # bullet 2.3.0 with rails 3.0.12
118
+ # user system total real
119
+ # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 26.120000 0.430000 26.550000 ( 27.179304)
120
+ #
121
+ #
122
+ # bullet 2.2.1 with rails 3.0.12
123
+ # user system total real
124
+ # Querying & Iterating 1000 Posts with 10000 Comments and 100 Users 29.970000 0.270000 30.240000 ( 30.452083)
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Detector
5
+ describe Association do
6
+ before :all do
7
+ @post1 = Post.first
8
+ @post2 = Post.last
9
+ end
10
+ before(:each) { Association.clear }
11
+
12
+ context ".start_request" do
13
+ it "should set @@checked to false" do
14
+ Association.start_request
15
+ Association.class_variable_get(:@@checked).should be_false
16
+ end
17
+ end
18
+
19
+ context ".clear" do
20
+ it "should clear all class variables" do
21
+ Association.clear
22
+ Association.class_variable_get(:@@object_associations).should be_nil
23
+ Association.class_variable_get(:@@possible_objects).should be_nil
24
+ Association.class_variable_get(:@@impossible_objects).should be_nil
25
+ Association.class_variable_get(:@@call_object_associations).should be_nil
26
+ Association.class_variable_get(:@@eager_loadings).should be_nil
27
+ end
28
+ end
29
+
30
+ context ".add_object_association" do
31
+ it "should add object, associations pair" do
32
+ Association.add_object_associations(@post1, :associations)
33
+ Association.send(:object_associations).should be_include(@post1.ar_key, :associations)
34
+ end
35
+ end
36
+
37
+ context ".add_call_object_associations" do
38
+ it "should add call object, associations pair" do
39
+ Association.add_call_object_associations(@post1, :associations)
40
+ Association.send(:call_object_associations).should be_include(@post1.ar_key, :associations)
41
+ end
42
+ end
43
+
44
+ context ".add_possible_objects" do
45
+ it "should add possible objects" do
46
+ Association.add_possible_objects([@post1, @post2])
47
+ Association.send(:possible_objects).should be_include(@post1.ar_key)
48
+ Association.send(:possible_objects).should be_include(@post2.ar_key)
49
+ end
50
+ end
51
+
52
+ context ".add_impossible_object" do
53
+ it "should add impossible object" do
54
+ Association.add_impossible_object(@post1)
55
+ Association.send(:impossible_objects).should be_include(@post1.ar_key)
56
+ end
57
+ end
58
+
59
+ context ".add_eager_loadings" do
60
+ it "should add objects, associations pair when eager_loadings are empty" do
61
+ Association.add_eager_loadings([@post1, @post2], :associations)
62
+ Association.send(:eager_loadings).should be_include([@post1.ar_key, @post2.ar_key], :associations)
63
+ end
64
+
65
+ it "should add objects, associations pair for existing eager_loadings" do
66
+ Association.add_eager_loadings([@post1, @post2], :association1)
67
+ Association.add_eager_loadings([@post1, @post2], :association2)
68
+ Association.send(:eager_loadings).should be_include([@post1.ar_key, @post2.ar_key], :association1)
69
+ Association.send(:eager_loadings).should be_include([@post1.ar_key, @post2.ar_key], :association2)
70
+ end
71
+
72
+ it "should merge objects, associations pair for existing eager_loadings" do
73
+ Association.add_eager_loadings(@post1, :association1)
74
+ Association.add_eager_loadings([@post1, @post2], :association2)
75
+ Association.send(:eager_loadings).should be_include([@post1.ar_key], :association1)
76
+ Association.send(:eager_loadings).should be_include([@post1.ar_key], :association2)
77
+ Association.send(:eager_loadings).should be_include([@post1.ar_key, @post2.ar_key], :association2)
78
+ end
79
+
80
+ it "should delete objects, associations pair for existing eager_loadings" do
81
+ Association.add_eager_loadings([@post1, @post2], :association1)
82
+ Association.add_eager_loadings(@post1, :association2)
83
+ Association.send(:eager_loadings).should be_include([@post1.ar_key], :association1)
84
+ Association.send(:eager_loadings).should be_include([@post1.ar_key], :association2)
85
+ Association.send(:eager_loadings).should be_include([@post2.ar_key], :association1)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Detector
5
+ describe Base do
6
+ context ".end_request" do
7
+ it "should call clear" do
8
+ Base.should_receive(:clear)
9
+ Base.end_request
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Detector
5
+ describe Counter do
6
+ before :all do
7
+ @post1 = Post.first
8
+ @post2 = Post.last
9
+ end
10
+ before(:each) { Counter.clear }
11
+
12
+ context ".clear" do
13
+ it "should clear all class variables" do
14
+ Counter.clear
15
+ Counter.class_variable_get(:@@possible_objects).should be_nil
16
+ Counter.class_variable_get(:@@impossible_objects).should be_nil
17
+ end
18
+ end
19
+
20
+ context ".add_counter_cache" do
21
+ it "should create notification if conditions met" do
22
+ Counter.should_receive(:conditions_met?).with(@post1.ar_key, [:comments]).and_return(true)
23
+ Counter.should_receive(:create_notification).with("Post", [:comments])
24
+ Counter.add_counter_cache(@post1, [:comments])
25
+ end
26
+
27
+ it "should not create notification if conditions not met" do
28
+ Counter.should_receive(:conditions_met?).with(@post1.ar_key, [:comments]).and_return(false)
29
+ Counter.should_receive(:create_notification).never
30
+ Counter.add_counter_cache(@post1, [:comments])
31
+ end
32
+ end
33
+
34
+ context ".add_possible_objects" do
35
+ it "should add possible objects" do
36
+ Counter.add_possible_objects([@post1, @post2])
37
+ Counter.send(:possible_objects).should be_include(@post1.ar_key)
38
+ Counter.send(:possible_objects).should be_include(@post2.ar_key)
39
+ end
40
+
41
+ it "should add impossible object" do
42
+ Counter.add_impossible_object(@post1)
43
+ Counter.send(:impossible_objects).should be_include(@post1.ar_key)
44
+ end
45
+ end
46
+
47
+ context ".conditions_met?" do
48
+ it "should be true when object is possible, not impossible" do
49
+ Counter.add_possible_objects(@post1)
50
+ Counter.send(:conditions_met?, @post1.ar_key, :associations).should be_true
51
+ end
52
+
53
+ it "should be false when object is not possible" do
54
+ Counter.send(:conditions_met?, @post1.ar_key, :associations).should be_false
55
+ end
56
+
57
+ it "should be true when object is possible, and impossible" do
58
+ Counter.add_possible_objects(@post1)
59
+ Counter.add_impossible_object(@post1)
60
+ Counter.send(:conditions_met?, @post1.ar_key, :associations).should be_false
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Detector
5
+ describe NPlusOneQuery do
6
+ before(:all) { @post = Post.first }
7
+ before(:each) { NPlusOneQuery.clear }
8
+
9
+ context ".call_association" do
10
+ it "should set @@checked to true" do
11
+ NPlusOneQuery.call_association(@post, :associations)
12
+ NPlusOneQuery.class_variable_get(:@@checked).should be_true
13
+ end
14
+
15
+ it "should add call_object_associations" do
16
+ NPlusOneQuery.should_receive(:add_call_object_associations).with(@post, :associations)
17
+ NPlusOneQuery.call_association(@post, :associations)
18
+ end
19
+ end
20
+
21
+ context ".possible?" do
22
+ it "should be true if possible_objects contain" do
23
+ NPlusOneQuery.add_possible_objects(@post)
24
+ NPlusOneQuery.send(:possible?, @post.ar_key).should be_true
25
+ end
26
+ end
27
+
28
+ context ".impossible?" do
29
+ it "should be true if impossible_objects contain" do
30
+ NPlusOneQuery.add_impossible_object(@post)
31
+ NPlusOneQuery.send(:impossible?, @post.ar_key).should be_true
32
+ end
33
+ end
34
+
35
+ context ".association?" do
36
+ it "should be true if object, associations pair is already existed" do
37
+ NPlusOneQuery.add_object_associations(@post, :association)
38
+ NPlusOneQuery.send(:association?, @post.ar_key, :association).should be_true
39
+ end
40
+
41
+ it "should be false if object, association pair is not existed" do
42
+ NPlusOneQuery.add_object_associations(@post, :association1)
43
+ NPlusOneQuery.send(:association?, @post.ar_key, :associatio2).should be_false
44
+ end
45
+ end
46
+
47
+ context ".conditions_met?" do
48
+ it "should be true if object is possible, not impossible and object, associations pair is not already existed" do
49
+ NPlusOneQuery.stub(:possible?).with(@post.ar_key).and_return(true)
50
+ NPlusOneQuery.stub(:impossible?).with(@post.ar_key).and_return(false)
51
+ NPlusOneQuery.stub(:association?).with(@post.ar_key, :associations).and_return(false)
52
+ NPlusOneQuery.send(:conditions_met?, @post.ar_key, :associations).should be_true
53
+ end
54
+
55
+ it "should be false if object is not possible, not impossible and object, associations pair is not already existed" do
56
+ NPlusOneQuery.stub(:possible?).with(@post.ar_key).and_return(false)
57
+ NPlusOneQuery.stub(:impossible?).with(@post.ar_key).and_return(false)
58
+ NPlusOneQuery.stub(:association?).with(@post.ar_key, :associations).and_return(false)
59
+ NPlusOneQuery.send(:conditions_met?, @post.ar_key, :associations).should be_false
60
+ end
61
+
62
+ it "should be false if object is possible, but impossible and object, associations pair is not already existed" do
63
+ NPlusOneQuery.stub(:possible?).with(@post.ar_key).and_return(true)
64
+ NPlusOneQuery.stub(:impossible?).with(@post.ar_key).and_return(true)
65
+ NPlusOneQuery.stub(:association?).with(@post.ar_key, :associations).and_return(false)
66
+ NPlusOneQuery.send(:conditions_met?, @post.ar_key, :associations).should be_false
67
+ end
68
+
69
+ it "should be false if object is possible, not impossible and object, associations pair is already existed" do
70
+ NPlusOneQuery.stub(:possible?).with(@post.ar_key).and_return(true)
71
+ NPlusOneQuery.stub(:impossible?).with(@post.ar_key).and_return(false)
72
+ NPlusOneQuery.stub(:association?).with(@post.ar_key, :associations).and_return(true)
73
+ NPlusOneQuery.send(:conditions_met?, @post.ar_key, :associations).should be_false
74
+ end
75
+ end
76
+
77
+ context ".call_association" do
78
+ it "should create notification if conditions met" do
79
+ NPlusOneQuery.should_receive(:conditions_met?).with(@post.ar_key, :association).and_return(true)
80
+ NPlusOneQuery.should_receive(:caller_in_project).and_return(["caller"])
81
+ NPlusOneQuery.should_receive(:create_notification).with(["caller"], "Post", :association)
82
+ NPlusOneQuery.call_association(@post, :association)
83
+ end
84
+
85
+ it "should not create notification if conditions not met" do
86
+ NPlusOneQuery.should_receive(:conditions_met?).with(@post.ar_key, :association).and_return(false)
87
+ NPlusOneQuery.should_not_receive(:caller_in_project!)
88
+ NPlusOneQuery.should_not_receive(:create_notification).with("Post", :association)
89
+ NPlusOneQuery.call_association(@post, :association)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Detector
5
+ describe UnusedEagerAssociation do
6
+ before(:all) { @post = Post.first }
7
+ before(:each) { UnusedEagerAssociation.clear }
8
+
9
+ context ".call_associations" do
10
+ it "should get empty array if eager_loadgins" do
11
+ UnusedEagerAssociation.send(:call_associations, @post.ar_key, Set.new([:association])).should be_empty
12
+ end
13
+
14
+ it "should get call associations if object and association are both in eager_loadings and call_object_associations" do
15
+ UnusedEagerAssociation.add_eager_loadings(@post, :association)
16
+ UnusedEagerAssociation.add_call_object_associations(@post, :association)
17
+ UnusedEagerAssociation.send(:call_associations, @post.ar_key, Set.new([:association])).should == [:association]
18
+ end
19
+
20
+ it "should not get call associations if not exist in call_object_associations" do
21
+ UnusedEagerAssociation.add_eager_loadings(@post, :association)
22
+ UnusedEagerAssociation.send(:call_associations, @post.ar_key, Set.new([:association])).should be_empty
23
+ end
24
+ end
25
+
26
+ context ".diff_object_association" do
27
+ it "should return associations not exist in call_association" do
28
+ UnusedEagerAssociation.send(:diff_object_association, @post.ar_key, Set.new([:association])).should == [:association]
29
+ end
30
+
31
+ it "should return empty if associations exist in call_association" do
32
+ UnusedEagerAssociation.add_eager_loadings(@post, :association)
33
+ UnusedEagerAssociation.add_call_object_associations(@post, :association)
34
+ UnusedEagerAssociation.send(:diff_object_association, @post.ar_key, Set.new([:association])).should be_empty
35
+ end
36
+ end
37
+
38
+ context ".check_unused_preload_associations" do
39
+ it "should set @@checked to true" do
40
+ UnusedEagerAssociation.check_unused_preload_associations
41
+ UnusedEagerAssociation.class_variable_get(:@@checked).should be_true
42
+ end
43
+
44
+ it "should create notification if object_association_diff is not empty" do
45
+ UnusedEagerAssociation.add_object_associations(@post, :association)
46
+ UnusedEagerAssociation.should_receive(:create_notification).with("Post", [:association])
47
+ UnusedEagerAssociation.check_unused_preload_associations
48
+ end
49
+
50
+ it "should not create notification if object_association_diff is empty" do
51
+ UnusedEagerAssociation.clear
52
+ UnusedEagerAssociation.add_object_associations(@post, :association)
53
+ UnusedEagerAssociation.add_eager_loadings(@post, :association)
54
+ UnusedEagerAssociation.add_call_object_associations(@post, :association)
55
+ UnusedEagerAssociation.send(:diff_object_association, @post.ar_key, Set.new([:association])).should be_empty
56
+ UnusedEagerAssociation.should_not_receive(:create_notification).with("Post", [:association])
57
+ UnusedEagerAssociation.check_unused_preload_associations
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ module Bullet
4
+ module Notification
5
+ describe Base do
6
+ subject { Base.new(Post, [:comments, :votes]) }
7
+
8
+ context "#title" do
9
+ it "should raise NoMethodError" do
10
+ lambda { subject.title }.should raise_error(NoMethodError)
11
+ end
12
+ end
13
+
14
+ context "#body" do
15
+ it "should raise NoMethodError" do
16
+ lambda { subject.body }.should raise_error(NoMethodError)
17
+ end
18
+ end
19
+
20
+ context "#whoami" do
21
+ it "should display user name" do
22
+ user = `whoami`.chomp
23
+ subject.whoami.should == "user: #{user}"
24
+ end
25
+ end
26
+
27
+ context "#body_with_caller" do
28
+ it "should return body" do
29
+ subject.stub(:body => "body")
30
+ subject.body_with_caller.should == "body"
31
+ end
32
+ end
33
+
34
+ context "#standard_notice" do
35
+ it "should return title + body" do
36
+ subject.stub(:title => "title", :body => "body")
37
+ subject.standard_notice.should == "title\nbody"
38
+ end
39
+ end
40
+
41
+ context "#full_notice" do
42
+ it "should return whoami + url + title + body_with_caller" do
43
+ subject.stub(:whoami => "whoami", :url => "url", :title => "title", :body_with_caller => "body_with_caller")
44
+ subject.full_notice.should == "whoami\nurl\ntitle\nbody_with_caller"
45
+ end
46
+ end
47
+
48
+ context "#notify_inline" do
49
+ it "should send full_notice to notifier" do
50
+ notifier = stub
51
+ subject.stub(:notifier => notifier, :full_notice => "full_notice")
52
+ notifier.should_receive(:inline_notify).with("full_notice")
53
+ subject.notify_inline
54
+ end
55
+ end
56
+
57
+ context "#notify_out_of_channel" do
58
+ it "should send full_out_of_channel to notifier" do
59
+ notifier = stub
60
+ subject.stub(:notifier => notifier, :full_notice => "full_notice")
61
+ notifier.should_receive(:out_of_channel_notify).with("full_notice")
62
+ subject.notify_out_of_channel
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end