motion-support 0.2.3 → 0.2.4

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.
data/motion/concern.rb ADDED
@@ -0,0 +1,122 @@
1
+ module MotionSupport
2
+ # A typical module looks like this:
3
+ #
4
+ # module M
5
+ # def self.included(base)
6
+ # base.extend ClassMethods
7
+ # base.class_eval do
8
+ # scope :disabled, -> { where(disabled: true) }
9
+ # end
10
+ # end
11
+ #
12
+ # module ClassMethods
13
+ # ...
14
+ # end
15
+ # end
16
+ #
17
+ # By using <tt>MotionSupport::Concern</tt> the above module could instead be
18
+ # written as:
19
+ #
20
+ # module M
21
+ # extend MotionSupport::Concern
22
+ #
23
+ # included do
24
+ # scope :disabled, -> { where(disabled: true) }
25
+ # end
26
+ #
27
+ # module ClassMethods
28
+ # ...
29
+ # end
30
+ # end
31
+ #
32
+ # Moreover, it gracefully handles module dependencies. Given a +Foo+ module
33
+ # and a +Bar+ module which depends on the former, we would typically write the
34
+ # following:
35
+ #
36
+ # module Foo
37
+ # def self.included(base)
38
+ # base.class_eval do
39
+ # def self.method_injected_by_foo
40
+ # ...
41
+ # end
42
+ # end
43
+ # end
44
+ # end
45
+ #
46
+ # module Bar
47
+ # def self.included(base)
48
+ # base.method_injected_by_foo
49
+ # end
50
+ # end
51
+ #
52
+ # class Host
53
+ # include Foo # We need to include this dependency for Bar
54
+ # include Bar # Bar is the module that Host really needs
55
+ # end
56
+ #
57
+ # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
58
+ # could try to hide these from +Host+ directly including +Foo+ in +Bar+:
59
+ #
60
+ # module Bar
61
+ # include Foo
62
+ # def self.included(base)
63
+ # base.method_injected_by_foo
64
+ # end
65
+ # end
66
+ #
67
+ # class Host
68
+ # include Bar
69
+ # end
70
+ #
71
+ # Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
72
+ # is the +Bar+ module, not the +Host+ class. With <tt>MotionSupport::Concern</tt>,
73
+ # module dependencies are properly resolved:
74
+ #
75
+ # module Foo
76
+ # extend MotionSupport::Concern
77
+ # included do
78
+ # def self.method_injected_by_foo
79
+ # ...
80
+ # end
81
+ # end
82
+ # end
83
+ #
84
+ # module Bar
85
+ # extend MotionSupport::Concern
86
+ # include Foo
87
+ #
88
+ # included do
89
+ # self.method_injected_by_foo
90
+ # end
91
+ # end
92
+ #
93
+ # class Host
94
+ # include Bar # works, Bar takes care now of its dependencies
95
+ # end
96
+ module Concern
97
+ def self.extended(base) #:nodoc:
98
+ base.instance_variable_set("@_dependencies", [])
99
+ end
100
+
101
+ def append_features(base)
102
+ if base.instance_variable_defined?("@_dependencies")
103
+ base.instance_variable_get("@_dependencies") << self
104
+ return false
105
+ else
106
+ return false if base < self
107
+ @_dependencies.each { |dep| base.send(:include, dep) }
108
+ super
109
+ base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
110
+ base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
111
+ end
112
+ end
113
+
114
+ def included(base = nil, &block)
115
+ if base.nil?
116
+ @_included_block = block
117
+ else
118
+ super
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,6 @@
1
+ module Kernel
2
+ # class_eval on an object acts like singleton_class.class_eval.
3
+ def class_eval(*args, &block)
4
+ singleton_class.class_eval(*args, &block)
5
+ end
6
+ end
data/motion/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module MotionSupport
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
@@ -0,0 +1,702 @@
1
+ module CallbacksTest
2
+ class Record
3
+ include MotionSupport::Callbacks
4
+
5
+ define_callbacks :save
6
+
7
+ def self.before_save(*filters, &blk)
8
+ set_callback(:save, :before, *filters, &blk)
9
+ end
10
+
11
+ def self.after_save(*filters, &blk)
12
+ set_callback(:save, :after, *filters, &blk)
13
+ end
14
+
15
+ class << self
16
+ def callback_symbol(callback_method)
17
+ method_name = :"#{callback_method}_method"
18
+ define_method(method_name) do
19
+ history << [callback_method, :symbol]
20
+ end
21
+ method_name
22
+ end
23
+
24
+ def callback_proc(callback_method)
25
+ Proc.new { |model| model.history << [callback_method, :proc] }
26
+ end
27
+
28
+ def callback_object(callback_method)
29
+ klass = Class.new
30
+ klass.send(:define_method, callback_method) do |model|
31
+ model.history << [:"#{callback_method}_save", :object]
32
+ end
33
+ klass.new
34
+ end
35
+ end
36
+
37
+ def history
38
+ @history ||= []
39
+ end
40
+ end
41
+
42
+ class Person < Record
43
+ [:before_save, :after_save].each do |callback_method|
44
+ callback_method_sym = callback_method.to_sym
45
+ send(callback_method, callback_symbol(callback_method_sym))
46
+ send(callback_method, callback_proc(callback_method_sym))
47
+ send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, '')))
48
+ send(callback_method) { |model| model.history << [callback_method_sym, :block] }
49
+ end
50
+
51
+ def save
52
+ run_callbacks :save
53
+ end
54
+ end
55
+
56
+ class PersonSkipper < Person
57
+ skip_callback :save, :before, :before_save_method, :if => :yes
58
+ skip_callback :save, :after, :before_save_method, :unless => :yes
59
+ skip_callback :save, :after, :before_save_method, :if => :no
60
+ skip_callback :save, :before, :before_save_method, :unless => :no
61
+ def yes; true; end
62
+ def no; false; end
63
+ end
64
+
65
+ class ParentController
66
+ include MotionSupport::Callbacks
67
+
68
+ define_callbacks :dispatch
69
+
70
+ set_callback :dispatch, :before, :log, :unless => proc {|c| c.action_name == :index || c.action_name == :show }
71
+ set_callback :dispatch, :after, :log2
72
+
73
+ attr_reader :action_name, :logger
74
+ def initialize(action_name)
75
+ @action_name, @logger = action_name, []
76
+ end
77
+
78
+ def log
79
+ @logger << action_name
80
+ end
81
+
82
+ def log2
83
+ @logger << action_name
84
+ end
85
+
86
+ def dispatch
87
+ run_callbacks :dispatch do
88
+ @logger << "Done"
89
+ end
90
+ self
91
+ end
92
+ end
93
+
94
+ class Child < ParentController
95
+ skip_callback :dispatch, :before, :log, :if => proc {|c| c.action_name == :update}
96
+ skip_callback :dispatch, :after, :log2
97
+ end
98
+
99
+ class OneTimeCompile < Record
100
+ @@starts_true, @@starts_false = true, false
101
+
102
+ def initialize
103
+ super
104
+ end
105
+
106
+ before_save Proc.new {|r| r.history << [:before_save, :starts_true, :if] }, :if => :starts_true
107
+ before_save Proc.new {|r| r.history << [:before_save, :starts_false, :if] }, :if => :starts_false
108
+ before_save Proc.new {|r| r.history << [:before_save, :starts_true, :unless] }, :unless => :starts_true
109
+ before_save Proc.new {|r| r.history << [:before_save, :starts_false, :unless] }, :unless => :starts_false
110
+
111
+ def starts_true
112
+ if @@starts_true
113
+ @@starts_true = false
114
+ return true
115
+ end
116
+ @@starts_true
117
+ end
118
+
119
+ def starts_false
120
+ unless @@starts_false
121
+ @@starts_false = true
122
+ return false
123
+ end
124
+ @@starts_false
125
+ end
126
+
127
+ def save
128
+ run_callbacks :save
129
+ end
130
+ end
131
+
132
+ class AfterSaveConditionalPerson < Record
133
+ after_save Proc.new { |r| r.history << [:after_save, :string1] }
134
+ after_save Proc.new { |r| r.history << [:after_save, :string2] }
135
+ def save
136
+ run_callbacks :save
137
+ end
138
+ end
139
+
140
+ class ConditionalPerson < Record
141
+ # proc
142
+ before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true }
143
+ before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false }
144
+ before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false }
145
+ before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true }
146
+ # symbol
147
+ before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes
148
+ before_save Proc.new { |r| r.history << "b00m" }, :if => :no
149
+ before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no
150
+ before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes
151
+ # string
152
+ before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes'
153
+ before_save Proc.new { |r| r.history << "b00m" }, :if => 'no'
154
+ before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no'
155
+ before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes'
156
+ # Combined if and unless
157
+ before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no
158
+ before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes
159
+
160
+ def yes; true; end
161
+ def other_yes; true; end
162
+ def no; false; end
163
+ def other_no; false; end
164
+
165
+ def save
166
+ run_callbacks :save
167
+ end
168
+ end
169
+
170
+ class CleanPerson < ConditionalPerson
171
+ reset_callbacks :save
172
+ end
173
+
174
+ class MySuper
175
+ include MotionSupport::Callbacks
176
+ define_callbacks :save
177
+ end
178
+
179
+ class AroundPerson < MySuper
180
+ attr_reader :history
181
+
182
+ set_callback :save, :before, :nope, :if => :no
183
+ set_callback :save, :before, :nope, :unless => :yes
184
+ set_callback :save, :after, :tweedle
185
+ set_callback :save, :before, "tweedle_dee"
186
+ set_callback :save, :before, proc {|m| m.history << "yup" }
187
+ set_callback :save, :before, :nope, :if => proc { false }
188
+ set_callback :save, :before, :nope, :unless => proc { true }
189
+ set_callback :save, :before, :yup, :if => proc { true }
190
+ set_callback :save, :before, :yup, :unless => proc { false }
191
+ set_callback :save, :around, :tweedle_dum
192
+ set_callback :save, :around, :w0tyes, :if => :yes
193
+ set_callback :save, :around, :w0tno, :if => :no
194
+ set_callback :save, :around, :tweedle_deedle
195
+
196
+ def no; false; end
197
+ def yes; true; end
198
+
199
+ def nope
200
+ @history << "boom"
201
+ end
202
+
203
+ def yup
204
+ @history << "yup"
205
+ end
206
+
207
+ def w0tyes
208
+ @history << "w0tyes before"
209
+ yield
210
+ @history << "w0tyes after"
211
+ end
212
+
213
+ def w0tno
214
+ @history << "boom"
215
+ yield
216
+ end
217
+
218
+ def tweedle_dee
219
+ @history << "tweedle dee"
220
+ end
221
+
222
+ def tweedle_dum
223
+ @history << "tweedle dum pre"
224
+ yield
225
+ @history << "tweedle dum post"
226
+ end
227
+
228
+ def tweedle
229
+ @history << "tweedle"
230
+ end
231
+
232
+ def tweedle_deedle
233
+ @history << "tweedle deedle pre"
234
+ yield
235
+ @history << "tweedle deedle post"
236
+ end
237
+
238
+ def initialize
239
+ @history = []
240
+ end
241
+
242
+ def save
243
+ run_callbacks :save do
244
+ @history << "running"
245
+ end
246
+ end
247
+ end
248
+
249
+ class AroundPersonResult < MySuper
250
+ attr_reader :result
251
+
252
+ set_callback :save, :after, :tweedle_1
253
+ set_callback :save, :around, :tweedle_dum
254
+ set_callback :save, :after, :tweedle_2
255
+
256
+ def tweedle_dum
257
+ @result = yield
258
+ end
259
+
260
+ def tweedle_1
261
+ :tweedle_1
262
+ end
263
+
264
+ def tweedle_2
265
+ :tweedle_2
266
+ end
267
+
268
+ def save
269
+ run_callbacks :save do
270
+ :running
271
+ end
272
+ end
273
+ end
274
+
275
+ class HyphenatedCallbacks
276
+ include MotionSupport::Callbacks
277
+ define_callbacks :save
278
+ attr_reader :stuff
279
+
280
+ set_callback :save, :before, :action, :if => :yes
281
+
282
+ def yes() true end
283
+
284
+ def action
285
+ @stuff = "ACTION"
286
+ end
287
+
288
+ def save
289
+ run_callbacks :save do
290
+ @stuff
291
+ end
292
+ end
293
+ end
294
+
295
+ module ExtendModule
296
+ def self.extended(base)
297
+ base.class_eval do
298
+ set_callback :save, :before, :record3
299
+ end
300
+ end
301
+ def record3
302
+ @recorder << 3
303
+ end
304
+ end
305
+
306
+ module IncludeModule
307
+ def self.included(base)
308
+ base.class_eval do
309
+ set_callback :save, :before, :record2
310
+ end
311
+ end
312
+ def record2
313
+ @recorder << 2
314
+ end
315
+ end
316
+
317
+ class ExtendCallbacks
318
+
319
+ include MotionSupport::Callbacks
320
+
321
+ define_callbacks :save
322
+ set_callback :save, :before, :record1
323
+
324
+ include IncludeModule
325
+
326
+ def save
327
+ run_callbacks :save
328
+ end
329
+
330
+ attr_reader :recorder
331
+
332
+ def initialize
333
+ @recorder = []
334
+ end
335
+
336
+ private
337
+
338
+ def record1
339
+ @recorder << 1
340
+ end
341
+ end
342
+
343
+ class CallbackTerminator
344
+ include MotionSupport::Callbacks
345
+
346
+ define_callbacks :save, :terminator => lambda { |result| result == :halt }
347
+
348
+ set_callback :save, :before, :first
349
+ set_callback :save, :before, :second
350
+ set_callback :save, :around, :around_it
351
+ set_callback :save, :before, :third
352
+ set_callback :save, :after, :first
353
+ set_callback :save, :around, :around_it
354
+ set_callback :save, :after, :second
355
+ set_callback :save, :around, :around_it
356
+ set_callback :save, :after, :third
357
+
358
+
359
+ attr_reader :history, :saved, :halted
360
+ def initialize
361
+ @history = []
362
+ end
363
+
364
+ def around_it
365
+ @history << "around1"
366
+ yield
367
+ @history << "around2"
368
+ end
369
+
370
+ def first
371
+ @history << "first"
372
+ end
373
+
374
+ def second
375
+ @history << "second"
376
+ :halt
377
+ end
378
+
379
+ def third
380
+ @history << "third"
381
+ end
382
+
383
+ def save
384
+ run_callbacks :save do
385
+ @saved = true
386
+ end
387
+ end
388
+
389
+ def halted_callback_hook(filter)
390
+ @halted = filter
391
+ end
392
+ end
393
+
394
+ class CallbackObject
395
+ def before(caller)
396
+ caller.record << "before"
397
+ end
398
+
399
+ def before_save(caller)
400
+ caller.record << "before save"
401
+ end
402
+
403
+ def around(caller)
404
+ caller.record << "around before"
405
+ yield
406
+ caller.record << "around after"
407
+ end
408
+ end
409
+
410
+ class UsingObjectBefore
411
+ include MotionSupport::Callbacks
412
+
413
+ define_callbacks :save
414
+ set_callback :save, :before, CallbackObject.new
415
+
416
+ attr_accessor :record
417
+ def initialize
418
+ @record = []
419
+ end
420
+
421
+ def save
422
+ run_callbacks :save do
423
+ @record << "yielded"
424
+ end
425
+ end
426
+ end
427
+
428
+ class UsingObjectAround
429
+ include MotionSupport::Callbacks
430
+
431
+ define_callbacks :save
432
+ set_callback :save, :around, CallbackObject.new
433
+
434
+ attr_accessor :record
435
+ def initialize
436
+ @record = []
437
+ end
438
+
439
+ def save
440
+ run_callbacks :save do
441
+ @record << "yielded"
442
+ end
443
+ end
444
+ end
445
+
446
+ class CustomScopeObject
447
+ include MotionSupport::Callbacks
448
+
449
+ define_callbacks :save, :scope => [:kind, :name]
450
+ set_callback :save, :before, CallbackObject.new
451
+
452
+ attr_accessor :record
453
+ def initialize
454
+ @record = []
455
+ end
456
+
457
+ def save
458
+ run_callbacks :save do
459
+ @record << "yielded"
460
+ "CallbackResult"
461
+ end
462
+ end
463
+ end
464
+
465
+ class OneTwoThreeSave
466
+ include MotionSupport::Callbacks
467
+
468
+ define_callbacks :save
469
+
470
+ attr_accessor :record
471
+
472
+ def initialize
473
+ @record = []
474
+ end
475
+
476
+ def save
477
+ run_callbacks :save do
478
+ @record << "yielded"
479
+ end
480
+ end
481
+
482
+ def first
483
+ @record << "one"
484
+ end
485
+
486
+ def second
487
+ @record << "two"
488
+ end
489
+
490
+ def third
491
+ @record << "three"
492
+ end
493
+ end
494
+
495
+ class DuplicatingCallbacks < OneTwoThreeSave
496
+ set_callback :save, :before, :first, :second
497
+ set_callback :save, :before, :first, :third
498
+ end
499
+
500
+ class DuplicatingCallbacksInSameCall < OneTwoThreeSave
501
+ set_callback :save, :before, :first, :second, :first, :third
502
+ end
503
+
504
+ class WriterSkipper < Person
505
+ attr_accessor :age
506
+ skip_callback :save, :before, :before_save_method, :if => lambda { |obj| obj.age > 21 }
507
+ end
508
+ end
509
+
510
+ describe "Callbacks" do
511
+ describe "around filter" do
512
+ it "should optimize on first compile" do
513
+ around = CallbacksTest::OneTimeCompile.new
514
+ around.save
515
+ around.history.should == [
516
+ [:before_save, :starts_true, :if],
517
+ [:before_save, :starts_true, :unless]
518
+ ]
519
+ end
520
+
521
+ it "should call nested around filter" do
522
+ around = CallbacksTest::AroundPerson.new
523
+ around.save
524
+ around.history.should == [
525
+ "tweedle dee",
526
+ "yup", "yup",
527
+ "tweedle dum pre",
528
+ "w0tyes before",
529
+ "tweedle deedle pre",
530
+ "running",
531
+ "tweedle deedle post",
532
+ "w0tyes after",
533
+ "tweedle dum post",
534
+ "tweedle"
535
+ ]
536
+ end
537
+
538
+ it "should return result" do
539
+ around = CallbacksTest::AroundPersonResult.new
540
+ around.save
541
+ around.result.should == :running
542
+ end
543
+ end
544
+
545
+ describe "after save" do
546
+ it "should run in reverse order" do
547
+ person = CallbacksTest::AfterSaveConditionalPerson.new
548
+ person.save
549
+ person.history.should == [
550
+ [:after_save, :string2],
551
+ [:after_save, :string1]
552
+ ]
553
+ end
554
+ end
555
+
556
+ describe "skip callbacks" do
557
+ it "should skip callback" do
558
+ person = CallbacksTest::PersonSkipper.new
559
+ person.history.should == []
560
+ person.save
561
+ person.history.should == [
562
+ [:before_save, :proc],
563
+ [:before_save, :object],
564
+ [:before_save, :block],
565
+ [:after_save, :block],
566
+ [:after_save, :object],
567
+ [:after_save, :proc],
568
+ [:after_save, :symbol]
569
+ ]
570
+ end
571
+
572
+ it "should not skip if condition is not met" do
573
+ writer = CallbacksTest::WriterSkipper.new
574
+ writer.age = 18
575
+ writer.history.should == []
576
+ writer.save
577
+ writer.history.should == [
578
+ [:before_save, :symbol],
579
+ [:before_save, :proc],
580
+ [:before_save, :object],
581
+ [:before_save, :block],
582
+ [:after_save, :block],
583
+ [:after_save, :object],
584
+ [:after_save, :proc],
585
+ [:after_save, :symbol]
586
+ ]
587
+ end
588
+ end
589
+
590
+ it "should run callbacks" do
591
+ person = CallbacksTest::Person.new
592
+ person.history.should == []
593
+ person.save
594
+ person.history.should == [
595
+ [:before_save, :symbol],
596
+ [:before_save, :proc],
597
+ [:before_save, :object],
598
+ [:before_save, :block],
599
+ [:after_save, :block],
600
+ [:after_save, :object],
601
+ [:after_save, :proc],
602
+ [:after_save, :symbol]
603
+ ]
604
+ end
605
+
606
+ it "should run conditional callbacks" do
607
+ person = CallbacksTest::ConditionalPerson.new
608
+ person.save
609
+ person.history.should == [
610
+ [:before_save, :proc],
611
+ [:before_save, :proc],
612
+ [:before_save, :symbol],
613
+ [:before_save, :symbol],
614
+ [:before_save, :string],
615
+ [:before_save, :string],
616
+ [:before_save, :combined_symbol],
617
+ ]
618
+ end
619
+
620
+ it "should return result" do
621
+ obj = CallbacksTest::HyphenatedCallbacks.new
622
+ obj.save
623
+ obj.stuff.should == "ACTION"
624
+ end
625
+
626
+ describe "reset_callbacks" do
627
+ it "should reset callbacks" do
628
+ person = CallbacksTest::CleanPerson.new
629
+ person.save
630
+ person.history.should == []
631
+ end
632
+ end
633
+
634
+ describe "callback object" do
635
+ it "should use callback object for before callback" do
636
+ u = CallbacksTest::UsingObjectBefore.new
637
+ u.save
638
+ u.record.should == ["before", "yielded"]
639
+ end
640
+
641
+ it "should use callback object for around callback" do
642
+ u = CallbacksTest::UsingObjectAround.new
643
+ u.save
644
+ u.record.should == ["around before", "yielded", "around after"]
645
+ end
646
+
647
+ describe "custom scope" do
648
+ it "should use custom scope" do
649
+ u = CallbacksTest::CustomScopeObject.new
650
+ u.save
651
+ u.record.should == ["before save", "yielded"]
652
+ end
653
+
654
+ it "should return block result" do
655
+ u = CallbacksTest::CustomScopeObject.new
656
+ u.save.should == "CallbackResult"
657
+ end
658
+ end
659
+ end
660
+
661
+ describe "callback terminator" do
662
+ it "should terminate" do
663
+ terminator = CallbacksTest::CallbackTerminator.new
664
+ terminator.save
665
+ terminator.history.should == ["first", "second", "third", "second", "first"]
666
+ end
667
+
668
+ it "should invoke hook" do
669
+ terminator = CallbacksTest::CallbackTerminator.new
670
+ terminator.save
671
+ terminator.halted.should == ":second"
672
+ end
673
+
674
+ it "should never call block if terminated" do
675
+ obj = CallbacksTest::CallbackTerminator.new
676
+ obj.save
677
+ obj.saved.should.be.nil
678
+ end
679
+ end
680
+
681
+ describe "extending object" do
682
+ it "should work with extending object" do
683
+ model = CallbacksTest::ExtendCallbacks.new.extend CallbacksTest::ExtendModule
684
+ model.save
685
+ model.recorder.should == [1, 2, 3]
686
+ end
687
+ end
688
+
689
+ describe "exclude duplicates" do
690
+ it "should exclude duplicates in separate calls" do
691
+ model = CallbacksTest::DuplicatingCallbacks.new
692
+ model.save
693
+ model.record.should == ["two", "one", "three", "yielded"]
694
+ end
695
+
696
+ it "should exclude duplicates in one call" do
697
+ model = CallbacksTest::DuplicatingCallbacksInSameCall.new
698
+ model.save
699
+ model.record.should == ["two", "one", "three", "yielded"]
700
+ end
701
+ end
702
+ end