Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event transitions should work with multiple state targets #78

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions lib/statesman/machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,21 @@ def transition_to!(new_state, metadata = nil)
end

def trigger!(event_name, metadata = nil)
transitions = self.class.events.fetch(event_name) do
transition_targets = transitions_for(event_name).fetch(current_state) do
raise Statesman::TransitionFailedError,
"Event #{event_name} not found"
"State #{current_state} not found for Event #{event_name}"
end

new_state = transitions.fetch(current_state) do
raise Statesman::TransitionFailedError,
"State #{current_state} not found for Event #{event_name}"
failed_targets = []

transition_targets.each do |target_state|
break if transition_to(target_state, metadata)
failed_targets << target_state
end

transition_to!(new_state.first, metadata)
raise Statesman::GuardFailedError,
"All guards returned false when triggering event #{event_name}" if
transition_targets == failed_targets
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right @isaacseymour - this will only be raised if guards on all transitions fail. This is a more accurate error to raise.

I also added tests for successful and failed transitions from/to the same state. This new condition against the failed states rather than simply the post-event current_state now covers that scenario.

true
end

Expand Down Expand Up @@ -273,6 +277,13 @@ def adapter_class(transition_class)
end
end

def transitions_for(event_name)
self.class.events.fetch(event_name) do
raise Statesman::TransitionFailedError,
"Event #{event_name} not found"
end
end

def successors_for(from)
self.class.successors[from] || []
end
Expand Down
87 changes: 87 additions & 0 deletions spec/statesman/machine_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ def after_initialize; end
describe "#event" do
before do
machine.class_eval do
state :w
state :x, initial: true
state :y
state :z
Expand All @@ -628,6 +629,14 @@ def after_initialize; end
event :event_2 do
transition from: :y, to: :z
end

event :event_3 do
transition from: :x, to: [:y, :z]
end

event :event_4 do
transition from: :x, to: [:w, :x]
end
end
end

Expand All @@ -641,6 +650,84 @@ def after_initialize; end
end
end

context "when two states can be transitioned to" do
it "changes to the first available state" do
instance.trigger!(:event_3)
expect(instance.current_state).to eq('y')
end

context "and one state is the current_state" do
let(:result) { false }
let(:result2) { true }

let(:guard_cb) { ->(*_args) { result } }
before { machine.guard_transition(from: :x, to: :w, &guard_cb) }

let(:guard_cb2) { ->(*_args) { result2 } }
before { machine.guard_transition(from: :x, to: :x, &guard_cb2) }

context "successfully transitioning back to current_state" do
it "does not raise an exception" do
expect do
instance.trigger!(:event_4)
end.not_to raise_error
end
end

context "failing to transition to either state" do
let(:result2) { false }
it "raises an exception" do
expect do
instance.trigger!(:event_4)
end.to raise_error(Statesman::GuardFailedError)
end
end
end

context "with a guard on the first state" do
let(:result) { true }
# rubocop:disable UnusedBlockArgument
let(:guard_cb) { ->(*args) { result } }
# rubocop:enable UnusedBlockArgument
before { machine.guard_transition(from: :x, to: :y, &guard_cb) }

context "which passes" do
it "changes state" do
instance.trigger!(:event_3)
expect(instance.current_state).to eq("y")
end
end

context "which fails" do
let(:result) { false }

it 'changes to the next passing state' do
instance.trigger(:event_3)
expect(instance.current_state).to eq('z')
end
end

context "and the second state" do
let(:result2) { true }
# rubocop:disable UnusedBlockArgument
let(:guard2_cb) { ->(*args) { result } }
# rubocop:enable UnusedBlockArgument
before { machine.guard_transition(from: :x, to: :z, &guard2_cb) }

context "both of which fail" do
let(:result) { false }
let(:result2) { false }

it "raises an exception" do
expect do
instance.trigger!(:event_3)
end.to raise_error(Statesman::GuardFailedError)
end
end
end
end
end

context "when the state can be transitioned to" do
it "changes state" do
instance.trigger!(:event_1)
Expand Down