├── .busted ├── .travis.yml ├── LICENSE ├── README.md ├── luafsm.lua └── spec ├── advanced_spec.lua ├── async_spec.lua ├── basic_spec.lua ├── classes_spec.lua └── initialize_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | ROOT = "spec" 4 | } 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | env: 4 | - LUA="" 5 | - LUA="luajit" 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | install: 12 | - sudo apt-get install luajit 13 | - sudo apt-get install luarocks 14 | - sudo luarocks install luafilesystem 15 | - sudo luarocks install busted 16 | 17 | script: "busted" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 recih 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lua Finite State Machine (v2.3.2) 2 | ======================================== 3 | 4 | [![Build Status](https://travis-ci.org/recih/lua-fsm.svg?branch=master)](https://travis-ci.org/recih/lua-fsm) 5 | This standalone lua micro-framework provides a finite state machine for your pleasure. 6 | 7 | * fully ported (including test) from [javascript-state-machine](https://github.com/jakesgordon/javascript-state-machine), great thanks to jakesgordon 8 | * You can find the [code here](https://github.com/recih/lua-fsm) 9 | * You can read [specs code](https://github.com/recih/lua-fsm/tree/master/spec) as examples 10 | 11 | Download 12 | ======== 13 | 14 | You can download [luafsm.lua](https://github.com/recih/lua-fsm/raw/master/luafsm.js), 15 | 16 | Alternatively: 17 | 18 | git clone https://github.com/recih/lua-fsm.git 19 | 20 | * All code is in luafsm.lua 21 | * No 3rd party library is required 22 | * Busted tests can be run with "busted" (after installing busted with "luarocks install busted") 23 | 24 | Usage 25 | ===== 26 | 27 | Simply use `require`: 28 | 29 | ```lua 30 | local luafsm = require("luafsm") 31 | ``` 32 | 33 | In its simplest form, create a standalone state machine using: 34 | 35 | 36 | ```lua 37 | local fsm = luafsm.create { 38 | initial = 'green', 39 | events = { 40 | { name = 'warn', from = 'green', to = 'yellow' }, 41 | { name = 'panic', from = 'yellow', to = 'red' }, 42 | { name = 'calm', from = 'red', to = 'yellow' }, 43 | { name = 'clear', from = 'yellow', to = 'green' }, 44 | } 45 | } 46 | ``` 47 | 48 | ... will create an object with a method for each event: 49 | 50 | * fsm:warn() - transition from 'green' to 'yellow' 51 | * fsm:panic() - transition from 'yellow' to 'red' 52 | * fsm:calm() - transition from 'red' to 'yellow' 53 | * fsm:clear() - transition from 'yellow' to 'green' 54 | 55 | along with the following members: 56 | 57 | * fsm.current - contains the current state 58 | * fsm:is(s) - return true if state `s` is the current state 59 | * fsm:can(e) - return true if event `e` can be fired in the current state 60 | * fsm:cannot(e) - return true if event `e` cannot be fired in the current state 61 | 62 | Multiple 'from' and 'to' states for a single event 63 | ================================================== 64 | 65 | If an event is allowed **from** multiple states, and always transitions to the same 66 | state, then simply provide an array of states in the `from` attribute of an event. However, 67 | if an event is allowed from multiple states, but should transition **to** a different 68 | state depending on the current state, then provide multiple event entries with 69 | the same name: 70 | 71 | ```lua 72 | local fsm = luafsm.create{ 73 | initial = 'hungry', 74 | events = { 75 | { name = 'eat', from = 'hungry', to = 'satisfied' }, 76 | { name = 'eat', from = 'satisfied', to = 'full' }, 77 | { name = 'eat', from = 'full', to = 'sick' }, 78 | { name = 'rest', from = {'hungry', 'satisfied', 'full', 'sick'}, to = 'hungry' }, 79 | } 80 | } 81 | ``` 82 | 83 | This example will create an object with 2 event methods: 84 | 85 | * fsm:eat() 86 | * fsm:rest() 87 | 88 | The `rest` event will always transition to the `hungry` state, while the `eat` event 89 | will transition to a state that is dependent on the current state. 90 | 91 | >> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be 92 | allowed from any current state. 93 | 94 | >> NOTE: The `rest` event in the above example can also be specified as multiple events with 95 | the same name if you prefer the verbose approach. 96 | 97 | Callbacks 98 | ========= 99 | 100 | 4 types of callback are available by attaching methods to your StateMachine using the following naming conventions: 101 | 102 | * `onbeforeEVENT` - fired before the event 103 | * `onleaveSTATE` - fired when leaving the old state 104 | * `onenterSTATE` - fired when entering the new state 105 | * `onafterEVENT` - fired after the event 106 | 107 | >> (using your **specific** EVENT and STATE names) 108 | 109 | For convenience, the 2 most useful callbacks can be shortened: 110 | 111 | * `onEVENT` - convenience shorthand for `onafterEVENT` 112 | * `onSTATE` - convenience shorthand for `onenterSTATE` 113 | 114 | In addition, 4 general-purpose callbacks can be used to capture **all** event and state changes: 115 | 116 | * `onbeforeevent` - fired before *any* event 117 | * `onleavestate` - fired when leaving *any* state 118 | * `onenterstate` - fired when entering *any* state 119 | * `onafterevent` - fired after *any* event 120 | 121 | All callbacks will be passed the same arguments: 122 | 123 | * **self** state machine instance itself 124 | * **event** a event object (a table) like this: { name = 'warn', from = 'green', to = 'yellow' } 125 | * _(followed by any arguments you passed into the original event method)_ 126 | 127 | Callbacks can be specified when the state machine is first created: 128 | 129 | ```lua 130 | local fsm = luafsm.create{ 131 | initial = 'green', 132 | events = { 133 | { name = 'warn', from = 'green', to = 'yellow' }, 134 | { name = 'panic', from = 'yellow', to = 'red' }, 135 | { name = 'calm', from = 'red', to = 'yellow' }, 136 | { name = 'clear', from = 'yellow', to = 'green' }, 137 | }, 138 | callbacks = { 139 | onpanic = function(self, event, msg) print('panic! ' .. msg) end, 140 | onclear = function(self, event, msg) print('thanks to ' .. msg) end, 141 | ongreen = function(self, event) print('green') end, 142 | onyellow = function(self, event) print('yellow') end, 143 | onred = function(self, event) print('red') end, 144 | } 145 | } 146 | 147 | fsm:panic('killer bees') 148 | fsm:clear('sedatives in the honey pots') 149 | ... 150 | ``` 151 | 152 | Additionally, they can be added and removed from the state machine at any time: 153 | 154 | ```lua 155 | fsm.ongreen = nil 156 | fsm.onyellow = nil 157 | fsm.onred = nil 158 | fsm.onenterstate = function(self, event) print(event.to) end 159 | ``` 160 | 161 | 162 | The order in which callbacks occur is as follows: 163 | 164 | >> assume event **go** transitions from **red** state to **green** 165 | 166 | * `onbeforego` - specific handler for the **go** event only 167 | * `onbeforeevent` - generic handler for all events 168 | * `onleavered` - specific handler for the **red** state only 169 | * `onleavestate` - generic handler for all states 170 | * `onentergreen` - specific handler for the **green** state only 171 | * `onenterstate` - generic handler for all states 172 | * `onaftergo` - specific handler for the **go** event only 173 | * `onafterevent` - generic handler for all events 174 | 175 | >> NOTE: the legacy `onchangestate` handler has been deprecated and will be removed in a future version 176 | 177 | You can affect the event in 3 ways: 178 | 179 | * return `false` from an `onbeforeEVENT` handler to cancel the event. 180 | * return `false` from an `onleaveSTATE` handler to cancel the event. 181 | * return `ASYNC` from an `onleaveSTATE` handler to perform an asynchronous state transition (see next section) 182 | 183 | Asynchronous State Transitions 184 | ============================== 185 | 186 | Sometimes, you need to execute some asynchronous code during a state transition and ensure the 187 | new state is not entered until your code has completed. 188 | 189 | A good example of this is when you transition out of a `menu` state, perhaps you want to gradually 190 | fade the menu away, or slide it off the screen and don't want to transition to your `game` state 191 | until after that animation has been performed. 192 | 193 | You can now return `luafsm.ASYNC` from your `onleavestate` handler and the state machine 194 | will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()` 195 | method. 196 | 197 | For example: 198 | 199 | ```lua 200 | local fsm = luafsm.create{ 201 | 202 | initial = 'menu', 203 | 204 | events = { 205 | { name = 'play', from = 'menu', to = 'game' }, 206 | { name = 'quit', from = 'game', to = 'menu' } 207 | }, 208 | 209 | callbacks = { 210 | 211 | onentermenu = function() menu_show() end, 212 | onentergame = function() game_show() end, 213 | 214 | onleavemenu = function() 215 | menu_fade_out(function() 216 | fsm:transition() 217 | end) 218 | return luafsm.ASYNC -- tell luafsm to defer next state until we call transition (in fadeOut callback above) 219 | end, 220 | 221 | onleavegame = function() 222 | game_slide_down(function() 223 | fsm:transition() 224 | end) 225 | return luafsm.ASYNC -- tell luafsm to defer next state until we call transition (in slideDown callback above) 226 | end, 227 | 228 | } 229 | } 230 | ``` 231 | 232 | >> _NOTE: If you decide to cancel the ASYNC event, you can call `fsm.transition.cancel()` 233 | 234 | State Machine Classes 235 | ===================== 236 | 237 | You can also turn all instances of a _class_ into an FSM by applying 238 | the state machine functionality to the prototype, including your callbacks 239 | in your prototype, and providing a `startup` event for use when constructing 240 | instances: 241 | 242 | ```lua 243 | local my_fsm = {} 244 | local prototype = { 245 | 246 | onpanic = function(self, event) print('panic') end, 247 | onclear = function(self, event) print('all is clear') end, 248 | 249 | -- my other prototype methods 250 | 251 | }; 252 | 253 | function my_fsm.new() -- my constructor function 254 | local t = {} 255 | setmetatable(t, {__index = prototype}) 256 | t:startup() 257 | return t 258 | end 259 | 260 | luafsm.create { 261 | target = prototype, 262 | events = { 263 | { name = 'startup', from = 'none', to = 'green' }, 264 | { name = 'warn', from = 'green', to = 'yellow' }, 265 | { name = 'panic', from = 'yellow', to = 'red' }, 266 | { name = 'calm', from = 'red', to = 'yellow' }, 267 | { name = 'clear', from = 'yellow', to = 'green' }, 268 | } 269 | } 270 | ``` 271 | 272 | This should be easy to adjust to fit your appropriate mechanism for object construction. 273 | 274 | >> _NOTE: the `startup` event can be given any name, but it must be present in some form to 275 | ensure that each instance constructed is initialized with its own unique `current` state._ 276 | 277 | Initialization Options 278 | ====================== 279 | 280 | How the state machine should initialize can depend on your application requirements, so 281 | the library provides a number of simple options. 282 | 283 | By default, if you dont specify any initial state, the state machine will be in the `'none'` 284 | state and you would need to provide an event to take it out of this state: 285 | 286 | ```lua 287 | local fsm = luafsm.create { 288 | events = { 289 | { name = 'startup', from = 'none', to = 'green' }, 290 | { name = 'panic', from = 'green', to = 'red' }, 291 | { name = 'calm', from = 'red', to = 'green' }, 292 | } 293 | } 294 | print(fsm.current) -- "none" 295 | fsm:startup() 296 | print(fsm.current) -- "green" 297 | ``` 298 | 299 | If you specify the name of your initial state (as in all the earlier examples), then an 300 | implicit `startup` event will be created for you and fired when the state machine is constructed. 301 | 302 | ```lua 303 | local fsm = luafsm.create { 304 | initial = 'green', 305 | events = { 306 | { name = 'panic', from = 'green', to = 'red' }, 307 | { name = 'calm', from = 'red', to = 'green' }, 308 | } 309 | } 310 | print(fsm.current) -- "green" 311 | ``` 312 | 313 | If your object already has a `startup` method you can use a different name for the initial event 314 | 315 | ```lua 316 | local fsm = luafsm.create { 317 | initial = { state = 'green', event = 'init' }, 318 | events = { 319 | { name = 'panic', from = 'green', to = 'red' }, 320 | { name = 'calm', from = 'red', to = 'green' }, 321 | } 322 | } 323 | print(fsm.current) -- "green" 324 | ``` 325 | 326 | Finally, if you want to wait to call the initial state transition event until a later date you 327 | can `defer` it: 328 | 329 | ```lua 330 | local fsm = luafsm.create { 331 | initial = { state = 'green', event = 'init', defer = true }, 332 | events = { 333 | { name = 'panic', from = 'green', to = 'red' }, 334 | { name = 'calm', from = 'red', to = 'green' }, 335 | } 336 | } 337 | print(fsm.current) -- "none" 338 | fsm:init() 339 | print(fsm.current) -- "green" 340 | ``` 341 | 342 | Of course, we have now come full circle, this last example is pretty much functionally the 343 | same as the first example in this section where you simply define your own startup event. 344 | 345 | So you have a number of choices available to you when initializing your state machine. 346 | 347 | >> _IMPORTANT NOTE: if you are using the pattern described in the previous section "State Machine 348 | Classes", and wish to declare an `initial` state in this manner, you MUST use the `defer: true` 349 | attribute and manually call the starting event in your constructor function. This will ensure 350 | that each instance gets its own unique `current` state, rather than an (unwanted) shared 351 | `current` state on the prototype object itself._ 352 | 353 | Handling Failures 354 | ====================== 355 | 356 | By default, if you try to call an event method that is not allowed in the current state, the 357 | state machine will throw an exception. If you prefer to handle the problem yourself, you can 358 | define a custom `error` handler: 359 | 360 | ```lua 361 | local fsm = luafsm.create { 362 | initial = 'green', 363 | error = function(self, event, error_code, error_message) 364 | return 'event ' .. event.name .. ' was naughty :- ' .. error_message 365 | end, 366 | events = { 367 | { name = 'panic', from = 'green', to = 'red' }, 368 | { name = 'calm', from = 'red', to = 'green' }, 369 | } 370 | } 371 | print(fsm:calm()) -- "event calm was naughty :- event not allowed in current state green" 372 | ``` 373 | 374 | License 375 | ======= 376 | 377 | See [LICENSE](https://github.com/recih/lua-fsm/blob/master/LICENSE) file. 378 | 379 | 380 | 381 | 382 | 383 | 384 | -------------------------------------------------------------------------------- /luafsm.lua: -------------------------------------------------------------------------------- 1 | --[[================================================= 2 | Lua State Machine Library 3 | ----=================================================]] 4 | module("luafsm", package.seeall) 5 | 6 | VERSION = "2.3.2" 7 | 8 | SUCCEEDED = 1 -- the event transitioned successfully from one state to another 9 | NOTRANSITION = 2 -- the event was successfull but no state transition was necessary 10 | CANCELLED = 3 -- the event was cancelled by the caller in a beforeEvent callback 11 | PENDING = 4 -- the event is asynchronous and the caller is in control of when the transition occurs 12 | 13 | INVALID_TRANSITION_ERROR = 'INVALID_TRANSITION_ERROR' -- caller tried to fire an event that was innapropriate in the current state 14 | PENDING_TRANSITION_ERROR = 'PENDING_TRANSITION_ERROR' -- caller tried to fire an event while an async transition was still pending 15 | INVALID_CALLBACK_ERROR = 'INVALID_CALLBACK_ERROR' -- caller provided callback function threw an exception 16 | 17 | WILDCARD = '*' 18 | ASYNC = 'async' 19 | 20 | local function do_callback(fsm, func, event, params) 21 | if type(func) == 'function' then 22 | local success, ret = pcall(func, unpack(params)) 23 | if not success then 24 | local err = ret 25 | fsm:error(event, INVALID_CALLBACK_ERROR, err) 26 | end 27 | return ret 28 | end 29 | end 30 | 31 | local function before_any_event(fsm, event, params) 32 | return do_callback(fsm, fsm.onbeforeevent, event, params) 33 | end 34 | 35 | local function after_any_event(fsm, event, params) 36 | return do_callback(fsm, fsm.onafterevent or fsm.onevent, event, params) 37 | end 38 | 39 | local function leave_any_state(fsm, event, params) 40 | return do_callback(fsm, fsm.onleavestate, event, params) 41 | end 42 | 43 | local function enter_any_state(fsm, event, params) 44 | return do_callback(fsm, fsm.onenterstate or fsm.onstate, event, params) 45 | end 46 | 47 | local function change_state(fsm, event, params) 48 | return do_callback(fsm, fsm.onchangestate, event, params) 49 | end 50 | 51 | local function before_this_event(fsm, event, params) 52 | return do_callback(fsm, fsm['onbefore' .. event.name], event, params) 53 | end 54 | 55 | local function after_this_event(fsm, event, params) 56 | return do_callback(fsm, fsm['onafter' .. event.name] or fsm['on' .. event.name], event, params) 57 | end 58 | 59 | local function leave_this_state(fsm, event, params) 60 | return do_callback(fsm, fsm['onleave' .. event.from], event, params) 61 | end 62 | 63 | local function enter_this_state(fsm, event, params) 64 | return do_callback(fsm, fsm['onenter' .. event.to] or fsm['on' .. event.to], event, params) 65 | end 66 | 67 | local function before_event(fsm, event, params) 68 | if before_this_event(fsm, event, params) == false or before_any_event(fsm, event, params) == false then 69 | return false 70 | end 71 | end 72 | 73 | local function after_event(fsm, event, params) 74 | after_this_event(fsm, event, params) 75 | after_any_event(fsm, event, params) 76 | end 77 | 78 | local function leave_state(fsm, event, params) 79 | local specific = leave_this_state(fsm, event, params) 80 | local general = leave_any_state(fsm, event, params) 81 | if specific == false or general == false then 82 | return false 83 | elseif specific == ASYNC or general == ASYNC then 84 | return ASYNC 85 | end 86 | end 87 | 88 | local function enter_state(fsm, event, params) 89 | enter_this_state(fsm, event, params) 90 | enter_any_state(fsm, event, params) 91 | end 92 | 93 | local function build_event(name, entry) 94 | return function(self, ...) 95 | local from = self.current 96 | local to = entry[from] or entry[WILDCARD] or from 97 | local event = { 98 | name = name, 99 | from = from, 100 | to = to, 101 | } 102 | local params = {self, event, ...} 103 | 104 | if self.transition then 105 | return self:error(event, PENDING_TRANSITION_ERROR, ('event %s inappropriate because previous transition did not complete'):format(name)) 106 | end 107 | 108 | if self:cannot(name) then 109 | return self:error(event, INVALID_TRANSITION_ERROR, ('event %s inappropriate in current state %s'):format(name, self.current)) 110 | end 111 | 112 | if before_event(self, event, params) == false then 113 | return CANCELLED 114 | end 115 | 116 | if from == to then 117 | after_event(self, event, params) 118 | return NOTRANSITION 119 | end 120 | 121 | -- prepare a transition method for use EITHER lower down, 122 | -- or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) 123 | local fsm = self 124 | self.transition = { 125 | -- provide a way for caller to cancel async transition if desired 126 | cancel = function() 127 | fsm.transition = nil 128 | after_event(fsm, event, params) 129 | end 130 | } 131 | setmetatable(self.transition, { 132 | __call = function() 133 | fsm.transition = nil -- this method should only ever be called once 134 | fsm.current = to 135 | enter_state(fsm, event, params) 136 | change_state(fsm, event, params) 137 | after_event(fsm, event, params) 138 | return SUCCEEDED 139 | end 140 | }) 141 | 142 | local leave = leave_state(fsm, event, params) 143 | if leave == false then 144 | self.transition = nil 145 | return CANCELLED 146 | elseif leave == ASYNC then 147 | return PENDING 148 | else 149 | if self.transition then -- need to check in case user manually called transition() but forgot to return ASYNC 150 | return self.transition() 151 | end 152 | end 153 | end 154 | end 155 | 156 | function create(cfg, target) 157 | assert(type(cfg) == 'table', 'cfg must be a table') 158 | 159 | -- allow for a simple string, or an object with { state: = 'foo', event = 'setup', defer = true|false } 160 | local initial = type(cfg.initial) == 'string' and { state = cfg.initial } or cfg.initial 161 | local terminal = cfg.terminal or cfg.final 162 | local fsm = target or cfg.target or {} 163 | local events = cfg.events or {} 164 | local callbacks = cfg.callbacks or {} 165 | local map = {} 166 | 167 | local function add(e) 168 | -- allow 'wildcard' transition if 'from' is not specified 169 | local from = type(e.from) == 'table' and e.from or (e.from and {e.from} or {WILDCARD}) 170 | local entry = map[e.name] or {} 171 | map[e.name] = entry 172 | for _, v in ipairs(from) do 173 | entry[v] = e.to or v -- allow no-op transition if 'to' is not specified 174 | end 175 | end 176 | 177 | if initial then 178 | initial.event = initial.event or 'startup' 179 | add { name = initial.event, from = 'none', to = initial.state } 180 | end 181 | 182 | for _, e in ipairs(events) do 183 | add(e) 184 | end 185 | 186 | for k, v in pairs(map) do 187 | fsm[k] = build_event(k, v) 188 | end 189 | 190 | for k, v in pairs(callbacks) do 191 | fsm[k] = v 192 | end 193 | 194 | fsm.current = 'none' 195 | fsm.is = function(self, state) 196 | if type(state) == 'table' then 197 | for _, s in ipairs(state) do 198 | if s == self.current then 199 | return true 200 | end 201 | end 202 | return false 203 | else 204 | return self.current == state 205 | end 206 | end 207 | fsm.can = function(self, event) 208 | if (not self.transition) and map[event] and 209 | (map[event][self.current] or map[event][WILDCARD]) then 210 | return true 211 | else 212 | return false 213 | end 214 | end 215 | fsm.cannot = function(self, event) return not self:can(event) end 216 | -- default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired 217 | fsm.error = cfg.error or function(self, event, error_code, err) error(error_code .. " " .. err) end 218 | fsm.is_finished = function(self) return self:is(terminal) end 219 | 220 | if initial and not initial.defer then 221 | fsm[initial.event](fsm) 222 | end 223 | 224 | return fsm 225 | end 226 | 227 | return _M -------------------------------------------------------------------------------- /spec/advanced_spec.lua: -------------------------------------------------------------------------------- 1 | local luafsm = require("luafsm") 2 | 3 | describe("advanced", function() 4 | 5 | it("multiple 'from' states for the same event", function() 6 | local fsm = luafsm.create { 7 | initial = 'green', 8 | events = { 9 | { name = 'warn', from = 'green', to = 'yellow' }, 10 | { name = 'panic', from = {'green', 'yellow'}, to = 'red' }, 11 | { name = 'calm', from = 'red', to = 'yellow' }, 12 | { name = 'clear', from = {'yellow', 'red'}, to = 'green' }, 13 | }, 14 | } 15 | 16 | assert.equals('green', fsm.current) 17 | 18 | assert.truthy(fsm:can('warn')) -- should be able to warn from green state 19 | assert.truthy(fsm:can('panic')) -- should be able to panic from green state 20 | assert.truthy(fsm:cannot('calm')) -- should NOT be able to calm from green state 21 | assert.truthy(fsm:cannot('clear')) -- should NOT be able to clear from green state 22 | 23 | fsm:warn() assert.equals('yellow',fsm.current) -- warn event should transition from green to yellow 24 | fsm:panic() assert.equals('red', fsm.current) -- panic event should transition from yellow to red 25 | fsm:calm() assert.equals('yellow',fsm.current) -- calm event should transition from red to yellow 26 | fsm:clear() assert.equals('green', fsm.current) -- clear event should transition from yellow to green 27 | 28 | fsm:panic() assert.equals('red', fsm.current) -- panic event should transition from green to red 29 | fsm:clear() assert.equals('green', fsm.current) -- clear event should transition from red to green 30 | end) 31 | 32 | it("multiple 'to' states for the same event", function() 33 | local fsm = luafsm.create { 34 | initial = 'hungry', 35 | events = { 36 | { name = 'eat', from = 'hungry', to = 'satisfied' }, 37 | { name = 'eat', from = 'satisfied', to = 'full' }, 38 | { name = 'eat', from = 'full', to = 'sick' }, 39 | { name = 'rest', from = {'hungry', 'satisfied', 'full', 'sick'}, to = 'hungry' }, 40 | }, 41 | } 42 | 43 | assert.equals('hungry', fsm.current) 44 | 45 | assert.truthy(fsm:can('eat')) 46 | assert.truthy(fsm:can('rest')) 47 | 48 | fsm:eat() 49 | assert.equals('satisfied', fsm.current) 50 | 51 | fsm:eat() 52 | assert.equals('full', fsm.current) 53 | 54 | fsm:eat() 55 | assert.equals('sick', fsm.current) 56 | 57 | fsm:rest() 58 | assert.equals('hungry', fsm.current) 59 | end) 60 | 61 | it("no-op transitions with multiple from states", function() 62 | local fsm = luafsm.create { 63 | initial = 'green', 64 | events = { 65 | { name = 'warn', from = 'green', to = 'yellow' }, 66 | { name = 'panic', from = {'green', 'yellow'}, to = 'red' }, 67 | { name = 'noop', from = {'green', 'yellow'} }, -- NOTE = 'to' not specified 68 | { name = 'calm', from = 'red', to = 'yellow' }, 69 | { name = 'clear', from = {'yellow', 'red'}, to = 'green' }, 70 | }, 71 | } 72 | 73 | assert.equals('green', fsm.current) --initial state should be green 74 | 75 | assert.truthy(fsm:can('warn'), "should be able to warn from green state") 76 | assert.truthy(fsm:can('panic'), "should be able to panic from green state") 77 | assert.truthy(fsm:can('noop'), "should be able to noop from green state") 78 | assert.truthy(fsm:cannot('calm'), "should NOT be able to calm from green state") 79 | assert.truthy(fsm:cannot('clear'), "should NOT be able to clear from green state") 80 | 81 | fsm:noop() assert.equals('green', fsm.current) -- noop event should not transition 82 | fsm:warn() assert.equals('yellow', fsm.current) -- warn event should transition from green to yellow 83 | 84 | assert.truthy(fsm:cannot('warn'), "should NOT be able to warn from yellow state") 85 | assert.truthy(fsm:can('panic'), "should be able to panic from yellow state") 86 | assert.truthy(fsm:can('noop'), "should be able to noop from yellow state") 87 | assert.truthy(fsm:cannot('calm'), "should NOT be able to calm from yellow state") 88 | assert.truthy(fsm:can('clear'), "should be able to clear from yellow state") 89 | 90 | fsm:noop() assert.equals('yellow', fsm.current) -- noop event should not transition 91 | fsm:panic() assert.equals('red', fsm.current) -- panic event should transition from yellow to red 92 | 93 | assert.truthy(fsm:cannot('warn'), "should NOT be able to warn from red state") 94 | assert.truthy(fsm:cannot('panic'), "should NOT be able to panic from red state") 95 | assert.truthy(fsm:cannot('noop'), "should NOT be able to noop from red state") 96 | assert.truthy(fsm:can('calm'), "should be able to calm from red state") 97 | assert.truthy(fsm:can('clear'), "should be able to clear from red state") 98 | end) 99 | 100 | it("callbacks are called when appropriate for multiple 'from' and 'to' transitions", function() 101 | local called = {} 102 | local fsm = luafsm.create { 103 | initial = 'hungry', 104 | events = { 105 | { name = 'eat', from = 'hungry', to = 'satisfied' }, 106 | { name = 'eat', from = 'satisfied', to = 'full' }, 107 | { name = 'eat', from = 'full', to = 'sick' }, 108 | { name = 'rest', from = {'hungry', 'satisfied', 'full', 'sick'}, to = 'hungry' }, 109 | }, 110 | callbacks = { 111 | -- generic callbacks 112 | onbeforeevent = function(self, event) table.insert(called, ('onbefore(%s)'):format(event.name)) end, 113 | onafterevent = function(self, event) table.insert(called, ('onafter(%s)'):format(event.name)) end, 114 | onleavestate = function(self, event) table.insert(called, ('onleave(%s)'):format(event.from)) end, 115 | onenterstate = function(self, event) table.insert(called, ('onenter(%s)'):format(event.to)) end, 116 | onchangestate = function(self, event) table.insert(called, ('onchange(%s,%s)'):format(event.from, event.to))end, 117 | 118 | -- specific state callbacks 119 | onenterhungry = function() table.insert(called, 'onenterhungry') end, 120 | onleavehungry = function() table.insert(called, 'onleavehungry') end, 121 | onentersatisfied = function() table.insert(called, 'onentersatisfied') end, 122 | onleavesatisfied = function() table.insert(called, 'onleavesatisfied') end, 123 | onenterfull = function() table.insert(called, 'onenterfull') end, 124 | onleavefull = function() table.insert(called, 'onleavefull') end, 125 | onentersick = function() table.insert(called, 'onentersick') end, 126 | onleavesick = function() table.insert(called, 'onleavesick') end, 127 | -- specific event callbacks 128 | onbeforeeat = function() table.insert(called, 'onbeforeeat') end, 129 | onaftereat = function() table.insert(called, 'onaftereat') end, 130 | onbeforerest = function() table.insert(called, 'onbeforerest') end, 131 | onafterrest = function() table.insert(called, 'onafterrest') end, 132 | } 133 | } 134 | 135 | called = {} 136 | fsm:eat() 137 | assert.same({ 138 | 'onbeforeeat', 139 | 'onbefore(eat)', 140 | 'onleavehungry', 141 | 'onleave(hungry)', 142 | 'onentersatisfied', 143 | 'onenter(satisfied)', 144 | 'onchange(hungry,satisfied)', 145 | 'onaftereat', 146 | 'onafter(eat)' 147 | }, called) 148 | 149 | called = {} 150 | fsm:eat() 151 | assert.same({ 152 | 'onbeforeeat', 153 | 'onbefore(eat)', 154 | 'onleavesatisfied', 155 | 'onleave(satisfied)', 156 | 'onenterfull', 157 | 'onenter(full)', 158 | 'onchange(satisfied,full)', 159 | 'onaftereat', 160 | 'onafter(eat)', 161 | }, called) 162 | 163 | called = {} 164 | fsm:eat() 165 | assert.same({ 166 | 'onbeforeeat', 167 | 'onbefore(eat)', 168 | 'onleavefull', 169 | 'onleave(full)', 170 | 'onentersick', 171 | 'onenter(sick)', 172 | 'onchange(full,sick)', 173 | 'onaftereat', 174 | 'onafter(eat)' 175 | }, called) 176 | 177 | called = {} 178 | fsm:rest() 179 | assert.same({ 180 | 'onbeforerest', 181 | 'onbefore(rest)', 182 | 'onleavesick', 183 | 'onleave(sick)', 184 | 'onenterhungry', 185 | 'onenter(hungry)', 186 | 'onchange(sick,hungry)', 187 | 'onafterrest', 188 | 'onafter(rest)' 189 | }, called) 190 | end) 191 | 192 | it("callbacks are called when appropriate for prototype based state machine", function() 193 | local my_fsm = {} 194 | local prototype = { 195 | -- generic callbacks 196 | onbeforeevent = function(self, event) table.insert(self.called, ('onbefore(%s)'):format(event.name)) end, 197 | onafterevent = function(self, event) table.insert(self.called, ('onafter(%s)'):format(event.name)) end, 198 | onleavestate = function(self, event) table.insert(self.called, ('onleave(%s)'):format(event.from)) end, 199 | onenterstate = function(self, event) table.insert(self.called, ('onenter(%s)'):format(event.to)) end, 200 | onchangestate = function(self, event) table.insert(self.called, ('onchange(%s,%s)'):format(event.from, event.to))end, 201 | 202 | -- specific state callbacks 203 | onenternone = function(self) table.insert(self.called, 'onenternone') end, 204 | onleavenone = function(self) table.insert(self.called, 'onleavenone') end, 205 | onentergreen = function(self) table.insert(self.called, 'onentergreen') end, 206 | onleavegreen = function(self) table.insert(self.called, 'onleavegreen') end, 207 | onenteryellow = function(self) table.insert(self.called, 'onenteryellow') end, 208 | onleaveyellow = function(self) table.insert(self.called, 'onleaveyellow') end, 209 | onenterred = function(self) table.insert(self.called, 'onenterred') end, 210 | onleavered = function(self) table.insert(self.called, 'onleavered') end, 211 | 212 | -- specific event callbacks 213 | onbeforestartup = function(self) table.insert(self.called, 'onbeforestartup') end, 214 | onafterstartup = function(self) table.insert(self.called, 'onafterstartup') end, 215 | onbeforewarn = function(self) table.insert(self.called, 'onbeforewarn') end, 216 | onafterwarn = function(self) table.insert(self.called, 'onafterwarn') end, 217 | onbeforepanic = function(self) table.insert(self.called, 'onbeforepanic') end, 218 | onafterpanic = function(self) table.insert(self.called, 'onafterpanic') end, 219 | onbeforeclear = function(self) table.insert(self.called, 'onbeforeclear') end, 220 | onafterclear = function(self) table.insert(self.called, 'onafterclear') end, 221 | } 222 | 223 | function my_fsm.new() 224 | local t = { 225 | called = {}, 226 | } 227 | setmetatable(t, {__index = prototype}) 228 | t:startup() 229 | return t 230 | end 231 | 232 | local fsm = luafsm.create { 233 | target = prototype, 234 | events = { 235 | { name = 'startup', from = 'none', to = 'green' }, 236 | { name = 'warn', from = 'green', to = 'yellow' }, 237 | { name = 'panic', from = 'yellow', to = 'red' }, 238 | { name = 'clear', from = 'yellow', to = 'green' }, 239 | }, 240 | } 241 | 242 | local a = my_fsm.new() 243 | local b = my_fsm.new() 244 | 245 | assert.equals('green', a.current) 246 | assert.equals('green', b.current) 247 | 248 | assert.same(a.called, {'onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)'}) 249 | assert.same(b.called, {'onbeforestartup', 'onbefore(startup)', 'onleavenone', 'onleave(none)', 'onentergreen', 'onenter(green)', 'onchange(none,green)', 'onafterstartup', 'onafter(startup)'}) 250 | 251 | a.called = {} 252 | b.called = {} 253 | 254 | a:warn() 255 | 256 | assert.equals('yellow', a.current) -- maintain independent current state 257 | assert.equals('green', b.current) -- maintain independent current state 258 | 259 | assert.same(a.called, {'onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)'}) 260 | assert.same(b.called, {}) 261 | end) 262 | 263 | end) -------------------------------------------------------------------------------- /spec/async_spec.lua: -------------------------------------------------------------------------------- 1 | local luafsm = require("luafsm") 2 | 3 | describe("async", function() 4 | 5 | it("state transitions", function() 6 | local fsm = luafsm.create { 7 | initial = 'green', 8 | events = { 9 | { name = 'warn', from = 'green', to = 'yellow' }, 10 | { name = 'panic', from = 'yellow', to = 'red' }, 11 | { name = 'calm', from = 'red', to = 'yellow' }, 12 | { name = 'clear', from = 'yellow', to = 'green' }, 13 | }, 14 | callbacks = { 15 | onleavegreen = function() return luafsm.ASYNC end, 16 | onleaveyellow = function() return luafsm.ASYNC end, 17 | onleavered = function() return luafsm.ASYNC end, 18 | } 19 | } 20 | 21 | assert.equals('green', fsm.current) 22 | 23 | fsm:warn() 24 | assert.equals('green', fsm.current) -- should still be green because we haven't transitioned yet 25 | fsm:transition() 26 | assert.equals('yellow', fsm.current) -- warn event should transition from green to yellow 27 | fsm:panic() 28 | assert.equals('yellow', fsm.current) -- should still be yellow because we haven't transitioned yet 29 | fsm:transition() 30 | assert.equals('red', fsm.current) -- panic event should transition from yellow to red 31 | fsm:calm() 32 | assert.equals('red', fsm.current) -- should still be red because we haven't transitioned yet 33 | fsm:transition() 34 | assert.equals('yellow', fsm.current) -- calm event should transition from red to yellow 35 | fsm:clear() 36 | assert.equals('yellow', fsm.current) -- should still be yellow because we haven't transitioned yet 37 | fsm:transition() 38 | assert.equals('green', fsm.current) -- clear event should transition from yellow to green 39 | end) 40 | 41 | pending("state transitions with delays", function() 42 | end) 43 | 44 | it("state transition fired during onleavestate callback - immediate", function() 45 | local fsm = luafsm.create { 46 | initial = 'green', 47 | events = { 48 | { name = 'warn', from = 'green', to = 'yellow' }, 49 | { name = 'panic', from = 'yellow', to = 'red' }, 50 | { name = 'calm', from = 'red', to = 'yellow' }, 51 | { name = 'clear', from = 'yellow', to = 'green' }, 52 | }, 53 | callbacks = { 54 | onleavegreen = function(self) self:transition() return luafsm.ASYNC end, -- self.transition() / self:transition() both work 55 | onleaveyellow = function(self) self.transition() return luafsm.ASYNC end, 56 | onleavered = function(self) self:transition() return luafsm.ASYNC end, 57 | } 58 | } 59 | 60 | assert.equals('green', fsm.current) 61 | 62 | fsm:warn() 63 | assert.equals(fsm.current, 'yellow') -- warn event should transition from green to yellow 64 | fsm:panic() 65 | assert.equals(fsm.current, 'red') -- panic event should transition from yellow to red 66 | fsm:calm() 67 | assert.equals(fsm.current, 'yellow') -- calm event should transition from red to yellow 68 | fsm:clear() 69 | assert.equals(fsm.current, 'green') -- clear event should transition from yellow to green 70 | end) 71 | 72 | pending("state transition fired during onleavestate callback - with delay", function() 73 | end) 74 | 75 | it("state transition fired during onleavestate callback - but forgot to return ASYNC!", function() 76 | local fsm = luafsm.create { 77 | initial = 'green', 78 | events = { 79 | { name = 'warn', from = 'green', to = 'yellow' }, 80 | { name = 'panic', from = 'yellow', to = 'red' }, 81 | { name = 'calm', from = 'red', to = 'yellow' }, 82 | { name = 'clear', from = 'yellow', to = 'green' }, 83 | }, 84 | callbacks = { 85 | onleavegreen = function(self) self:transition() --[[ return luafsm.ASYNC ]] end, 86 | onleaveyellow = function(self) self:transition() --[[ return luafsm.ASYNC ]] end, 87 | onleavered = function(self) self:transition() --[[ return luafsm.ASYNC ]] end, 88 | } 89 | } 90 | 91 | assert.equals('green', fsm.current) 92 | 93 | fsm:warn() 94 | assert.equals('yellow', fsm.current) -- warn event should transition from green to yellow 95 | fsm:panic() 96 | assert.equals('red', fsm.current) -- panic event should transition from yellow to red 97 | fsm:calm() 98 | assert.equals('yellow', fsm.current) -- calm event should transition from red to yellow 99 | fsm:clear() 100 | assert.equals('green', fsm.current) -- clear event should transition from yellow to green 101 | end) 102 | 103 | pending("state transitions sometimes synchronous and sometimes asynchronous", function() 104 | end) 105 | 106 | it("state transition fired without completing previous transition", function() 107 | local fsm = luafsm.create { 108 | initial = 'green', 109 | events = { 110 | { name = 'warn', from = 'green', to = 'yellow' }, 111 | { name = 'panic', from = 'yellow', to = 'red' }, 112 | { name = 'calm', from = 'red', to = 'yellow' }, 113 | { name = 'clear', from = 'yellow', to = 'green' }, 114 | }, 115 | callbacks = { 116 | onleavegreen = function(self) return luafsm.ASYNC end, 117 | onleaveyellow = function(self) return luafsm.ASYNC end, 118 | onleavered = function(self) return luafsm.ASYNC end, 119 | } 120 | } 121 | 122 | assert.equals('green', fsm.current) 123 | 124 | fsm:warn() 125 | assert.equals('green', fsm.current) -- should still be green because we haven't transitioned yet 126 | fsm:transition() 127 | assert.equals('yellow', fsm.current) -- warn event should transition from green to yellow 128 | fsm:panic() 129 | assert.equals('yellow', fsm.current) -- should still be yellow because we haven't transitioned yet 130 | 131 | assert.has.errors(function() fsm:calm() end) 132 | end) 133 | 134 | it("state transition can be cancelled", function() 135 | local fsm = luafsm.create { 136 | initial = 'green', 137 | events = { 138 | { name = 'warn', from = 'green', to = 'yellow' }, 139 | { name = 'panic', from = 'yellow', to = 'red' }, 140 | { name = 'calm', from = 'red', to = 'yellow' }, 141 | { name = 'clear', from = 'yellow', to = 'green' }, 142 | }, 143 | callbacks = { 144 | onleavegreen = function(self) return luafsm.ASYNC end, 145 | onleaveyellow = function(self) return luafsm.ASYNC end, 146 | onleavered = function(self) return luafsm.ASYNC end, 147 | } 148 | } 149 | 150 | assert.equals('green', fsm.current) 151 | 152 | fsm:warn() 153 | assert.equals('green', fsm.current) -- should still be green because we haven't transitioned yet 154 | fsm:transition() 155 | assert.equals('yellow', fsm.current) -- warn event should transition from green to yellow 156 | fsm:panic() 157 | assert.equals('yellow', fsm.current) -- should still be yellow because we haven't transitioned yet 158 | 159 | assert.falsy(fsm:can('panic')) -- but cannot panic a 2nd time because a transition is still pending 160 | 161 | assert.has.errors(function() fsm:panic() end) 162 | 163 | fsm.transition.cancel() 164 | 165 | assert.equals('yellow', fsm.current) -- should still be yellow because we cancelled the async transition 166 | assert.truthy(fsm:can('panic')) -- can now panic again because we cancelled previous async transition 167 | 168 | fsm:panic() 169 | fsm:transition() 170 | 171 | assert.equals('red', fsm.current) -- should finally be red now that we completed the async transition 172 | end) 173 | 174 | it("callbacks are ordered correctly", function() 175 | local called = {} 176 | local fsm = luafsm.create { 177 | initial = 'green', 178 | events = { 179 | { name = 'warn', from = 'green', to = 'yellow' }, 180 | { name = 'panic', from = 'yellow', to = 'red' }, 181 | { name = 'calm', from = 'red', to = 'yellow' }, 182 | { name = 'clear', from = 'yellow', to = 'green' }, 183 | }, 184 | callbacks = { 185 | -- generic callbacks 186 | onbeforeevent = function(self, event) table.insert(called, ('onbefore(%s)'):format(event.name)) end, 187 | onafterevent = function(self, event) table.insert(called, ('onafter(%s)'):format(event.name)) end, 188 | onleavestate = function(self, event) table.insert(called, ('onleave(%s)'):format(event.from)) end, 189 | onenterstate = function(self, event) table.insert(called, ('onenter(%s)'):format(event.to)) end, 190 | onchangestate = function(self, event) table.insert(called, ('onchange(%s,%s)'):format(event.from, event.to))end, 191 | 192 | -- specific state callbacks 193 | onentergreen = function() table.insert(called, 'onentergreen') end, 194 | onenteryellow = function() table.insert(called, 'onenteryellow') end, 195 | onenterred = function() table.insert(called, 'onenterred') end, 196 | onleavegreen = function() table.insert(called, 'onleavegreen') return luafsm.ASYNC end, 197 | onleaveyellow = function() table.insert(called, 'onleaveyellow') return luafsm.ASYNC end, 198 | onleavered = function() table.insert(called, 'onleavered') return luafsm.ASYNC end, 199 | 200 | -- specific event callbacks 201 | onbeforewarn = function() table.insert(called, 'onbeforewarn') end, 202 | onbeforepanic = function() table.insert(called, 'onbeforepanic') end, 203 | onbeforecalm = function() table.insert(called, 'onbeforecalm') end, 204 | onbeforeclear = function() table.insert(called, 'onbeforeclear') end, 205 | onafterwarn = function() table.insert(called, 'onafterwarn') end, 206 | onafterpanic = function() table.insert(called, 'onafterpanic') end, 207 | onaftercalm = function() table.insert(called, 'onaftercalm') end, 208 | onafterclear = function() table.insert(called, 'onafterclear') end, 209 | } 210 | } 211 | 212 | called = {} 213 | fsm:warn() assert.same(called, {'onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)'}) 214 | fsm:transition() assert.same(called, {'onbeforewarn', 'onbefore(warn)', 'onleavegreen', 'onleave(green)', 'onenteryellow', 'onenter(yellow)', 'onchange(green,yellow)', 'onafterwarn', 'onafter(warn)'}) 215 | 216 | called = {} 217 | fsm:panic() assert.same(called, {'onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)'}) 218 | fsm:transition() assert.same(called, {'onbeforepanic', 'onbefore(panic)', 'onleaveyellow', 'onleave(yellow)', 'onenterred', 'onenter(red)', 'onchange(yellow,red)', 'onafterpanic', 'onafter(panic)'}) 219 | 220 | called = {} 221 | fsm:calm() assert.same(called, {'onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)'}) 222 | fsm:transition() assert.same(called, {'onbeforecalm', 'onbefore(calm)', 'onleavered', 'onleave(red)', 'onenteryellow', 'onenter(yellow)', 'onchange(red,yellow)', 'onaftercalm', 'onafter(calm)'}) 223 | 224 | called = {} 225 | fsm:clear() assert.same(called, {'onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)'}) 226 | fsm:transition() assert.same(called, {'onbeforeclear', 'onbefore(clear)', 'onleaveyellow', 'onleave(yellow)', 'onentergreen', 'onenter(green)', 'onchange(yellow,green)', 'onafterclear', 'onafter(clear)'}) 227 | end) 228 | 229 | it("cannot fire event during existing transition", function() 230 | local fsm = luafsm.create { 231 | initial = 'green', 232 | events = { 233 | { name = 'warn', from = 'green', to = 'yellow' }, 234 | { name = 'panic', from = 'yellow', to = 'red' }, 235 | { name = 'calm', from = 'red', to = 'yellow' }, 236 | { name = 'clear', from = 'yellow', to = 'green' }, 237 | }, 238 | callbacks = { 239 | onleavegreen = function(self) return luafsm.ASYNC end, 240 | onleaveyellow = function(self) return luafsm.ASYNC end, 241 | onleavered = function(self) return luafsm.ASYNC end, 242 | } 243 | } 244 | 245 | assert.equals('green', fsm.current ) -- initial state should be green 246 | assert.equals( true, fsm:can('warn') ) -- should be able to warn 247 | assert.equals( false, fsm:can('panic')) -- should NOT be able to panic 248 | assert.equals( false, fsm:can('calm') ) -- should NOT be able to calm 249 | assert.equals( false, fsm:can('clear')) -- should NOT be able to clear 250 | 251 | fsm:warn() 252 | 253 | assert.equals('green', fsm.current ) -- should still be green because we haven't transitioned yet 254 | assert.equals( false, fsm:can('warn') ) -- should NOT be able to warn - during transition 255 | assert.equals( false, fsm:can('panic')) -- should NOT be able to panic - during transition 256 | assert.equals( false, fsm:can('calm') ) -- should NOT be able to calm - during transition 257 | assert.equals( false, fsm:can('clear')) -- should NOT be able to clear - during transition 258 | 259 | fsm:transition() 260 | 261 | assert.equals('yellow', fsm.current ) -- warn event should transition from green to yellow 262 | assert.equals( false, fsm:can('warn') ) -- should NOT be able to warn 263 | assert.equals( true, fsm:can('panic')) -- should be able to panic 264 | assert.equals( false, fsm:can('calm') ) -- should NOT be able to calm 265 | assert.equals( true, fsm:can('clear')) -- should be able to clear 266 | 267 | fsm:panic() 268 | 269 | assert.equals('yellow', fsm.current ) -- should still be yellow because we haven't transitioned yet 270 | assert.equals( false, fsm:can('warn') ) -- should NOT be able to warn - during transition 271 | assert.equals( false, fsm:can('panic')) -- should NOT be able to panic - during transition 272 | assert.equals( false, fsm:can('calm') ) -- should NOT be able to calm - during transition 273 | assert.equals( false, fsm:can('clear')) -- should NOT be able to clear - during transition 274 | 275 | fsm:transition() 276 | 277 | assert.equals('red', fsm.current ) -- panic event should transition from yellow to red 278 | assert.equals( false, fsm:can('warn') ) -- should NOT be able to warn 279 | assert.equals( false, fsm:can('panic')) -- should NOT be able to panic 280 | assert.equals( true, fsm:can('calm') ) -- should be able to calm 281 | assert.equals( false, fsm:can('clear')) -- should NOT be able to clear 282 | end) 283 | end) -------------------------------------------------------------------------------- /spec/basic_spec.lua: -------------------------------------------------------------------------------- 1 | local luafsm = require("luafsm") 2 | 3 | describe("basic", function() 4 | 5 | it("test standalone state machine", function() 6 | local fsm = luafsm.create { 7 | initial = 'green', 8 | events = { 9 | { name = 'warn', from = 'green', to = 'yellow' }, 10 | { name = 'panic', from = 'yellow', to = 'red' }, 11 | { name = 'calm', from = 'red', to = 'yellow' }, 12 | { name = 'clear', from = 'yellow', to = 'green' }, 13 | }, 14 | } 15 | assert.equals('green', fsm.current) 16 | 17 | fsm:warn() 18 | assert.equals('yellow', fsm.current) 19 | fsm:panic() 20 | assert.equals('red', fsm.current) 21 | fsm:calm() 22 | assert.equals('yellow', fsm.current) 23 | fsm:clear() 24 | assert.equals('green', fsm.current) 25 | end) 26 | 27 | it("test targeted state machine", function() 28 | local target = { foo = 'bar' } 29 | local fsm = luafsm.create { 30 | target = target, 31 | initial = 'green', 32 | events = { 33 | { name = 'warn', from = 'green', to = 'yellow' }, 34 | { name = 'panic', from = 'yellow', to = 'red' }, 35 | { name = 'calm', from = 'red', to = 'yellow' }, 36 | { name = 'clear', from = 'yellow', to = 'green' }, 37 | }, 38 | } 39 | 40 | assert.equals(target, fsm) 41 | assert.equals('bar', target.foo) 42 | 43 | assert.equals('green', target.current) 44 | 45 | target:warn() 46 | assert.equals('yellow', target.current) 47 | target:panic() 48 | assert.equals('red', target.current) 49 | target:calm() 50 | assert.equals('yellow', target.current) 51 | target:clear() 52 | assert.equals('green', target.current) 53 | end) 54 | 55 | it("test can & cannot", function() 56 | local fsm = luafsm.create { 57 | initial = 'green', 58 | events = { 59 | { name = 'warn', from = 'green', to = 'yellow' }, 60 | { name = 'panic', from = 'yellow', to = 'red' }, 61 | { name = 'calm', from = 'red', to = 'yellow' }, 62 | }, 63 | } 64 | 65 | assert.equals('green', fsm.current) 66 | assert.truthy(fsm:can('warn'), "should be able to warn from green state") 67 | assert.truthy(fsm:cannot('panic'), "should NOT be able to panic from green state") 68 | assert.truthy(fsm:cannot('calm'), "should NOT be able to calm from green state") 69 | 70 | fsm:warn() 71 | assert.equals('yellow', fsm.current) 72 | assert.truthy(fsm:cannot('warn'), "should NOT be able to warn from yellow state") 73 | assert.truthy(fsm:can('panic'), "should be able to panic from yellow state") 74 | assert.truthy(fsm:cannot('calm'), "should NOT be able to calm from yellow state") 75 | 76 | fsm:panic() 77 | assert.equals('red', fsm.current) 78 | assert.truthy(fsm:cannot('warn'), "should NOT be able to warn from red state") 79 | assert.truthy(fsm:cannot('panic'), "should NOT be able to panic from red state") 80 | assert.truthy(fsm:can('calm'), "should be able to calm from red state") 81 | end) 82 | 83 | it("test is", function() 84 | local fsm = luafsm.create { 85 | initial = 'green', 86 | events = { 87 | { name = 'warn', from = 'green', to = 'yellow' }, 88 | { name = 'panic', from = 'yellow', to = 'red' }, 89 | { name = 'calm', from = 'red', to = 'yellow' }, 90 | { name = 'clear', from = 'yellow', to = 'green' }, 91 | }, 92 | } 93 | 94 | assert.equals('green', fsm.current) 95 | assert.is_true(fsm:is('green'), "current state should match") 96 | assert.is_not_true(fsm:is('yellow'), "current state should NOT match") 97 | assert.is_true(fsm:is{'green', 'red'}, "current state should match when included in array") 98 | assert.is_not_true(fsm:is{'yellow', 'red'}, "current state should NOT match when not included in array") 99 | 100 | fsm:warn() 101 | 102 | assert.equals('yellow', fsm.current) 103 | assert.is_not_true(fsm:is('green'), "current state should NOT match") 104 | assert.is_true(fsm:is('yellow'), "current state should match") 105 | assert.is_not_true(fsm:is{'green', 'red'}, "current state should NOT match when not included in array") 106 | assert.is_true(fsm:is{'yellow', 'red'}, "current state should match when included in array") 107 | end) 108 | 109 | it("test is_finished", function() 110 | local fsm = luafsm.create { 111 | initial = 'green', terminal = 'red', 112 | events = { 113 | { name = 'warn', from = 'green', to = 'yellow' }, 114 | { name = 'panic', from = 'yellow', to = 'red' }, 115 | }, 116 | } 117 | 118 | assert.equals('green', fsm.current) 119 | assert.is_not_true(fsm:is_finished()) 120 | 121 | fsm:warn() 122 | assert.equals('yellow', fsm.current) 123 | assert.is_not_true(fsm:is_finished()) 124 | 125 | fsm:panic() 126 | assert.equals('red', fsm.current) 127 | assert.is_true(fsm:is_finished()) 128 | end) 129 | 130 | it("test is_finished - without specifying terminal state", function() 131 | local fsm = luafsm.create { 132 | initial = 'green', 133 | events = { 134 | { name = 'warn', from = 'green', to = 'yellow' }, 135 | { name = 'panic', from = 'yellow', to = 'red' }, 136 | }, 137 | } 138 | 139 | assert.equals('green', fsm.current) 140 | assert.is_not_true(fsm:is_finished()) 141 | 142 | fsm:warn() 143 | assert.equals('yellow', fsm.current) 144 | assert.is_not_true(fsm:is_finished()) 145 | 146 | fsm:panic() 147 | assert.equals('red', fsm.current) 148 | assert.is_not_true(fsm:is_finished()) 149 | end) 150 | 151 | it("test inappropriate events", function() 152 | local fsm = luafsm.create { 153 | initial = 'green', 154 | events = { 155 | { name = 'warn', from = 'green', to = 'yellow' }, 156 | { name = 'panic', from = 'yellow', to = 'red' }, 157 | { name = 'calm', from = 'red', to = 'yellow' }, 158 | }, 159 | } 160 | 161 | assert.equals('green', fsm.current) 162 | assert.has.errors(function() fsm:panic() end) 163 | assert.has.errors(function() fsm:calm() end) 164 | 165 | fsm:warn() 166 | assert.equals('yellow', fsm.current) 167 | assert.has.errors(function() fsm:warn() end) 168 | assert.has.errors(function() fsm:calm() end) 169 | 170 | fsm:panic() 171 | assert.equals('red', fsm.current) 172 | assert.has.errors(function() fsm:warn() end) 173 | assert.has.errors(function() fsm:panic() end) 174 | end) 175 | 176 | it("test inappropriate event handling can be customized", function() 177 | local fsm = luafsm.create { 178 | error = function(self, event, error_code, err) return error_code end, 179 | initial = 'green', 180 | events = { 181 | { name = 'warn', from = 'green', to = 'yellow' }, 182 | { name = 'panic', from = 'yellow', to = 'red' }, 183 | { name = 'calm', from = 'red', to = 'yellow' }, 184 | }, 185 | } 186 | 187 | assert.equals('green', fsm.current) 188 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:panic()) 189 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:calm()) 190 | 191 | fsm:warn() 192 | assert.equals('yellow', fsm.current) 193 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:warn()) 194 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:calm()) 195 | 196 | fsm:panic() 197 | assert.equals('red', fsm.current) 198 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:warn()) 199 | assert.equals(luafsm.INVALID_TRANSITION_ERROR, fsm:panic()) 200 | end) 201 | 202 | it("test event is cancelable", function() 203 | local fsm = luafsm.create { 204 | initial = 'green', 205 | events = { 206 | { name = 'warn', from = 'green', to = 'yellow' }, 207 | { name = 'panic', from = 'yellow', to = 'red' }, 208 | { name = 'calm', from = 'red', to = 'yellow' }, 209 | }, 210 | } 211 | 212 | assert.equals('green', fsm.current) 213 | 214 | fsm.onbeforewarn = function() return false end 215 | assert.equals(luafsm.CANCELLED, fsm:warn()) 216 | 217 | assert.equals('green', fsm.current) 218 | end) 219 | 220 | it("test callbacks are ordered correctly", function() 221 | local called = {} 222 | local fsm = luafsm.create { 223 | initial = 'green', 224 | events = { 225 | { name = 'warn', from = 'green', to = 'yellow' }, 226 | { name = 'panic', from = 'yellow', to = 'red' }, 227 | { name = 'calm', from = 'red', to = 'yellow' }, 228 | { name = 'clear', from = 'yellow', to = 'green' }, 229 | }, 230 | callbacks = { 231 | -- generic callbacks 232 | onbeforeevent = function(self, event) table.insert(called, ('onbefore(%s)'):format(event.name)) end, 233 | onafterevent = function(self, event) table.insert(called, ('onafter(%s)'):format(event.name)) end, 234 | onleavestate = function(self, event) table.insert(called, ('onleave(%s)'):format(event.from)) end, 235 | onenterstate = function(self, event) table.insert(called, ('onenter(%s)'):format(event.to)) end, 236 | onchangestate = function(self, event) table.insert(called, ('onchange(%s,%s)'):format(event.from, event.to))end, 237 | 238 | -- specific state callbacks 239 | onentergreen = function() table.insert(called, 'onentergreen') end, 240 | onenteryellow = function() table.insert(called, 'onenteryellow') end, 241 | onenterred = function() table.insert(called, 'onenterred') end, 242 | onleavegreen = function() table.insert(called, 'onleavegreen') end, 243 | onleaveyellow = function() table.insert(called, 'onleaveyellow') end, 244 | onleavered = function() table.insert(called, 'onleavered') end, 245 | 246 | -- specific event callbacks 247 | onbeforewarn = function() table.insert(called, 'onbeforewarn') end, 248 | onbeforepanic = function() table.insert(called, 'onbeforepanic') end, 249 | onbeforecalm = function() table.insert(called, 'onbeforecalm') end, 250 | onbeforeclear = function() table.insert(called, 'onbeforeclear') end, 251 | onafterwarn = function() table.insert(called, 'onafterwarn') end, 252 | onafterpanic = function() table.insert(called, 'onafterpanic') end, 253 | onaftercalm = function() table.insert(called, 'onaftercalm') end, 254 | onafterclear = function() table.insert(called, 'onafterclear') end, 255 | } 256 | } 257 | 258 | called = {} 259 | fsm:warn() 260 | assert.same({ 261 | 'onbeforewarn', 262 | 'onbefore(warn)', 263 | 'onleavegreen', 264 | 'onleave(green)', 265 | 'onenteryellow', 266 | 'onenter(yellow)', 267 | 'onchange(green,yellow)', 268 | 'onafterwarn', 269 | 'onafter(warn)' 270 | }, called) 271 | 272 | called = {} 273 | fsm:panic() 274 | assert.same({ 275 | 'onbeforepanic', 276 | 'onbefore(panic)', 277 | 'onleaveyellow', 278 | 'onleave(yellow)', 279 | 'onenterred', 280 | 'onenter(red)', 281 | 'onchange(yellow,red)', 282 | 'onafterpanic', 283 | 'onafter(panic)' 284 | }, called) 285 | 286 | called = {} 287 | fsm:calm() 288 | assert.same({ 289 | 'onbeforecalm', 290 | 'onbefore(calm)', 291 | 'onleavered', 292 | 'onleave(red)', 293 | 'onenteryellow', 294 | 'onenter(yellow)', 295 | 'onchange(red,yellow)', 296 | 'onaftercalm', 297 | 'onafter(calm)' 298 | }, called) 299 | 300 | called = {} 301 | fsm:clear() 302 | assert.same({ 303 | 'onbeforeclear', 304 | 'onbefore(clear)', 305 | 'onleaveyellow', 306 | 'onleave(yellow)', 307 | 'onentergreen', 308 | 'onenter(green)', 309 | 'onchange(yellow,green)', 310 | 'onafterclear', 311 | 'onafter(clear)' 312 | }, called) 313 | end) 314 | 315 | it("test callbacks are ordered correctly - for same state transition", function() 316 | local called = {} 317 | local fsm = luafsm.create { 318 | initial = 'waiting', 319 | events = { 320 | { name = 'data', from = {'waiting', 'receipt'}, to = 'receipt' }, 321 | { name = 'nothing', from = {'waiting', 'receipt'}, to = 'waiting' }, 322 | { name = 'error', from = {'waiting', 'receipt'}, to = 'error' }, -- bad practice to have event name same as state name - but I'll let it slide just this once 323 | }, 324 | callbacks = { 325 | -- generic callbacks 326 | onbeforeevent = function(self, event) table.insert(called, ('onbefore(%s)'):format(event.name)) end, 327 | onafterevent = function(self, event) table.insert(called, ('onafter(%s)'):format(event.name)) end, 328 | onleavestate = function(self, event) table.insert(called, ('onleave(%s)'):format(event.from)) end, 329 | onenterstate = function(self, event) table.insert(called, ('onenter(%s)'):format(event.to)) end, 330 | onchangestate = function(self, event) table.insert(called, ('onchange(%s,%s)'):format(event.from, event.to))end, 331 | 332 | -- specific state callbacks 333 | onenterwaiting = function() table.insert(called, 'onenterwaiting') end, 334 | onenterreceipt = function() table.insert(called, 'onenterreceipt') end, 335 | onentererror = function() table.insert(called, 'onentererror') end, 336 | onleavewaiting = function() table.insert(called, 'onleavewaiting') end, 337 | onleavereceipt = function() table.insert(called, 'onleavereceipt') end, 338 | onleaveerror = function() table.insert(called, 'onleaveerror') end, 339 | 340 | -- specific event callbacks 341 | onbeforedata = function() table.insert(called, 'onbeforedata') end, 342 | onbeforenothing = function() table.insert(called, 'onbeforenothing') end, 343 | onbeforeerror = function() table.insert(called, 'onbeforeerror') end, 344 | onafterdata = function() table.insert(called, 'onafterdata') end, 345 | onafternothing = function() table.insert(called, 'onafternothing') end, 346 | onaftereerror = function() table.insert(called, 'onaftererror') end, 347 | } 348 | } 349 | 350 | called = {} 351 | fsm:data() 352 | assert.same({ 353 | 'onbeforedata', 354 | 'onbefore(data)', 355 | 'onleavewaiting', 356 | 'onleave(waiting)', 357 | 'onenterreceipt', 358 | 'onenter(receipt)', 359 | 'onchange(waiting,receipt)', 360 | 'onafterdata', 361 | 'onafter(data)' 362 | }, called) 363 | 364 | called = {} 365 | fsm:data() -- same-state transition 366 | assert.same({ -- so NO enter/leave/change state callbacks are fired 367 | 'onbeforedata', 368 | 'onbefore(data)', 369 | 'onafterdata', 370 | 'onafter(data)' 371 | }, called) 372 | 373 | called = {} 374 | fsm:data() -- same-state transition 375 | assert.same({ -- so NO enter/leave/change state callbacks are fired 376 | 'onbeforedata', 377 | 'onbefore(data)', 378 | 'onafterdata', 379 | 'onafter(data)' 380 | }, called) 381 | 382 | called = {} 383 | fsm:nothing() 384 | assert.same({ 385 | 'onbeforenothing', 386 | 'onbefore(nothing)', 387 | 'onleavereceipt', 388 | 'onleave(receipt)', 389 | 'onenterwaiting', 390 | 'onenter(waiting)', 391 | 'onchange(receipt,waiting)', 392 | 'onafternothing', 393 | 'onafter(nothing)' 394 | }, called) 395 | end) 396 | 397 | it("test callback arguments are correct", function() 398 | local expected = { event = { name = 'startup', from = 'none', to = 'green'} } -- first expected callback 399 | 400 | local function verify_expected(event, a, b, c) 401 | assert.same(expected.event, event) 402 | assert.equals(expected.a, a) 403 | assert.equals(expected.b, b) 404 | assert.equals(expected.c, c) 405 | end 406 | 407 | fsm = luafsm.create { 408 | initial = 'green', 409 | events = { 410 | { name = 'warn', from = 'green', to = 'yellow' }, 411 | { name = 'panic', from = 'yellow', to = 'red' }, 412 | { name = 'calm', from = 'red', to = 'yellow' }, 413 | { name = 'clear', from = 'yellow', to = 'green' }, 414 | }, 415 | callbacks = { 416 | -- generic callbacks 417 | onbeforeevent = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 418 | onafterevent = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 419 | onleavestate = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 420 | onenterstate = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 421 | onchangestate = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 422 | 423 | -- specific state callbacks 424 | onenterwaiting = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 425 | onenterreceipt = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 426 | onentererror = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 427 | onleavewaiting = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 428 | onleavereceipt = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 429 | onleaveerror = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 430 | 431 | -- specific event callbacks 432 | onbeforedata = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 433 | onbeforenothing = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 434 | onbeforeerror = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 435 | onafterdata = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 436 | onafternothing = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 437 | onaftereerror = function(self, event, a, b, c) verify_expected(event, a, b, c) end, 438 | } 439 | } 440 | 441 | expected = { event = {name = 'warn', from = 'green', to = 'yellow'}, a = 1, b = 2, c = 3 } 442 | fsm:warn(1, 2, 3) 443 | 444 | expected = { event = {name = 'panic', from = 'yellow', to = 'red'}, a = 4, b = 5, c = 6 } 445 | fsm:panic(4, 5, 6) 446 | 447 | expected = { event = {name = 'calm', from = 'red', to = 'yellow'}, a = 'foo', b = 'bar', c = nil } 448 | fsm:calm('foo', 'bar') 449 | 450 | expected = { event = {name = 'clear', from = 'yellow', to = 'green'}, a = nil, b = nil, c = nil } 451 | fsm:clear() 452 | end) 453 | 454 | it("exceptions in caller-provided callbacks are not swallowed", function() 455 | local fsm = luafsm.create { 456 | initial = 'green', 457 | events = { 458 | { name = 'warn', from = 'green', to = 'yellow' }, 459 | { name = 'panic', from = 'yellow', to = 'red' }, 460 | { name = 'calm', from = 'red', to = 'yellow' }, 461 | }, 462 | callbacks = { 463 | onenteryellow = function() error('oops') end, 464 | } 465 | } 466 | 467 | assert.equals('green', fsm.current) 468 | assert.has.errors(function() fsm:warn() end) 469 | end) 470 | 471 | it("no-op transitions", function() 472 | local fsm = luafsm.create { 473 | initial = 'green', 474 | events = { 475 | { name = 'noop', from = 'green', --[[ no-op ]] }, 476 | { name = 'warn', from = 'green', to = 'yellow' }, 477 | { name = 'panic', from = 'yellow', to = 'red' }, 478 | { name = 'calm', from = 'red', to = 'yellow' }, 479 | { name = 'clear', from = 'yellow', to = 'green' }, 480 | }, 481 | } 482 | 483 | assert.equals('green', fsm.current) 484 | assert.is_true(fsm:can('noop')) 485 | assert.is_true(fsm:can('warn')) 486 | 487 | fsm:noop() 488 | assert.equals('green', fsm.current) 489 | fsm:warn() 490 | assert.equals('yellow', fsm.current) 491 | 492 | assert.is_not_true(fsm:can('noop')) 493 | assert.is_not_true(fsm:can('warn')) 494 | end) 495 | 496 | it("wildcard 'from' allows event from any state", function() 497 | local fsm = luafsm.create { 498 | initial = 'stopped', 499 | events = { 500 | { name = 'prepare', from = 'stopped', to = 'ready' }, 501 | { name = 'start', from = 'ready', to = 'running' }, 502 | { name = 'resume', from = 'paused', to = 'running' }, 503 | { name = 'pause', from = 'running', to = 'paused' }, 504 | { name = 'stop', from = '*', to = 'stopped' } 505 | }, 506 | } 507 | 508 | assert.equals('stopped', fsm.current) 509 | 510 | fsm:prepare() 511 | assert.equals('ready', fsm.current) 512 | fsm:stop() 513 | assert.equals('stopped', fsm.current) 514 | 515 | fsm:prepare() 516 | assert.equals('ready', fsm.current) 517 | fsm:start() 518 | assert.equals('running', fsm.current) 519 | fsm:stop() 520 | assert.equals('stopped', fsm.current) 521 | 522 | fsm:prepare() 523 | assert.equals('ready', fsm.current) 524 | fsm:start() 525 | assert.equals('running', fsm.current) 526 | fsm:pause() 527 | assert.equals('paused', fsm.current) 528 | fsm:stop() 529 | assert.equals('stopped', fsm.current) 530 | end) 531 | 532 | it("wildcard 'from' allows event from any state", function() 533 | local fsm = luafsm.create { 534 | initial = 'stopped', 535 | events = { 536 | { name = 'prepare', from = 'stopped', to = 'ready' }, 537 | { name = 'start', from = 'ready', to = 'running' }, 538 | { name = 'resume', from = 'paused', to = 'running' }, 539 | { name = 'pause', from = 'running', to = 'paused' }, 540 | { name = 'stop', --[[ any from state ]] to = 'stopped' } 541 | }, 542 | } 543 | 544 | assert.equals('stopped', fsm.current) 545 | 546 | fsm:prepare() 547 | assert.equals('ready', fsm.current) 548 | fsm:stop() 549 | assert.equals('stopped', fsm.current) 550 | 551 | fsm:prepare() 552 | assert.equals('ready', fsm.current) 553 | fsm:start() 554 | assert.equals('running', fsm.current) 555 | fsm:stop() 556 | assert.equals('stopped', fsm.current) 557 | 558 | fsm:prepare() 559 | assert.equals('ready', fsm.current) 560 | fsm:start() 561 | assert.equals('running', fsm.current) 562 | fsm:pause() 563 | assert.equals('paused', fsm.current) 564 | fsm:stop() 565 | assert.equals('stopped', fsm.current) 566 | end) 567 | 568 | it("event return values", function() 569 | local fsm = luafsm.create { 570 | initial = 'stopped', 571 | events = { 572 | { name = 'prepare', from = 'stopped', to = 'ready' }, 573 | { name = 'fake', from = 'ready', to = 'running' }, 574 | { name = 'start', from = 'ready', to = 'running' }, 575 | }, 576 | callbacks = { 577 | onbeforefake = function(self, event) return false end, -- this event will be cancelled 578 | onleaveready = function(self, event) return luafsm.ASYNC end, -- this state transition is ASYNC 579 | } 580 | } 581 | 582 | assert.equals('stopped', fsm.current) 583 | 584 | assert.equals(luafsm.SUCCEEDED, fsm:prepare()) 585 | assert.equals('ready', fsm.current) 586 | 587 | assert.equals(luafsm.CANCELLED, fsm:fake()) 588 | assert.equals('ready', fsm.current) 589 | 590 | assert.equals(luafsm.PENDING, fsm:start()) 591 | assert.equals('ready', fsm.current) 592 | 593 | assert.equals(luafsm.SUCCEEDED, fsm:transition()) 594 | assert.equals('running', fsm.current) 595 | end) 596 | end) -------------------------------------------------------------------------------- /spec/classes_spec.lua: -------------------------------------------------------------------------------- 1 | local luafsm = require("luafsm") 2 | 3 | describe("classes", function() 4 | 5 | it("prototype based state machine", function() 6 | local my_fsm = {} 7 | local prototype = {} 8 | 9 | function my_fsm.new() 10 | local t = { 11 | counter = 42, 12 | } 13 | setmetatable(t, {__index = prototype}) 14 | t:startup() 15 | return t 16 | end 17 | 18 | function prototype:onwarn() 19 | self.counter = self.counter + 1 20 | end 21 | 22 | local fsm = luafsm.create { 23 | target = prototype, 24 | events = { 25 | { name = 'startup', from = 'none', to = 'green' }, 26 | { name = 'warn', from = 'green', to = 'yellow' }, 27 | { name = 'panic', from = 'yellow', to = 'red' }, 28 | { name = 'clear', from = 'yellow', to = 'green' }, 29 | }, 30 | } 31 | 32 | local a = my_fsm.new() 33 | local b = my_fsm.new() 34 | 35 | assert.equals(42, a.counter) 36 | assert.equals(42, b.counter) 37 | 38 | a:warn() 39 | 40 | assert.equals('yellow', a.current) 41 | assert.equals('green', b.current) 42 | 43 | assert.equals(43, a.counter) 44 | assert.equals(42, b.counter) 45 | 46 | assert.truthy(rawget(a, "current")) 47 | assert.truthy(rawget(b, "current")) 48 | assert.falsy(rawget(a, "warn")) 49 | assert.falsy(rawget(b, "warn")) 50 | assert.equals(a.warn, b.warn, prototype.warn) 51 | end) 52 | 53 | it("github.com/jakesgordon/javascript-state-machine issue#19", function() 54 | local Foo = {} 55 | local prototype = {} 56 | 57 | function Foo.new() 58 | local t = { 59 | counter = 7, 60 | } 61 | setmetatable(t, {__index = prototype}) 62 | t:initFSM() 63 | return t 64 | end 65 | 66 | function prototype:onenterready() 67 | self.counter = self.counter + 1 68 | end 69 | 70 | function prototype:onenterrunning() 71 | self.counter = self.counter + 1 72 | end 73 | 74 | local fsm = luafsm.create { 75 | target = prototype, 76 | initial = { state = 'ready', event = 'initFSM', defer = true }, -- unfortunately, trying to apply an IMMEDIATE initial state wont work on prototype based FSM, it MUST be deferred and called in the constructor for each instance 77 | events = { 78 | { name = 'execute', from = 'ready', to = 'running'}, 79 | { name = 'abort', from = 'running', to = 'ready' }, 80 | }, 81 | } 82 | 83 | local a = Foo.new() 84 | local b = Foo.new() 85 | 86 | assert.equals('ready', a.current) 87 | assert.equals('ready', b.current) 88 | 89 | -- start with correct counter 7 (from constructor) + 1 (from onenterready) 90 | assert.equals(8, a.counter) 91 | assert.equals(8, b.counter) 92 | 93 | a:execute() 94 | 95 | assert.equals('running', a.current) 96 | assert.equals('ready', b.current) 97 | 98 | assert.equals(9, a.counter) 99 | assert.equals(8, b.counter) 100 | end) 101 | end) -------------------------------------------------------------------------------- /spec/initialize_spec.lua: -------------------------------------------------------------------------------- 1 | local luafsm = require("luafsm") 2 | 3 | describe("special initialization options", function() 4 | local target = {} 5 | local fsm = nil 6 | 7 | setup(function() 8 | target.called = {} 9 | target.onbeforeevent = function(self, event) table.insert(target.called, ('onbefore(%s)'):format(event.name)) end 10 | target.onafterevent = function(self, event) table.insert(target.called, ('onafter(%s)'):format(event.name)) end 11 | target.onleavestate = function(self, event) table.insert(target.called, ('onleave(%s)'):format(event.from)) end 12 | target.onenterstate = function(self, event) table.insert(target.called, ('onenter(%s)'):format(event.to)) end 13 | target.onchangestate = function(self, event) table.insert(target.called, ('onchange(%s,%s)'):format(event.from, event.to))end 14 | target.onbeforeinit = function() table.insert(target.called, "onbeforeinit") end 15 | target.onafterinit = function() table.insert(target.called, "onafterinit") end 16 | target.onbeforestartup = function() table.insert(target.called, "onbeforestartup") end 17 | target.onafterstartup = function() table.insert(target.called, "onafterstartup") end 18 | target.onbeforepanic = function() table.insert(target.called, "onbeforepanic") end 19 | target.onafterpanic = function() table.insert(target.called, "onafterpanic") end 20 | target.onbeforecalm = function() table.insert(target.called, "onbeforecalm") end 21 | target.onaftercalm = function() table.insert(target.called, "onaftercalm") end 22 | target.onenternone = function() table.insert(target.called, "onenternone") end 23 | target.onentergreen = function() table.insert(target.called, "onentergreen") end 24 | target.onenterred = function() table.insert(target.called, "onenterred") end 25 | target.onleavenone = function() table.insert(target.called, "onleavenone") end 26 | target.onleavegreen = function() table.insert(target.called, "onleavegreen") end 27 | target.onleavered = function() table.insert(target.called, "onleavered") end 28 | end) 29 | 30 | before_each(function() 31 | target.called = {} 32 | end) 33 | 34 | it("can initial state defaults to 'none'", function() 35 | local fsm = luafsm.create { 36 | target = target, 37 | events = { 38 | { name = 'panic', from = 'green', to = 'red' }, 39 | { name = 'calm', from = 'red', to = 'green' }, 40 | }, 41 | } 42 | assert.equals('none', fsm.current) 43 | assert.same({}, fsm.called) 44 | end) 45 | 46 | it("can initial state can be specified", function() 47 | local fsm = luafsm.create { 48 | target = target, 49 | initial = 'green', 50 | events = { 51 | { name = 'panic', from = 'green', to = 'red' }, 52 | { name = 'calm', from = 'red', to = 'green' }, 53 | }, 54 | } 55 | 56 | assert.equals('green', fsm.current) 57 | assert.same({ 58 | "onbeforestartup", 59 | "onbefore(startup)", 60 | "onleavenone", 61 | "onleave(none)", 62 | "onentergreen", 63 | "onenter(green)", 64 | "onchange(none,green)", 65 | "onafterstartup", 66 | "onafter(startup)" 67 | }, fsm.called) 68 | end) 69 | 70 | it("startup event name can be specified", function() 71 | local fsm = luafsm.create { 72 | target = target, 73 | initial = { state = 'green', event = 'init' }, 74 | events = { 75 | { name = 'panic', from = 'green', to = 'red' }, 76 | { name = 'calm', from = 'red', to = 'green' }, 77 | }, 78 | } 79 | 80 | assert.equals('green', fsm.current) 81 | assert.same({ 82 | "onbeforeinit", 83 | "onbefore(init)", 84 | "onleavenone", 85 | "onleave(none)", 86 | "onentergreen", 87 | "onenter(green)", 88 | "onchange(none,green)", 89 | "onafterinit", 90 | "onafter(init)" 91 | }, fsm.called) 92 | end) 93 | 94 | it("startup event can be deferred", function() 95 | local fsm = luafsm.create { 96 | target = target, 97 | initial = { state = 'green', event = 'init', defer = true }, 98 | events = { 99 | { name = 'panic', from = 'green', to = 'red' }, 100 | { name = 'calm', from = 'red', to = 'green' }, 101 | }, 102 | } 103 | 104 | assert.equals('none', fsm.current) 105 | assert.same({}, fsm.called) 106 | 107 | fsm:init() 108 | 109 | assert.same({ 110 | "onbeforeinit", 111 | "onbefore(init)", 112 | "onleavenone", 113 | "onleave(none)", 114 | "onentergreen", 115 | "onenter(green)", 116 | "onchange(none,green)", 117 | "onafterinit", 118 | "onafter(init)" 119 | }, fsm.called) 120 | end) 121 | end) --------------------------------------------------------------------------------