statesman 1.3.1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Statesman
2
- VERSION = "1.3.1"
2
+ VERSION = "2.0.0.rc1"
3
3
  end
@@ -1,5 +1,8 @@
1
1
  require "statesman"
2
2
  require "sqlite3"
3
+ require "mysql2"
4
+ require "pg"
5
+ require "mongoid"
3
6
  require "active_record"
4
7
  # We have to include all of Rails to make rspec-rails work
5
8
  require "rails"
@@ -8,7 +11,6 @@ require "action_dispatch"
8
11
  require "action_controller"
9
12
  require "rspec/rails"
10
13
  require "support/active_record"
11
- require "mongoid"
12
14
  require "rspec/its"
13
15
 
14
16
  RSpec.configure do |config|
@@ -17,16 +19,28 @@ RSpec.configure do |config|
17
19
 
18
20
  config.order = "random"
19
21
 
22
+ def connection_failure
23
+ if defined?(Moped)
24
+ Moped::Errors::ConnectionFailure
25
+ else
26
+ Mongo::Error::NoServerAvailable
27
+ end
28
+ end
29
+
20
30
  # Try a mongo connection at the start of the suite and raise if it fails
21
31
  begin
22
32
  Mongoid.configure do |mongo_config|
23
- mongo_config.connect_to("statesman_test")
24
- mongo_config.sessions["default"]["options"]["max_retries"] = 2
33
+ if defined?(Moped)
34
+ mongo_config.connect_to("statesman_test")
35
+ mongo_config.sessions["default"]["options"]["max_retries"] = 2
36
+ else
37
+ mongo_config.connect_to("statesman_test", server_selection_timeout: 2)
38
+ end
25
39
  end
26
40
  # Attempting a mongo operation will trigger 2 retries then throw an
27
41
  # exception if mongo is not running.
28
42
  Mongoid.purge! unless config.exclusion_filter[:mongo]
29
- rescue Moped::Errors::ConnectionFailure => error
43
+ rescue connection_failure => error
30
44
  puts "The spec suite requires MongoDB to be installed and running locally"
31
45
  puts "Mongo dependent specs can be filtered with rspec --tag '~mongo'"
32
46
  raise(error)
@@ -71,13 +85,6 @@ RSpec.configure do |config|
71
85
  end
72
86
  end
73
87
 
74
- def drop_most_recent_column
75
- silence_stream(STDOUT) do
76
- DropMostRecentColumn.migrate(:up)
77
- MyActiveRecordModelTransition.reset_column_information
78
- end
79
- end
80
-
81
88
  def prepare_other_model_table
82
89
  silence_stream(STDOUT) do
83
90
  CreateOtherActiveRecordModelMigration.migrate(:up)
@@ -61,129 +61,66 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
61
61
  model
62
62
  end
63
63
 
64
- context "with a most_recent column" do
65
- describe ".in_state" do
66
- context "given a single state" do
67
- subject { MyActiveRecordModel.in_state(:succeeded) }
68
-
69
- it { is_expected.to include model }
70
- it { is_expected.not_to include other_model }
71
- its(:to_sql) { is_expected.to include('.most_recent ') }
72
- end
64
+ describe ".in_state" do
65
+ context "given a single state" do
66
+ subject { MyActiveRecordModel.in_state(:succeeded) }
73
67
 
74
- context "given multiple states" do
75
- subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
76
-
77
- it { is_expected.to include model }
78
- it { is_expected.to include other_model }
79
- end
68
+ it { is_expected.to include model }
69
+ it { is_expected.not_to include other_model }
70
+ end
80
71
 
81
- context "given the initial state" do
82
- subject { MyActiveRecordModel.in_state(:initial) }
72
+ context "given multiple states" do
73
+ subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
83
74
 
84
- it { is_expected.to include initial_state_model }
85
- it { is_expected.to include returned_to_initial_model }
86
- end
75
+ it { is_expected.to include model }
76
+ it { is_expected.to include other_model }
77
+ end
87
78
 
88
- context "given an array of states" do
89
- subject { MyActiveRecordModel.in_state([:succeeded, :failed]) }
79
+ context "given the initial state" do
80
+ subject { MyActiveRecordModel.in_state(:initial) }
90
81
 
91
- it { is_expected.to include model }
92
- it { is_expected.to include other_model }
93
- end
82
+ it { is_expected.to include initial_state_model }
83
+ it { is_expected.to include returned_to_initial_model }
84
+ end
94
85
 
95
- context "merging two queries" do
96
- subject do
97
- MyActiveRecordModel.in_state(:succeeded).
98
- joins(:other_active_record_model).
99
- merge(OtherActiveRecordModel.in_state(:initial))
100
- end
86
+ context "given an array of states" do
87
+ subject { MyActiveRecordModel.in_state([:succeeded, :failed]) }
101
88
 
