tpitale-mongo_mapper 0.6.9

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 (75) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper/associations/base.rb +110 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +26 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +21 -0
  10. data/lib/mongo_mapper/associations/collection.rb +19 -0
  11. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +26 -0
  12. data/lib/mongo_mapper/associations/many_documents_proxy.rb +115 -0
  13. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +31 -0
  14. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +54 -0
  15. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +113 -0
  17. data/lib/mongo_mapper/associations.rb +70 -0
  18. data/lib/mongo_mapper/callbacks.rb +109 -0
  19. data/lib/mongo_mapper/dirty.rb +136 -0
  20. data/lib/mongo_mapper/document.rb +472 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +74 -0
  22. data/lib/mongo_mapper/embedded_document.rb +384 -0
  23. data/lib/mongo_mapper/finder_options.rb +133 -0
  24. data/lib/mongo_mapper/key.rb +36 -0
  25. data/lib/mongo_mapper/observing.rb +50 -0
  26. data/lib/mongo_mapper/pagination.rb +55 -0
  27. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  29. data/lib/mongo_mapper/serialization.rb +54 -0
  30. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  31. data/lib/mongo_mapper/support.rb +206 -0
  32. data/lib/mongo_mapper/validations.rb +41 -0
  33. data/lib/mongo_mapper.rb +120 -0
  34. data/mongo_mapper.gemspec +173 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_documents_proxy.rb +387 -0
  41. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  42. data/test/functional/associations/test_many_embedded_proxy.rb +192 -0
  43. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1235 -0
  49. data/test/functional/test_embedded_document.rb +135 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +378 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/associations/test_base.rb +166 -0
  60. data/test/unit/associations/test_proxy.rb +91 -0
  61. data/test/unit/serializers/test_json_serializer.rb +189 -0
  62. data/test/unit/test_document.rb +204 -0
  63. data/test/unit/test_dynamic_finder.rb +125 -0
  64. data/test/unit/test_embedded_document.rb +718 -0
  65. data/test/unit/test_finder_options.rb +296 -0
  66. data/test/unit/test_key.rb +172 -0
  67. data/test/unit/test_mongo_mapper.rb +65 -0
  68. data/test/unit/test_observing.rb +101 -0
  69. data/test/unit/test_pagination.rb +113 -0
  70. data/test/unit/test_rails_compatibility.rb +49 -0
  71. data/test/unit/test_serializations.rb +52 -0
  72. data/test/unit/test_support.rb +342 -0
  73. data/test/unit/test_time_zones.rb +40 -0
  74. data/test/unit/test_validations.rb +503 -0
  75. metadata +235 -0
