{"id":23163220,"url":"https://github.com/brucou/cycle-state-machine-demo","last_synced_at":"2025-07-10T05:34:20.420Z","repository":{"id":81701933,"uuid":"142492825","full_name":"brucou/cycle-state-machine-demo","owner":"brucou","description":"Non-trivial, real use case demo of a hierarchical state machine library with cyclejs","archived":false,"fork":false,"pushed_at":"2018-12-03T14:10:48.000Z","size":2827,"stargazers_count":28,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-05T19:22:50.568Z","etag":null,"topics":["automata","cyclejs","functional-programming","functional-reactive-programming","hierarchical-state-machine","reactive-programming","state-machine"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brucou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-07-26T20:51:44.000Z","updated_at":"2024-06-03T16:37:16.000Z","dependencies_parsed_at":null,"dependency_job_id":"791b3c1f-1e90-407b-baa1-ba7253adb52d","html_url":"https://github.com/brucou/cycle-state-machine-demo","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/brucou/cycle-state-machine-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Fcycle-state-machine-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Fcycle-state-machine-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Fcycle-state-machine-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Fcycle-state-machine-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brucou","download_url":"https://codeload.github.com/brucou/cycle-state-machine-demo/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brucou%2Fcycle-state-machine-demo/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264535996,"owners_count":23624405,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automata","cyclejs","functional-programming","functional-reactive-programming","hierarchical-state-machine","reactive-programming","state-machine"],"created_at":"2024-12-18T00:17:18.881Z","updated_at":"2025-07-10T05:34:20.374Z","avatar_url":"https://github.com/brucou.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Motivation\nThis demo aims at showing how state machines can be used to modelize reactive systems, in \nparticular user interfaces. They have long been used for embedded systems, in particular for \nsafety-critical software.\n\nWe will use a real case of a multi-step workflow (the visual interface however has been changed, \nbut the logic is the same). A user is applying to a volunteering opportunity, and to do so \nnavigate through a 5-step process, with a specific screen dedicated to each step. When moving \nfrom one step to another, the data entered by the user is validated then saved asynchronously.\n \nThat multi-step workflow will be implemented in two iterations :\n \n- In the first iteration, we will do optimistic saves, i.e. we will not wait or check \nfor a confirmation message and directly move to the next step. We will also fetch data remotely \nand assume that fetch will always be successful (call that optimistic fetch). This will helps us \nshowcase the definition and behaviour of an extended state machine.\n- In the second iteration, we will implement retries with exponential back-off for the initial \ndata-fetching. We will also implement pessimistic save for the most 'expensive' step in the \nworkflow. This will in turn serve to showcase an hierarchical extended state machine.\n\nWith those two examples, we will be able to conclude by recapitulating the advantages and \ntrade-off associated to using state machines for specifying and implementing user interfaces. \n\nThe implementation uses `cyclejs` as framework, and [`state-transducer`](https://github.com/brucou/state-transducer#example-run) as a state machine library.\n\n# General specifications\nHere are the initial specifications for the volunteer application workflow, as extracted from the\n UX designers. Those initial specifications are light in details, and are simple lo-fi wireframes.\n\n![wireframes](public/assets/images/graphs/application%20process.png)\n\nIn addition, the following must hold :\n\n- it should be possible for the user to interrupt at any time its application and continue it \nlater from where it stopped\n- user-generated data must be validated\n- after entering all necessary data for his application, the user can review them and decide to \nmodify some of them, by returning to the appropriate screen (cf. pencil icons in the wireframe)\n\n# First iteration\n## Modelizing the user flow with an extended state machine\nOn the first iteration, the provided wireframes are refined into a workable state machine, which \nreproduces the provided user flow, while addressing key implementation details (error flows, data\n fetching).\n\n![extended state machine](public/assets/images/graphs/sparks%20application%20process%20with%20comeback%20proper%20syntax%20-%20flat%20fsm.png)\n\nThe behaviour is pretty self-explanatory. The machines moves from its initial state to the fetch \nstate which awaits for a fetch event carrying the fetched data (previously saved application \ndata). From that, the sequence of screens flows in function of the user flow and rules \ndefined.\n\nNote that we could have included processing of the fetch event inside our state machine. We could\n have instead fetched the relevant data, and then start the state machine with an initial \n INIT event which carries the fetched data. Another option is also to start the state machine \n with an initial extended state which includes the fetched data.\n\n![demo](public/assets/images/animated_demo.gif)\n\n## Tests\n### Test strategy\nIt is important to understand that the defined state machine acts as a precise specification for \nthe reactive system under development. The model is precise enough to double as implementation for \nthat reactive system (partial implementation, as our model does not modelize actual actions, nor \n the interfaced systems, e.g. HTTP requests, the network, etc.), but is primarily a specification\n  of the system under study. In the context of this illustrative example, we used our state \n   transducer library to actually implement the specified state machine.\n  \n It ensues two consequences for our tests :\n - the effectful part of the reactive system must be tested separately, for instance during \n end-to-end or acceptance tests\n- assuming that our library is correct (!), **testing the implementation is testing the model**, \nas the correctness of any one means the correctness of the other.\n\nWe thus need to test the implementation to discover possible mistakes in our model. The only way \nto do this is manually : we cannot use the outputs produced by the model as oracle, as they are \nprecisely what is being tested against. Hence test generation and execution can be automated, but\n test validation remains manual.\n  \nThat is the first point. The second point is that the test space for our implementation consists \nof any sequence of events admitted by the machine (assuming that events not accepted by the \nmachine have the same effect that if they did not exist in the first place : the machine ignores \nthem). That sequence is essentially infinite, so any testing of such reactive system necessarily \ninvolves only a finite subset of the test space. How to pick that subset in a way to generate a minimum \n**confidence** level is the crux of the matter and conditions the testing strategy to adopt.\n\nBecause our model is both specification and implementation target, testing our model \ninvolves **testing the different paths in the model**[^1]. Creating the abstract test suite  is \nan easily automatable process of simply traversing through the states and transitions in the \nmodel, until the wanted model coverage is met. The abstract test suite can be \nreified into executable concrete test suites, and actual outputs (from the model implementation) \nare compared manually to expected outputs (derived from the informal requirements which originated \nthe model).\n\n[^1]: Those paths can be split into control paths and data paths (the latter relating to the set of \nvalues the extended state can take, and addressed by [**data coverage** criteria](http://www.cse.chalmers.se/edu/year/2012/course/DIT848/files/06-Selecting-Tests.pdf)). We will \naddress only the control paths. \n\nMiscellaneous model coverage criteria[^2] are commonly used when designing a test suite with the \nhelp of a model:\n\n- **All states coverage** is achieved when the test reaches every state in the model\nat least once. This is usually not a sufficient level of coverage, because behavior\nfaults are only accidentally found. If there is a bug in a transition between a\nspecific state pair, it can be missed even if all states coverage is reached.\n- **All transitions coverage** is achieved when the test executes every transition in\nthe model at least once. This automatically entails also all states coverage.\nReaching all transitions coverage doesn’t require that any specific sequence is\nexecuted, as long as all transitions are executed once. A bug that is revealed\nonly when a specific sequence of transitions is executed, is missed even in this\ncoverage level. The coverage can be increased by requiring :\n- **All n-transition coverage**, meaning that all possible transition sequences of `n` or more \ntransitions are included in the test suite.\n- **All path coverage** is achieved when all possible branches of the underlying model graph are \ntaken (**exhaustive** test of the control structure). This corresponds to the previous coverage \ncriteria for a high enough `n`\n- **All one-loop path**, and **All loop-free paths** are more restrictive criteria focusing on \nloops in the model\n\n![testing criteria](public/assets/images/structural%20fsm%20test%20-%20Imgur.png)\n\n[^2]: Bin99 Binder, R. V., Testing object-oriented systems: models, patterns, and\n    tools. Addison-Wesley Longman Publishing Co., Inc., Boston, MA,\n    USA, 1999.\n\nUsing a dedicated [graph testing library](https://github.com/brucou/graph-adt), we computed the \nabstract test suite for the *All one-loop path* criteria and ended up with around 1.500 tests!! \nWe reproduce below extract of the abstract test suite:\n \n - A test is specified by a sequence of inputs \n - Every line below is a the sequence of control states the machine go through based on the \n sequence of inputs it receives. Note that you can have repetition of control states, anytime a \n transition happens between a state and itself. Because we have used a *All one-loop path* \n criteria to enumerate the paths to test, every `Team_Detail` loop corresponds to a different \n edge in the model graph. Here such loop transitions could be `Skip Team` or `Join Team (valid \n form)` or `Join Team (invalid form)`. We can see from the extract how the graph search works \n (depth-first search).\n\n```javascript\n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"Teams\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"State_Applied\"], \n[\"nok\",\"INIT_S\",\"Review\",\"About\",\"Review\",\"Question\",\"Question\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"],\n... \n[\"nok\",\"INIT_S\",\"Review\",\"State_Applied\"]\n[\"nok\",\"INIT_S\",\"About\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"About\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"Question\",\"Question\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"About\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"State_Applied\"],\n...\n[\"nok\",\"INIT_S\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"Question\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"Question\",\"Question\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"About\",\"About\",\"Review\",\"Question\",\"Review\",\"State_Applied\"],\n...(1000+ lines)\n\n```\n\n### Test selection \nAs we mentioned, even for a relatively simple reactive system, we handed up with 1.000+ tests to \nexhaust the paths between initial state and terminal state, and that even with excluding n-loops.\n\nWe finally selected only 4 tests from the **All path coverage** set, for a total of around 50 \ntransitions taken:\n\n```javascript\n[\"nok\",\"INIT_S\",\"About\",\"About\",\"Question\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"Question\",\"Review\",\"About\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Question\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"],\n[\"nok\",\"INIT_S\",\"Review\",\"Teams\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Team_Detail\",\"Teams\",\"Review\",\"State_Applied\"] \n\n```\n\nThose tests :\n\n- fulfill the *All transitions coverage* criteria: 4 input sequences are sufficient\n- involves all the loops in the model graph (cf. first test sequence)\n- insist slightly more on the core functionality of the system, which is to apply to volunteer \nteams (e.g. `TEAM_DETAIL` loop transitions)\n  - the transition space for that control state is the permutations of `Join(Invalid Form) x Skip x \n  Join(Valid Form)`, with `|set| = 2` for `Join` and `Skip` (an event triggering the associated \n  transition happens or not). We have `|Join(Invalid Form) x Skip x Join(Valid Form)| = 8`, so \n  `3! x 8 = 48` transition permutations for that control state. Rather than exhaustively testing \n  all permutations, we pick 4 of them, fit into the 4 input sequences that are necessary to cover\n  the model.\n\nIn summary the process is :\n\n- we have informal UI requirements which are refined ino a state-machine-based \ndetailed specification \n- we generate input sequences and the corresponding output sequences, according to some \nmodel coverage criteria, our target confidence level and our testing priorities (happy path, \nerror path, core scenario, etc.)\n- we validate the selected tests manually\n\n### Test implementation\nOnce test sequences are chosen, test implementation is pretty straightforward. Because state \ntransducers from our library are causal functions, i.e. function whose outputs depend exclusively\n on past inputs, it is enough to feed an freshly initialized state machine with a given sequence \n of inputs and validate the result sequence of outputs.\n\ncf. test repository\n\n### Integration tests\nNote that once the model is validated, we can use it as an oracle. This means for instance that we \ncan take any input sequence, run it through the model, gather the resulting outputs, generate the\n corresponding BDD test, and run them. Most of this process can be automatized.\n\n## Implementation\nWe use the stream-oriented `cyclejs` framework to showcase our [state machine library](https://github.com/brucou/state-transducer). To that purpose, we use the `makeStreamingStateMachine` from our library to match a stream of actions to a stream of events. \nWe then wire that stream of actions with cyclejs sinks. In this iteration, we make use of two \n drivers : the DOM driver for updating the screen, and a domain driver for fetching data. \n \nCode available in [dedicated branch](https://github.com/brucou/cycle-state-machine-demo/tree/first-iteration).\n \n ## Run\nCheck-out the branch on your local computer then type `npm run start` in the root directory for \nthat branch.\n\n# Second iteration\n**coming soon**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucou%2Fcycle-state-machine-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrucou%2Fcycle-state-machine-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrucou%2Fcycle-state-machine-demo/lists"}