statesman 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,11 +2,11 @@ require "spec_helper"
2
2
  require "json"
3
3
 
4
4
  describe Statesman::Adapters::ActiveRecordTransition do
5
- let(:transition_class) { Class.new }
5
+ let(:transition_class) { Class.new { def self.serialize(*_args); end } }
6
6
 
7
7
  describe "including behaviour" do
8
8
  it "calls Class.serialize" do
9
- transition_class.should_receive(:serialize).with(:metadata, JSON).once
9
+ expect(transition_class).to receive(:serialize).with(:metadata, JSON).once
10
10
  transition_class.send(:include, described_class)
11
11
  end
12
12
  end
@@ -11,7 +11,7 @@ describe Statesman::Adapters::Mongoid, mongo: true do
11
11
  end
12
12
  let(:observer) do
13
13
  result = double(Statesman::Machine)
14
- result.stub(:execute)
14
+ allow(result).to receive(:execute)
15
15
  result
16
16
  end
17
17
  let(:model) { MyMongoidModel.create(current_state: :pending) }
@@ -20,7 +20,8 @@ describe Statesman::Adapters::Mongoid, mongo: true do
20
20
  describe "#initialize" do
21
21
  context "with unserialized metadata" do
22
22
  before do
23
- described_class.any_instance.stub(transition_class_hash_fields: [])
23
+ allow_any_instance_of(described_class)
24
+ .to receive_messages(transition_class_hash_fields: [])
24
25
  end
25
26
 
26
27
  it "raises an exception if metadata is not serialized" do
@@ -44,8 +45,8 @@ describe Statesman::Adapters::Mongoid, mongo: true do
44
45
  end
45
46
 
46
47
  it "caches the transition" do
47
- MyMongoidModel.any_instance
48
- .should_receive(:my_mongoid_model_transitions).never
48
+ expect_any_instance_of(MyMongoidModel)
49
+ .to receive(:my_mongoid_model_transitions).never
49
50
  adapter.last
50
51
  end
51
52
 
@@ -14,16 +14,16 @@ require "spec_helper"
14
14
  shared_examples_for "an adapter" do |adapter_class, transition_class|
15
15
  let(:observer) do
16
16
  result = double(Statesman::Machine)
17
- result.stub(:execute)
17
+ allow(result).to receive(:execute)
18
18
  result
19
19
  end
20
20
  let(:adapter) { adapter_class.new(transition_class, model, observer) }
21
21
 
22
22
  describe "#initialize" do
23
23
  subject { adapter }
24
- its(:transition_class) { should be(transition_class) }
25
- its(:parent_model) { should be(model) }
26
- its(:history) { should eq([]) }
24
+ its(:transition_class) { is_expected.to be(transition_class) }
25
+ its(:parent_model) { is_expected.to be(model) }
26
+ its(:history) { is_expected.to eq([]) }
27
27
  end
28
28
 
29
29
  describe "#create" do
@@ -33,32 +33,32 @@ shared_examples_for "an adapter" do |adapter_class, transition_class|
33
33
  let(:create) { adapter.create(from, to) }
34
34
  subject { -> { create } }
35
35
 
36
- it { should change(adapter.history, :count).by(1) }
36
+ it { is_expected.to change(adapter.history, :count).by(1) }
37
37
 
38
38
  context "the new transition" do
39
39
  subject { create }
40
- it { should be_a(transition_class) }
40
+ it { is_expected.to be_a(transition_class) }
41
41
 
42
42
  it "should have the initial state" do
43
43
  expect(subject.to_state.to_sym).to eq(to)
44
44
  end
45
45
 
46
46
  context "with no previous transition" do
47
- its(:sort_key) { should be(0) }
47
+ its(:sort_key) { is_expected.to be(0) }
48
48
  end
49
49
 
50
50
  context "with a previous transition" do
51
51
  before { adapter.create(from, to) }
52
- its(:sort_key) { should be(10) }
52
+ its(:sort_key) { is_expected.to be(10) }
53
53
  end
54
54
  end
