statesman 0.7.0 → 0.8.0

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.
@@ -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