kashmir 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ require 'ar_test_helper'
2
+
3
+ describe 'ActiveRecord performance tricks' do
4
+
5
+ before(:all) do
6
+ TestData.create_tom
7
+ TestData.create_netto
8
+ end
9
+
10
+ describe "Query preload to avoid N+1 queries" do
11
+
12
+ it 'tries to preload records whenever possible' do
13
+ selects = track_queries do
14
+ AR::Chef.all.each do |chef|
15
+ chef.recipes.to_a
16
+ end
17
+ end
18
+ # SELECT * FROM chefs
19
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
20
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
21
+ assert_equal 3, selects.size
22
+
23
+ selects = track_queries do
24
+ AR::Chef.all.represent([:recipes])
25
+ end
26
+ # SELECT "chefs".* FROM "chefs"
27
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
28
+ assert_equal 2, selects.size
29
+ end
30
+
31
+ it 'preloads queries per each level in the tree' do
32
+ selects = track_queries do
33
+ AR::Chef.all.each do |chef|
34
+ chef.recipes.each do |recipe|
35
+ recipe.ingredients.to_a
36
+ end
37
+ end
38
+ end
39
+ # SELECT "chefs".* FROM "chefs"
40
+ # (2x) SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" = ?
41
+ # (4x) SELECT "ingredients".* FROM "ingredients" INNER JOIN "recipes_ingredients" ...
42
+ assert_equal 7, selects.size
43
+
44
+ selects = track_queries do
45
+ AR::Chef.all.represent([ :recipes => [ :ingredients => [:name] ] ])
46
+ end
47
+ # SELECT "chefs".* FROM "chefs"
48
+ # SELECT "recipes".* FROM "recipes" WHERE "recipes"."chef_id" IN (1, 2)
49
+ # SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2, 3, 4)
50
+ # SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4, 5, 6, 7, 8)
51
+ assert_equal 4, selects.size
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+ require 'minitest/around'
3
+
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
7
+
8
+ require 'support/schema'
9
+ require 'support/ar_models'
10
+ require 'support/factories'
11
+
12
+
13
+ class Minitest::Test
14
+ def around(&block)
15
+ ActiveRecord::Base.transaction do
16
+ block.call
17
+ raise ActiveRecord::Rollback
18
+ end
19
+ end
20
+ end
21
+
22
+ def track_queries
23
+ selects = []
24
+ queries_collector = lambda do |name, start, finish, id, payload|
25
+ selects << payload
26
+ end
27
+
28
+ ActiveRecord::Base.connection.clear_query_cache
29
+ ActiveSupport::Notifications.subscribed(queries_collector, 'sql.active_record') do
30
+ yield
31
+ end
32
+
33
+ selects.map { |sel| sel[:sql] }
34
+ end
@@ -0,0 +1,197 @@
1
+ require 'ar_test_helper'
2
+
3
+ describe 'Caching' do
4
+
5
+ before(:all) do
6
+
7
+ Kashmir.init(
8
+ cache_client: Kashmir::Caching::Memory.new
9
+ )
10
+
11
+ TestData.create_tom
12
+ @restaurant = AR::Restaurant.find_by_name('Chef Tom Belly Burgers')
13
+ end
14
+
15
+ before(:each) do
16
+ Kashmir.caching.flush!
17
+ end
18
+
19
+ def from_cache(definition, model)
20
+ Kashmir.caching.from_cache(definition, model)
21
+ end
22
+
23
+ describe 'flat data' do
24
+ it 'loads the same exact data from cache' do
25
+ representation = @restaurant.represent([:name])
26
+ @restaurant.reload
27
+ cached_representation = @restaurant.represent([:name])
28
+ assert_equal representation, cached_representation
29
+ end
30
+
31
+ it 'stores the data in cache' do
32
+ representation = @restaurant.represent([:name])
33
+ cached_representation = from_cache([:name], @restaurant)
34
+ assert_equal representation, cached_representation
35
+ end
36
+ end
37
+
38
+ describe 'references to other representations' do
39
+ it 'does not perform the same query twice' do
40
+ selects = track_queries do
41
+ @restaurant.represent([:name, :owner])
42
+ end
43
+ assert_equal 1, selects.size
44
+
45
+ # clear active record cache for this instance
46
+ @restaurant.reload
47
+ selects = track_queries do
48
+ @restaurant.represent([:name, :owner])
49
+ end
50
+ assert_equal 0, selects.size
51
+ end
52
+
53
+ it 'loads the same exact data from cache' do
54
+ representation = @restaurant.represent([:name, :owner])
55
+ @restaurant.reload
56
+ cached_representation = @restaurant.represent([:name, :owner])
57
+ assert_equal representation, cached_representation
58
+ end
59
+
60
+ it 'stores the data in cache at every level' do
61
+ representation = @restaurant.represent([:name, owner: [:name] ])
62
+ cached_restaurant_with_chef = from_cache([:name, owner: [ :name ]], @restaurant)
63
+
64
+ assert_equal representation, cached_restaurant_with_chef
65
+
66
+ chef = @restaurant.owner
67
+ cached_chef = from_cache([:name], chef)
68
+
69
+ assert_equal cached_chef, representation[:owner]
70
+ end
71
+ end
72
+
73
+ describe 'nesting' do
74
+ before(:all) do
75
+ @chef = @restaurant.owner
76
+ @chef.reload
77
+ end
78
+
79
+ it 'caches at every level' do
80
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
81
+ fully_cached_chef = from_cache([:name, :restaurant =>[ :name, :rating =>[ :value ]]], @chef)
82
+ assert_equal representation, fully_cached_chef
83
+
84
+ fully_cached_restaurant = from_cache([ :name, :rating => [:value] ], @restaurant)
85
+ assert_equal representation[:restaurant], fully_cached_restaurant
86
+
87
+ cached_rating = from_cache([:value], @restaurant.rating)
88
+ assert_equal representation[:restaurant][:rating], cached_rating
89
+
90
+ assert_equal 3, Kashmir.caching.keys.size
91
+ end
92
+
93
+ it 'tries to hit the cache at every level' do
94
+ selects = track_queries do
95
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
96
+ end
97
+ # SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
98
+ # SELECT "ratings".* FROM "ratings" WHERE "ratings"."restaurant_id" = ? LIMIT 1
99
+ assert_equal selects.size, 2
100
+
101
+ @chef.reload
102
+ selects = track_queries do
103
+ representation = @chef.represent([:name, :restaurant =>[ :name, :rating =>[ :value ]]])
104
+ end
105
+ assert_equal selects.size, 0
106
+ end
107
+
108
+ it 'tries to fill holes in the cache graph' do
109
+ definition = [:name, :restaurant =>[ :name, :rating =>[ :value ]]]
110
+ representation = @chef.represent(definition)
111
+ Kashmir.caching.clear(definition, @chef)
112
+
113
+ assert_equal 2, Kashmir.caching.keys.size
114
+
115
+ @chef.reload
116
+ selects = track_queries do
117
+ @chef.represent(definition)
118
+ end
119
+ # ratings is still cached
120
+ # SELECT "restaurants".* FROM "restaurants" WHERE "restaurants"."owner_id" = ? LIMIT 1
121
+ assert_equal selects.size, 1
122
+ end
123
+ end
124
+
125
+ describe 'collections' do
126
+ it 'caches every item' do
127
+ presented_recipes = AR::Recipe.all.represent([:title])
128
+
129
+ cached_keys = %w(
130
+ presenter:AR::Recipe:1:[:title]
131
+ presenter:AR::Recipe:2:[:title]
132
+ )
133
+
134
+ assert_equal cached_keys, Kashmir.caching.keys
135
+ end
136
+
137
+ it 'presents from cache' do
138
+ selects = track_queries do
139
+ AR::Recipe.all.represent([:title, :ingredients => [:name]])
140
+ end
141
+ # SELECT "recipes_ingredients".* FROM "recipes_ingredients" WHERE "recipes_ingredients"."recipe_id" IN (1, 2)
142
+ # SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."id" IN (1, 2, 3, 4)
143
+ assert_equal 3, selects.size
144
+
145
+ selects = track_queries do
146
+ AR::Recipe.all.represent([:title, :ingredients => [:name]])
147
+ end
148
+ # SELECT "recipes".* FROM "recipes"
149
+ assert_equal 1, selects.size
150
+
151
+ cache_keys = [
152
+ "presenter:AR::Ingredient:1:[:name]",
153
+ "presenter:AR::Ingredient:2:[:name]",
154
+ "presenter:AR::Recipe:1:[:title, {:ingredients=>[:name]}]",
155
+ "presenter:AR::Ingredient:3:[:name]",
156
+ "presenter:AR::Ingredient:4:[:name]",
157
+ "presenter:AR::Recipe:2:[:title, {:ingredients=>[:name]}]"
158
+ ]
159
+
160
+ assert_equal cache_keys, Kashmir.caching.keys
161
+ end
162
+ end
163
+
164
+ describe 'misc' do
165
+
166
+ it 'does not cache already cached results' do
167
+ @restaurant.represent([:name])
168
+ assert_equal 1, Kashmir.caching.keys.size
169
+
170
+ Kashmir.caching.expects(:store_presenter).never
171
+ @restaurant.represent([:name])
172
+ end
173
+
174
+ describe 'skipping cache' do
175
+ # see current_customer_count in test/support/ar_models.rb
176
+
177
+ before(:each) do
178
+ @restaurant.update_attributes(current_customer_count: 10)
179
+ @representation = @restaurant.represent([:name, :current_customer_count])
180
+ end
181
+
182
+ it 'still renders everything normally' do
183
+ assert_equal @representation, { name: 'Chef Tom Belly Burgers', current_customer_count: 10 }
184
+ end
185
+
186
+ it 'does not include that field in the key' do
187
+ assert_equal %w(presenter:AR::Restaurant:1:[:name]), Kashmir.caching.keys
188
+ end
189
+
190
+ it 'does not insert value in the cached results' do
191
+ assert_nil Kashmir.caching.from_cache([:name, :current_customar_count], @restaurant)
192
+ assert_equal Kashmir.caching.from_cache([:name], @restaurant), { name: 'Chef Tom Belly Burgers' }
193
+ end
194
+ end
195
+
196
+ end
197
+ end
data/test/dsl_test.rb ADDED
@@ -0,0 +1,162 @@
1
+ require 'test_helper'
2
+
3
+ describe Kashmir do
4
+
5
+ module DSLTesting
6
+ class Recipe < OpenStruct
7
+ include Kashmir
8
+
9
+ representations do
10
+ rep(:num_steps)
11
+ rep(:title)
12
+ rep(:chef)
13
+ rep(:ingredients)
14
+ end
15
+ end
16
+
17
+ class Chef < OpenStruct
18
+ include Kashmir
19
+
20
+ representations do
21
+ rep(:full_name)
22
+ end
23
+
24
+ def full_name
25
+ "#{first_name} #{last_name}"
26
+ end
27
+ end
28
+
29
+ class SimpleRecipeRepresenter
30
+ include Kashmir::Dsl
31
+
32
+ prop :num_steps
33
+ prop :title
34
+ end
35
+
36
+ class ChefRepresenter
37
+ include Kashmir::Dsl
38
+
39
+ prop :full_name
40
+ end
41
+
42
+ class RecipeWithChefRepresenter
43
+ include Kashmir::Dsl
44
+
45
+ prop :title
46
+ embed :chef, ChefRepresenter
47
+ end
48
+ end
49
+
50
+ before do
51
+ @chef = DSLTesting::Chef.new(first_name: 'Netto', last_name: 'Farah')
52
+ @brisket = DSLTesting::Recipe.new(title: 'BBQ Brisket', num_steps: 2, chef: @chef)
53
+ end
54
+
55
+ it 'translates to representation definitions' do
56
+ definitions = DSLTesting::SimpleRecipeRepresenter.definitions
57
+ assert_equal definitions, [:num_steps, :title]
58
+ end
59
+
60
+ it 'generates the same representations as hardcoded definitions' do
61
+ cooked_brisket = @brisket.represent([:num_steps, :title])
62
+ assert_equal cooked_brisket, { title: 'BBQ Brisket', num_steps: 2 }
63
+ assert_equal cooked_brisket, @brisket.represent(DSLTesting::SimpleRecipeRepresenter.definitions)
64
+ end
65
+
66
+ it 'generates nested representations' do
67
+ brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
68
+ assert_equal brisket_with_chef, {
69
+ title: 'BBQ Brisket',
70
+ chef: {
71
+ full_name: 'Netto Farah'
72
+ }
73
+ }
74
+
75
+ assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithChefRepresenter.definitions)
76
+ end
77
+
78
+ describe 'Nested inline representations' do
79
+
80
+ module DSLTesting
81
+ class RecipeWithInlineChefRepresenter
82
+ include Kashmir::Dsl
83
+
84
+ prop :title
85
+
86
+ inline :chef do
87
+ prop :full_name
88
+ end
89
+ end
90
+
91
+ class Ingredient < OpenStruct
92
+ include Kashmir
93
+
94
+ representations do
95
+ rep(:name)
96
+ rep(:quantity)
97
+ end
98
+ end
99
+ end
100
+
101
+ it 'generates nested representations' do
102
+ brisket_with_chef = @brisket.represent([:title, { :chef => [:full_name] }])
103
+ assert_equal brisket_with_chef, {
104
+ title: 'BBQ Brisket',
105
+ chef: {
106
+ full_name: 'Netto Farah'
107
+ }
108
+ }
109
+
110
+ assert_equal brisket_with_chef, @brisket.represent(DSLTesting::RecipeWithInlineChefRepresenter.definitions)
111
+ end
112
+ end
113
+
114
+ describe 'Collections' do
115
+ module DSLTesting
116
+ class RecipeWithIngredientsRepresenter
117
+ include Kashmir::Dsl
118
+
119
+ prop :title
120
+
121
+ inline :ingredients do
122
+ prop :name
123
+ prop :quantity
124
+ end
125
+ end
126
+ end
127
+
128
+ it 'generates nested collections' do
129
+ @brisket.ingredients = [
130
+ DSLTesting::Ingredient.new(name: 'Beef', quantity: '8oz'),
131
+ DSLTesting::Ingredient.new(name: 'BBQ Sauce', quantity: 'plenty!'),
132
+ ]
133
+
134
+ cooked_brisket = {
135
+ title: 'BBQ Brisket',
136
+ ingredients: [
137
+ { name: 'Beef', quantity: '8oz' },
138
+ { name: 'BBQ Sauce', quantity: 'plenty!' }
139
+ ]
140
+ }
141
+
142
+ assert_equal cooked_brisket, @brisket.represent(DSLTesting::RecipeWithIngredientsRepresenter.definitions)
143
+ end
144
+ end
145
+
146
+ describe "Kashmir.props" do
147
+
148
+ module DSLTesting
149
+
150
+ class MultiPropRecipeRepresenter
151
+ include Kashmir::Dsl
152
+
153
+ props :num_steps, :title
154
+ end
155
+ end
156
+
157
+ it 'allows the client to specify many props at once' do
158
+ definitions = DSLTesting::MultiPropRecipeRepresenter.definitions
159
+ assert_equal definitions, [:num_steps, :title]
160
+ end
161
+ end
162
+ end