55
55
 
56
56
  context "with before callbacks" do
57
57
  it "is called before the state transition" do
58
- observer.should_receive(:execute).with do
59
- |phase, from_state, to_state, transition|
60
- expect(adapter.history.length).to eq(0) if phase == :before
61
- end.once
58
+ expect(observer).to receive(:execute)
59
+ .with(:before, anything, anything, anything) {
60
+ expect(adapter.history.length).to eq(0)
61
+ }.once
62
62
  adapter.create(from, to)
63
63
  expect(adapter.history.length).to eq(1)
64
64
  end
@@ -66,20 +66,22 @@ shared_examples_for "an adapter" do |adapter_class, transition_class|
66
66
 
67
67
  context "with after callbacks" do
68
68
  it "is called after the state transition" do
69
- observer.should_receive(:execute).with do
70
- |phase, from_state, to_state, transition|
71
- expect(adapter.last).to eq(transition) if phase == :after
72
- end.once
69
+ expect(observer).to receive(:execute)
70
+ .with(:after, anything, anything, anything) {
71
+ |_phase, _from_state, _to_state, transition|
72
+ expect(adapter.last).to eq(transition)
73
+ }.once
73
74
  adapter.create(from, to)
74
75
  end
75
76
 
76
77
  it "exposes the new transition for subsequent transitions" do
77
78
  adapter.create(from, to)
78
79
 
79
- observer.should_receive(:execute).with do
80
- |phase, from_state, to_state, transition|
81
- expect(adapter.last).to eq(transition) if phase == :after
82
- end.once
80
+ expect(observer).to receive(:execute)
81
+ .with(:after, anything, anything, anything) {
82
+ |_phase, _from_state, _to_state, transition|
83
+ expect(adapter.last).to eq(transition)
84
+ }.once
83
85
  adapter.create(to, there)
84
86
  end
85
87
  end
@@ -87,22 +89,22 @@ shared_examples_for "an adapter" do |adapter_class, transition_class|
87
89
  context "with metadata" do
88
90
  let(:metadata) { { "some" => "hash" } }
89
91
  subject { adapter.create(from, to, metadata) }
90
- its(:metadata) { should eq(metadata) }
92
+ its(:metadata) { is_expected.to eq(metadata) }
91
93
  end
92
94
  end
93
95
 
94
96
  describe "#history" do
95
97
  subject { adapter.history }
96
- it { should eq([]) }
98
+ it { is_expected.to eq([]) }
97
99
 
98
100
  context "with transitions" do
99
101
  let!(:transition) { adapter.create(:x, :y) }
100
- it { should eq([transition]) }
102
+ it { is_expected.to eq([transition]) }
101
103
 
102
104
  context "sorting" do
103
105
  let!(:transition2) { adapter.create(:x, :y) }
104
106
  subject { adapter.history }
105
- it { should eq(adapter.history.sort_by(&:sort_key)) }
107
+ it { is_expected.to eq(adapter.history.sort_by(&:sort_key)) }
106
108
  end
107
109
  end
108
110
  end
@@ -114,7 +116,7 @@ shared_examples_for "an adapter" do |adapter_class, transition_class|
114
116
  end
115
117
  subject { adapter.last }
116
118
 
117
- it { should be_a(transition_class) }
119
+ it { is_expected.to be_a(transition_class) }
118
120
  specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
119
121
  end
120
122
  end
@@ -37,12 +37,12 @@ describe Statesman::Callback do
37
37
 
38
38
  context "and an allowed to value" do
39
39
  let(:to) { :y }
40
- it { should be_true }
40
+ it { is_expected.to be_truthy }
41
41
  end
42
42
 
43
43
  context "and a disallowed to value" do
44
44
  let(:to) { :a }
45
- it { should be_false }
45
+ it { is_expected.to be_falsey }
46
46
  end
47
47
  end
48
48
 
@@ -51,12 +51,12 @@ describe Statesman::Callback do
51
51
 
52
52
  context "and an allowed 'from' value" do
53
53
  let(:from) { :x }
