├── TODO.md ├── .gitignore ├── Cargo.toml ├── CHANGELOG.md ├── .travis.yml ├── README.md ├── LICENSE └── src └── lib.rs /TODO.md: -------------------------------------------------------------------------------- 1 | * add stats on FSM & transitions to e.g. count # uses 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Rust template 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | /target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | .idea/* 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "extfsm" 3 | description="Extended Finite State Machine with internal event queue and entry/exit transitions" 4 | version = "0.12.1" 5 | authors = ["prz@juniper.net "] 6 | readme = "README.md" 7 | keywords = [ "fsm", "state", "state-machine" ] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/przygienda/rust-extfsm.git" 10 | 11 | [dependencies] 12 | slog="~2" 13 | uuid = { version = "~0.8", features = ["v4"] } 14 | custom_derive = "~0.1" 15 | enum_derive = "~0.1" 16 | itertools = "~0.10" 17 | lazy_static = "~1" 18 | dot="~0.1" 19 | 20 | [dev-dependencies] 21 | slog-term="~2" 22 | slog-atomic="~2" 23 | slog-async="~2" 24 | 25 | [features] 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version 0.7 2 | =========== 3 | * breaks API from 0.6 making the addition of transition more of a 'stacked' API where 4 | properties of transitions can be added as needed. This will prevent further breakage 5 | in the future when arguments are added. 6 | * summarize graph edges much better 7 | * allow for colored edges which are kept in groups 8 | 9 | Version 0.6 10 | =========== 11 | 12 | * added `extend_events` which can use any `IntoIterator` on Events 13 | * changed FSM DOT output to stack self->self events on top of each other to improve readability, 14 | however to be able to dotfile a graph `Events` and `States` must provide `Ord` 15 | * allow to render a non-mutable fsm reference given the results are ephemeral every time 16 | * changed the return value of the transition to hold a vector which is syntactically easier 17 | on the user, this is an somewhat API breaking change of course albeit if code written does 18 | something like 19 | 20 | `vec![..].drain(..)::collect()` 21 | 22 | it should be just inefficient but still conform. Ideal for performance would be 23 | boxed slices but those precondition Copy traits which is very limiting. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | # necessary for `travis-cargo coveralls --no-sudo` 4 | addons: 5 | apt: 6 | packages: 7 | - libcurl4-openssl-dev 8 | - libelf-dev 9 | - libdw-dev 10 | - binutils-dev # optional: only required for the --verify flag of coveralls 11 | 12 | # run builds for all the trains (and more) 13 | rust: 14 | # no unstable features 15 | # - nightly 16 | - beta 17 | # check it compiles on the latest stable compiler 18 | - stable 19 | 20 | # load travis-cargo 21 | before_script: 22 | - | 23 | pip install 'travis-cargo<0.2' --user && 24 | export PATH=$HOME/.local/bin:$PATH 25 | 26 | # the main build 27 | script: 28 | - | 29 | travis-cargo build && 30 | travis-cargo test && 31 | travis-cargo bench && 32 | travis-cargo --only stable doc 33 | after_success: 34 | # upload the documentation from the build with stable (automatically only actually 35 | # runs on the master branch, not individual PRs) 36 | - travis-cargo --only stable doc-upload 37 | # measure code coverage and upload to coveralls.io (the verify 38 | # argument mitigates kcov crashes due to malformed debuginfo, at the 39 | # cost of some speed ) 40 | - travis-cargo coveralls --no-sudo --verify 41 | 42 | env: 43 | global: 44 | # override the default `--features unstable` used for the nightly branch (optional) 45 | # - TRAVIS_CARGO_NIGHTLY_FEATURE=nightly 46 | # encrypted github token for doc upload (see `GH_TOKEN` link above) 47 | - secure: "8f840401ddddc64cd85022b4116f9630f8f41a65" 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Extended Finite State Machine in Rust 2 | ===================================== 3 | 4 | [![crates.io](http://meritbadge.herokuapp.com/coap)](https://crates.io/crates/extfsm) 5 | [![Travis Build Status](https://travis-ci.org/przygienda/rust-extfsm.svg?branch=master)](https://travis-ci.org/przygienda/rust-extfsm) 6 | [![Coverage Status](https://coveralls.io/repos/przygienda/rust-extfsm/badge.svg?branch=master&service=github)](https://coveralls.io/github/przygienda/rust-extfsm?branch=master) 7 | 8 | Introduction 9 | ============ 10 | 11 | Library to support programming of Extended Finite State Machines (FSM) in Rust. 12 | The machine is not necessarily built for fastest zero-copy speed but greatest 13 | flexibility and maintanability. Non zero-copy design has been chosen since after 14 | event processing it stores state and passes control back out which would make 15 | management of lifetimes very onerous on the user otherwise. 16 | 17 | FSMs are from long engineering experience the cleanest way to implement 18 | asynchronous protocols between components. 19 | 20 | Features 21 | ======== 22 | 23 | * internal event queue allows a machine to post events 24 | against itself on transition completion 25 | * supports optional transition per state entry/exit 26 | * events can carry dynamic arguments accessible when 27 | transition is executed 28 | * machine generates its own .dot graphs easily converted into .pdf or .svg supporting 29 | colored edges and summarizing edges for same start and end state transitions 30 | * slog debugging support 31 | * machine's extended state can be examined when not processing events 32 | * machine transitions can be traversed read-only 33 | * flyweight pattern to run many instances of same FSM with different extended state 34 | 35 | License 36 | ======= 37 | 38 | Copyright (c) 2017, Juniper Networks, Inc. 39 | All rights reserved. 40 | 41 | Licensed under the Apache License, Version 2.0 (the "License"); 42 | you may not use this file except in compliance with the License. 43 | This code is not an official Juniper product. 44 | You may obtain a copy of the License at 45 | 46 | http://www.apache.org/licenses/LICENSE-2.0 47 | 48 | Unless required by applicable law or agreed to in writing, software 49 | distributed under the License is distributed on an "AS IS" BASIS, 50 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 51 | See the License for the specific language governing permissions and 52 | limitations under the License. 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of a generic finite state machine with 2 | //! extended state. Features worth mentioning: 3 | //! 4 | //! * optional exit/enter transitions on states 5 | //! * each event instance can provide boxed arguments to transiton closure 6 | //! * each transition closure can return with vector of arguments that 7 | //! are queued at the end of outstanding events queue 8 | //! * can generate dot representation of itself, edges are grouped on color 9 | //! and annotated with events and optional names to provide readable, 10 | //! dense representation for dense graphs 11 | //! 12 | //! # Author 13 | //! Tony Przygienda, 2016 14 | //! 15 | //! # Examples 16 | //! Check out the tests in the implementation for a good example of use 17 | //! 18 | //! # Panics 19 | //! Never 20 | //! 21 | //! # Errors 22 | //! refer to `Errors` 23 | //! 24 | //! # Copyrights 25 | //! 26 | //! Copyright (c) 2017, Juniper Networks, Inc. 27 | //! All rights reserved. 28 | //! 29 | //! Licensed under the Apache License, Version 2.0 (the "License"); 30 | //! you may not use this file except in compliance with the License. 31 | //! This code is not an official Juniper product. 32 | //! You may obtain a copy of the License at 33 | //! 34 | //! http://www.apache.org/licenses/LICENSE-2.0 35 | //! 36 | //! Unless required by applicable law or agreed to in writing, software 37 | //! distributed under the License is distributed on an "AS IS" BASIS, 38 | //! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | //! See the License for the specific language governing permissions and 40 | //! limitations under the License. 41 | 42 | #[macro_use] 43 | extern crate custom_derive; 44 | extern crate dot; 45 | #[macro_use] 46 | extern crate enum_derive; 47 | extern crate itertools; 48 | #[macro_use] 49 | extern crate lazy_static; 50 | #[macro_use] 51 | extern crate slog; 52 | extern crate uuid; 53 | 54 | use dot::LabelText; 55 | use itertools::Itertools; 56 | use slog::Logger; 57 | use std::cell::{Ref, RefCell, RefMut}; 58 | use std::cmp::Ordering; 59 | use std::collections::{HashMap, VecDeque, HashSet}; 60 | use std::default::Default; 61 | use std::fmt::Debug; 62 | use std::fs; 63 | use std::hash::Hash; 64 | use std::io; 65 | use std::iter::Iterator; 66 | use std::mem::swap; 67 | use std::rc::Rc; 68 | use uuid::Uuid; 69 | 70 | /// types of transitions on states 71 | #[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] 72 | pub enum EntryExit { 73 | EntryTransition, 74 | ExitTransition, 75 | } 76 | 77 | #[derive(Debug, Clone, PartialEq, Eq)] 78 | /// Errors that can occur when running FSMs 79 | pub enum Errors { 80 | OK, 81 | /// internal error at a given place that can be generated by transition implementation 82 | InternalError(EventType, StateType, ErrorType), 83 | /// the requested transition does not exist, FSM needs to be shut down 84 | NoTransition(EventType, StateType), 85 | /// transition failed, you have to shut down the FSM 86 | TransitionFailure, 87 | } 88 | 89 | /// type representing an optional argument to a transition function call 90 | pub type OptionalFnArg = Option; 91 | 92 | /// set of events to execute with according optional argument on call of transition function 93 | pub type EventQueue = 94 | VecDeque<(EventType, OptionalFnArg)>; 95 | 96 | /// type to be returned by all transitions 97 | /// an optional vector of events to be added to the FSM event queue or an error is returned 98 | pub type TransitionResult = Result< 99 | Option)>>, 100 | Errors, 101 | >; 102 | 103 | /// transition function used, takes optional argument and returns either with error 104 | /// or an optional set of events to be added to processing (at the end of event queue) 105 | pub type TransitionFn = 106 | dyn Fn(RefMut>, EventType, OptionalFnArg) 107 | -> TransitionResult; 108 | 109 | /// transition function to either enter or exit a specific state, return same as 110 | /// `FSMTransitionFn`. `StateType` passed in is previous state before currently entered or 111 | /// exited state. `EventType` is indicating which event causes the enter or exit. 112 | /// This allows to track where an FSM entered or exited a state from which can be of 113 | /// high interest in "stable" states of the FSM. 114 | /// In case of Entry on the first state it may be all `None`. 115 | pub type EntryExitTransitionFn< 116 | ExtendedState, 117 | EventType, 118 | StateType, 119 | TransitionFnArguments, 120 | ErrorType, 121 | > = dyn Fn(RefMut>, Option, Option) 122 | -> TransitionResult; 123 | 124 | /// *Finite state machine type* 125 | /// 126 | /// # Template parameters 127 | /// 128 | /// * `ExtendedState` - provides a structure that every transition can access and 129 | /// stores extended state 130 | /// * `TransitionFnArguments` - type that can be boxed as parameters to an event instance 131 | /// * `ErrorType` - Errors that transitions can generate internally 132 | pub struct FSM 133 | where 134 | StateType: Clone + Eq + Hash + Sized, 135 | EventType: Clone + Eq + Hash + Sized, 136 | { 137 | pub extended_state: RefCell>, 138 | 139 | name: String, 140 | start_state: StateType, 141 | current_state: StateType, 142 | event_queue: EventQueue, 143 | transitions: Rc< 144 | RefCell< 145 | TransitionTable, 146 | >, 147 | >, 148 | statetransitions: Rc< 149 | RefCell< 150 | EntryExitTransitionTable< 151 | ExtendedState, 152 | StateType, 153 | EventType, 154 | TransitionFnArguments, 155 | ErrorType, 156 | >, 157 | >, 158 | >, 159 | 160 | /// optional logger, do not provide if performance becomes a problem on complex logging 161 | log: Option, 162 | 163 | /// dotgraph structure for output 164 | dotgraph: Rc>>, 165 | 166 | last_state: Option, 167 | } 168 | 169 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 170 | struct ColorGroupedTransitions 171 | where 172 | StateType: Clone + Sized + Eq + Hash, 173 | { 174 | color: DotColor, 175 | source: StateType, 176 | target: StateType, // all the transitions point to same endnode in the group 177 | } 178 | 179 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 180 | enum DotEdgeKey 181 | where 182 | StateType: Clone + Sized + Eq + Hash, 183 | { 184 | /// complex key providing per color in a state all `TransitionSource`s that can be grouped 185 | /// into a single arrow. First boolean indicates when the transition is self->self 186 | TransitionsSet(ColorGroupedTransitions), 187 | EntryExit(EntryExitKey), 188 | } 189 | 190 | impl DotEdgeKey 191 | where 192 | StateType: Clone + Eq + Hash + Sized, 193 | { 194 | pub fn new_set(color: DotColor, source: StateType, target: StateType) -> DotEdgeKey { 195 | DotEdgeKey::TransitionsSet(ColorGroupedTransitions { 196 | color: color, 197 | source: source, 198 | target: target, 199 | }) 200 | } 201 | 202 | pub fn new_entryexit(into: EntryExitKey) -> DotEdgeKey { 203 | DotEdgeKey::EntryExit(into) 204 | } 205 | } 206 | 207 | custom_derive! { 208 | #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, 209 | IterVariants(ColorVariants), IterVariantNames(ColorNames))] 210 | #[allow(non_camel_case_types)] 211 | /// available dot colors for the transition destinations 212 | pub enum DotColor { 213 | red, 214 | green, 215 | blue, 216 | yellow, 217 | black, 218 | gray, 219 | cyan, 220 | gold 221 | } 222 | } 223 | 224 | lazy_static! { 225 | static ref COLORS: HashMap = zipenumvariants( 226 | Box::new(DotColor::iter_variants()), 227 | Box::new(DotColor::iter_variant_names()) 228 | ); 229 | } 230 | 231 | /// zips together two variants to allow translation over a hashmap 232 | fn zipenumvariants( 233 | i1: Box>, 234 | i2: Box>, 235 | ) -> HashMap 236 | where 237 | ET: Sized + Eq + Hash, 238 | { 239 | i1.zip(i2).collect::>() 240 | } 241 | 242 | /// color can be translated into its name 243 | impl Into<&'static str> for DotColor { 244 | fn into(self) -> &'static str { 245 | COLORS.get(&self).expect("dot color cannot be translated") 246 | } 247 | } 248 | 249 | /// internal edge to generate DOT graphical view 250 | #[derive(Clone, PartialEq, Eq)] 251 | struct DotEdge 252 | where 253 | StateType: Clone + Sized + Eq + Hash, 254 | { 255 | key: DotEdgeKey, 256 | style: dot::Style, 257 | label: String, 258 | color: DotColor, 259 | } 260 | 261 | /// None for EntryExit signifies normal state node, 262 | /// otherwise it's a "shadow node" that is invisible in graph but is used for 263 | /// entry-exit transition anchoring 264 | #[derive(Clone, PartialEq, Eq, Hash)] 265 | struct DotNodeKey { 266 | entryexit: Option, 267 | state: StateType, 268 | } 269 | 270 | impl DotNodeKey { 271 | pub fn new(entryexit: Option, state: StateType) -> DotNodeKey { 272 | DotNodeKey { 273 | entryexit: entryexit, 274 | state: state, 275 | } 276 | } 277 | } 278 | 279 | /// internal node to generate DOT graphical view 280 | #[derive(Clone, PartialEq, Eq)] 281 | struct DotNode 282 | where 283 | StateType: Clone + Sized + Eq + Hash, 284 | { 285 | key: DotNodeKey, 286 | id: Uuid, 287 | shape: Option, 288 | style: dot::Style, 289 | label: String, 290 | } 291 | 292 | /// graph containing the DOT equivalent of the FSM 293 | struct DotGraph { 294 | nodes: HashMap, DotNode>, 295 | edges: HashMap, DotEdge>, 296 | id: Uuid, 297 | /// starting state of FSM 298 | start_state: Option, 299 | } 300 | 301 | impl Default for DotGraph 302 | where 303 | StateType: Clone + Sized + Eq + Hash, 304 | { 305 | fn default() -> DotGraph { 306 | DotGraph { 307 | nodes: HashMap::new(), 308 | edges: HashMap::new(), 309 | id: Uuid::new_v4(), 310 | start_state: None, 311 | } 312 | } 313 | } 314 | 315 | /// graphwalk 316 | impl<'a, ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> 317 | dot::GraphWalk<'a, DotNodeKey, DotEdgeKey> 318 | for FSM 319 | where 320 | StateType: Clone + PartialEq + Eq + Hash + Sized, 321 | EventType: Clone + PartialEq + Eq + Hash + Sized, 322 | { 323 | fn nodes(&'a self) -> dot::Nodes<'a, DotNodeKey> { 324 | self.dotgraph.borrow().nodes.keys().cloned().collect() 325 | } 326 | 327 | fn edges(&'a self) -> dot::Edges<'a, DotEdgeKey> { 328 | self.dotgraph.borrow().edges.keys().cloned().collect() 329 | } 330 | 331 | fn source(&self, e: &DotEdgeKey) -> DotNodeKey { 332 | match *e { 333 | DotEdgeKey::EntryExit(ref eek) => { 334 | if eek.entryexit == EntryExit::EntryTransition { 335 | DotNodeKey::new(Some(eek.entryexit.clone()), eek.state.clone()) 336 | } else { 337 | if let Some(_) = self.statetransitions.borrow().get(eek) { 338 | DotNodeKey::new(None, eek.state.clone()) 339 | } else { 340 | unreachable!(); 341 | } 342 | } 343 | } 344 | DotEdgeKey::TransitionsSet(ref tk) => DotNodeKey::new(None, tk.source.clone()), 345 | } 346 | } 347 | 348 | fn target(&self, e: &DotEdgeKey) -> DotNodeKey { 349 | // target more tricky, we have to lookup the real table 350 | match *e { 351 | DotEdgeKey::EntryExit(ref eek) => { 352 | if eek.entryexit == EntryExit::ExitTransition { 353 | DotNodeKey::new(Some(eek.entryexit.clone()), eek.state.clone()) 354 | } else { 355 | if let Some(_) = self.statetransitions.borrow().get(eek) { 356 | DotNodeKey::new(None, eek.state.clone()) 357 | } else { 358 | unreachable!(); 359 | } 360 | } 361 | } 362 | 363 | DotEdgeKey::TransitionsSet(ref tk) => DotNodeKey::new(None, tk.target.clone()), 364 | } 365 | } 366 | } 367 | 368 | /// graph labelling 369 | impl<'a, ExtendedState, StateType, EventType, TransitionFnArguments, ErrorType> 370 | dot::Labeller<'a, DotNodeKey, DotEdgeKey> 371 | for FSM 372 | where 373 | StateType: Clone + PartialEq + Eq + Hash + Sized, 374 | EventType: Clone + PartialEq + Eq + Hash + Sized, 375 | { 376 | fn graph_id(&'a self) -> dot::Id<'a> { 377 | let gid = format!("G{:X}", self.dotgraph.borrow().id.as_u128()); 378 | dot::Id::new(gid).unwrap() 379 | } 380 | 381 | fn node_id(&'a self, n: &DotNodeKey) -> dot::Id<'a> { 382 | // get the node 383 | match self.dotgraph.borrow().nodes.get(n) { 384 | Some(realnode) => { 385 | let fid = format!("N{:X}", realnode.id.as_u128()); 386 | dot::Id::new(fid).unwrap() 387 | } 388 | None => unreachable!(), 389 | } 390 | } 391 | 392 | fn node_shape(&'a self, n: &DotNodeKey) -> Option> { 393 | let borrowed = self.dotgraph.borrow(); 394 | match borrowed.nodes.get(n) { 395 | Some(realnode) => { 396 | if let Some(ref r) = realnode.shape { 397 | let v = r.clone(); 398 | Some(dot::LabelText::LabelStr(v.into())) 399 | } else { 400 | Some(dot::LabelText::LabelStr("oval".into())) 401 | } 402 | } 403 | None => unreachable!(), 404 | } 405 | } 406 | 407 | fn node_style(&'a self, n: &DotNodeKey) -> dot::Style { 408 | match self.dotgraph.borrow().nodes.get(n) { 409 | Some(realnode) => realnode.style, 410 | None => unreachable!(), 411 | } 412 | } 413 | 414 | fn edge_end_arrow(&'a self, _e: &DotEdgeKey) -> dot::Arrow { 415 | dot::Arrow::normal() 416 | } 417 | 418 | fn edge_start_arrow(&'a self, _e: &DotEdgeKey) -> dot::Arrow { 419 | dot::Arrow::none() 420 | } 421 | 422 | fn edge_style(&'a self, _e: &DotEdgeKey) -> dot::Style { 423 | dot::Style::None 424 | } 425 | 426 | fn node_label<'b>(&'b self, n: &DotNodeKey) -> dot::LabelText<'b> { 427 | match self.dotgraph.borrow().nodes.get(n) { 428 | Some(ref realnode) => dot::LabelText::LabelStr(realnode.label.clone().into()), 429 | None => unreachable!(), 430 | } 431 | } 432 | 433 | fn edge_label<'b>(&'b self, ek: &DotEdgeKey) -> dot::LabelText<'b> { 434 | match self.dotgraph.borrow().edges.get(ek) { 435 | Some(realedge) => dot::LabelText::LabelStr(realedge.label.clone().into()), 436 | None => unreachable!(), 437 | } 438 | } 439 | 440 | fn edge_color(&'a self, ek: &DotEdgeKey) -> Option> { 441 | match self.dotgraph.borrow().edges.get(ek) { 442 | Some(realedge) => { 443 | let cs: &str = realedge.color.into(); 444 | Some(dot::LabelText::LabelStr(String::from(cs).into())) 445 | } 446 | None => unreachable!(), 447 | } 448 | } 449 | } 450 | 451 | /// trait that can process events from a queue using a transition table 452 | pub trait RunsFSM { 453 | /// add events to the event queue @ the back, events are _not_ processed 454 | fn add_events( 455 | &mut self, 456 | events: &mut Vec<(EventType, OptionalFnArg)>, 457 | ) -> Result>; 458 | /// add events to the event queue @ the back from an iterator, events are _not_ processed 459 | fn extend_events(&mut self, iter: I) 460 | where 461 | I: IntoIterator)>; 462 | 463 | /// process the whole event queue. Observe that this can generate multiple messages 464 | /// and queue events against the FSM itself again so don't rely which state the machine ends 465 | /// up in 466 | /// 467 | /// `returns` - number of events processed or errors encountered. 468 | /// On errors not much can be done 469 | /// except killing the FSM instance 470 | fn process_event_queue(&mut self) -> Result>; 471 | } 472 | 473 | /// implementation of methods to contstruct the machine 474 | impl 475 | FSM 476 | where 477 | StateType: Clone + Eq + Hash + Sized, 478 | EventType: Clone + Eq + Hash + Sized, 479 | { 480 | /// new FSM with an initial extended state box'ed up so it can be passed around easily 481 | pub fn new( 482 | start_state: StateType, 483 | extended_init: Box, 484 | name: &str, 485 | log: Option, 486 | ) -> Self { 487 | let mut g = DotGraph::default(); 488 | g.start_state = Some(start_state.clone()); 489 | 490 | FSM { 491 | log, 492 | name: String::from(name), 493 | current_state: start_state.clone(), 494 | start_state, 495 | event_queue: VecDeque::<(EventType, OptionalFnArg)>::new(), 496 | transitions: Rc::new(RefCell::new(TransitionTable::new())), 497 | statetransitions: Rc::new(RefCell::new(EntryExitTransitionTable::new())), 498 | extended_state: RefCell::new(extended_init), 499 | dotgraph: Rc::new(RefCell::new(g)), 500 | last_state: None, 501 | } 502 | } 503 | 504 | /// new FSM copy sharing transitions with this instance. This allows to pull many lightweight 505 | /// copies of a single `FSM template` and run them as independent instances. Observe that any 506 | /// transition modifications will modify all flyweights. 507 | pub fn flyweight( 508 | &self, 509 | extended_init: Box, 510 | name: &str, 511 | log: Option, 512 | ) -> Self { 513 | FSM { 514 | log, 515 | name: String::from(name), 516 | current_state: self.start_state.clone(), 517 | start_state: self.start_state.clone(), 518 | event_queue: VecDeque::<(EventType, OptionalFnArg)>::new(), 519 | transitions: self.transitions.clone(), 520 | statetransitions: self.statetransitions.clone(), 521 | extended_state: RefCell::new(extended_init), 522 | dotgraph: self.dotgraph.clone(), 523 | last_state: None, 524 | } 525 | } 526 | 527 | /// new transition 528 | /// 529 | /// `returns` - TRUE if transition has been inserted, 530 | /// FALSE if a previous has been overwritten! 531 | pub fn add_transition( 532 | &mut self, 533 | from: TransitionSource, 534 | to: TransitionTarget, 535 | ) -> bool { 536 | self.transitions.borrow_mut().insert(from, to).is_none() 537 | } 538 | 539 | /// read only access to transition table so it can be traversed. 540 | pub fn transitions( 541 | &self, 542 | ) -> Ref> 543 | { 544 | self.transitions.borrow() 545 | } 546 | 547 | /// read only access to the entry/exit transition table. 548 | pub fn entry_exit_transitions( 549 | &self, 550 | ) -> Ref< 551 | EntryExitTransitionTable< 552 | ExtendedState, 553 | StateType, 554 | EventType, 555 | TransitionFnArguments, 556 | ErrorType, 557 | >, 558 | > { 559 | self.statetransitions.borrow() 560 | } 561 | 562 | /// new enter/exit transition per state 563 | /// executed _after_ the transition right before 564 | /// the state is entered or called _before_ transition 565 | /// on exit. If the machine remains in the same state 566 | /// neither the enter nor the exit transitions are called. 567 | /// 568 | /// `returns` - TRUE if transition has been inserted, FALSE if a 569 | /// previous has been overwritten! 570 | pub fn add_enter_transition( 571 | &mut self, 572 | case: (StateType, EntryExit), 573 | trans: EntryExitTransition< 574 | ExtendedState, 575 | StateType, 576 | EventType, 577 | TransitionFnArguments, 578 | ErrorType, 579 | >, 580 | ) -> bool { 581 | self.statetransitions 582 | .borrow_mut() 583 | .insert( 584 | EntryExitKey { 585 | state: case.0, 586 | entryexit: case.1, 587 | }, 588 | trans, 589 | ).is_none() 590 | } 591 | 592 | pub fn name(&self) -> &String { 593 | &self.name 594 | } 595 | 596 | /// gives a read only peek into the extended state from the outside of transitions. 597 | /// Must be given up before running machine of course 598 | pub fn extended_state(&self) -> Ref> { 599 | self.extended_state.borrow() 600 | } 601 | 602 | /// check current state read-only 603 | pub fn current_state(&self) -> StateType { 604 | self.current_state.clone() 605 | } 606 | 607 | /// `returns` - TRUE if machine has outstanding events queued to process 608 | pub fn events_pending(&self) -> bool { 609 | self.event_queue.len() > 0 610 | } 611 | } 612 | 613 | /// machine can be dotted if we have ordering on events & states 614 | 615 | impl 616 | FSM 617 | where 618 | StateType: Clone + Eq + Ord + Hash + Sized, 619 | EventType: Clone + Eq + Ord + Hash + Sized, 620 | { 621 | /// provides output of the FSM in dot format 622 | /// 623 | /// * `filename` - optional filename 624 | /// * `state2name, event2name` - states to human readable name translations 625 | /// * `omitstates, omitevents` - any transition starting/stopping on those states and 626 | /// the given events will be omitted from the representation 627 | pub fn dotfile( 628 | &self, 629 | filename: Option, 630 | state2name: &HashMap, 631 | event2name: &HashMap, 632 | omitstates: Option<&HashSet>, 633 | omitevents: Option<&HashSet>, 634 | ) -> Result<(), io::Error> { 635 | let fileattempt = if let Some(fname) = filename { 636 | fs::File::create(fname).map(|f| Some(f)) 637 | } else { 638 | Ok(None) 639 | }; 640 | 641 | fn omitstate(omitstates: &Option<&HashSet>, n: &ST, ) -> bool { 642 | omitstates 643 | .map_or(false, 644 | |os| os.contains(n)) 645 | } 646 | 647 | fn omitevent(omitevents: &Option<&HashSet>, n: &EV, ) -> bool { 648 | omitevents 649 | .map_or(false, 650 | |os| os.contains(n)) 651 | } 652 | 653 | if let Ok(maybef) = fileattempt { 654 | let sout = io::stdout(); 655 | 656 | let sv = state2name.keys().cloned().collect::>(); 657 | 658 | { 659 | let mut dotgraphwork = self.dotgraph.borrow_mut(); 660 | 661 | // generate the graph, nodes first 662 | for n in sv.iter() { 663 | // first _real_ nodes, i.e. not entry/exit 664 | let key = DotNodeKey::new(None, n.clone()); 665 | 666 | let shape = if let Some(ref sn) = dotgraphwork.start_state { 667 | if sn == n { 668 | Some(String::from("diamond")) 669 | } else { 670 | None 671 | } 672 | } else { 673 | None 674 | }; 675 | 676 | if !omitstate(&omitstates, n) { 677 | dotgraphwork.nodes.insert( 678 | key.clone(), 679 | DotNode { 680 | key: key, 681 | id: Uuid::new_v4(), 682 | shape: shape, 683 | style: dot::Style::None, 684 | label: String::from(*state2name.get(n).unwrap_or(&"?")), 685 | }, 686 | ); 687 | 688 | // now, let's generate pseudo nodes if necessary with entry, exit with 689 | // invisible shapes 690 | 691 | for t in &[EntryExit::EntryTransition, EntryExit::ExitTransition] { 692 | let eek = EntryExitKey { 693 | state: n.clone(), 694 | entryexit: t.clone(), 695 | }; 696 | 697 | match self.statetransitions.borrow().get(&eek) { 698 | None => {} 699 | Some(st) => { 700 | let label = match t { 701 | &EntryExit::EntryTransition => "Enter".into(), 702 | &EntryExit::ExitTransition => "Exit".into(), 703 | }; 704 | let key = DotNodeKey::new( 705 | Some(t.clone()), n.clone()); 706 | dotgraphwork.nodes.insert( 707 | key.clone(), 708 | DotNode { 709 | key: key, 710 | id: Uuid::new_v4(), 711 | shape: Some(String::from("plain")), 712 | style: if st.is_visible() { 713 | dot::Style::Dashed 714 | } else { 715 | dot::Style::Invisible 716 | }, 717 | label: label, 718 | }, 719 | ); 720 | } 721 | } 722 | } 723 | } 724 | } 725 | 726 | // generate the edges now & label them 727 | // we partition on destination color 728 | 729 | // we summarize all transitions to same destination into edges coded by same color 730 | 731 | for (target, pertargetsource) in self 732 | .transitions 733 | .borrow() 734 | .iter() 735 | .sorted_by(|&(_, e1t), &(_, e2t)| e1t.endstate.cmp(&e2t.endstate)) 736 | .into_iter() 737 | .group_by(|&(_, to)| to.endstate.clone()) 738 | .into_iter() 739 | .filter(|(tgt, _)| !omitstate(&omitstates, tgt)) 740 | { 741 | for (source, pertargetsource) in pertargetsource 742 | .into_iter() 743 | .filter(|e1| !omitevent(&omitevents, &e1.0.event)) 744 | .sorted_by(|e1, e2| 745 | match e1.0.state.cmp(&e2.0.state) { 746 | Ordering::Equal => 747 | e1.0.event.cmp(&e2.0.event), 748 | v @ _ => v, 749 | }) 750 | .into_iter() 751 | // group them by state 752 | .group_by(|&(from, _)| 753 | from.state.clone()) 754 | .into_iter() 755 | .filter(|(src, _)| !omitstate(&omitstates, src)) { 756 | // let's group per destination, drop invisible ones 757 | 758 | for (color, pertargetsourcecolor) in pertargetsource 759 | .into_iter() 760 | .sorted_by(|&(_, e1t), &(_, e2t)| e1t.color.cmp(&e2t.color)) 761 | .into_iter() 762 | .filter(|&(_, to)| to.is_visible()) 763 | .group_by(|&(_, to)| to.color) 764 | .into_iter() 765 | { 766 | // we have source,target and color grouped, each of them generates one 767 | // edge with all the events stacked as labels 768 | 769 | let key = DotEdgeKey::new_set(color, source.clone(), target.clone()); 770 | 771 | dotgraphwork.edges.insert( 772 | key.clone(), 773 | DotEdge { 774 | key: key, 775 | style: dot::Style::None, 776 | label: pertargetsourcecolor 777 | .into_iter() 778 | .map(|(source, dest)| { 779 | format!( 780 | "{}|{}|", 781 | dest.name 782 | .as_ref() 783 | .map(|n| if n.len() > 0 { 784 | format!("{}\n", n) 785 | } else { 786 | "".into() 787 | }).unwrap_or("".into()), 788 | event2name 789 | .get(&source.event.clone()) 790 | .unwrap_or(&"") 791 | ) 792 | }).collect::>() 793 | .join("\n"), 794 | color: color, 795 | }, 796 | ); 797 | } 798 | } 799 | } 800 | 801 | // entry/exit, no color grouping necessary given we have one per state 802 | for (tk, tv) in self 803 | .statetransitions 804 | .borrow() 805 | .iter() 806 | .filter(|&(st, _)| !omitstate(&omitstates, &st.state)) 807 | .filter(|&(_, tv)| tv.is_visible()) 808 | { 809 | let key: DotEdgeKey = DotEdgeKey::new_entryexit(tk.clone()); 810 | 811 | dotgraphwork.edges.insert( 812 | key.clone(), 813 | DotEdge { 814 | key: key, 815 | style: dot::Style::None, 816 | label: format!("{}", tv.get_name().clone().unwrap_or(String::from(""))), 817 | color: tv.get_color(), 818 | }, 819 | ); 820 | } 821 | } // all generated, give up mutable refcell borrow on graph 822 | 823 | let render = move |mut mf, mut sout| { 824 | match &mut mf { 825 | &mut Some(ref mut f) => dot::render(self, f), 826 | _ => dot::render(self, &mut sout), // as io::Write 827 | } 828 | }; 829 | 830 | render(maybef, sout) 831 | } else { 832 | Err(fileattempt.err().unwrap()) // error 833 | } 834 | } 835 | } 836 | 837 | /// describes a transition origination point 838 | #[derive(Hash, Eq, PartialEq, Clone)] 839 | pub struct TransitionSource { 840 | state: StateType, 841 | event: EventType, 842 | } 843 | 844 | impl TransitionSource { 845 | /// create a transition source 846 | /// * `state` - original state 847 | /// * `event` - event occuring 848 | pub fn new(state: StateType, event: EventType) -> TransitionSource { 849 | TransitionSource { 850 | state: state, 851 | event: event, 852 | } 853 | } 854 | /// read state 855 | pub fn state(&self) -> &StateType { 856 | &self.state 857 | } 858 | /// read event 859 | pub fn event(&self) -> &EventType { 860 | &self.event 861 | } 862 | } 863 | 864 | pub trait Annotated 865 | where 866 | Self: std::marker::Sized, 867 | { 868 | /// set optional name 869 | fn name(self, _name: &str) -> Self; 870 | /// set optional description 871 | fn description(self, _name: &str) -> Self; 872 | /// set color 873 | fn color(self, _color: DotColor) -> Self; 874 | /// set visibility 875 | fn visible(self, _visibility: bool) -> Self; 876 | 877 | fn get_name(&self) -> &Option; 878 | fn get_description(&self) -> &Option; 879 | fn get_color(&self) -> DotColor; 880 | fn is_visible(&self) -> bool { 881 | true 882 | } 883 | } 884 | 885 | #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 886 | pub struct EntryExitKey { 887 | state: StateType, 888 | entryexit: EntryExit, 889 | } 890 | 891 | impl EntryExitKey { 892 | /// read state 893 | pub fn state(&self) -> &StateType { 894 | &self.state 895 | } 896 | pub fn entry(&self) -> bool { 897 | self.entryexit == EntryExit::EntryTransition 898 | } 899 | } 900 | 901 | /// implements the target of a transition upon an event 902 | pub struct TransitionTarget { 903 | endstate: StateType, 904 | transfn: 905 | Box>, 906 | /// optional name of the transition used for the src->dst arrow beside the event 907 | name: Option, 908 | /// optional description of the transition 909 | description: Option, 910 | /// visibility 911 | visible: bool, 912 | /// optional color of the transition used for the src->dst arrow 913 | color: DotColor, 914 | } 915 | 916 | impl 917 | TransitionTarget 918 | { 919 | /// create a transition target 920 | /// * `endstate` - state resulting after correct transition 921 | /// * `transfn` - transition as a boxed function taking in extended state, 922 | /// event and possible arguments 923 | pub fn new( 924 | endstate: StateType, 925 | transfn: Box< 926 | TransitionFn, 927 | >, 928 | ) -> Self { 929 | TransitionTarget { 930 | endstate: endstate, 931 | transfn: transfn, 932 | name: None, 933 | description: None, 934 | visible: true, 935 | color: DotColor::black, 936 | } 937 | } 938 | /// read endstate 939 | pub fn state(&self) -> &StateType { 940 | &self.endstate 941 | } 942 | } 943 | 944 | impl Annotated 945 | for TransitionTarget 946 | { 947 | fn name(mut self, name: &str) -> Self { 948 | self.name = Some(name.into()); 949 | self 950 | } 951 | fn description(mut self, desc: &str) -> Self { 952 | self.description = Some(desc.into()); 953 | self 954 | } 955 | fn color(mut self, color: DotColor) -> Self { 956 | self.color = color; 957 | self 958 | } 959 | fn visible(mut self, vis: bool) -> Self { 960 | self.visible = vis; 961 | self 962 | } 963 | fn get_name(&self) -> &Option { 964 | &self.name 965 | } 966 | fn get_description(&self) -> &Option { 967 | &self.description 968 | } 969 | fn get_color(&self) -> DotColor { 970 | self.color 971 | } 972 | fn is_visible(&self) -> bool { 973 | self.visible 974 | } 975 | } 976 | 977 | /// map of from state/event to end state/transition 978 | type TransitionTable = 979 | HashMap< 980 | // from 981 | TransitionSource, 982 | TransitionTarget, 983 | >; 984 | 985 | /// stores the transition 986 | pub struct EntryExitTransition< 987 | ExtendedState, 988 | StateType, 989 | EventType, 990 | TransitionFnArguments, 991 | ErrorType, 992 | > { 993 | transfn: Box< 994 | EntryExitTransitionFn< 995 | ExtendedState, 996 | EventType, 997 | StateType, 998 | TransitionFnArguments, 999 | ErrorType, 1000 | >, 1001 | >, 1002 | /// optional name of the transition used for the arrow beside the event 1003 | name: Option, 1004 | /// optional description of the transition 1005 | description: Option, 1006 | /// visibility 1007 | visible: bool, 1008 | /// optional color of the transition used for the arrow 1009 | color: DotColor, 1010 | } 1011 | 1012 | impl 1013 | EntryExitTransition 1014 | { 1015 | pub fn new( 1016 | transfn: Box< 1017 | EntryExitTransitionFn< 1018 | ExtendedState, 1019 | EventType, 1020 | StateType, 1021 | TransitionFnArguments, 1022 | ErrorType, 1023 | >, 1024 | >, 1025 | ) -> Self { 1026 | EntryExitTransition { 1027 | transfn: transfn, 1028 | name: None, 1029 | description: None, 1030 | color: DotColor::black, 1031 | visible: true, 1032 | } 1033 | } 1034 | } 1035 | 1036 | impl Annotated 1037 | for EntryExitTransition 1038 | { 1039 | fn name(mut self, name: &str) -> Self { 1040 | self.name = Some(name.into()); 1041 | self 1042 | } 1043 | fn description(mut self, desc: &str) -> Self { 1044 | self.description = Some(desc.into()); 1045 | self 1046 | } 1047 | fn color(mut self, color: DotColor) -> Self { 1048 | self.color = color; 1049 | self 1050 | } 1051 | fn visible(mut self, vis: bool) -> Self { 1052 | self.visible = vis; 1053 | self 1054 | } 1055 | fn get_name(&self) -> &Option { 1056 | &self.name 1057 | } 1058 | fn get_description(&self) -> &Option { 1059 | &self.description 1060 | } 1061 | fn get_color(&self) -> DotColor { 1062 | self.color 1063 | } 1064 | fn is_visible(&self) -> bool { 1065 | self.visible 1066 | } 1067 | } 1068 | 1069 | /// map for state entry/exit transitions 1070 | type EntryExitTransitionTable< 1071 | ExtendedState, 1072 | StateType, 1073 | EventType, 1074 | TransitionFnArguments, 1075 | ErrorType, 1076 | > = HashMap< 1077 | // from 1078 | EntryExitKey, 1079 | EntryExitTransition, 1080 | >; 1081 | 1082 | impl 1083 | RunsFSM 1084 | for FSM 1085 | where 1086 | StateType: Clone + PartialEq + Eq + Hash + Debug + Sized, 1087 | EventType: Clone + PartialEq + Eq + Hash + Debug + Sized + Debug, 1088 | ErrorType: Debug, 1089 | { 1090 | fn add_events( 1091 | &mut self, 1092 | events: &mut Vec<(EventType, OptionalFnArg)>, 1093 | ) -> Result> { 1094 | let el = events.len(); 1095 | 1096 | if let Some(ref l) = self.log { 1097 | debug!( 1098 | l, 1099 | "FSM {} adding {} events: {:?}", 1100 | self.name, 1101 | el, 1102 | events.iter().map(|e| e.0.clone()).collect::>() 1103 | ); 1104 | } 1105 | 1106 | // move the queue into the closure and add events 1107 | self.event_queue.extend(events.drain(..)); 1108 | Ok(el as u32) 1109 | } 1110 | 1111 | fn extend_events(&mut self, events: I) 1112 | where 1113 | I: IntoIterator)>, 1114 | { 1115 | if let Some(ref l) = self.log { 1116 | debug!(l, "FSM {} adding events from iterator", self.name); 1117 | } 1118 | 1119 | self.event_queue.extend(events) 1120 | } 1121 | 1122 | fn process_event_queue(&mut self) -> Result> { 1123 | let mut evs = VecDeque::new(); 1124 | // drain out the current queue to operate on it, we'll add while running transitions again 1125 | swap(&mut evs, &mut self.event_queue); 1126 | 1127 | let nrev = evs.len() as u32; 1128 | 1129 | let mut lr: Vec> = evs 1130 | .drain(..) 1131 | .map(|e| { 1132 | let state = self.current_state.clone(); 1133 | let event = e.0.clone(); 1134 | let entryexittransitions = self.statetransitions.borrow(); 1135 | let transitions = self.transitions.borrow(); 1136 | let transition = 1137 | transitions.get(&TransitionSource::new(state.clone(), event.clone())); 1138 | let ref mut q = self.event_queue; 1139 | let name = &self.name; 1140 | 1141 | if let Some(ref l) = self.log { 1142 | debug!(l, "FSM {} processing event {:?}/{:?}", name, event, state); 1143 | } 1144 | 1145 | // play the entry, exit transition draining the event queues if necessary 1146 | fn entryexit< 1147 | ExtendedState, 1148 | EventType, 1149 | StateType, 1150 | TransitionFnArguments, 1151 | ErrorType, 1152 | >( 1153 | log: Option<&Logger>, 1154 | extended_state: RefMut>, 1155 | name: &str, 1156 | on_state: StateType, 1157 | direction: EntryExit, 1158 | event_queue: &mut EventQueue, 1159 | transitions: &Ref< 1160 | EntryExitTransitionTable< 1161 | ExtendedState, 1162 | StateType, 1163 | EventType, 1164 | TransitionFnArguments, 1165 | ErrorType, 1166 | >, 1167 | >, 1168 | last_state: Option, 1169 | last_event: Option, 1170 | ) -> Errors 1171 | where 1172 | StateType: Clone + PartialEq + Eq + Hash + Debug, 1173 | EventType: Clone + PartialEq + Eq + Hash + Debug, 1174 | ErrorType: Debug, 1175 | { 1176 | match transitions.get(&EntryExitKey { 1177 | state: on_state.clone(), 1178 | entryexit: direction, 1179 | }) { 1180 | None => Errors::OK, 1181 | Some(ref entryexittrans) => { 1182 | let ref func = entryexittrans.transfn; 1183 | let ref tname = entryexittrans.get_name(); 1184 | 1185 | if let Some(ref l) = log { 1186 | debug!( 1187 | l, 1188 | "FSM {} exit/entry state transition for {:?} {:?}", 1189 | name, 1190 | on_state, 1191 | tname 1192 | ); 1193 | } 1194 | match func(extended_state, last_state, last_event) { 1195 | Err(v) => v, 1196 | Ok(v) => match v { 1197 | Some(mut eventset) => { 1198 | event_queue.extend(eventset.drain(..)); 1199 | Errors::OK 1200 | } 1201 | None => Errors::OK, 1202 | }, 1203 | } 1204 | } 1205 | } 1206 | } 1207 | 1208 | match transition { 1209 | Some(itrans) => { 1210 | let endstate = itrans.endstate.clone(); 1211 | let transfn = &itrans.transfn; 1212 | 1213 | let mut res = Errors::OK; 1214 | 1215 | let lls = self.last_state.clone(); 1216 | 1217 | res = if state == endstate.clone() { 1218 | res 1219 | } else { 1220 | // run exit for state 1221 | let extstate = self.extended_state.borrow_mut(); 1222 | entryexit( 1223 | self.log.as_ref(), 1224 | extstate, 1225 | name, 1226 | state.clone(), 1227 | EntryExit::ExitTransition, 1228 | q, 1229 | &entryexittransitions, 1230 | lls.clone(), 1231 | Some(event.clone()), 1232 | ) 1233 | }; 1234 | 1235 | // only continue if exit was ok 1236 | res = match res { 1237 | Errors::OK => { 1238 | let extstate = self.extended_state.borrow_mut(); 1239 | // match ref mutably the resulting event set of the transition and 1240 | // drain it into our queue back 1241 | match transfn(extstate, e.0, e.1) { 1242 | Err(v) => v, 1243 | Ok(v) => { 1244 | match v { 1245 | None => {} 1246 | Some(mut eventset) => { 1247 | q.extend(eventset.drain(..)); 1248 | } 1249 | } 1250 | 1251 | if let Some(ref l) = self.log { 1252 | debug!( 1253 | l, 1254 | "FSM {} moving machine to {:?}", name, endstate 1255 | ); 1256 | } 1257 | self.last_state = Some(self.current_state.clone()); 1258 | self.current_state = endstate.clone(); 1259 | Errors::OK 1260 | } 1261 | } 1262 | } 1263 | r => r, 1264 | }; 1265 | 1266 | // see whether we have entry into the next one 1267 | match res { 1268 | Errors::OK => { 1269 | if state == endstate.clone() { 1270 | res 1271 | } else { 1272 | let extstate = self.extended_state.borrow_mut(); 1273 | entryexit( 1274 | self.log.as_ref(), 1275 | extstate, 1276 | name, 1277 | endstate.clone(), 1278 | EntryExit::EntryTransition, 1279 | q, 1280 | &entryexittransitions, 1281 | lls, 1282 | Some(event.clone()), 1283 | ) 1284 | } 1285 | } 1286 | r => r, 1287 | } 1288 | } 1289 | None => Errors::NoTransition(event, state), 1290 | } 1291 | // check for any errors in the whole transitions of the queue 1292 | }).filter(|e| match *e { 1293 | Errors::OK => false, 1294 | _ => true, 1295 | }).take(1) 1296 | .collect::>(); // try to get first error out if any 1297 | 1298 | // check whether we got any errors on transitions 1299 | match lr.pop() { 1300 | Some(x) => { 1301 | if let Some(ref l) = self.log { 1302 | debug!( 1303 | l, 1304 | "FSM {} filter on transition failures yields {:?}", self.name, &x 1305 | ); 1306 | } 1307 | Err(x) 1308 | } 1309 | _ => Ok(nrev), 1310 | } 1311 | } 1312 | } 1313 | 1314 | #[cfg(test)] 1315 | mod tests { 1316 | //! small test of a coin machine opening/closing and checking coins 1317 | //! it does check event generation in the transition, extended state, 1318 | //! transitions on state enter/exit and error returns 1319 | extern crate slog; 1320 | extern crate slog_async; 1321 | extern crate slog_atomic; 1322 | extern crate slog_term; 1323 | 1324 | use std::cell::RefMut; 1325 | use std::collections::HashMap; 1326 | 1327 | use self::slog_atomic::*; 1328 | use slog::*; 1329 | use std; 1330 | use std::borrow::Borrow; 1331 | 1332 | use super::{ 1333 | zipenumvariants, Annotated, DotColor, EntryExit, EntryExitTransition, Errors, RunsFSM, 1334 | TransitionSource, TransitionTarget, FSM, 1335 | }; 1336 | 1337 | fn build_logger(level: Level) -> Logger { 1338 | let decorator = slog_term::PlainDecorator::new(std::io::stdout()); 1339 | let drain = slog_term::CompactFormat::new(decorator).build().fuse(); 1340 | let drain = slog_async::Async::new(drain).build().fuse(); 1341 | 1342 | let drain = AtomicSwitch::new(drain); 1343 | 1344 | // Get a root logger that will log into a given drain. 1345 | Logger::root( 1346 | LevelFilter::new(drain, level).fuse(), 1347 | o!("version" => env!("CARGO_PKG_VERSION"),), 1348 | ) 1349 | } 1350 | 1351 | #[derive(Debug, Clone)] 1352 | enum StillCoinType { 1353 | Good, 1354 | Bad, 1355 | } 1356 | 1357 | #[derive(Debug, Clone)] 1358 | enum StillArguments { 1359 | Coin(StillCoinType), 1360 | } 1361 | 1362 | custom_derive! { 1363 | #[derive(IterVariants(StillStateVariants), IterVariantNames(StillStateNames), 1364 | Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] 1365 | enum StillStates { 1366 | ClosedWaitForMoney, 1367 | CheckingMoney, 1368 | OpenWaitForTimeOut, 1369 | } 1370 | } 1371 | 1372 | custom_derive! { 1373 | #[derive(IterVariants(StillEventVariants), IterVariantNames(StillEventNames), 1374 | Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] 1375 | enum StillEvents { 1376 | GotCoin, 1377 | // needs coin type 1378 | AcceptMoney, 1379 | RejectMoney, 1380 | Timeout, 1381 | } 1382 | } 1383 | 1384 | #[derive(Debug)] 1385 | enum StillErrors { 1386 | CoinArgumentMissing, 1387 | } 1388 | 1389 | struct StillExtState { 1390 | coincounter: u32, 1391 | opened: u32, 1392 | closed: u32, 1393 | exitedon: Vec<(Option, Option)>, 1394 | enteredon: Vec<(Option, Option)>, 1395 | } 1396 | 1397 | type CoinStillFSM = FSM; 1398 | 1399 | fn coin_fsm_extstate() -> Box { 1400 | Box::new(StillExtState { 1401 | coincounter: 0, 1402 | opened: 0, 1403 | closed: 0, 1404 | exitedon: vec![], 1405 | enteredon: vec![], 1406 | }) 1407 | } 1408 | 1409 | fn build_coin_fsm() -> CoinStillFSM { 1410 | let mainlog = build_logger(Level::Info); 1411 | 1412 | let mut still_fsm = 1413 | FSM::::new( 1414 | StillStates::ClosedWaitForMoney, 1415 | coin_fsm_extstate(), 1416 | "coin_still", 1417 | Some(mainlog), 1418 | ); 1419 | 1420 | let check_money = move |_extstate: RefMut>, 1421 | _ev: StillEvents, 1422 | arg: Option| { 1423 | match arg { 1424 | None => Err(Errors::InternalError( 1425 | StillEvents::GotCoin, 1426 | StillStates::ClosedWaitForMoney, 1427 | StillErrors::CoinArgumentMissing, 1428 | )), 1429 | Some(arg) => match arg { 1430 | StillArguments::Coin(t) => match t { 1431 | StillCoinType::Good => Ok(Some(vec![(StillEvents::AcceptMoney, None)])), 1432 | StillCoinType::Bad => Ok(Some(vec![(StillEvents::RejectMoney, None)])), 1433 | }, 1434 | }, 1435 | } 1436 | }; 1437 | 1438 | assert!( 1439 | still_fsm.add_transition( 1440 | TransitionSource::new(StillStates::ClosedWaitForMoney, StillEvents::GotCoin), 1441 | TransitionTarget::new(StillStates::CheckingMoney, Box::new(check_money)) 1442 | .name("ProcessCoin") 1443 | .color(DotColor::green), 1444 | ) 1445 | ); 1446 | 1447 | assert!( 1448 | still_fsm.add_transition( 1449 | TransitionSource::new(StillStates::CheckingMoney, StillEvents::RejectMoney), 1450 | TransitionTarget::new( 1451 | StillStates::ClosedWaitForMoney, 1452 | Box::new(|_, _, _| Ok(None)) 1453 | ).name("Rejected") 1454 | .color(DotColor::red), 1455 | ) 1456 | ); 1457 | 1458 | assert!( 1459 | still_fsm.add_transition( 1460 | TransitionSource::new(StillStates::CheckingMoney, StillEvents::GotCoin), 1461 | TransitionTarget::new(StillStates::CheckingMoney, Box::new(|_, _, _| Ok(None))) 1462 | .name("IgnoreAnotherCoin") 1463 | .color(DotColor::red) 1464 | ) 1465 | ); 1466 | 1467 | assert!( 1468 | still_fsm.add_transition( 1469 | TransitionSource::new(StillStates::CheckingMoney, StillEvents::AcceptMoney), 1470 | TransitionTarget::new( 1471 | StillStates::OpenWaitForTimeOut, 1472 | Box::new(|mut estate: RefMut>, _, _| { 1473 | estate.coincounter += 1; 1474 | // we count open/close on entry/exit 1475 | Ok(None) 1476 | }) 1477 | ).name("Accepted") 1478 | .color(DotColor::green) 1479 | ) 1480 | ); 1481 | 1482 | assert!( 1483 | still_fsm.add_transition( 1484 | TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::GotCoin), 1485 | TransitionTarget::new( 1486 | StillStates::OpenWaitForTimeOut, 1487 | Box::new(|_, _, _| Ok(Some(vec![(StillEvents::RejectMoney, None)]))), 1488 | ).name("Reject") 1489 | .color(DotColor::red), 1490 | ) 1491 | ); 1492 | 1493 | assert!( 1494 | still_fsm.add_transition( 1495 | TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::RejectMoney), 1496 | TransitionTarget::new( 1497 | StillStates::OpenWaitForTimeOut, 1498 | Box::new(|_, _, _| Ok(None)) 1499 | ).name("Rejected") 1500 | .color(DotColor::red) 1501 | ) 1502 | ); 1503 | 1504 | assert!( 1505 | still_fsm.add_transition( 1506 | TransitionSource::new(StillStates::OpenWaitForTimeOut, StillEvents::Timeout), 1507 | TransitionTarget::new( 1508 | StillStates::ClosedWaitForMoney, 1509 | Box::new(|_, _, _| Ok(None)) 1510 | ).name("TimeOut") 1511 | .color(DotColor::blue) 1512 | ) 1513 | ); 1514 | 1515 | assert!( 1516 | still_fsm.add_enter_transition( 1517 | (StillStates::OpenWaitForTimeOut, EntryExit::EntryTransition), 1518 | EntryExitTransition::new(Box::new( 1519 | |mut estate: RefMut>, laststate, lastevent| { 1520 | estate.opened += 1; 1521 | estate.enteredon.push((laststate, lastevent)); 1522 | Ok(None) 1523 | } 1524 | )).name("CountOpens") 1525 | .color(DotColor::gold) 1526 | ) 1527 | ); 1528 | 1529 | assert!( 1530 | still_fsm.add_enter_transition( 1531 | (StillStates::OpenWaitForTimeOut, EntryExit::ExitTransition), 1532 | EntryExitTransition::new(Box::new( 1533 | |mut estate: RefMut>, laststate, lastevent| { 1534 | estate.closed += 1; 1535 | estate.exitedon.push((laststate, lastevent)); 1536 | Ok(None) 1537 | } 1538 | )).name("CountClose") 1539 | .color(DotColor::gold) 1540 | ) 1541 | ); 1542 | 1543 | still_fsm 1544 | } 1545 | 1546 | #[test] 1547 | fn coin_machine_test() { 1548 | let mut still_fsm = build_coin_fsm(); 1549 | // timeout should give no transition error 1550 | let mut onevec = vec![(StillEvents::Timeout, None)]; 1551 | still_fsm.extend_events(onevec.drain(..)); 1552 | match still_fsm.process_event_queue() { 1553 | Ok(v) => panic!("failed with {:?} # processed tokens as Ok(_)", v), 1554 | Err(v) => match v { 1555 | Errors::NoTransition(StillEvents::Timeout, StillStates::ClosedWaitForMoney) => (), 1556 | _ => panic!("failed with wrong FSM error"), 1557 | }, 1558 | } 1559 | 1560 | // that's how we package arguments, we need to clone the coins then 1561 | let goodcoin = StillArguments::Coin(StillCoinType::Good); 1562 | let badcoin = StillArguments::Coin(StillCoinType::Bad); 1563 | 1564 | let mut still_fsm = build_coin_fsm(); 1565 | assert_eq!( 1566 | still_fsm 1567 | .add_events(&mut vec![ 1568 | (StillEvents::GotCoin, Some(goodcoin.clone())), 1569 | (StillEvents::GotCoin, Some(badcoin.clone())), 1570 | (StillEvents::GotCoin, Some(goodcoin.clone())), 1571 | (StillEvents::GotCoin, Some(goodcoin.clone())), 1572 | ]).unwrap(), 1573 | 4 1574 | ); 1575 | while still_fsm.events_pending() { 1576 | assert!(!still_fsm.process_event_queue().is_err()); 1577 | } 1578 | 1579 | assert!(still_fsm.current_state() == StillStates::OpenWaitForTimeOut); 1580 | 1581 | assert_eq!( 1582 | still_fsm 1583 | .add_events(&mut vec![(StillEvents::Timeout, None)]) 1584 | .unwrap(), 1585 | 1 1586 | ); 1587 | while still_fsm.events_pending() { 1588 | assert!(!still_fsm.process_event_queue().is_err()); 1589 | } 1590 | 1591 | assert!(still_fsm.current_state() == StillStates::ClosedWaitForMoney); 1592 | 1593 | let es = still_fsm.extended_state(); 1594 | 1595 | assert!(es.borrow().coincounter == 1); 1596 | assert!(es.borrow().opened == 1); 1597 | assert!(es.borrow().closed == 1); 1598 | 1599 | assert_eq!( 1600 | es.borrow().exitedon, 1601 | vec![(Some(StillStates::CheckingMoney), Some(StillEvents::Timeout))] 1602 | ); 1603 | assert_eq!( 1604 | es.borrow().enteredon, 1605 | vec![( 1606 | Some(StillStates::CheckingMoney), 1607 | Some(StillEvents::AcceptMoney) 1608 | )] 1609 | ); 1610 | } 1611 | 1612 | #[test] 1613 | fn coin_machine_dot() { 1614 | let still_fsm = build_coin_fsm(); 1615 | 1616 | for fname in vec![None, Some("target/tmp.dot".into())] { 1617 | still_fsm 1618 | .dotfile( 1619 | fname, 1620 | &zipenumvariants( 1621 | Box::new(StillStates::iter_variants()), 1622 | Box::new(StillStates::iter_variant_names()), 1623 | ), 1624 | &zipenumvariants( 1625 | Box::new(StillEvents::iter_variants()), 1626 | Box::new(StillEvents::iter_variant_names()), 1627 | ), 1628 | None, 1629 | None, 1630 | ).expect("cannot dotfile"); 1631 | } 1632 | } 1633 | 1634 | #[derive(Debug, Clone)] 1635 | enum DotTestArguments {} 1636 | 1637 | custom_derive! { 1638 | #[derive(IterVariants(StateVariants), IterVariantNames(StateNames), 1639 | Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] 1640 | enum DotTestStates { 1641 | Init, 1642 | One, 1643 | Two, 1644 | Three, 1645 | } 1646 | } 1647 | 1648 | custom_derive! { 1649 | #[derive(IterVariants(EventVariants), IterVariantNames(EventNames), 1650 | Debug, Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)] 1651 | enum DotTestEvents { 1652 | Event1, 1653 | Event2, 1654 | Event3, 1655 | Event4, 1656 | RedEvent1, 1657 | RedEvent2, 1658 | RedEvent3, 1659 | CyanEvent1, 1660 | CyanEvent2, 1661 | CyanEvent3, 1662 | InvisibleEvent, 1663 | } 1664 | } 1665 | 1666 | #[derive(Debug)] 1667 | enum DotTestErrors {} 1668 | 1669 | struct DotTestExtState {} 1670 | 1671 | type DotTestFSM = 1672 | FSM; 1673 | 1674 | fn build_dottest_fsm() -> DotTestFSM { 1675 | let mainlog = build_logger(Level::Debug); 1676 | 1677 | let mut dottest_fsm = FSM::< 1678 | DotTestExtState, 1679 | DotTestStates, 1680 | DotTestEvents, 1681 | DotTestArguments, 1682 | DotTestErrors, 1683 | >::new( 1684 | DotTestStates::Init, 1685 | Box::new(DotTestExtState {}), 1686 | "DotTest", 1687 | Some(mainlog), 1688 | ); 1689 | 1690 | // stack a bunch self transitions onto each other to test complex dot output 1691 | assert!( 1692 | dottest_fsm.add_transition( 1693 | TransitionSource::new(DotTestStates::Init, DotTestEvents::Event1), 1694 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1695 | .name("Init2One-GRAY") 1696 | .color(DotColor::gray), 1697 | ) 1698 | ); 1699 | 1700 | // stack a bunch self transitions onto each other to test complex dot output 1701 | assert!( 1702 | dottest_fsm.add_transition( 1703 | TransitionSource::new(DotTestStates::One, DotTestEvents::Event1), 1704 | TransitionTarget::new(DotTestStates::Two, Box::new(|_, _, _| Ok(None))) 1705 | .name("One2Two-GREEN") 1706 | .color(DotColor::green) 1707 | ) 1708 | ); 1709 | 1710 | // stack a bunch self transitions onto each other to test complex dot output 1711 | assert!( 1712 | dottest_fsm.add_transition( 1713 | TransitionSource::new(DotTestStates::Two, DotTestEvents::Event1), 1714 | TransitionTarget::new(DotTestStates::Three, Box::new(|_, _, _| Ok(None))) 1715 | .name("Two2Three-BLUE") 1716 | .color(DotColor::blue) 1717 | ) 1718 | ); 1719 | 1720 | assert!( 1721 | dottest_fsm.add_transition( 1722 | TransitionSource::new(DotTestStates::Three, DotTestEvents::Event1), 1723 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1724 | .name("Three2One-1-RED") 1725 | .color(DotColor::red) 1726 | ) 1727 | ); 1728 | 1729 | assert!( 1730 | dottest_fsm.add_transition( 1731 | TransitionSource::new(DotTestStates::Three, DotTestEvents::Event2), 1732 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1733 | .name("Three2One-2-RED") 1734 | .color(DotColor::red) 1735 | ) 1736 | ); 1737 | 1738 | assert!( 1739 | dottest_fsm.add_transition( 1740 | TransitionSource::new(DotTestStates::Three, DotTestEvents::Event3), 1741 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1742 | .name("Three2One-3-BLUE") 1743 | .color(DotColor::blue) 1744 | ) 1745 | ); 1746 | 1747 | assert!( 1748 | dottest_fsm.add_transition( 1749 | TransitionSource::new(DotTestStates::Three, DotTestEvents::Event4), 1750 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1751 | .name("Three2One-4-BLUE") 1752 | .color(DotColor::blue) 1753 | ) 1754 | ); 1755 | 1756 | assert!( 1757 | dottest_fsm.add_transition( 1758 | TransitionSource::new(DotTestStates::Three, DotTestEvents::InvisibleEvent), 1759 | TransitionTarget::new(DotTestStates::One, Box::new(|_, _, _| Ok(None))) 1760 | .name("Three2One-INVISIBLE-BLUE") 1761 | .color(DotColor::blue) 1762 | .visible(false) 1763 | ) 1764 | ); 1765 | 1766 | assert!( 1767 | dottest_fsm.add_enter_transition( 1768 | (DotTestStates::Three, EntryExit::EntryTransition), 1769 | EntryExitTransition::new(Box::new(|_, _, _| Ok(None))) 1770 | .name("Three3-INVISIBLE-ENTRY") 1771 | .color(DotColor::gold) 1772 | .visible(false) 1773 | ) 1774 | ); 1775 | 1776 | for e in &[ 1777 | DotTestEvents::RedEvent1, 1778 | DotTestEvents::RedEvent2, 1779 | DotTestEvents::RedEvent3, 1780 | ] { 1781 | for s in &[DotTestStates::One, DotTestStates::Two, DotTestStates::Three] { 1782 | assert!( 1783 | dottest_fsm.add_transition( 1784 | TransitionSource::new(*s, *e), 1785 | TransitionTarget::new(*s, Box::new(|_, _, _| Ok(None))) 1786 | .name(&format!("Self2Self-{}", DOTTESTEVENTS.get(e).unwrap())) 1787 | .color(DotColor::red) 1788 | .description("simple description"), 1789 | ) 1790 | ); 1791 | } 1792 | } 1793 | 1794 | for e in &[ 1795 | DotTestEvents::CyanEvent1, 1796 | DotTestEvents::CyanEvent2, 1797 | DotTestEvents::CyanEvent3, 1798 | ] { 1799 | for s in &[DotTestStates::One, DotTestStates::Two, DotTestStates::Three] { 1800 | assert!( 1801 | dottest_fsm.add_transition( 1802 | TransitionSource::new(*s, *e), 1803 | TransitionTarget::new(*s, Box::new(|_, _, _| Ok(None))) 1804 | .name("Self2Self-RED") 1805 | .color(DotColor::cyan) 1806 | ) 1807 | ); 1808 | } 1809 | } 1810 | 1811 | dottest_fsm 1812 | } 1813 | 1814 | lazy_static! { 1815 | static ref DOTTESTEVENTS: HashMap = zipenumvariants( 1816 | Box::new(DotTestEvents::iter_variants()), 1817 | Box::new(DotTestEvents::iter_variant_names()) 1818 | ); 1819 | } 1820 | 1821 | #[test] 1822 | fn dottest_fsm_dot() { 1823 | let dottest_fsm = build_dottest_fsm(); 1824 | 1825 | for fname in vec![None, Some("target/dottest.dot".into())] { 1826 | dottest_fsm 1827 | .dotfile( 1828 | fname, 1829 | &zipenumvariants( 1830 | Box::new(DotTestStates::iter_variants()), 1831 | Box::new(DotTestStates::iter_variant_names()), 1832 | ), 1833 | &DOTTESTEVENTS, 1834 | None, None, 1835 | ).expect("cannot dotfile"); 1836 | } 1837 | } 1838 | 1839 | #[test] 1840 | fn flyweight() { 1841 | let mut c1 = build_coin_fsm(); 1842 | let mut c2 = c1.flyweight( 1843 | coin_fsm_extstate(), 1844 | "coin_still flyweight", 1845 | Some(build_logger(Level::Info)), 1846 | ); 1847 | 1848 | // run c1 with one coin & c2 with two coins & compare stats 1849 | let goodcoin = StillArguments::Coin(StillCoinType::Good); 1850 | 1851 | assert_eq!( 1852 | c1.add_events(&mut vec![(StillEvents::GotCoin, Some(goodcoin.clone())), ]) 1853 | .unwrap(), 1854 | 1 1855 | ); 1856 | while c1.events_pending() { 1857 | assert!(!c1.process_event_queue().is_err()); 1858 | } 1859 | 1860 | assert!(c1.current_state() == StillStates::OpenWaitForTimeOut); 1861 | assert!(c2.current_state() == StillStates::ClosedWaitForMoney); 1862 | 1863 | assert_eq!( 1864 | c1.add_events(&mut vec![(StillEvents::Timeout, None)]) 1865 | .unwrap(), 1866 | 1 1867 | ); 1868 | while c1.events_pending() { 1869 | assert!(!c1.process_event_queue().is_err()); 1870 | } 1871 | 1872 | assert!(c1.current_state() == StillStates::ClosedWaitForMoney); 1873 | 1874 | let es = c1.extended_state(); 1875 | 1876 | assert!(es.borrow().coincounter == 1); 1877 | assert!(es.borrow().opened == 1); 1878 | assert!(es.borrow().closed == 1); 1879 | 1880 | assert_eq!( 1881 | c2.add_events(&mut vec![(StillEvents::GotCoin, Some(goodcoin.clone())), ]) 1882 | .unwrap(), 1883 | 1 1884 | ); 1885 | while c2.events_pending() { 1886 | assert!(!c2.process_event_queue().is_err()); 1887 | } 1888 | 1889 | let es = c2.extended_state(); 1890 | 1891 | assert!(es.borrow().closed == 0); 1892 | assert!(es.borrow().coincounter == 1); 1893 | } 1894 | } 1895 | --------------------------------------------------------------------------------