jodosha-cached-models 0.0.3

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 (40) hide show
  1. data/CHANGELOG +142 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README +95 -0
  4. data/Rakefile +81 -0
  5. data/about.yml +8 -0
  6. data/cached-models.gemspec +18 -0
  7. data/init.rb +2 -0
  8. data/install.rb +1 -0
  9. data/lib/activerecord/lib/active_record.rb +4 -0
  10. data/lib/activerecord/lib/active_record/associations.rb +404 -0
  11. data/lib/activerecord/lib/active_record/associations/association_collection.rb +127 -0
  12. data/lib/activerecord/lib/active_record/associations/association_proxy.rb +39 -0
  13. data/lib/activerecord/lib/active_record/associations/has_many_association.rb +7 -0
  14. data/lib/activerecord/lib/active_record/base.rb +73 -0
  15. data/lib/cached-models.rb +1 -0
  16. data/lib/cached_models.rb +4 -0
  17. data/setup.rb +1585 -0
  18. data/tasks/cached_models_tasks.rake +117 -0
  19. data/test/active_record/associations/has_and_belongs_to_many_association_test.rb +12 -0
  20. data/test/active_record/associations/has_many_association_test.rb +355 -0
  21. data/test/active_record/associations/has_one_association_test.rb +12 -0
  22. data/test/active_record/base_test.rb +32 -0
  23. data/test/fixtures/addresses.yml +7 -0
  24. data/test/fixtures/authors.yml +13 -0
  25. data/test/fixtures/blogs.yml +7 -0
  26. data/test/fixtures/categories.yml +3 -0
  27. data/test/fixtures/categories_posts.yml +3 -0
  28. data/test/fixtures/comments.yml +19 -0
  29. data/test/fixtures/posts.yml +23 -0
  30. data/test/fixtures/tags.yml +14 -0
  31. data/test/models/address.rb +3 -0
  32. data/test/models/author.rb +12 -0
  33. data/test/models/blog.rb +4 -0
  34. data/test/models/category.rb +3 -0
  35. data/test/models/comment.rb +3 -0
  36. data/test/models/post.rb +8 -0
  37. data/test/models/tag.rb +3 -0
  38. data/test/test_helper.rb +88 -0
  39. data/uninstall.rb +1 -0
  40. metadata +112 -0