54
- it { should be_true }
54
+ it { is_expected.to be_truthy }
55
55
  end
56
56
 
57
57
  context "and a disallowed 'from' value" do
58
58
  let(:from) { :a }
59
- it { should be_false }
59
+ it { is_expected.to be_falsey }
60
60
  end
61
61
  end
62
62
 
@@ -67,7 +67,7 @@ describe Statesman::Callback do
67
67
  let(:from) { :x }
68
68
  let(:to) { :y }
69
69
 
70
- it { should be_true }
70
+ it { is_expected.to be_truthy }
71
71
  end
72
72
 
73
73
  context "with any from value on the callback" do
@@ -78,12 +78,12 @@ describe Statesman::Callback do
78
78
 
79
79
  context "and an allowed to value" do
80
80
  let(:to) { :y }
81
- it { should be_true }
81
+ it { is_expected.to be_truthy }
82
82
  end
83
83
 
84
84
  context "and a disallowed to value" do
85
85
  let(:to) { :a }
86
- it { should be_false }
86
+ it { is_expected.to be_falsey }
87
87
  end
88
88
  end
89
89
 
@@ -95,25 +95,25 @@ describe Statesman::Callback do
95
95
 
96
96
  context "and an allowed to value" do
97
97
  let(:from) { :x }
98
- it { should be_true }
98
+ it { is_expected.to be_truthy }
99
99
  end
100
100
 
101
101
  context "and a disallowed to value" do
102
102
  let(:from) { :a }
103
- it { should be_false }
103
+ it { is_expected.to be_falsey }
104
104
  end
105
105
  end
106
106
 
107
107
  context "with allowed 'from' and 'to' values" do
108
108
  let(:from) { :x }
109
109
  let(:to) { :y }
110
- it { should be_true }
110
+ it { is_expected.to be_truthy }
111
111
  end
112
112
 
113
113
  context "with disallowed 'from' and 'to' values" do
114
114
  let(:from) { :a }
115
115
  let(:to) { :b }
116
- it { should be_false }
116
+ it { is_expected.to be_falsey }
117
117
  end
118
118
  end
119
119
  end
@@ -12,7 +12,7 @@ describe Statesman::Config do
12
12
  let(:adapter) { Class.new }
13
13
  before { instance.storage_adapter(adapter) }
14
14
  subject { instance.adapter_class }
15
- it { should be(adapter) }
15
+ it { is_expected.to be(adapter) }
16
16
 
17
17
  it "is DSL configurable" do
18
18
  new_adapter = adapter
@@ -23,6 +23,80 @@ describe Statesman::Machine do
23
23
  end
24
24
  end
25
25
 
26
+ describe ".retry_conflicts" do
27
+ before do
28
+ machine.class_eval do
29
+ state :x, initial: true
30
+ state :y
31
+ state :z
32
+ transition from: :x, to: :y
33
+ transition from: :y, to: :z
34
+ end
35
+ end
36
+ let(:instance) { machine.new(my_model) }
37
+ let(:retry_attempts) { 2 }
38
+
39
+ subject(:transition_state) do
40
+ Statesman::Machine.retry_conflicts(retry_attempts) do
41
+ instance.transition_to(:y)
42
+ end
43
+ end
44
+
45
+ context "when no exception occurs" do
46
+ it "runs the transition once" do
47
+ expect(instance).to receive(:transition_to).once
48
+ transition_state
49
+ end
50
+ end
51
+
52
+ context "when an irrelevant exception occurs" do
53
+ it "runs the transition once" do
54
+ expect(instance)
55
+ .to receive(:transition_to).once
56
+ .and_raise(StandardError)
57
+ transition_state rescue nil # rubocop:disable RescueModifier
58
+ end
59
+
60
+ it "re-raises the exception" do
61
+ allow(instance).to receive(:transition_to).once
62
+ .and_raise(StandardError)
63
+ expect { transition_state }.to raise_error(StandardError)
64
+ end
65
+ end
66
+
67
+ context "when a TransitionConflictError occurs" do
68
+ context "and is resolved on the second attempt" do
69
+ it "runs the transition twice" do
70
+ expect(instance)
71
+ .to receive(:transition_to).once
72
+ .and_raise(Statesman::TransitionConflictError)
73
+ .ordered
74
+ expect(instance)
75
+ .to receive(:transition_to).once.ordered.and_call_original
76
+ transition_state
77
+ end
78
+ end
79
+
80
+ context "and keeps occurring" do
81
+ it "runs the transition `retry_attempts + 1` times" do
82
+ expect(instance)
83
+ .to receive(:transition_to)
84
+ .exactly(retry_attempts + 1).times
85
+ .and_raise(Statesman::TransitionConflictError)
86
+ transition_state rescue nil # rubocop:disable RescueModifier
87
+ end
88
+
89
+ it "re-raises the conflict" do
90
+ allow(instance)
91
+ .to receive(:transition_to)
92
+ .and_raise(Statesman::TransitionConflictError)
93
+ expect { transition_state }
94
+ .to raise_error(Statesman::TransitionConflictError)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
26
100
  describe ".transition" do