102
- it { is_expected.to be_empty }
103
- end
89
+ it { is_expected.to include model }
90
+ it { is_expected.to include other_model }
104
91
  end
105
92
 
106
- describe ".not_in_state" do
107
- context "given a single state" do
108
- subject { MyActiveRecordModel.not_in_state(:failed) }
109
- it { is_expected.to include model }
110
- it { is_expected.not_to include other_model }
111
- its(:to_sql) { is_expected.to include('.most_recent ') }
93
+ context "merging two queries" do
94
+ subject do
95
+ MyActiveRecordModel.in_state(:succeeded).
96
+ joins(:other_active_record_model).
97
+ merge(OtherActiveRecordModel.in_state(:initial))
112
98
  end
113
99
 
114
- context "given multiple states" do
115
- subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
116
- it do
117
- is_expected.to match_array([initial_state_model,
118
- returned_to_initial_model])
119
- end
120
- end
121
-
122
- context "given an array of states" do
123
- subject { MyActiveRecordModel.not_in_state([:succeeded, :failed]) }
124
- it do
125
- is_expected.to match_array([initial_state_model,
126
- returned_to_initial_model])
127
- end
128
- end
100
+ it { is_expected.to be_empty }
129
101
  end
130
102
  end
131
103
 
132
- context "without a most_recent column" do
133
- before { drop_most_recent_column }
134
-
135
- describe ".in_state" do
136
- context "given a single state" do
137
- subject { MyActiveRecordModel.in_state(:succeeded) }
138
-
139
- it { is_expected.to include model }
140
- its(:to_sql) { is_expected.not_to include('.most_recent ') }
141
- end
142
-
143
- context "given multiple states" do
144
- subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
145
-
146
- it { is_expected.to include model }
147
- it { is_expected.to include other_model }
148
- end
149
-
150
- context "given the initial state" do
151
- subject { MyActiveRecordModel.in_state(:initial) }
152
-
153
- it { is_expected.to include initial_state_model }
154
- it { is_expected.to include returned_to_initial_model }
155
- end
156
-
157
- context "given an array of states" do
158
- subject { MyActiveRecordModel.in_state([:succeeded, :failed]) }
159
-
160
- it { is_expected.to include model }
161
- it { is_expected.to include other_model }
162
- end
104
+ describe ".not_in_state" do
105
+ context "given a single state" do
106
+ subject { MyActiveRecordModel.not_in_state(:failed) }
107
+ it { is_expected.to include model }
108
+ it { is_expected.not_to include other_model }
163
109
  end
164
110
 
165
- describe ".not_in_state" do
166
- context "given a single state" do
167
- subject { MyActiveRecordModel.not_in_state(:failed) }
168
- it { is_expected.to include model }
169
- it { is_expected.not_to include other_model }
170
- its(:to_sql) { is_expected.not_to include('.most_recent ') }
171
- end
172
-
173
- context "given multiple states" do
174
- subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
175
- it do
176
- is_expected.to match_array([initial_state_model,
177
- returned_to_initial_model])
178
- end
111
+ context "given multiple states" do
112
+ subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
113
+ it do
114
+ is_expected.to match_array([initial_state_model,
115
+ returned_to_initial_model])
179
116
  end
117
+ end
180
118
 
181
- context "given an array of states" do
182
- subject { MyActiveRecordModel.not_in_state([:succeeded, :failed]) }
183
- it do
184
- is_expected.to match_array([initial_state_model,
185
- returned_to_initial_model])
186
- end
119
+ context "given an array of states" do
120
+ subject { MyActiveRecordModel.not_in_state([:succeeded, :failed]) }
121
+ it do
122
+ is_expected.to match_array([initial_state_model,
123
+ returned_to_initial_model])
187
124
  end
188
125
  end
189
126
  end
@@ -200,20 +137,9 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
200
137
  end
201
138
  end
202
139
 
203
- context "with a most_recent column" do
204
- describe ".in_state" do
205
- subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
206
- specify { expect { query }.to_not raise_error }
207
- end
208
- end
209
-
210
- context "without a most_recent column" do
211
- before { drop_most_recent_column }
212
-
213
- describe ".in_state" do
214
- subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
215
- specify { expect { query }.to_not raise_error }
216
- end
140
+ describe ".in_state" do
141
+ subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
142
+ specify { expect { query }.to_not raise_error }
217
143
  end
218
144
  end
219
145
  end
@@ -131,7 +131,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
131
131
  end
132
132
  end
133
133
 
134
- context "when the transition_class has a most_recent column" do
134
+ describe "updating the most_recent column" do
135
135
  subject { create }