@@ -0,0 +1,117 @@
1
+ # RAILS_ENV = "test"
2
+
3
+ require 'rubygems'
4
+ require 'active_record'
5
+ require 'active_record/fixtures'
6
+
7
+ path_to_fixtures = File.dirname(__FILE__) + '/../test/fixtures'
8
+ fixtures = %w( addresses authors blogs posts categories categories_posts comments tags )
9
+
10
+ desc 'Run default task (test)'
11
+ task :cached_models => 'cached_models:test'
12
+
13
+ namespace :cached_models do
14
+ desc 'Reset the CachedModels data'
15
+ task :reset => [ :teardown, :setup ]
16
+
17
+ desc 'Create CachedModels test database tables and load fixtures'
18
+ task :setup => [ :create_tables, :load_fixtures ]
19
+
20
+ desc 'Remove all CachedModels data'
21
+ task :teardown => :drop_tables
22
+
23
+ desc 'Create CachedModels test database tables'
24
+ task :create_tables => :environment do
25
+ ActiveRecord::Schema.define do
26
+ create_table :addresses, :force => true do |t|
27
+ t.integer :author_id
28
+ t.string :street
29
+ t.string :zip
30
+ t.string :city
31
+ t.string :state
32
+ t.string :country
33
+
34
+ t.timestamps
35
+ end
36
+
37
+ create_table :authors, :force => true do |t|
38
+ t.integer :blog_id
39
+ t.string :first_name
40
+ t.string :last_name
41
+ t.integer :age
42
+
43
+ t.timestamps
44
+ end
45
+
46
+ create_table :blogs, :force => true do |t|
47
+ t.string :title
48
+
49
+ t.timestamps
50
+ end
51
+
52
+ create_table :posts, :force => true do |t|
53
+ t.integer :author_id
54
+ t.integer :blog_id
55
+ t.string :title
56
+ t.text :text
57
+ t.datetime :published_at
58
+
59
+ t.timestamps
60
+ end
61
+
62
+ create_table :categories, :force => true do |t|
63
+ t.string :name
64
+
65
+ t.timestamps
66
+ end
67
+
68
+ create_table :categories_posts, :force => true do |t|
69
+ t.integer :category_id
70
+ t.integer :post_id
71
+ end
72
+
73
+ create_table :comments, :force => true do |t|
74
+ t.integer :post_id
75
+ t.string :email
76
+ t.text :text
77
+
78
+ t.timestamps
79
+ end
80
+
81
+ create_table :tags, :force => true do |t|
82
+ t.integer :taggable_id
83
+ t.string :taggable_type
84
+ t.string :name
85
+
86
+ t.timestamps
87
+ end
88
+ end
89
+ end
90
+
91
+ desc 'Drops CachedModels test database tables'
92
+ task :drop_tables => :environment do
93
+ ActiveRecord::Base.connection.drop_table :addresses
94
+ ActiveRecord::Base.connection.drop_table :authors
95
+ ActiveRecord::Base.connection.drop_table :posts
96
+ ActiveRecord::Base.connection.drop_table :comments
97
+ ActiveRecord::Base.connection.drop_table :tags
98
+ ActiveRecord::Base.connection.drop_table :categories
99
+ ActiveRecord::Base.connection.drop_table :categories_posts
100
+ end
101
+
102
+ desc 'Load fixtures'
103
+ task :load_fixtures => :environment do
104
+ fixtures.each { |f| Fixtures.create_fixtures(path_to_fixtures, f) }
105
+ end
106
+
107
+ desc 'Test CachedModels'
108
+ task :test => [ :setup, 'test:all' ]
109
+
110
+ namespace :test do
111
+ desc 'Run CachedModels tests'
112
+ Rake::TestTask.new(:all) do |t|
113
+ t.test_files = FileList["#{File.dirname( __FILE__ )}/../test/**/*_test.rb"]
114
+ t.verbose = true
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+
3
+ class HasAndBelongsToManyAssociationTest < Test::Unit::TestCase
4
+ include ActiveRecord::Associations
5
+
6
+ def test_should_not_raise_exception
7
+ assert_nothing_raised ArgumentError do
8
+ posts(:welcome).categories
9
+ categories(:announcements).posts
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,355 @@
1
+ require File.dirname(__FILE__) + '/../../test_helper'
2
+ require 'active_record/associations/has_many_association'
3
+
4
+ class HasManyAssociationTest < Test::Unit::TestCase
5
+ include ActiveRecord::Associations
6
+
7
+ def setup
8
+ cache.clear rescue nil
9
+ end
10
+
11
+ uses_mocha 'HasManyAssociationTest' do
12
+ def test_should_always_use_cache_for_all_instances_which_reference_the_same_record
13
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
14
+ expected = authors(:luca).cached_posts
15
+ actual = Author.first.cached_posts
16
+ assert_equal expected, actual
17
+ end
18
+
19
+ def test_should_expire_cache_on_update
20
+ author = authors(:luca)
21
+ old_cache_key = author.cache_key
22
+
23
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
24
+ cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
25
+ cache.expects(:delete).with("#{cache_key}/cached_comments").returns true
26
+ cache.expects(:delete).with("#{cache_key}/cached_posts_with_comments").returns true
27
+
28
+ author.cached_posts # force cache loading
29
+ author.update_attributes :first_name => author.first_name.upcase
30
+
31
+ # assert_not_equal old_cache_key, author.cache_key
32
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
33
+ end
34
+
35
+ def test_should_use_cache_when_find_with_scope
36
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
37
+ post = authors(:luca).cached_posts.find(posts(:welcome).id)
38
+ assert_equal posts(:welcome), post
39
+ end
40
+
41
+ def test_should_use_cache_when_find_with_scope_using_multiple_ids
42
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
43
+ ids = posts_by_author(:luca).map(&:id)
44
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts.find(ids)
45
+ end
46
+
47
+ def test_should_use_cache_when_fetch_first_from_collection
48
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
49
+ assert_equal [ posts_by_author(:luca).first ], authors(:luca).cached_posts.first(1)
50
+ end
51
+
52
+ def test_should_use_cache_when_fetch_last_from_collection
53
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
54
+ assert_equal [ posts_by_author(:luca).last ], authors(:luca).cached_posts.last(1)
55
+ end
56
+
57
+ def test_should_unload_cache_when_reset_collection
58
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
59
+ assert_false authors(:luca).cached_posts.reset
60
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
61
+ end
62
+
63
+ def test_should_not_use_cache_on_collection_sum
64
+ # calculations aren't supported for now
65
+ cache.expects(:read).with("#{blogs(:weblog).cache_key}/authors").never
66
+ assert_equal authors_by_blog(:weblog).map(&:age).sum, blogs(:weblog).authors.sum(:age)
67
+ end
68
+
69
+ def test_should_not_use_cache_on_false_cached_option
70
+ cache.expects(:read).never
71
+ authors(:luca).posts
72
+ authors(:luca).posts(true) # force reload
73
+ end
74
+
75
+ def test_should_cache_associated_objects
76
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
77
+ posts = authors(:luca).cached_posts
78
+ assert_equal posts, authors(:luca).cached_posts
79
+ end
80
+
81
+ def test_should_safely_use_pagination
82
+ # pagination for now bypass cache and using database.
83
+ # the expectation is due to #cached_posts invocation.
84
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
85
+ posts = authors(:luca).cached_posts.paginate(:all, :page => 1, :per_page => 1)
86
+ assert_equal [ posts_by_author(:luca).first ], posts
87
+ end
88
+
89
+ def test_should_reload_association_and_refresh_the_cache_on_force_reload
90
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns(posts_by_author(:luca))
91
+ cache.expects(:write).times(3).returns true
92
+ reloaded_posts = authors(:luca).cached_posts(true)
93
+ assert_equal reloaded_posts, authors(:luca).cached_posts
94
+ end
95
+
96
+ def test_should_cache_associated_ids
97
+ ids = posts_by_author(:luca).map(&:id)
98
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns(posts_by_author(:luca))
99
+ cache.expects(:fetch).with("#{cache_key}/cached_post_ids").returns ids
100
+ assert_equal ids, authors(:luca).cached_post_ids
101
+ end
102
+
103
+ def test_should_not_cache_associated_ids_on_false_cached_option
104
+ cache.expects(:fetch).never
105
+ authors(:luca).post_ids
106
+ end
107
+
108
+ def test_should_cache_all_eager_loaded_objects
109
+ cache.expects(:read).with("#{cache_key}/cached_posts_with_comments").returns(posts_by_author(:luca, true))
110
+ posts = authors(:luca).cached_posts_with_comments
111
+ assert_equal posts, authors(:luca).cached_posts_with_comments
112
+ end
113
+
114
+ def test_should_not_cache_eager_loaded_objects_on_false_cached_option
115
+ cache.expects(:read).never
116
+ authors(:luca).posts_with_comments
117
+ end
118
+
119
+ def test_should_cache_polymorphic_associations
120
+ cache.expects(:read).with("#{posts(:cached_models).cache_key}/cached_tags").returns(tags_by_post(:cached_models))
121
+ tags = posts(:cached_models).cached_tags
122
+ assert_equal tags, posts(:cached_models).cached_tags
123
+ end
124
+
125
+ def test_should_not_cache_polymorphic_associations_on_false_cached_option
126
+ cache.expects(:read).never
127
+ posts(:cached_models).tags
128
+ end
129
+
130
+ def test_should_cache_habtm_associations
131
+ cache.expects(:read).with("#{cache_key}/cached_comments").returns(comments_by_author(:luca))
132
+ comments = authors(:luca).cached_comments
133
+ assert_equal comments, authors(:luca).cached_comments
134
+ end
135
+
136
+ def test_should_not_cache_habtm_associations_on_false_cached_option
137
+ cache.expects(:read).never
138
+ authors(:luca).comments
139
+ end
140
+
141
+ def test_should_refresh_cache_when_associated_elements_change
142
+ cache.expects(:read).with("#{cache_key}/cached_posts").never
143
+ cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
144
+ post = authors(:luca).cached_posts.last # force cache loading and fetch a post
145
+ post.update_attributes :title => 'Cached Models!'
146
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
147
+ end
148
+
149
+ def test_should_refresh_cache_when_pushing_element_to_association
150
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
151
+ cache.expects(:write).with("#{cache_key}/cached_posts", association_proxy).returns true
152
+ post = create_post :author_id => nil
153
+ authors(:luca).cached_posts << post
154
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
155
+ end
156
+
157
+ def test_should_not_use_cache_when_pushing_element_to_association_on_false_cached_option
158
+ cache.expects(:write).never
159
+ post = create_post :author_id => nil
160
+ authors(:luca).posts << post
161
+ end
162
+
163
+ def test_should_not_use_cache_when_pushing_element_to_association_belonging_to_anotner_model_on_false_cached_option
164
+ cache.expects(:delete).with("#{blogs(:weblog).cache_key}/posts").never
165
+ cache.expects(:delete).with("#{cache_key}/cached_posts_with_comments").never
166
+ cache.expects(:delete).with("#{cache_key}/cached_posts").returns true
167
+ cache.expects(:delete).with("#{posts(:cached_models).cache_key}/cached_tags").returns true
168
+ post = blogs(:weblog).posts.last
169
+ blogs(:blog).posts << post
170
+ assert_equal posts_by_blog(:blog), blogs(:blog).posts
171
+ end
172
+
173
+ def test_should_not_use_cache_when_pushing_element_to_polymorphic_association_belonging_to_another_model_on_false_cached_option
174
+ cache.expects(:delete).never
175
+ tag = posts(:welcome).tags.last
176
+ posts(:cached_models).tags << tag
177
+ assert_equal tags_by_post(:cached_models), posts(:cached_models).tags
178
+ end
179
+
180
+ def test_should_update_cache_when_directly_assigning_a_new_collection
181
+ posts = [ posts_by_author(:luca).first ]
182
+ cache.expects(:read).with("#{cache_key}/cached_posts").times(2).returns association_proxy
183
+ authors(:luca).cached_posts = posts
184
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
185
+ end
186
+
187
+ def test_should_use_cache_for_collection_size
188
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
189
+ assert_equal posts_by_author(:luca).size, authors(:luca).cached_posts.size
190
+ end
191
+
192
+ def test_should_use_cache_and_return_uniq_records_for_collection_size_on_uniq_option
193
+ cache.expects(:read).with("#{cache_key}/uniq_cached_posts").never # wuh?!
194
+ assert_equal posts_by_author(:luca).size, authors(:luca).uniq_cached_posts.size
195
+ end
196
+
197
+ def test_should_use_cache_for_collection_length
198
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
199
+ assert_equal posts_by_author(:luca).length, authors(:luca).cached_posts.length
200
+ end
201
+
202
+ def test_should_use_cache_for_collection_empty
203
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
204
+ assert_equal posts_by_author(:luca).empty?, authors(:luca).cached_posts.empty?
205
+ end
206
+
207
+ def test_should_use_cache_for_collection_any
208
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
209
+ assert_equal posts_by_author(:luca).any?, authors(:luca).cached_posts.any?
210
+ end
211
+
212
+ def test_should_use_cache_for_collection_include
213
+ cache.expects(:read).with("#{cache_key}/cached_posts").returns association_proxy
214
+ post = posts_by_author(:luca).first
215
+ assert authors(:luca).cached_posts.include?(post)
216
+ end
217
+ end
218
+
219
+ uses_memcached 'HasManyAssociationTest' do
220
+ def test_should_refresh_caches_when_pushing_element_to_association_belonging_to_another_model
221
+ post = authors(:chuck).cached_posts.last
222
+ authors(:luca).cached_posts << post
223
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
224
+ assert_equal posts_by_author(:chuck), authors(:chuck).cached_posts
225
+ end
226
+
227
+ def test_should_refresh_caches_when_pushing_element_to_polymorphic_association_belonging_to_another_model
228
+ tag = posts(:welcome).cached_tags.last
229
+ posts(:cached_models).cached_tags << tag
230
+
231
+ # NOTE for some weird reason the assertion fails, even if the collections are equals.
232
+ # I forced the comparision between the ids.
233
+ assert_equal tags_by_post(:cached_models).map(&:id).sort,
234
+ posts(:cached_models).cached_tags.map(&:id).sort
235
+ end
236
+
237
+ def test_should_update_cache_when_pushing_element_with_build
238
+ author = authors(:luca)
239
+ post = author.cached_posts.build post_options
240
+ post.save
241
+ assert_equal posts_by_author(:luca), author.cached_posts
242
+ end
243
+
244
+ def test_should_update_cache_when_pushing_element_with_create
245
+ author = authors(:luca)
246
+ author.cached_posts.create post_options(:title => "CM Overview")
247
+ assert_equal posts_by_author(:luca), author.cached_posts
248
+ end
249
+
250
+ def test_should_update_cache_when_pushing_element_with_create_bang_method
251
+ author = authors(:luca)
252
+ author.cached_posts.create! post_options(:title => "CM Overview!!")
253
+ assert_equal posts_by_author(:luca), author.cached_posts
254
+ end
255
+
256
+ def test_should_expire_cache_when_delete_all_elements_from_collection
257
+ authors(:luca).cached_posts.delete_all
258
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
259
+ end
260
+
261
+ def test_should_expire_cache_when_destroy_all_elements_from_collection
262
+ authors(:luca).cached_posts.destroy_all
263
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
264
+ end
265
+
266
+ def test_should_update_cache_when_clearing_collection
267
+ authors(:luca).cached_posts.clear
268
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
269
+ end
270
+
271
+ def test_should_update_cache_when_clearing_collection_with_dependent_destroy_option
272
+ authors(:luca).cached_dependent_posts.clear
273
+ assert_equal posts_by_author(:luca), authors(:luca).cached_dependent_posts
274
+ end
275
+
276
+ def test_should_update_cache_when_deleting_element_from_collection
277
+ authors(:luca).cached_posts.delete(posts_by_author(:luca).first)
278
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
279
+ end
280
+
281
+ def test_should_update_cache_when_replace_collection
282
+ post = create_post; post.save
283
+ posts = [ posts_by_author(:luca).first, post ]
284
+ authors(:luca).cached_posts.replace(posts)
285
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
286
+ end
287
+
288
+ def test_should_not_expire_cache_on_update_on_missing_updated_at
289
+ author = authors(:luca)
290
+ old_cache_key = author.cache_key
291
+
292
+ author.cached_posts # force cache loading
293
+ author.update_attributes :first_name => author.first_name.upcase
294
+
295
+ # assert_not_equal old_cache_key, author.cache_key
296
+ assert_equal posts_by_author(:luca), authors(:luca).cached_posts
297
+ end
298
+ end
299
+
300
+ private
301
+ def posts_by_author(author, include_comments = false)
302
+ conditions = include_comments ? { :include => :comments } : { }
303
+ Post.find_all_by_author_id(authors(author).id, conditions)
304
+ end
305
+
306
+ def posts_by_blog(blog)
307
+ Post.find_all_by_blog_id(blogs(blog).id)
308
+ end
309
+
310
+ def tags_by_post(post)
311
+ Tag.find_all_by_taggable_id(posts(post).id)
312
+ end
313
+
314
+ def comments_by_author(author)
315
+ Comment.find(:all, :conditions => ["post_id IN (?)", authors(author).post_ids])
316
+ end
317
+
318
+ def authors_by_blog(blog)
319
+ Author.find_all_by_blog_id(blogs(blog).id)
320
+ end
321
+
322
+ def association_proxy(author = :luca)
323
+ HasManyAssociation.new(authors(author), Author.reflect_on_association(:cached_posts))
324
+ end
325
+
326
+ def authors_association_proxy(blog = :weblog)
327
+ HasManyAssociation.new(blogs(blog), Blog.reflect_on_association(:authors))
328
+ end
329
+
330
+ def tags_association_proxy(post = :welcome)
331
+ HasManyAssociation.new(posts(post), Post.reflect_on_association(:cached_tags))
332
+ end
333
+
334
+ def comments_association_proxy(author = :luca)
335
+ HasManyThroughAssociation.new(authors(author), Author.reflect_on_association(:cached_comments))
336
+ end
337
+
338
+ def create_post(options = {})
339
+ Post.new({ :author_id => 1,
340
+ :title => 'CachedModels',
341
+ :text => 'Introduction to CachedModels plugin',
342
+ :published_at => 1.week.ago }.merge(options))
343
+ end
344
+
345
+ def post_options(options = {})
346
+ { :blog_id => blogs(:weblog).id,
347
+ :title => "Cached models review",
348
+ :text => "Cached models review..",
349
+ :published_at => 1.week.ago }.merge(options)
350
+ end
351
+
352
+ def cache_key
353
+ @cache_key ||= authors(:luca).cache_key
354
+ end
355
+ end