27
101
  before do
28
102
  machine.class_eval do
@@ -192,21 +266,21 @@ describe Statesman::Machine do
192
266
 
193
267
  context "transition class" do
194
268
  it "sets a default" do
195
- Statesman.storage_adapter.should_receive(:new).once
269
+ expect(Statesman.storage_adapter).to receive(:new).once
196
270
  .with(Statesman::Adapters::MemoryTransition, my_model, anything)
197
271
  machine.new(my_model)
198
272
  end
199
273
 
200
274
  it "sets the passed class" do
201
275
  my_transition_class = Class.new
202
- Statesman.storage_adapter.should_receive(:new).once
276
+ expect(Statesman.storage_adapter).to receive(:new).once
203
277
  .with(my_transition_class, my_model, anything)
204
278
  machine.new(my_model, transition_class: my_transition_class)
205
279
  end
206
280
 
207
281
  it "falls back to Memory without transaction_class" do
208
- Statesman.stub(:storage_adapter).and_return(Class.new)
209
- Statesman::Adapters::Memory.should_receive(:new).once
282
+ allow(Statesman).to receive(:storage_adapter).and_return(Class.new)
283
+ expect(Statesman::Adapters::Memory).to receive(:new).once
210
284
  .with(Statesman::Adapters::MemoryTransition, my_model, anything)
211
285
  machine.new(my_model)
212
286
  end
@@ -238,7 +312,7 @@ describe Statesman::Machine do
238
312
  subject { instance.current_state }
239
313
 
240
314
  context "with no transitions" do
241
- it { should eq(machine.initial_state) }
315
+ it { is_expected.to eq(machine.initial_state) }
242
316
  end
243
317
 
244
318
  context "with multiple transitions" do
@@ -247,7 +321,7 @@ describe Statesman::Machine do
247
321
  instance.transition_to!(:z)
248
322
  end
249
323
 
250
- it { should eq("z") }
324
+ it { is_expected.to eq("z") }
251
325
  end
252
326
  end
253
327
 
@@ -266,7 +340,7 @@ describe Statesman::Machine do
266
340
  subject { instance.allowed_transitions }
267
341
 
268
342
  context "with multiple possible states" do
269
- it { should eq(%w(y z)) }
343
+ it { is_expected.to eq(%w(y z)) }
270
344
  end
271
345
 
272
346
  context "with one possible state" do
@@ -274,7 +348,7 @@ describe Statesman::Machine do
274
348
  instance.transition_to!(:y)
275
349
  end
276
350
 
277
- it { should eq(['z']) }
351
+ it { is_expected.to eq(['z']) }
278
352
  end
279
353
 
280
354
  context "with no possible transitions" do
@@ -282,7 +356,7 @@ describe Statesman::Machine do
282
356
  instance.transition_to!(:z)
283
357
  end
284
358
 
285
- it { should eq([]) }
359
+ it { is_expected.to eq([]) }
286
360
  end
