statesman 7.4.0 → 12.1.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/tests.yml +112 -0
- data/.gitignore +65 -15
- data/.rspec +1 -0
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +37 -28
- data/.ruby-version +1 -0
- data/CHANGELOG.md +262 -41
- data/CONTRIBUTING.md +23 -4
- data/Gemfile +4 -6
- data/README.md +243 -43
- data/docs/COMPATIBILITY.md +2 -2
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +12 -4
- data/lib/statesman/adapters/active_record.rb +84 -55
- data/lib/statesman/adapters/active_record_queries.rb +19 -7
- data/lib/statesman/adapters/active_record_transition.rb +5 -1
- data/lib/statesman/adapters/memory.rb +5 -1
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
- data/lib/statesman/callback.rb +2 -2
- data/lib/statesman/config.rb +3 -10
- data/lib/statesman/exceptions.rb +13 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +68 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +5 -5
- data/lib/tasks/statesman.rake +5 -5
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
- data/spec/generators/statesman/migration_generator_spec.rb +5 -1
- data/spec/spec_helper.rb +44 -7
- data/spec/statesman/adapters/active_record_queries_spec.rb +34 -12
- data/spec/statesman/adapters/active_record_spec.rb +176 -51
- data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
- data/spec/statesman/adapters/memory_spec.rb +0 -1
- data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
- data/spec/statesman/adapters/shared_examples.rb +3 -4
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +206 -0
- data/spec/statesman/callback_spec.rb +0 -2
- data/spec/statesman/config_spec.rb +0 -2
- data/spec/statesman/exceptions_spec.rb +17 -4
- data/spec/statesman/guard_spec.rb +0 -2
- data/spec/statesman/machine_spec.rb +252 -15
- data/spec/statesman/utils_spec.rb +0 -2
- data/spec/support/active_record.rb +156 -24
- data/spec/support/exactly_query_databases.rb +35 -0
- data/statesman.gemspec +9 -10
- metadata +32 -59
- data/.circleci/config.yml +0 -187
data/lib/tasks/statesman.rake
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
namespace :statesman do
|
4
|
-
desc "Set most_recent to false for old transitions and to true for the "\
|
4
|
+
desc "Set most_recent to false for old transitions and to true for the " \
|
5
5
|
"latest one. Safe to re-run"
|
6
6
|
task :backfill_most_recent, [:parent_model_name] => :environment do |_, args|
|
7
7
|
parent_model_name = args.parent_model_name
|
@@ -21,8 +21,8 @@ namespace :statesman do
|
|
21
21
|
batch_size = 500
|
22
22
|
|
23
23
|
parent_class.find_in_batches(batch_size: batch_size) do |models|
|
24
|
-
|
25
|
-
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
|
24
|
+
transition_class.transaction(requires_new: true) do
|
25
|
+
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(transition_class)
|
26
26
|
# Set all transitions' most_recent to FALSE
|
27
27
|
transition_class.where(parent_fk => models.map(&:id)).
|
28
28
|
update_all(most_recent: false, updated_at: updated_at)
|
@@ -56,8 +56,8 @@ namespace :statesman do
|
|
56
56
|
end
|
57
57
|
|
58
58
|
done_models += batch_size
|
59
|
-
puts "Updated #{transition_class.name.pluralize} for "\
|
60
|
-
"#{[done_models, total_models].min}/#{total_models} "\
|
59
|
+
puts "Updated #{transition_class.name.pluralize} for " \
|
60
|
+
"#{[done_models, total_models].min}/#{total_models} " \
|
61
61
|
"#{parent_model_name.pluralize}"
|
62
62
|
end
|
63
63
|
end
|
@@ -1,10 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
3
|
require "support/generators_shared_examples"
|
5
4
|
require "generators/statesman/active_record_transition_generator"
|
6
5
|
|
7
6
|
describe Statesman::ActiveRecordTransitionGenerator, type: :generator do
|
7
|
+
before do
|
8
|
+
stub_const("Bacon", Class.new(ActiveRecord::Base))
|
9
|
+
stub_const("BaconTransition", Class.new(ActiveRecord::Base))
|
10
|
+
stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
|
11
|
+
stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
|
12
|
+
end
|
13
|
+
|
8
14
|
it_behaves_like "a generator" do
|
9
15
|
let(:migration_name) { "db/migrate/create_bacon_transitions.rb" }
|
10
16
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
3
|
require "support/generators_shared_examples"
|
5
4
|
require "generators/statesman/migration_generator"
|
6
5
|
|
7
6
|
describe Statesman::MigrationGenerator, type: :generator do
|
7
|
+
before do
|
8
|
+
stub_const("Yummy::Bacon", Class.new(ActiveRecord::Base))
|
9
|
+
stub_const("Yummy::BaconTransition", Class.new(ActiveRecord::Base))
|
10
|
+
end
|
11
|
+
|
8
12
|
it_behaves_like "a generator" do
|
9
13
|
let(:migration_name) { "db/migrate/add_statesman_to_bacon_transitions.rb" }
|
10
14
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,13 +5,14 @@ require "sqlite3"
|
|
5
5
|
require "mysql2"
|
6
6
|
require "pg"
|
7
7
|
require "active_record"
|
8
|
+
require "active_record/database_configurations"
|
8
9
|
# We have to include all of Rails to make rspec-rails work
|
9
10
|
require "rails"
|
10
11
|
require "action_view"
|
11
12
|
require "action_dispatch"
|
12
13
|
require "action_controller"
|
13
14
|
require "rspec/rails"
|
14
|
-
require "support/
|
15
|
+
require "support/exactly_query_databases"
|
15
16
|
require "rspec/its"
|
16
17
|
require "pry"
|
17
18
|
|
@@ -28,10 +29,31 @@ RSpec.configure do |config|
|
|
28
29
|
if config.exclusion_filter[:active_record]
|
29
30
|
puts "Skipping ActiveRecord tests"
|
30
31
|
else
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
|
33
|
+
|
34
|
+
# We have to parse this to a hash since ActiveRecord::Base.configurations
|
35
|
+
# will only consider a single URL config.
|
36
|
+
url_config = if ENV["DATABASE_URL"]
|
37
|
+
ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.
|
38
|
+
new(ENV["DATABASE_URL"]).to_hash.merge({ sslmode: "disable" })
|
39
|
+
end
|
40
|
+
|
41
|
+
db_config = {
|
42
|
+
current_env => {
|
43
|
+
primary: url_config || {
|
44
|
+
adapter: "sqlite3",
|
45
|
+
database: "/tmp/statesman.db",
|
46
|
+
},
|
47
|
+
secondary: url_config || {
|
48
|
+
adapter: "sqlite3",
|
49
|
+
database: "/tmp/statesman.db",
|
50
|
+
},
|
51
|
+
},
|
52
|
+
}
|
53
|
+
|
54
|
+
# Connect to the primary database for activerecord tests.
|
55
|
+
ActiveRecord::Base.configurations = db_config
|
56
|
+
ActiveRecord::Base.establish_connection(:primary)
|
35
57
|
|
36
58
|
db_adapter = ActiveRecord::Base.connection.adapter_name
|
37
59
|
puts "Running with database adapter '#{db_adapter}'"
|
@@ -40,7 +62,9 @@ RSpec.configure do |config|
|
|
40
62
|
ActiveRecord::Migration.verbose = false
|
41
63
|
end
|
42
64
|
|
43
|
-
|
65
|
+
# Since our primary and secondary connections point to the same database, we don't
|
66
|
+
# need to worry about applying these actions to both.
|
67
|
+
config.before(:each, :active_record) do
|
44
68
|
tables = %w[
|
45
69
|
my_active_record_models
|
46
70
|
my_active_record_model_transitions
|
@@ -48,9 +72,12 @@ RSpec.configure do |config|
|
|
48
72
|
my_namespace_my_active_record_model_transitions
|
49
73
|
other_active_record_models
|
50
74
|
other_active_record_model_transitions
|
75
|
+
sti_active_record_models
|
76
|
+
sti_active_record_model_transitions
|
51
77
|
]
|
52
78
|
tables.each do |table_name|
|
53
79
|
sql = "DROP TABLE IF EXISTS #{table_name};"
|
80
|
+
|
54
81
|
ActiveRecord::Base.connection.execute(sql)
|
55
82
|
end
|
56
83
|
|
@@ -72,6 +99,16 @@ RSpec.configure do |config|
|
|
72
99
|
OtherActiveRecordModelTransition.reset_column_information
|
73
100
|
end
|
74
101
|
|
75
|
-
|
102
|
+
def prepare_sti_model_table
|
103
|
+
CreateStiActiveRecordModelMigration.migrate(:up)
|
104
|
+
end
|
105
|
+
|
106
|
+
def prepare_sti_transitions_table
|
107
|
+
CreateStiActiveRecordModelTransitionMigration.migrate(:up)
|
108
|
+
StiActiveRecordModelTransition.reset_column_information
|
109
|
+
end
|
76
110
|
end
|
77
111
|
end
|
112
|
+
|
113
|
+
# We have to require this after the databases are configured.
|
114
|
+
require "support/active_record"
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
3
|
+
describe Statesman::Adapters::ActiveRecordQueries, :active_record do
|
6
4
|
def configure_old(klass, transition_class)
|
7
5
|
klass.define_singleton_method(:transition_class) { transition_class }
|
8
6
|
klass.define_singleton_method(:initial_state) { :initial }
|
@@ -50,10 +48,11 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
50
48
|
|
51
49
|
shared_examples "testing methods" do
|
52
50
|
before do
|
53
|
-
|
51
|
+
case config_type
|
52
|
+
when :old
|
54
53
|
configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
|
55
54
|
configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
56
|
-
|
55
|
+
when :new
|
57
56
|
configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
|
58
57
|
configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
59
58
|
else
|
@@ -116,8 +115,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
116
115
|
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
117
116
|
|
118
117
|
it do
|
119
|
-
expect(not_in_state).to
|
120
|
-
|
118
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
119
|
+
returned_to_initial_model)
|
121
120
|
end
|
122
121
|
end
|
123
122
|
|
@@ -125,8 +124,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
125
124
|
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
126
125
|
|
127
126
|
it do
|
128
|
-
expect(not_in_state).to
|
129
|
-
|
127
|
+
expect(not_in_state).to contain_exactly(initial_state_model,
|
128
|
+
returned_to_initial_model)
|
130
129
|
end
|
131
130
|
end
|
132
131
|
end
|
@@ -154,6 +153,31 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
154
153
|
end
|
155
154
|
end
|
156
155
|
|
156
|
+
context "with a custom primary key for the model" do
|
157
|
+
before do
|
158
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
159
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
160
|
+
# this spec.
|
161
|
+
# Configure the relationship to use a different primary key,
|
162
|
+
MyActiveRecordModel.send(:has_many,
|
163
|
+
:custom_name,
|
164
|
+
class_name: "OtherActiveRecordModelTransition",
|
165
|
+
primary_key: :external_id)
|
166
|
+
|
167
|
+
MyActiveRecordModel.class_eval do
|
168
|
+
def self.transition_class
|
169
|
+
OtherActiveRecordModelTransition
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe ".in_state" do
|
175
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
176
|
+
|
177
|
+
specify { expect { query }.to_not raise_error }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
157
181
|
context "after_commit transactional integrity" do
|
158
182
|
before do
|
159
183
|
MyStateMachine.class_eval do
|
@@ -176,7 +200,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
176
200
|
MyActiveRecordModel.create
|
177
201
|
end
|
178
202
|
|
179
|
-
# rubocop:disable RSpec/ExampleLength
|
180
203
|
it do
|
181
204
|
expect do
|
182
205
|
ActiveRecord::Base.transaction do
|
@@ -185,7 +208,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
185
208
|
end
|
186
209
|
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
187
210
|
end
|
188
|
-
# rubocop:enable RSpec/ExampleLength
|
189
211
|
end
|
190
212
|
end
|
191
213
|
|
@@ -230,7 +252,7 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
230
252
|
end
|
231
253
|
|
232
254
|
it "does not raise an error" do
|
233
|
-
expect { check_missing_methods! }.to_not raise_exception
|
255
|
+
expect { check_missing_methods! }.to_not raise_exception
|
234
256
|
end
|
235
257
|
end
|
236
258
|
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
3
|
require "timecop"
|
5
4
|
require "statesman/adapters/shared_examples"
|
6
5
|
require "statesman/exceptions"
|
7
6
|
|
8
|
-
describe Statesman::Adapters::ActiveRecord, active_record
|
7
|
+
describe Statesman::Adapters::ActiveRecord, :active_record do
|
9
8
|
before do
|
10
9
|
prepare_model_table
|
11
10
|
prepare_transitions_table
|
12
11
|
|
13
|
-
|
12
|
+
prepare_sti_model_table
|
13
|
+
prepare_sti_transitions_table
|
14
14
|
|
15
15
|
Statesman.configure do
|
16
16
|
# Rubocop requires described_class to be used, but this block
|
@@ -23,8 +23,10 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
23
23
|
|
24
24
|
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
25
25
|
|
26
|
+
let(:model_class) { MyActiveRecordModel }
|
27
|
+
let(:transition_class) { MyActiveRecordModelTransition }
|
26
28
|
let(:observer) { double(Statesman::Machine, execute: nil) }
|
27
|
-
let(:model) {
|
29
|
+
let(:model) { model_class.create(current_state: :pending) }
|
28
30
|
|
29
31
|
it_behaves_like "an adapter", described_class, MyActiveRecordModelTransition
|
30
32
|
|
@@ -33,17 +35,11 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
33
35
|
before do
|
34
36
|
metadata_column = double
|
35
37
|
allow(metadata_column).to receive_messages(sql_type: "")
|
36
|
-
allow(MyActiveRecordModelTransition).
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
to receive(:type_for_attribute).with("metadata").
|
42
|
-
and_return(ActiveRecord::Type::Value.new)
|
43
|
-
else
|
44
|
-
expect(MyActiveRecordModelTransition).
|
45
|
-
to receive_messages(serialized_attributes: {})
|
46
|
-
end
|
38
|
+
allow(MyActiveRecordModelTransition).
|
39
|
+
to receive_messages(columns_hash: { "metadata" => metadata_column })
|
40
|
+
expect(MyActiveRecordModelTransition).
|
41
|
+
to receive(:type_for_attribute).with("metadata").
|
42
|
+
and_return(ActiveRecord::Type::Value.new)
|
47
43
|
end
|
48
44
|
|
49
45
|
it "raises an exception" do
|
@@ -60,10 +56,10 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
60
56
|
allow(metadata_column).to receive_messages(sql_type: "json")
|
61
57
|
allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
|
62
58
|
{ "metadata" => metadata_column })
|
63
|
-
if
|
64
|
-
|
65
|
-
serialized_type =
|
66
|
-
"",
|
59
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
60
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.2.0.a")
|
61
|
+
serialized_type = ActiveRecord::Type::Serialized.new(
|
62
|
+
"", ActiveRecord::Coders::JSON
|
67
63
|
)
|
68
64
|
expect(MyActiveRecordModelTransition).
|
69
65
|
to receive(:type_for_attribute).with("metadata").
|
@@ -88,18 +84,12 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
88
84
|
allow(metadata_column).to receive_messages(sql_type: "jsonb")
|
89
85
|
allow(MyActiveRecordModelTransition).to receive_messages(columns_hash:
|
90
86
|
{ "metadata" => metadata_column })
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
)
|
96
|
-
|
97
|
-
to receive(:type_for_attribute).with("metadata").
|
98
|
-
and_return(serialized_type)
|
99
|
-
else
|
100
|
-
expect(MyActiveRecordModelTransition).
|
101
|
-
to receive_messages(serialized_attributes: { "metadata" => "" })
|
102
|
-
end
|
87
|
+
serialized_type = ActiveRecord::Type::Serialized.new(
|
88
|
+
"", ActiveRecord::Coders::JSON
|
89
|
+
)
|
90
|
+
expect(MyActiveRecordModelTransition).
|
91
|
+
to receive(:type_for_attribute).with("metadata").
|
92
|
+
and_return(serialized_type)
|
103
93
|
end
|
104
94
|
|
105
95
|
it "raises an exception" do
|
@@ -112,15 +102,17 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
112
102
|
end
|
113
103
|
|
114
104
|
describe "#create" do
|
115
|
-
subject {
|
105
|
+
subject(:transition) { create }
|
116
106
|
|
117
|
-
let!(:adapter)
|
118
|
-
described_class.new(MyActiveRecordModelTransition, model, observer)
|
119
|
-
end
|
107
|
+
let!(:adapter) { described_class.new(transition_class, model, observer) }
|
120
108
|
let(:from) { :x }
|
121
109
|
let(:to) { :y }
|
122
110
|
let(:create) { adapter.create(from, to) }
|
123
111
|
|
112
|
+
it "only connects to the primary database" do
|
113
|
+
expect { create }.to exactly_query_databases({ primary: [:writing] })
|
114
|
+
end
|
115
|
+
|
124
116
|
context "when there is a race" do
|
125
117
|
it "raises a TransitionConflictError" do
|
126
118
|
adapter2 = adapter.dup
|
@@ -128,7 +120,33 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
128
120
|
adapter.last
|
129
121
|
adapter2.create(:y, :z)
|
130
122
|
expect { adapter.create(:y, :z) }.
|
131
|
-
to raise_exception(Statesman::TransitionConflictError)
|
123
|
+
to raise_exception(Statesman::TransitionConflictError).
|
124
|
+
and exactly_query_databases({ primary: [:writing] })
|
125
|
+
end
|
126
|
+
|
127
|
+
it "does not pollute the state when the transition fails" do
|
128
|
+
# this increments the sort_key in the database
|
129
|
+
adapter.create(:x, :y)
|
130
|
+
|
131
|
+
# we then pre-load the transitions for efficiency
|
132
|
+
preloaded_model = MyActiveRecordModel.
|
133
|
+
includes(:my_active_record_model_transitions).
|
134
|
+
find(model.id)
|
135
|
+
|
136
|
+
adapter2 = described_class.
|
137
|
+
new(MyActiveRecordModelTransition, preloaded_model, observer)
|
138
|
+
|
139
|
+
# Now we generate a race
|
140
|
+
adapter.create(:y, :z)
|
141
|
+
expect { adapter2.create(:y, :a) }.
|
142
|
+
to raise_error(Statesman::TransitionConflictError)
|
143
|
+
|
144
|
+
# The preloaded adapter should discard the preloaded info
|
145
|
+
expect(adapter2.last).to have_attributes(to_state: "z")
|
146
|
+
expect(adapter2.history).to contain_exactly(
|
147
|
+
have_attributes(to_state: "y"),
|
148
|
+
have_attributes(to_state: "z"),
|
149
|
+
)
|
132
150
|
end
|
133
151
|
end
|
134
152
|
|
@@ -140,27 +158,25 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
140
158
|
|
141
159
|
context "ActiveRecord::RecordNotUnique unrelated to this transition" do
|
142
160
|
let(:error) do
|
143
|
-
if
|
144
|
-
|
161
|
+
if ActiveRecord.respond_to?(:gem_version) &&
|
162
|
+
ActiveRecord.gem_version >= Gem::Version.new("4.0.0")
|
145
163
|
ActiveRecord::RecordNotUnique.new("unrelated")
|
146
164
|
else
|
147
165
|
ActiveRecord::RecordNotUnique.new("unrelated", nil)
|
148
166
|
end
|
149
167
|
end
|
150
168
|
|
151
|
-
it {
|
169
|
+
it { expect { transition }.to raise_exception(ActiveRecord::RecordNotUnique) }
|
152
170
|
end
|
153
171
|
|
154
172
|
context "other errors" do
|
155
173
|
let(:error) { StandardError }
|
156
174
|
|
157
|
-
it {
|
175
|
+
it { expect { transition }.to raise_exception(StandardError) }
|
158
176
|
end
|
159
177
|
end
|
160
178
|
|
161
179
|
describe "updating the most_recent column" do
|
162
|
-
subject { create }
|
163
|
-
|
164
180
|
context "with no previous transition" do
|
165
181
|
its(:most_recent) { is_expected.to eq(true) }
|
166
182
|
end
|
@@ -277,17 +293,90 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
277
293
|
from(true).to be_falsey
|
278
294
|
end
|
279
295
|
end
|
296
|
+
|
297
|
+
context "when transition uses STI" do
|
298
|
+
let(:sti_model) { StiActiveRecordModel.create }
|
299
|
+
|
300
|
+
let(:adapter_a) do
|
301
|
+
described_class.new(
|
302
|
+
StiAActiveRecordModelTransition,
|
303
|
+
sti_model,
|
304
|
+
observer,
|
305
|
+
{ association_name: :sti_a_active_record_model_transitions },
|
306
|
+
)
|
307
|
+
end
|
308
|
+
let(:adapter_b) do
|
309
|
+
described_class.new(
|
310
|
+
StiBActiveRecordModelTransition,
|
311
|
+
sti_model,
|
312
|
+
observer,
|
313
|
+
{ association_name: :sti_b_active_record_model_transitions },
|
314
|
+
)
|
315
|
+
end
|
316
|
+
let(:create) { adapter_a.create(from, to) }
|
317
|
+
|
318
|
+
context "with a previous unrelated transition" do
|
319
|
+
let!(:transition_b) { adapter_b.create(from, to) }
|
320
|
+
|
321
|
+
its(:most_recent) { is_expected.to eq(true) }
|
322
|
+
|
323
|
+
it "doesn't update the previous transition's most_recent flag" do
|
324
|
+
expect { create }.
|
325
|
+
to_not(change { transition_b.reload.most_recent })
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
context "with previous related and unrelated transitions" do
|
330
|
+
let!(:transition_a) { adapter_a.create(from, to) }
|
331
|
+
let!(:transition_b) { adapter_b.create(from, to) }
|
332
|
+
|
333
|
+
its(:most_recent) { is_expected.to eq(true) }
|
334
|
+
|
335
|
+
it "updates the previous transition's most_recent flag" do
|
336
|
+
expect { create }.
|
337
|
+
to change { transition_a.reload.most_recent }.
|
338
|
+
from(true).to be_falsey
|
339
|
+
end
|
340
|
+
|
341
|
+
it "doesn't update the previous unrelated transition's most_recent flag" do
|
342
|
+
expect { create }.
|
343
|
+
to_not(change { transition_b.reload.most_recent })
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
280
347
|
end
|
281
|
-
end
|
282
348
|
|
283
|
-
|
284
|
-
|
285
|
-
|
349
|
+
context "when using the secondary database" do
|
350
|
+
let(:model_class) { SecondaryActiveRecordModel }
|
351
|
+
let(:transition_class) { SecondaryActiveRecordModelTransition }
|
352
|
+
|
353
|
+
it "doesn't connect to the primary database" do
|
354
|
+
expect { create }.to exactly_query_databases({ secondary: [:writing] })
|
355
|
+
expect(adapter.last.to_state).to eq("y")
|
356
|
+
end
|
357
|
+
|
358
|
+
context "when there is a race" do
|
359
|
+
it "raises a TransitionConflictError and uses the correct database" do
|
360
|
+
adapter2 = adapter.dup
|
361
|
+
adapter2.create(:x, :y)
|
362
|
+
adapter.last
|
363
|
+
adapter2.create(:y, :z)
|
364
|
+
|
365
|
+
expect { adapter.create(:y, :z) }.
|
366
|
+
to raise_exception(Statesman::TransitionConflictError).
|
367
|
+
and exactly_query_databases({ secondary: [:writing] })
|
368
|
+
end
|
369
|
+
end
|
286
370
|
end
|
371
|
+
end
|
287
372
|
|
288
|
-
|
373
|
+
describe "#last" do
|
374
|
+
let(:transition_class) { MyActiveRecordModelTransition }
|
375
|
+
let(:adapter) { described_class.new(transition_class, model, observer) }
|
289
376
|
|
290
377
|
context "with a previously looked up transition" do
|
378
|
+
before { adapter.create(:x, :y) }
|
379
|
+
|
291
380
|
before { adapter.last }
|
292
381
|
|
293
382
|
it "caches the transition" do
|
@@ -300,8 +389,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
300
389
|
before { adapter.create(:y, :z, []) }
|
301
390
|
|
302
391
|
it "retrieves the new transition from the database" do
|
392
|
+
expect { adapter.last.to_state }.to exactly_query_databases({ primary: [:writing] })
|
303
393
|
expect(adapter.last.to_state).to eq("z")
|
304
394
|
end
|
395
|
+
|
396
|
+
context "when using the secondary database" do
|
397
|
+
let(:model_class) { SecondaryActiveRecordModel }
|
398
|
+
let(:transition_class) { SecondaryActiveRecordModelTransition }
|
399
|
+
|
400
|
+
it "retrieves the new transition from the database" do
|
401
|
+
expect { adapter.last.to_state }.to exactly_query_databases({ secondary: [:writing] })
|
402
|
+
expect(adapter.last.to_state).to eq("z")
|
403
|
+
end
|
404
|
+
end
|
305
405
|
end
|
306
406
|
|
307
407
|
context "when a new transition has been created elsewhere" do
|
@@ -353,6 +453,35 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
353
453
|
expect(adapter.last.to_state).to eq("y")
|
354
454
|
end
|
355
455
|
end
|
456
|
+
|
457
|
+
context "without previous transitions" do
|
458
|
+
it "does query the database only once" do
|
459
|
+
expect(model.my_active_record_model_transitions).
|
460
|
+
to receive(:order).once.and_call_original
|
461
|
+
|
462
|
+
expect(adapter.last).to eq(nil)
|
463
|
+
expect(adapter.last).to eq(nil)
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
describe "#reset" do
|
469
|
+
it "works with empty cache" do
|
470
|
+
expect { model.state_machine.reset }.to_not raise_error
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
it "resets last with #reload" do
|
475
|
+
model.save!
|
476
|
+
ActiveRecord::Base.transaction do
|
477
|
+
model.state_machine.transition_to!(:succeeded)
|
478
|
+
# force to cache value in last_transition instance variable
|
479
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
480
|
+
raise ActiveRecord::Rollback
|
481
|
+
end
|
482
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
483
|
+
model.reload
|
484
|
+
expect(model.state_machine.current_state).to eq("initial")
|
356
485
|
end
|
357
486
|
|
358
487
|
context "with a namespaced model" do
|
@@ -361,10 +490,6 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
361
490
|
CreateNamespacedARModelTransitionMigration.migrate(:up)
|
362
491
|
end
|
363
492
|
|
364
|
-
before do
|
365
|
-
MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
|
366
|
-
end
|
367
|
-
|
368
493
|
let(:observer) { double(Statesman::Machine, execute: nil) }
|
369
494
|
let(:model) do
|
370
495
|
MyNamespace::MyActiveRecordModel.create(current_state: :pending)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
3
|
require "json"
|
5
4
|
|
6
5
|
describe Statesman::Adapters::ActiveRecordTransition do
|
@@ -8,7 +7,11 @@ describe Statesman::Adapters::ActiveRecordTransition do
|
|
8
7
|
|
9
8
|
describe "including behaviour" do
|
10
9
|
it "calls Class.serialize" do
|
11
|
-
|
10
|
+
if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1")
|
11
|
+
expect(transition_class).to receive(:serialize).with(:metadata, coder: JSON).once
|
12
|
+
else
|
13
|
+
expect(transition_class).to receive(:serialize).with(:metadata, JSON).once
|
14
|
+
end
|
12
15
|
transition_class.send(:include, described_class)
|
13
16
|
end
|
14
17
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "spec_helper"
|
4
|
-
|
5
3
|
# All adpators must define seven methods:
|
6
4
|
# initialize: Accepts a transition class, parent model and state_attr.
|
7
5
|
# transition_class: Returns the transition class object passed to initialize.
|
@@ -30,14 +28,14 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
|
|
30
28
|
end
|
31
29
|
|
32
30
|
describe "#create" do
|
33
|
-
subject {
|
31
|
+
subject(:transition) { create }
|
34
32
|
|
35
33
|
let(:from) { :x }
|
36
34
|
let(:to) { :y }
|
37
35
|
let(:there) { :z }
|
38
36
|
let(:create) { adapter.create(from, to) }
|
39
37
|
|
40
|
-
it {
|
38
|
+
it { expect { transition }.to change(adapter.history, :count).by(1) }
|
41
39
|
|
42
40
|
context "the new transition" do
|
43
41
|
subject(:instance) { create }
|
@@ -128,6 +126,7 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
|
|
128
126
|
|
129
127
|
it { is_expected.to be_a(transition_class) }
|
130
128
|
specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
|
129
|
+
|
131
130
|
specify do
|
132
131
|
expect(adapter.last(force_reload: true).to_state.to_sym).to eq(:z)
|
133
132
|
end
|