diff --git a/README.md b/README.md index 6b306f3..7b336bc 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ local fsm = machine.create({ }) ``` -If you decide to cancel the async event, you can call `fsm.cancelTransition(eventName)` +If you decide to cancel the async event, you can call `fsm:cancelTransition(eventName)` Initialization Options ====================== @@ -243,14 +243,14 @@ fsm:startup() print(fsm.current) -- "green" ``` -If you specify the name of your initial event (as in all the earlier examples), then an -implicit `startup` event will be created for you and fired when the state machine is constructed. +If you specify an initial state (as in all the earlier examples), the state machine is +constructed in that state. No transition callbacks are fired during construction. ```lua local machine = require('statemachine') local fsm = machine.create({ - inital = 'green', + initial = 'green', events = { { name = 'panic', from = 'green', to = 'red' }, { name = 'calm', from = 'red', to = 'green' }, diff --git a/spec/fsm_spec.lua b/spec/fsm_spec.lua index ac72e03..36124be 100644 --- a/spec/fsm_spec.lua +++ b/spec/fsm_spec.lua @@ -46,6 +46,11 @@ describe("Lua state machine framework", function() assert.is_true(fsm:cannot('clear')) end) + it("should reject unknown events", function() + assert.is_false(fsm:can('missing')) + assert.is_true(fsm:cannot('missing')) + end) + it("should support checking states", function() assert.is_true(fsm:is('green')) assert.is_false(fsm:is('red')) @@ -139,17 +144,24 @@ describe("Lua state machine framework", function() assert.is_false(result) assert.are_equal(fsm.current, 'green') + assert.is_nil(fsm.currentTransitioningEvent) end) it("should cancel the warn event from onbeforewarn", function() + local leaves = 0 fsm.onbeforewarn = function(self, name, from, to) return false end + fsm.onleavegreen = function() + leaves = leaves + 1 + end local result = fsm:warn() assert.is_false(result) assert.are_equal(fsm.current, 'green') + assert.are_equal(leaves, 0) + assert.is_nil(fsm.currentTransitioningEvent) end) it("pauses when async is passed", function() @@ -245,6 +257,53 @@ describe("Lua state machine framework", function() assert.are_equal(fsm.current, 'red') end) + it("should reject an invalid event during an async transition", function() + fsm.onleavegreen = function(self, name, from, to) + return fsm.ASYNC + end + + assert.is_true(fsm:warn()) + assert.is_false(fsm:panic()) + assert.are_equal(fsm.current, 'green') + assert.are_equal(fsm.asyncState, fsm.NONE) + assert.is_nil(fsm.currentTransitioningEvent) + assert.is_nil(fsm:transition('warn')) + end) + + it("should preserve nil arguments through async handlers", function() + local received = {} + local function capture(...) + return { + count = select('#', ...), + first = select(1, ...), + second = select(2, ...), + third = select(3, ...), + fourth = select(4, ...) + } + end + + fsm.onleavegreen = function(self, name, from, to, ...) + received.leave = capture(...) + return fsm.ASYNC + end + fsm.onenteryellow = function(self, name, from, to, ...) + received.enter = capture(...) + end + + assert.is_true(fsm:warn('one', nil, 'three', nil)) + assert.is_true(fsm:transition('warn')) + + assert.is_not_nil(received.leave) + assert.is_not_nil(received.enter) + for _, callback in pairs(received) do + assert.are_equal(4, callback.count) + assert.are_equal('one', callback.first) + assert.is_nil(callback.second) + assert.are_equal('three', callback.third) + assert.is_nil(callback.fourth) + end + end) + it("should properly cancel the transition if asked", function() fsm.onleavegreen = function(self, name, from, to) return fsm.ASYNC diff --git a/statemachine.lua b/statemachine.lua index ba9da23..cab0ffe 100644 --- a/statemachine.lua +++ b/statemachine.lua @@ -9,7 +9,7 @@ local unpack = unpack or table.unpack local function call_handler(handler, params) if handler then - return handler(unpack(params)) + return handler(unpack(params, 1, params.n)) end end @@ -21,14 +21,21 @@ local function create_transition(name) can, to = self:can(name) from = self.current params = { self, name, from, to, ...} + params.n = 4 + select('#', ...) if not can then return false end self.currentTransitioningEvent = name local beforeReturn = call_handler(self["onbefore" .. name], params) + if beforeReturn == false then + self.currentTransitioningEvent = nil + return false + end + local leaveReturn = call_handler(self["onleave" .. from], params) - if beforeReturn == false or leaveReturn == false then + if leaveReturn == false then + self.currentTransitioningEvent = nil return false end @@ -58,11 +65,11 @@ local function create_transition(name) self.currentTransitioningEvent = nil return true else - if string.find(self.asyncState, "WaitingOnLeave") or string.find(self.asyncState, "WaitingOnEnter") then - self.asyncState = NONE - transition(self, ...) - return true - end + if string.find(self.asyncState, "WaitingOnLeave") or string.find(self.asyncState, "WaitingOnEnter") then + self.asyncState = NONE + self.currentTransitioningEvent = nil + return transition(self, ...) + end end self.currentTransitioningEvent = nil @@ -113,7 +120,7 @@ end function machine:can(e) local event = self.events[e] - local to = event and event.map[self.current] or event.map['*'] + local to = event and (event.map[self.current] or event.map['*']) return to ~= nil, to end