287
361
  end
288
362
 
@@ -291,7 +365,7 @@ describe Statesman::Machine do
291
365
  let(:last_action) { "Whatever" }
292
366
 
293
367
  it "delegates to the storage adapter" do
294
- Statesman.storage_adapter.any_instance.should_receive(:last).once
368
+ expect_any_instance_of(Statesman.storage_adapter).to receive(:last).once
295
369
  .and_return(last_action)
296
370
  expect(instance.last_transition).to be(last_action)
297
371
  end
@@ -314,13 +388,13 @@ describe Statesman::Machine do
314
388
  context "when the transition is invalid" do
315
389
  context "with an initial to state" do
316
390
  let(:new_state) { :x }
317
- it { should be_false }
391
+ it { is_expected.to be_falsey }
318
392
  end
319
393
 
320
394
  context "with a terminal from state" do
321
395
  before { instance.transition_to!(:y) }
322
396
  let(:new_state) { :y }
323
- it { should be_false }
397
+ it { is_expected.to be_falsey }
324
398
  end
325
399
 
326
400
  context "and is guarded" do
@@ -329,19 +403,19 @@ describe Statesman::Machine do
329
403
  before { machine.guard_transition(to: new_state, &guard_cb) }
330
404
 
331
405
  it "does not fire guard" do
332
- guard_cb.should_not_receive(:call)
333
- should be_false
406
+ expect(guard_cb).not_to receive(:call)
407
+ is_expected.to be_falsey
334
408
  end
335
409
  end
336
410
  end
337
411
 
338
412
  context "when the transition valid" do
339
413
  let(:new_state) { :y }
340
- it { should be_true }
414
+ it { is_expected.to be_truthy }
341
415
 
342
416
  context "but it has a failing guard" do
343
417
  before { machine.guard_transition(to: :y) { false } }
344
- it { should be_false }
418
+ it { is_expected.to be_falsey }
345
419
  end
346
420
  end
347
421
  end
@@ -390,19 +464,19 @@ describe Statesman::Machine do
390
464
  end
391
465
 
392
466
  it "returns true" do
393
- expect(instance.transition_to!(:y)).to be_true
467
+ expect(instance.transition_to!(:y)).to be_truthy
394
468
  end
395
469
 
396
470
  context "with a guard" do
397
471
  let(:result) { true }
398
- let(:guard_cb) { ->(*args) { result } }
472
+ let(:guard_cb) { ->(*_args) { result } }
399
473
  before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
400
474
 
401
475
  context "and an object to act on" do
402
476
  let(:instance) { machine.new(my_model) }
403
477
 
404
478
  it "passes the object to the guard" do
405
- guard_cb.should_receive(:call).once
479
+ expect(guard_cb).to receive(:call).once
406
480
  .with(my_model, instance.last_transition, nil).and_return(true)
407
481
  instance.transition_to!(:y)
408
482
  end
@@ -435,23 +509,24 @@ describe Statesman::Machine do
435
509
 
436
510
  context "when it is succesful" do
437
511
  before do
438
- instance.should_receive(:transition_to!).once
512
+ expect(instance).to receive(:transition_to!).once
439
513
  .with(:some_state, metadata).and_return(:some_state)
440
514
  end
441
- it { should be(:some_state) }
515
+ it { is_expected.to be(:some_state) }
442
516
  end
443
517
 
444
518
  context "when it is unsuccesful" do
445
519
  before do
446
- instance.stub(:transition_to!).and_raise(Statesman::GuardFailedError)
520
+ allow(instance).to receive(:transition_to!)
521
+ .and_raise(Statesman::GuardFailedError)
447
522
  end
448
- it { should be_false }
523
+ it { is_expected.to be_falsey }
449
524
  end
450
525
 
451
526
  context "when a non statesman exception is raised" do
452
527
  before do
453
- instance.stub(:transition_to!).and_raise(RuntimeError,
454
- 'user defined exception')
528
+ allow(instance).to receive(:transition_to!)
529
+ .and_raise(RuntimeError, 'user defined exception')
455
530
  end