136
136
 
137
137
  context "with no previous transition" do
@@ -148,17 +148,31 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
148
148
  from(true).to be_falsey
149
149
  end
150
150
 
151
- context "and the parent model is updated in a callback" do
152
- before do
153
- allow(observer).to receive(:execute) do |phase|
154
- if phase == :before
155
- model.update_attributes!(current_state: :ready)
151
+ context "and a query on the parent model's state is made" do
152
+ context "in a before action" do
153
+ it "still has the old state" do
154
+ allow(observer).to receive(:execute) do |phase|
155
+ next unless phase == :before
156
+ expect(
157
+ model.transitions.where(most_recent: true).first.to_state
158
+ ).to eq("y")
156
159
  end
160
+
161
+ adapter.create(:y, :z)
157
162
  end
158
163
  end
159
164
 
160
- it "doesn't save the transition too early" do
161
- expect { create }.to_not raise_exception
165
+ context "in an after action" do
166
+ it "still has the old state" do
167
+ allow(observer).to receive(:execute) do |phase|
168
+ next unless phase == :after
169
+ expect(
170
+ model.transitions.where(most_recent: true).first.to_state
171
+ ).to eq("z")
172
+ end
173
+
174
+ adapter.create(:y, :z)
175
+ end
162
176
  end
163
177
  end
164
178
  end
@@ -175,11 +189,6 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
175
189
  end
176
190
  end
177
191
  end
178
-
179
- context "when the transition_class doesn't have a most_recent column" do
180
- before { drop_most_recent_column }
181
- it { is_expected.to_not raise_exception }
182
- end
183
192
  end
184
193
 
185
194
  describe "#last" do
@@ -198,12 +207,32 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
198
207
  adapter.last
199
208
  end
200
209
 
201
- context "and a new transition" do
210
+ context "after then creating a new transition" do
202
211
  before { adapter.create(:y, :z, []) }
203
212
  it "retrieves the new transition from the database" do
204
213
  expect(adapter.last.to_state).to eq("z")
205
214
  end
206
215
  end
216
+
217
+ context "when a new transition has been created elsewhere" do
218
+ let(:alternate_adapter) do
219
+ described_class.new(MyActiveRecordModelTransition, model, observer)
220
+ end
221
+
222
+ before { alternate_adapter.create(:y, :z, []) }
223
+
224
+ it "still returns the cached value" do
225
+ expect_any_instance_of(MyActiveRecordModel).
226
+ to receive(:my_active_record_model_transitions).never
227
+ expect(adapter.last.to_state).to eq("y")
228
+ end
229
+
230
+ context "when explitly not using the cache" do
231
+ it "still returns the cached value" do
232
+ expect(adapter.last(force_reload: true).to_state).to eq("z")
233
+ end
234
+ end
235
+ end
207
236
  end
208
237
 
209
238
  context "with a pre-fetched transition history" do
@@ -47,12 +47,12 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
47
47
  end
48
48
 
49
49
  context "with no previous transition" do
50
- its(:sort_key) { is_expected.to be(0) }
50
+ its(:sort_key) { is_expected.to be(10) }
51
51
  end
52
52
 
53
53
  context "with a previous transition" do
54
54
  before { adapter.create(from, to) }
55
- its(:sort_key) { is_expected.to be(10) }
55
+ its(:sort_key) { is_expected.to be(20) }
56
56
  end
57
57
  end
58
58
 
@@ -118,5 +118,8 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
118
118
 
119
119
  it { is_expected.to be_a(transition_class) }
120
120
  specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
121
+ specify do
122
+ expect(adapter.last(force_reload: true).to_state.to_sym).to eq(:z)
123
+ end
121
124
  end
122
125
  end
@@ -374,6 +374,61 @@ describe Statesman::Machine do
374
374
  end
375
375
  end
376
376
 
377
+ describe "#in_state?" do
378
+ before do
379
+ machine.class_eval do
380
+ state :x, initial: true
381
+ state :y
382
+ transition from: :x, to: :y
383
+ end
384
+ end
385
+
386
+ let(:instance) { machine.new(my_model) }
387
+ subject { instance.in_state?(state) }
388
+ before { instance.transition_to!(:y) }
389
+
390
+ context "when machine is in given state" do
391
+ let(:state) { "y" }
392
+ it { is_expected.to eq(true) }
393
+ end
394
+
395
+ context "when machine is not in given state" do
396
+ let(:state) { "x" }
397
+ it { is_expected.to eq(false) }
398
+ end
399
+
400
+ context "when given a symbol" do
401
+ let(:state) { :y }
402
+ it { is_expected.to eq(true) }
403
+ end
404
+
405
+ context "when given multiple states" do
406
+ context "when given multiple arguments" do
407
+ context "when one of the states is the current state" do
408
+ subject { instance.in_state?(:x, :y) }
409
+ it { is_expected.to eq(true) }
410
+ end
411
+
412
+ context "when none of the states are the current state" do
413
+ subject { instance.in_state?(:x, :z) }
414
+ it { is_expected.to eq(false) }
415
+ end
416
+ end
417
+
418
+ context "when given an array" do
419
+ context "when one of the states is the current state" do
420
+ subject { instance.in_state?([:x, :y]) }
421
+ it { is_expected.to eq(true) }
422
+ end
423
+
424
+ context "when none of the states are the current state" do
425
+ subject { instance.in_state?([:x, :z]) }
426
+ it { is_expected.to eq(false) }
427
+ end
428
+ end
429
+ end
430
+ end
431
+
377
432
  describe "#allowed_transitions" do
