parent
dd0da3171f
commit
f94f73b36f
@ -0,0 +1,16 @@ |
||||
.*.sw* |
||||
.DS_Store |
||||
*.sqlite3 |
||||
*.sqlite3-wal |
||||
*.sqlite3-shm |
||||
debug |
||||
imgui.ini |
||||
coverage/ |
||||
.coverage |
||||
builddir |
||||
subprojects |
||||
*.csv |
||||
*.exe |
||||
*.dll |
||||
*~ |
||||
[0-9]* |
@ -0,0 +1,18 @@ |
||||
all: build test |
||||
|
||||
reset: |
||||
powershell -executionpolicy bypass .\scripts\reset_build.ps1
|
||||
|
||||
build: |
||||
meson compile -j 4 -C builddir
|
||||
|
||||
test: build |
||||
./builddir/runtests
|
||||
|
||||
install: build test |
||||
powershell "cp ./builddir/subprojects/libgit2-1.8.1/liblibgit2package.dll ."
|
||||
powershell "cp ./builddir/subprojects/efsw/libefsw.dll ."
|
||||
powershell "cp builddir/escape_turings_tarpit.exe ."
|
||||
|
||||
clean: |
||||
meson compile --clean -C builddir
|
@ -0,0 +1,40 @@ |
||||
#include "dbc.hpp" |
||||
|
||||
void dbc::log(const string &message) { |
||||
fmt::print("{}\n", message); |
||||
} |
||||
|
||||
void dbc::sentinel(const string &message) { |
||||
string err = fmt::format("[SENTINEL!] {}\n", message); |
||||
throw dbc::SentinelError{err}; |
||||
} |
||||
|
||||
void dbc::pre(const string &message, bool test) { |
||||
if(!test) { |
||||
string err = fmt::format("[PRE!] {}\n", message); |
||||
throw dbc::PreCondError{err}; |
||||
} |
||||
} |
||||
|
||||
void dbc::pre(const string &message, std::function<bool()> tester) { |
||||
dbc::pre(message, tester()); |
||||
} |
||||
|
||||
void dbc::post(const string &message, bool test) { |
||||
if(!test) { |
||||
string err = fmt::format("[POST!] {}\n", message); |
||||
throw dbc::PostCondError{err}; |
||||
} |
||||
} |
||||
|
||||
void dbc::post(const string &message, std::function<bool()> tester) { |
||||
dbc::post(message, tester()); |
||||
} |
||||
|
||||
void dbc::check(bool test, const string &message) { |
||||
if(!test) { |
||||
string err = fmt::format("[CHECK!] {}\n", message); |
||||
fmt::println("{}", err); |
||||
throw dbc::CheckError{err}; |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <fmt/core.h> |
||||
#include <functional> |
||||
|
||||
using std::string; |
||||
|
||||
namespace dbc { |
||||
class Error { |
||||
public: |
||||
const string message; |
||||
Error(string m) : message{m} {} |
||||
Error(const char *m) : message{m} {} |
||||
}; |
||||
|
||||
class CheckError : public Error {}; |
||||
class SentinelError : public Error {}; |
||||
class PreCondError : public Error {}; |
||||
class PostCondError : public Error {}; |
||||
|
||||
void log(const string &message); |
||||
void sentinel(const string &message); |
||||
void pre(const string &message, bool test); |
||||
void pre(const string &message, std::function<bool()> tester); |
||||
void post(const string &message, bool test); |
||||
void post(const string &message, std::function<bool()> tester); |
||||
void check(bool test, const string &message); |
||||
} |
@ -0,0 +1,28 @@ |
||||
#pragma once |
||||
|
||||
#include <fmt/core.h> |
||||
|
||||
#ifndef FSM_DEBUG |
||||
#define FSM_STATE(C, S, E, ...) case C::S: S(E, ##__VA_ARGS__); break |
||||
#else |
||||
#define FSM_STATE(C, S, E, ...) case C::S: fmt::println(">> " #C " " #S " event={}, state={}", int(E), int(_state)); S(E, ##__VA_ARGS__); fmt::println("<< " #C " state={}", int(_state)); break |
||||
#endif |
||||
|
||||
template<typename S, typename E> |
||||
class DeadSimpleFSM { |
||||
protected: |
||||
// BUG: don't put this in your class because state() won't work
|
||||
S _state = S::START; |
||||
|
||||
public: |
||||
template<typename... Types> |
||||
void event(E event, Types... args); |
||||
|
||||
void state(S next_state) { |
||||
_state = next_state; |
||||
} |
||||
|
||||
bool in_state(S state) { |
||||
return _state == state; |
||||
} |
||||
}; |
@ -0,0 +1,14 @@ |
||||
project('lcthw-utilities', 'cpp', |
||||
default_options: ['cpp_std=c++20']) |
||||
|
||||
catch2 = dependency('catch2-with-main') |
||||
fmt = dependency('fmt') |
||||
|
||||
runtests = executable('runtests', [ |
||||
'dbc.cpp', |
||||
'tests/fsm.cpp', |
||||
'tests/dbc.cpp', |
||||
], |
||||
dependencies: [catch2, fmt]) |
||||
|
||||
test('tests', runtests) |
@ -0,0 +1,11 @@ |
||||
mv .\subprojects\packagecache . |
||||
rm -recurse -force .\subprojects\,.\builddir\ |
||||
mkdir subprojects |
||||
mv .\packagecache .\subprojects\ |
||||
cp *.wrap subprojects |
||||
mkdir builddir |
||||
meson wrap install fmt |
||||
meson wrap install catch2 |
||||
# $env:CC="clang" |
||||
# $env:CXX="clang++" |
||||
meson setup --default-library=static --prefer-static builddir |
@ -0,0 +1,12 @@ |
||||
#!/usr/bin/env bash |
||||
set -e |
||||
|
||||
mv -f ./subprojects/packagecache . |
||||
rm -rf subprojects builddir |
||||
mkdir subprojects |
||||
mv packagecache ./subprojects/ |
||||
mkdir builddir |
||||
cp *.wrap subprojects |
||||
meson wrap install fmt |
||||
meson wrap install catch2 |
||||
meson setup builddir |
@ -0,0 +1,39 @@ |
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include "dbc.hpp" |
||||
|
||||
using namespace dbc; |
||||
|
||||
TEST_CASE("basic feature tests", "[utils]") { |
||||
log("Logging a message."); |
||||
|
||||
try { |
||||
sentinel("This shouldn't happen."); |
||||
} catch(SentinelError) { |
||||
log("Sentinel happened."); |
||||
} |
||||
|
||||
pre("confirm positive cases work", 1 == 1); |
||||
pre("confirm positive lambda", [&]{ return 1 == 1;}); |
||||
post("confirm positive post", 1 == 1); |
||||
post("confirm postitive post with lamdba", [&]{ return 1 == 1;}); |
||||
|
||||
check(1 == 1, "one equals 1"); |
||||
|
||||
try { |
||||
check(1 == 2, "this should fail"); |
||||
} catch(CheckError err) { |
||||
log("check fail worked"); |
||||
} |
||||
|
||||
try { |
||||
pre("failing pre", 1 == 3); |
||||
} catch(PreCondError err) { |
||||
log("pre fail worked"); |
||||
} |
||||
|
||||
try { |
||||
post("failing post", 1 == 4); |
||||
} catch(PostCondError err) { |
||||
log("post faile worked"); |
||||
} |
||||
} |
@ -0,0 +1,67 @@ |
||||
#include <catch2/catch_test_macros.hpp> |
||||
#include <fmt/core.h> |
||||
#include <string> |
||||
#include "../fsm.hpp" |
||||
|
||||
using namespace fmt; |
||||
using std::string; |
||||
|
||||
enum class MyState { |
||||
START, RUNNING, END |
||||
}; |
||||
|
||||
enum class MyEvent { |
||||
STARTED, PUSH, QUIT |
||||
}; |
||||
|
||||
class MyFSM : public DeadSimpleFSM<MyState, MyEvent> { |
||||
public: |
||||
void event(MyEvent ev, string data="") { |
||||
switch(_state) { |
||||
FSM_STATE(MyState, START, ev); |
||||
FSM_STATE(MyState, RUNNING, ev, data); |
||||
FSM_STATE(MyState, END, ev); |
||||
} |
||||
} |
||||
|
||||
void START(MyEvent ev) { |
||||
println("<<< START"); |
||||
state(MyState::RUNNING); |
||||
} |
||||
|
||||
void RUNNING(MyEvent ev, string &data) { |
||||
if(ev == MyEvent::QUIT) { |
||||
println("<<< QUITTING {}", data); |
||||
state(MyState::END); |
||||
} else { |
||||
println("<<< RUN: {}", data); |
||||
state(MyState::RUNNING); |
||||
} |
||||
} |
||||
|
||||
void END(MyEvent ev) { |
||||
println("<<< STOP"); |
||||
state(MyState::END); |
||||
} |
||||
}; |
||||
|
||||
TEST_CASE("confirm fsm works with optional data", "[utils]") { |
||||
MyFSM fsm; |
||||
|
||||
REQUIRE(fsm.in_state(MyState::START)); |
||||
|
||||
fsm.event(MyEvent::STARTED); |
||||
REQUIRE(fsm.in_state(MyState::RUNNING)); |
||||
|
||||
fsm.event(MyEvent::PUSH); |
||||
REQUIRE(fsm.in_state(MyState::RUNNING)); |
||||
|
||||
fsm.event(MyEvent::PUSH); |
||||
REQUIRE(fsm.in_state(MyState::RUNNING)); |
||||
|
||||
fsm.event(MyEvent::PUSH); |
||||
REQUIRE(fsm.in_state(MyState::RUNNING)); |
||||
|
||||
fsm.event(MyEvent::QUIT, "DONE!"); |
||||
REQUIRE(fsm.in_state(MyState::END)); |
||||
} |
@ -0,0 +1,221 @@ |
||||
/* |
||||
A very simple Finite State Machine system. If you're struggling with managing |
||||
a mixture of JavaScript API styles (Promise, callback, events) then this will |
||||
help get things organized. The best example is in the `client/components/HLSVideo.svelte` file. |
||||
|
||||
The main advantage an FSM has is it's ability to constrain and organize random events, and it's |
||||
ability to fully log what's happening in a stream of random events. The `HLSVideo.svelte` example |
||||
is a great demonstration of this because it has to juggle events from two different styles of APIs, |
||||
network events, user UI events, and __video__ loading events. Since all of these can come in at |
||||
random the FSM will help order them and do the right thing at the right time. |
||||
|
||||
### Theory |
||||
|
||||
One way to look at a Finite State Machine is an ___explicit if-while-loop___. |
||||
If you have a `while-loop` that is doing a lot of processing based on |
||||
variables, and you're using a lot of `if-statements` or `switch-case` |
||||
statements, then the `FSM` simply formalizes exactly __what__ states and when |
||||
they can run. |
||||
|
||||
For example, if you have a mess of nested `if-statements`, and each one might |
||||
not cover every possible variable, then you'll have bugs as different |
||||
branches of the `if-statements` run or don't run. The `FSM` simply says, "The only |
||||
allowed states at this point in time are X, Y, and Z. The only events I expect are |
||||
event1, event2, and event3. If I get anything else that's an error." |
||||
|
||||
It's a different way to think about data processing, but it is very reliable _and_ |
||||
easy to debug because you can log every event, state, and all the data while processing |
||||
random events. |
||||
|
||||
### FSMs in User Interfaces |
||||
|
||||
Most UIs are simple enough that you can get away with the basic Svelte variables updating |
||||
the state. When UIs get more complex--and especially when they trigger network events--you |
||||
run into problems with random events requiring different updates to the UI. For example, if |
||||
a user clicks on a button that loads a video, but they're already loading a different video, |
||||
then you have to deal with the current click, the old video loading state, and the new video |
||||
playing state. An FSM would be able to "serialize" the click, old video load, and new video |
||||
load and play better than the usual Svelte variables. |
||||
|
||||
In short, if you're struggling with properly updating a user interface as random events come |
||||
in from the user and the network, then switch to an FSM. |
||||
|
||||
### Usage |
||||
|
||||
The `class FSM` takes two things: data and a event transition object. The `data` is only |
||||
for logging and debugging the FSM, as it gets printed out with each log message. The core |
||||
of the FSM is your event handler, which has functions named after each event: |
||||
|
||||
```javascript
|
||||
class VideoEvents { |
||||
async mount(state) { |
||||
switch(state) { |
||||
case "START": |
||||
return "COUNTING_DOWN"; |
||||
default: |
||||
return "ERROR"; |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
In this snippet the `class VideoEvents` has a `async mount` event that works like this: |
||||
|
||||
+ This `mount()` event takes a single `state` which is just a string for the state the FSM is currently in. |
||||
+ You then figure out what to do based on this `state`, typically using a `switch/case`. |
||||
+ Based on what you've done, you return the next state as another string. |
||||
+ The `FSM` then waits for a new event, and calls that function with this new state. |
||||
|
||||
To trigger this `mount` event you would then write: |
||||
|
||||
```javascript
|
||||
await fsm.do("mount"); |
||||
``` |
||||
|
||||
You can pass additional arguments to the `do` function and they will be passed to the |
||||
event as additional arguments: |
||||
|
||||
```javascript
|
||||
// if fsm is in "START" state
|
||||
await fsm.do("mount", 1, 2, 3); |
||||
|
||||
// then becomes
|
||||
ev.mount("START", 1, 2, 3); |
||||
``` |
||||
|
||||
### Logging Events |
||||
|
||||
The `FSM` class will already call `log.debug` on many things, but if you want to do your own logging |
||||
you can use the `onTransition` callback: |
||||
|
||||
```javascript
|
||||
fsm = new FSM(video_config, new VideoEvents()); |
||||
fsm.onTransition(fsm => { |
||||
video_config.state = fsm.state |
||||
log.info("I'm logging something else too", the_other_thing); |
||||
}); |
||||
``` |
||||
|
||||
This is a slight modification of how the FSM is used in `HLSVideo.svelte`. Remember that the `video_config` is only passed to `FSM()` so that it gets logged while the `FSM` is running. This is _incredibly_ useful when debugging why your `FSM` is doing something. |
||||
|
||||
In the `onTransition` callback I'm simply updating the `state` in `video_config` and then I added a little `log.info` to show you could log anything else you wanted there. |
||||
|
||||
### Internal Transitions |
||||
|
||||
If I'm remembering my graduate courses right, this is called an "epsilon transition", but it's |
||||
basically triggering another event inside the current event, effectively changing state without |
||||
waiting for a new event. I could be wrong on the terminology, but you do this by returning an |
||||
`Array[state, cb()]` pair. The `FSM` will then set state to `state` and call your callback so |
||||
you can do further processing. |
||||
|
||||
___BUG___: This could be improved but for now it works. Look at `HLSVideo.svelte:detect_video` for an example of doing this. |
||||
|
||||
### State Names |
||||
|
||||
The first state is "START" by default, but after that you can use any string. I prefer to make |
||||
the states UPPERCASE so they aren't confused with events, which are `lowercase()` function |
||||
names in the event handler class (`VideoEvents` above). |
||||
|
||||
*/ |
||||
import assert from "./assert.js"; |
||||
import { log } from "./logging.js"; |
||||
|
||||
/* |
||||
A very simple Finite State Machine class. Nothing fancy, just a state |
||||
and some transition callbacks based on the function names found in `this.events`. |
||||
*/ |
||||
export class FSM { |
||||
|
||||
/* |
||||
Sets up this FSM to use the `events` callback as the transitions, and then |
||||
"START" as the first state. It does ___not___ call any initial transitions |
||||
so you have to do the first call to `FSM.do()`. The `data` isn't used |
||||
internally and only passed to logging functions for debugging. |
||||
|
||||
### Event Class Signature |
||||
|
||||
The `events` can be anything, and you construct it so it will have your own data. |
||||
The only requirement is that the functions have a `name(state)` signature and that |
||||
`state` is going to be a string. It should then return the next state string for |
||||
the next event to receive. |
||||
|
||||
+ `data Anything` -- Whatever you want to log while debugging. |
||||
+ `events Event Object` -- An object/class that can handle event calls sent to it. |
||||
*/ |
||||
constructor(data, events) { |
||||
// data is really only for logging/debugging
|
||||
this.data = data; |
||||
this.events = events; |
||||
this.state = "START"; |
||||
this.on_state = undefined; |
||||
} |
||||
|
||||
/* |
||||
A callback that runs whenever an event handler returns a state. The `cb` callback |
||||
receives this `FSM` so it can debug it or do more to it. |
||||
|
||||
___WARNING___: This is called called even if the state didn't change, which |
||||
might confuse people if they think it's only when a "transition" happens. |
||||
|
||||
+ `cb(FSM)` -- Callback after the transition. Receives this `FSM`. |
||||
*/ |
||||
onTransition(cb) { |
||||
this.transition_cb = cb; |
||||
} |
||||
|
||||
/* |
||||
Mostly used internally to perform the transition then call the `onTransition` callback, |
||||
but sometimes you might need to force a new state externally. |
||||
|
||||
+ `next_state string` -- The new state name. |
||||
*/ |
||||
transition(next_state) { |
||||
this.state = next_state; |
||||
|
||||
if(this.transition_cb) { |
||||
this.transition_cb(this); |
||||
} |
||||
} |
||||
|
||||
/* |
||||
All event names available in the `this.events` class. |
||||
*/ |
||||
event_names() { |
||||
return Object.getOwnPropertyNames(Object.getPrototypeOf(this.events)).filter(k => k !== "constructor"); |
||||
} |
||||
|
||||
/* |
||||
The core of this `FSM` is the `do()` function. It handles finding the next |
||||
event by it's name, loading it, and calling it with the given additional |
||||
arguments. |
||||
|
||||
+ `event string` -- The __name__ of the target event, not the actual function. It's a string. |
||||
+ `...args` -- Additional arguments to pass to the event. |
||||
+ ___return___ `this.state string` -- The current state of this `FSM`. |
||||
*/ |
||||
async do(event, ...args) { |
||||
const evhandler = this.events[event]; |
||||
assert(evhandler !== undefined, `Invalid event ${event}. Available ones are '${this.event_names()}'.`); |
||||
|
||||
// NOTE: you have to use .call to pass in the this.events object or else this === undefined in the call
|
||||
const next_state = await evhandler.call(this.events, this.state, ...args); |
||||
assert(next_state, `The event "${event}" returned "${next_state}" but must be a truthy true state.`); |
||||
|
||||
if(Array.isArray(next_state)) { |
||||
assert(next_state.length == 2, `Returning an array only allows 2 elements (state, func) but you returned ${next_state}`); |
||||
let [state, func] = next_state; |
||||
|
||||
log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${state} (${args})`, "DATA:", this.data, func ? `THEN ${func.name}()` : undefined); |
||||
|
||||
this.transition(state); |
||||
await func(); |
||||
} else { |
||||
log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${next_state} (${args})`, "DATA:", this.data); |
||||
this.transition(next_state); |
||||
} |
||||
|
||||
return this.state; |
||||
} |
||||
} |
||||
|
||||
export default FSM; |
Loading…
Reference in new issue