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