@@ -0,0 +1,378 @@
1
+ require 'test_helper'
2
+
3
+ class ValidationsTest < Test::Unit::TestCase
4
+ context "Saving a new document that is invalid" do
5
+ setup do
6
+ @document = Class.new do
7
+ include MongoMapper::Document
8
+ set_collection_name 'test'
9
+ key :name, String, :required => true
10
+ end
11
+ @document.collection.remove
12
+ end
13
+
14
+ should "not insert document" do
15
+ doc = @document.new
16
+ doc.save
17
+ @document.count.should == 0
18
+ end
19
+
20
+ should "populate document's errors" do
21
+ doc = @document.new
22
+ doc.errors.size.should == 0
23
+ doc.save
24
+ doc.errors.full_messages.should == ["Name can't be empty"]
25
+ end
26
+ end
27
+
28
+ context "Skipping validations when saving" do
29
+ setup do
30
+ @document = Class.new do
31
+ include MongoMapper::Document
32
+ set_collection_name 'test'
33
+ key :name, String, :required => true
34
+ end
35
+ @document.collection.remove
36
+ end
37
+
38
+ should "insert document" do
39
+ doc = @document.new
40
+ doc.save(false)
41
+ @document.count.should == 1
42
+ end
43
+ end
44
+
45
+ context "Saving a document that is invalid (destructive)" do
46
+ setup do
47
+ @document = Class.new do
48
+ include MongoMapper::Document
49
+ set_collection_name 'test'
50
+ key :name, String, :required => true
51
+ end
52
+ @document.collection.remove
53
+ end
54
+
55
+ should "raise error" do
56
+ doc = @document.new
57
+ lambda { doc.save! }.should raise_error(MongoMapper::DocumentNotValid)
58
+ end
59
+ end
60
+
61
+ context "Creating a document that is invalid (destructive)" do
62
+ setup do
63
+ @document = Class.new do
64
+ include MongoMapper::Document
65
+ set_collection_name 'test'
66
+ key :name, String, :required => true
67
+ end
68
+ @document.collection.remove
69
+ end
70
+
71
+ should "raise error" do
72
+ lambda { @document.create! }.should raise_error(MongoMapper::DocumentNotValid)
73
+ end
74
+
75
+ should "create a new document" do
76
+ instance = @document.create!(:name => "James")
77
+ instance.new_record?.should be_false
78
+ end
79
+ end
80
+
81
+ context "Saving an existing document that is invalid" do
82
+ setup do
83
+ @document = Class.new do
84
+ include MongoMapper::Document
85
+ set_collection_name 'test'
86
+ key :name, String, :required => true
87
+ end
88
+ @document.collection.remove
89
+
90
+ @doc = @document.create(:name => 'John Nunemaker')
91
+ end
92
+
93
+ should "not update document" do
94
+ @doc.name = nil
95
+ @doc.save
96
+ @doc.reload.name.should == 'John Nunemaker'
97
+ end
98
+
99
+ should "populate document's errors" do
100
+ @doc.name = nil
101
+ @doc.save
102
+ @doc.errors.full_messages.should == ["Name can't be empty"]
103
+ end
104
+ end
105
+
106
+ context "Adding validation errors" do
107
+ setup do
108
+ @document = Class.new do
109
+ include MongoMapper::Document
110
+ set_collection_name 'test'
111
+
112
+ key :action, String
113
+ def action_present
114
+ errors.add(:action, 'is invalid') if action.blank?
115
+ end
116
+ end
117
+ @document.collection.remove
118
+ end
119
+
120
+ should "work with validate_on_create callback" do
121
+ @document.validate_on_create :action_present
122
+
123
+ doc = @document.new
124
+ doc.action = nil
125
+ doc.should have_error_on(:action)
126
+
127
+ doc.action = 'kick'
128
+ doc.should_not have_error_on(:action)
129
+ doc.save
130
+
131
+ doc.action = nil
132
+ doc.should_not have_error_on(:action)
133
+ end
134
+
135
+ should "work with validate_on_update callback" do
136
+ @document.validate_on_update :action_present
137
+
138
+ doc = @document.new
139
+ doc.action = nil
140
+ doc.should_not have_error_on(:action)
141
+ doc.save
142
+
143
+ doc.action = nil
144
+ doc.should have_error_on(:action)
145
+
146
+ doc.action = 'kick'
147
+ doc.should_not have_error_on(:action)
148
+ end
149
+ end
150
+
151
+ context "validating uniqueness of" do
152
+ setup do
153
+ @document = Class.new do
154
+ include MongoMapper::Document
155
+ set_collection_name 'test'
156
+
157
+ key :name, String
158
+ validates_uniqueness_of :name
159
+ end
160
+ @document.collection.remove
161
+ end
162
+
163
+ should "not fail if object is new" do
164
+ doc = @document.new
165
+ doc.should_not have_error_on(:name)
166
+ end
167
+
168
+ should "not fail when new object is out of scope" do
169
+ document = Class.new do
170
+ include MongoMapper::Document
171
+ set_collection_name 'test'
172
+
173
+ key :name
174
+ key :adult
175
+ validates_uniqueness_of :name, :scope => :adult
176
+ end
177
+ doc = document.new("name" => "joe", :adult => true)
178
+ doc.save.should be_true
179
+
180
+ doc2 = document.new("name" => "joe", :adult => false)
181
+ doc2.should be_valid
182
+
183
+ end
184
+
185
+ should "allow to update an object" do
186
+ doc = @document.new("name" => "joe")
187
+ doc.save.should be_true
188
+
189
+ @document \
190
+ .stubs(:first) \
191
+ .with(:name => 'joe') \
192
+ .returns(doc)
193
+
194
+ doc.name = "joe"
195
+ doc.valid?.should be_true
196
+ doc.should_not have_error_on(:name)
197
+ end
198
+
199
+ should "fail if object name is not unique" do
200
+ doc = @document.new("name" => "joe")
201
+ doc.save.should be_true
202
+
203
+ @document \
204
+ .stubs(:first) \
205
+ .with(:name => 'joe') \
206
+ .returns(doc)
207
+
208
+ doc2 = @document.new("name" => "joe")
209
+ doc2.should have_error_on(:name)
210
+ end
211
+
212
+ should "allow multiple blank entries if :allow_blank => true" do
213
+ document = Class.new do
214
+ include MongoMapper::Document
215
+ set_collection_name 'test'
216
+
217
+ key :name
218
+ validates_uniqueness_of :name, :allow_blank => :true
219
+ end
220
+
221
+ doc = document.new("name" => "")
222
+ doc.save.should be_true
223
+
224
+ document \
225
+ .stubs(:first) \
226
+ .with(:name => '') \
227
+ .returns(doc)
228
+
229
+ doc2 = document.new("name" => "")
230
+ doc2.should_not have_error_on(:name)
231
+ end
232
+
233
+ should "allow entries that differ only in case by default" do
234
+ document = Class.new do
235
+ include MongoMapper::Document
236
+ set_collection_name 'test'
237
+
238
+ key :name
239
+ validates_uniqueness_of :name
240
+ end
241
+
242
+ doc = document.new("name" => "BLAMMO")
243
+ doc.save.should be_true
244
+
245
+ doc2 = document.new("name" => "blammo")
246
+ doc2.should_not have_error_on(:name)
247
+ end
248
+
249
+ context "with :case_sensitive => false" do
250
+ setup do
251
+ @document = Class.new do
252
+ include MongoMapper::Document
253
+ set_collection_name 'test'
254
+
255
+ key :name
256
+ validates_uniqueness_of :name, :case_sensitive => false
257
+ end
258
+ end
259
+
260
+ should "fail on entries that differ only in case" do
261
+ doc = @document.new("name" => "BLAMMO")
262
+ doc.save.should be_true
263
+
264
+ doc2 = @document.new("name" => "blammo")
265
+ doc2.should have_error_on(:name)
266
+ end
267
+
268
+ should "not raise an error if value is nil" do
269
+ doc = @document.new("name" => nil)
270
+ lambda { doc.valid? }.should_not raise_error
271
+ end
272
+ end
273
+
274
+ context "scoped by a single attribute" do
275
+ setup do
276
+ @document = Class.new do
277
+ include MongoMapper::Document
278
+ set_collection_name 'test'
279
+
280
+ key :name, String
281
+ key :scope, String
282
+ validates_uniqueness_of :name, :scope => :scope
283
+ end
284
+ @document.collection.remove
285
+ end
286
+
287
+ should "fail if the same name exists in the scope" do
288
+ doc = @document.new("name" => "joe", "scope" => "one")
289
+ doc.save.should be_true
290
+
291
+ @document \
292
+ .stubs(:first) \
293
+ .with(:name => 'joe', :scope => "one") \
294
+ .returns(doc)
295
+
296
+ doc2 = @document.new("name" => "joe", "scope" => "one")
297
+ doc2.should have_error_on(:name)
298
+ end
299
+
300
+ should "pass if the same name exists in a different scope" do
301
+ doc = @document.new("name" => "joe", "scope" => "one")
302
+ doc.save.should be_true
303
+
304
+ @document \
305
+ .stubs(:first) \
306
+ .with(:name => 'joe', :scope => 'two') \
307
+ .returns(nil)
308
+
309
+ doc2 = @document.new("name" => "joe", "scope" => "two")
310
+ doc2.should_not have_error_on(:name)
311
+ end
312
+ end
313
+
314
+ context "scoped by a multiple attributes" do
315
+ setup do
316
+ @document = Class.new do
317
+ include MongoMapper::Document
318
+ set_collection_name 'test'
319
+
320
+ key :name, String
321
+ key :first_scope, String
322
+ key :second_scope, String
323
+ validates_uniqueness_of :name, :scope => [:first_scope, :second_scope]
324
+ end
325
+ @document.collection.remove
326
+ end
327
+
328
+ should "fail if the same name exists in the scope" do
329
+ doc = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
330
+ doc.save.should be_true
331
+
332
+ @document \
333
+ .stubs(:first) \
334
+ .with(:name => 'joe', :first_scope => 'one', :second_scope => 'two') \
335
+ .returns(doc)
336
+
337
+ doc2 = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
338
+ doc2.should have_error_on(:name)
339
+ end
340
+
341
+ should "pass if the same name exists in a different scope" do
342
+ doc = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
343
+ doc.save.should be_true
344
+
345
+ @document \
346
+ .stubs(:first) \
347
+ .with(:name => 'joe', :first_scope => 'one', :second_scope => 'one') \
348
+ .returns(nil)
349
+
350
+ doc2 = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "one")
351
+ doc2.should_not have_error_on(:name)
352
+ end
353
+ end
354
+ end
355
+
356
+ context "validates uniqueness of with :unique shortcut" do
357
+ should "work" do
358
+ @document = Class.new do
359
+ include MongoMapper::Document
360
+ set_collection_name 'test'
361
+
362
+ key :name, String, :unique => true
363
+ end
364
+ @document.collection.remove
365
+
366
+ doc = @document.create(:name => 'John')
367
+ doc.should_not have_error_on(:name)
368
+
369
+ @document \
370
+ .stubs(:first) \
371
+ .with(:name => 'John') \
372
+ .returns(doc)
373
+
374
+ second_john = @document.create(:name => 'John')
375
+ second_john.should have_error_on(:name, 'has already been taken')
376
+ end
377
+ end
378
+ end
data/test/models.rb ADDED
@@ -0,0 +1,271 @@
1
+ # custom type
2
+ class WindowSize
3
+ attr_reader :width, :height
4
+
5
+ def self.to_mongo(value)
6
+ value.to_a
7
+ end
8
+
9
+ def self.from_mongo(value)
10
+ value.is_a?(self) ? value : WindowSize.new(value)
11
+ end
12
+
13
+ def initialize(*args)
14
+ @width, @height = args.flatten
15
+ end
16
+
17
+ def to_a
18
+ [width, height]
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(self.class) && other.width == width && other.height == height
23
+ end
24
+ end
25
+
26
+ module AccountsExtensions
27
+ def inactive
28
+ all(:last_logged_in => nil)
29
+ end
30
+ end
31
+
32
+ class Post
33
+ include MongoMapper::Document
34
+
35
+ key :title, String
36
+ key :body, String
37
+
38
+ many :comments, :as => :commentable, :class_name => 'PostComment'
39
+
40
+ timestamps!
41
+ end
42
+
43
+ class PostComment
44
+ include MongoMapper::Document
45
+
46
+ key :username, String, :default => 'Anonymous'
47
+ key :body, String
48
+
49
+ key :commentable_id, ObjectId
50
+ key :commentable_type, String
51
+ belongs_to :commentable, :polymorphic => true
52
+
53
+ timestamps!
54
+ end
55
+
56
+ class Address
57
+ include MongoMapper::EmbeddedDocument
58
+
59
+ key :address, String
60
+ key :city, String
61
+ key :state, String
62
+ key :zip, Integer
63
+ end
64
+
65
+ class Message
66
+ include MongoMapper::Document
67
+
68
+ key :body, String
69
+ key :position, Integer
70
+ key :_type, String
71
+ key :room_id, ObjectId
72
+
73
+ belongs_to :room
74
+ end
75
+
76
+ class Enter < Message; end
77
+ class Exit < Message; end
78
+ class Chat < Message; end
79
+
80
+ class Room
81
+ include MongoMapper::Document
82
+
83
+ key :name, String
84
+ many :messages, :polymorphic => true, :order => 'position' do
85
+ def older
86
+ all(:position => {'$gt' => 5})
87
+ end
88
+ end
89
+ many :latest_messages, :class_name => 'Message', :order => 'position desc', :limit => 2
90
+
91
+ many :accounts, :polymorphic => true, :extend => AccountsExtensions
92
+ end
93
+
94
+ class Account
95
+ include MongoMapper::Document
96
+
97
+ key :_type, String
98
+ key :room_id, ObjectId
99
+ key :last_logged_in, Time
100
+
101
+ belongs_to :room
102
+ end
103
+ class User < Account; end
104
+ class Bot < Account; end
105
+
106
+ class Answer
107
+ include MongoMapper::Document
108
+
109
+ key :body, String
110
+ end
111
+
112
+ module PeopleExtensions
113
+ def find_by_name(name)
114
+ detect { |p| p.name == name }
115
+ end
116
+ end
117
+
118
+ module CollaboratorsExtensions
119
+ def top
120
+ first
121
+ end
122
+ end
123
+
124
+ class Project
125
+ include MongoMapper::Document
126
+
127
+ key :name, String
128
+
129
+ many :people, :extend => PeopleExtensions
130
+ many :collaborators, :extend => CollaboratorsExtensions
131
+
132
+ many :statuses, :order => 'position' do
133
+ def open
134
+ all(:name => %w(New Assigned))
135
+ end
136
+ end
137
+
138
+ many :addresses do
139
+ def find_all_by_state(state)
140
+ # can't use select here for some reason
141
+ find_all { |a| a.state == state }
142
+ end
143
+ end
144
+ end
145
+
146
+ class Collaborator
147
+ include MongoMapper::Document
148
+ key :project_id, ObjectId
149
+ key :name, String
150
+ belongs_to :project
151
+ end
152
+
153
+ class Status
154
+ include MongoMapper::Document
155
+
156
+ key :project_id, ObjectId
157
+ key :target_id, ObjectId
158
+ key :target_type, String
159
+ key :name, String, :required => true
160
+ key :position, Integer
161
+
162
+ belongs_to :project
163
+ belongs_to :target, :polymorphic => true
164
+ end
165
+
166
+ class RealPerson
167
+ include MongoMapper::Document
168
+
169
+ key :room_id, ObjectId
170
+ key :name, String
171
+
172
+ belongs_to :room
173
+
174
+ many :pets
175
+
176
+ def realname=(n)
177
+ self.name = n
178
+ end
179
+ end
180
+
181
+ class Person
182
+ include MongoMapper::EmbeddedDocument
183
+
184
+ key :name, String
185
+ key :child, Person
186
+
187
+ many :pets
188
+ end
189
+
190
+ class Pet
191
+ include MongoMapper::EmbeddedDocument
192
+
193
+ key :name, String
194
+ key :species, String
195
+ end
196
+
197
+ class Media
198
+ include MongoMapper::EmbeddedDocument
199
+
200
+ key :_type, String
201
+ key :file, String
202
+
203
+ key :visible, Boolean
204
+ end
205
+
206
+ class Video < Media
207
+ key :length, Integer
208
+ end
209
+
210
+ class Image < Media
211
+ key :width, Integer
212
+ key :height, Integer
213
+ end
214
+
215
+ class Music < Media
216
+ key :bitrate, String
217
+ end
218
+
219
+ class Catalog
220
+ include MongoMapper::Document
221
+
222
+ many :medias, :polymorphic => true do
223
+ def visible
224
+ # for some reason we can't use select here
225
+ find_all { |m| m.visible? }
226
+ end
227
+ end
228
+ end
229
+
230
+ module TrModels
231
+ class Transport
232
+ include MongoMapper::EmbeddedDocument
233
+
234
+ key :_type, String
235
+ key :license_plate, String
236
+ key :purchased_on, Date
237
+ end
238
+
239
+ class Car < TrModels::Transport
240
+ include MongoMapper::EmbeddedDocument
241
+
242
+ key :model, String
243
+ key :year, Integer
244
+ end
245
+
246
+ class Bus < TrModels::Transport
247
+ include MongoMapper::EmbeddedDocument
248
+
249
+ key :max_passengers, Integer
250
+ end
251
+
252
+ class Ambulance < TrModels::Transport
253
+ include MongoMapper::EmbeddedDocument
254
+
255
+ key :icu, Boolean
256
+ end
257
+
258
+ class Fleet
259
+ include MongoMapper::Document
260
+
261
+ module TransportsExtension
262
+ def to_be_replaced
263
+ # for some reason we can't use select
264
+ find_all { |t| t.purchased_on < 2.years.ago.to_date }
265
+ end
266
+ end
267
+
268
+ many :transports, :polymorphic => true, :class_name => "TrModels::Transport", :extend => TransportsExtension
269
+ key :name, String
270
+ end
271
+ end
@@ -0,0 +1,55 @@
1
+ module CustomMatchers
2
+ custom_matcher :be_nil do |receiver, matcher, args|
3
+ matcher.positive_failure_message = "Expected #{receiver} to be nil but it wasn't"
4
+ matcher.negative_failure_message = "Expected #{receiver} not to be nil but it was"
5
+ receiver.nil?
6
+ end
7
+
8
+ custom_matcher :be_blank do |receiver, matcher, args|
9
+ matcher.positive_failure_message = "Expected #{receiver} to be blank but it wasn't"
10
+ matcher.negative_failure_message = "Expected #{receiver} not to be blank but it was"
11
+ receiver.blank?
12
+ end
13
+
14
+ custom_matcher :be_true do |receiver, matcher, args|
15
+ matcher.positive_failure_message = "Expected #{receiver} to be true but it wasn't"
16
+ matcher.negative_failure_message = "Expected #{receiver} not to be true but it was"
17
+ receiver.eql?(true)
18
+ end
19
+
20
+ custom_matcher :be_false do |receiver, matcher, args|
21
+ matcher.positive_failure_message = "Expected #{receiver} to be false but it wasn't"
22
+ matcher.negative_failure_message = "Expected #{receiver} not to be false but it was"
23
+ receiver.eql?(false)
24
+ end
25
+
26
+ custom_matcher :be_valid do |receiver, matcher, args|
27
+ matcher.positive_failure_message = "Expected to be valid but it was invalid #{receiver.errors.inspect}"
28
+ matcher.negative_failure_message = "Expected to be invalid but it was valid #{receiver.errors.inspect}"
29
+ receiver.valid?
30
+ end
31
+
32
+ custom_matcher :have_error_on do |receiver, matcher, args|
33
+ receiver.valid?
34
+ attribute = args[0]
35
+ expected_message = args[1]
36
+
37
+ if expected_message.nil?
38
+ matcher.positive_failure_message = "#{receiver} had no errors on #{attribute}"
39
+ matcher.negative_failure_message = "#{receiver} had errors on #{attribute} #{receiver.errors.inspect}"
40
+ !receiver.errors.on(attribute).blank?
41
+ else
42
+ actual = receiver.errors.on(attribute)
43
+ matcher.positive_failure_message = %Q(Expected error on #{attribute} to be "#{expected_message}" but was "#{actual}")
44
+ matcher.negative_failure_message = %Q(Expected error on #{attribute} not to be "#{expected_message}" but was "#{actual}")
45
+ actual == expected_message
46
+ end
47
+ end
48
+
49
+ custom_matcher :have_index do |receiver, matcher, args|
50
+ index_name = args[0]
51
+ matcher.positive_failure_message = "#{receiver} does not have index named #{index_name}, but should"
52
+ matcher.negative_failure_message = "#{receiver} does have index named #{index_name}, but should not"
53
+ !receiver.collection.index_information.detect { |index| index[0] == index_name }.nil?
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ class Test::Unit::TestCase
2
+ def run_with_test_timing(*args, &block)
3
+ begin_time = Time.now
4
+ run_without_test_timing(*args, &block)
5
+ end_time = Time.now
6
+
7
+ duration = end_time - begin_time
8
+ threshold = 0.5
9
+
10
+ if duration > threshold
11
+ puts "\nSLOW TEST: #{duration} - #{self.name}"
12
+ end
13
+ end
14
+
15
+ alias_method_chain :run, :test_timing unless method_defined?(:run_without_test_timing)
16
+ end