378
433
  before do
379
434
  machine.class_eval do
@@ -627,126 +682,4 @@ describe Statesman::Machine do
627
682
  describe "#after_callbacks_for" do
628
683
  it_behaves_like "a callback filter", :after_transition, :after
629
684
  end
630
-
631
- describe "#event" do
632
- before do
633
- machine.class_eval do
634
- state :x, initial: true
635
- state :y
636
- state :z
637
-
638
- event :event_1 do
639
- transition from: :x, to: :y
640
- end
641
-
642
- event :event_2 do
643
- transition from: :y, to: :z
644
- end
645
- end
646
- end
647
-
648
- let(:instance) { machine.new(my_model) }
649
-
650
- context "when the state cannot be transitioned to" do
651
- it "raises an error" do
652
- expect { instance.trigger!(:event_2) }.
653
- to raise_error(Statesman::TransitionFailedError)
654
- end
655
- end
656
-
657
- context "when the state can be transitioned to" do
658
- it "changes state" do
659
- instance.trigger!(:event_1)
660
- expect(instance.current_state).to eq("y")
661
- end
662
-
663
- it "creates a new transition object" do
664
- expect { instance.trigger!(:event_1) }.
665
- to change(instance.history, :count).by(1)
666
-
667
- expect(instance.history.first).
668
- to be_a(Statesman::Adapters::MemoryTransition)
669
- expect(instance.history.first.to_state).to eq("y")
670
- end
671
-
672
- it "sends metadata to the transition object" do
673
- meta = { "my" => "hash" }
674
- instance.trigger!(:event_1, meta)
675
- expect(instance.history.first.metadata).to eq(meta)
676
- end
677
-
678
- it "sets an empty hash as the metadata if not specified" do
679
- instance.trigger!(:event_1)
680
- expect(instance.history.first.metadata).to eq({})
681
- end
682
-
683
- it "returns true" do
684
- expect(instance.trigger!(:event_1)).to eq(true)
685
- end
686
-
687
- context "with a guard" do
688
- let(:result) { true }
689
- # rubocop:disable UnusedBlockArgument
690
- let(:guard_cb) { ->(*args) { result } }
691
- # rubocop:enable UnusedBlockArgument
692
- before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
693
-
694
- context "and an object to act on" do
695
- let(:instance) { machine.new(my_model) }
696
-
697
- it "passes the object to the guard" do
698
- expect(guard_cb).to receive(:call).once.
699
- with(my_model, instance.last_transition, {}).and_return(true)
700
- instance.trigger!(:event_1)
701
- end
702
- end
703
-
704
- context "which passes" do
705
- it "changes state" do
706
- expect { instance.trigger!(:event_1) }.
707
- to change { instance.current_state }.to("y")
708
- end
709
- end
710
-
711
- context "which fails" do
712
- let(:result) { false }
713
-
714
- it "raises an exception" do
715
- expect { instance.trigger!(:event_1) }.
716
- to raise_error(Statesman::GuardFailedError)
717
- end
718
- end
719
- end
720
- end
721
- end
722
-
723
- describe "#available_events" do
724
- before do
725
- machine.class_eval do
726
- state :x, initial: true
727
- state :y
728
- state :z
729
-
730
- event :event_1 do
731
- transition from: :x, to: :y
732
- end
733
-
734
- event :event_2 do
735
- transition from: :y, to: :z
736
- end
737
-
738
- event :event_3 do
739
- transition from: :x, to: :y
740
- transition from: :y, to: :x
741
- end
742
- end
743
- end
744
-
745
- let(:instance) { machine.new(my_model) }
746
- it "should return list of available events for the current state" do
747
- expect(instance.available_events).to eq([:event_1, :event_3])
748
- instance.trigger!(:event_1)
749
- expect(instance.available_events).to eq([:event_2, :event_3])
750
- end
751
- end
752
685
  end