456
531
 
457
532
  it "should not rescue the exception" do
@@ -514,4 +589,126 @@ describe Statesman::Machine do
514
589
  it_behaves_like "a callback filter", :after_transition,
515
590
  :after
516
591
  end
592
+
593
+ describe "#event" do
594
+ before do
595
+ machine.class_eval do
596
+ state :x, initial: true
597
+ state :y
598
+ state :z
599
+
600
+ event :event_1 do
601
+ transition from: :x, to: :y
602
+ end
603
+
604
+ event :event_2 do
605
+ transition from: :y, to: :z
606
+ end
607
+ end
608
+ end
609
+
610
+ let(:instance) { machine.new(my_model) }
611
+
612
+ context "when the state cannot be transitioned to" do
613
+ it "raises an error" do
614
+ expect do
615
+ instance.trigger!(:event_2)
616
+ end.to raise_error(Statesman::TransitionFailedError)
617
+ end
618
+ end
619
+
620
+ context "when the state can be transitioned to" do
621
+ it "changes state" do
622
+ instance.trigger!(:event_1)
623
+ expect(instance.current_state).to eq("y")
624
+ end
625
+
626
+ it "creates a new transition object" do
627
+ expect do
628
+ instance.trigger!(:event_1)
629
+ end.to change(instance.history, :count).by(1)
630
+
631
+ expect(instance.history.first)
632
+ .to be_a(Statesman::Adapters::MemoryTransition)
633
+ expect(instance.history.first.to_state).to eq("y")
634
+ end
635
+
636
+ it "sends metadata to the transition object" do
637
+ meta = { "my" => "hash" }
638
+ instance.trigger!(:event_1, meta)
639
+ expect(instance.history.first.metadata).to eq(meta)
640
+ end
641
+
642
+ it "returns true" do
643
+ expect(instance.trigger!(:event_1)).to eq(true)
644
+ end
645
+
646
+ context "with a guard" do
647
+ let(:result) { true }
648
+ # rubocop:disable UnusedBlockArgument
649
+ let(:guard_cb) { ->(*args) { result } }
650
+ # rubocop:enable UnusedBlockArgument
651
+ before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
652
+
653
+ context "and an object to act on" do
654
+ let(:instance) { machine.new(my_model) }
655
+
656
+ it "passes the object to the guard" do
657
+ expect(guard_cb).to receive(:call).once
658
+ .with(my_model, instance.last_transition, nil).and_return(true)
659
+ instance.trigger!(:event_1)
660
+ end
661
+ end
662
+
663
+ context "which passes" do
664
+ it "changes state" do
665
+ instance.trigger!(:event_1)
666
+ expect(instance.current_state).to eq("y")
667
+ end
668
+ end
669
+
670
+ context "which fails" do
671
+ let(:result) { false }
672
+
673
+ it "raises an exception" do
674
+ expect do
675
+ instance.trigger!(:event_1)
676
+ end.to raise_error(Statesman::GuardFailedError)
677
+ end
678
+ end
679
+ end
680
+ end
681
+
682
+ end
683
+
684
+ describe "#available_events" do
685
+ before do
686
+ machine.class_eval do
687
+ state :x, initial: true
688
+ state :y
689
+ state :z
690
+
691
+ event :event_1 do
692
+ transition from: :x, to: :y
693
+ end
694
+
695
+ event :event_2 do
696
+ transition from: :y, to: :z
697
+ end
698
+
699
+ event :event_3 do
700
+ transition from: :x, to: :y
701
+ transition from: :y, to: :x
702
+ end
703
+ end
704
+ end
705
+
706
+ let(:instance) { machine.new(my_model) }
707
+ it "should return list of available events for the current state" do
708
+ expect(instance.available_events).to eq([:event_1, :event_3])
709
+ instance.trigger!(:event_1)
710
+ expect(instance.available_events).to eq([:event_2, :event_3])
711
+ end
712
+ end
713
+
517
714
  end