statesman 1.3.1 → 2.0.0.rc1

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