Compare commits
No commits in common. "2a469e10e819c9b8866b94805c908c86b25f0486" and "96edecdf2aa5539bea162d6a998ce429acd7aa74" have entirely different histories.
2a469e10e8
...
96edecdf2a
@ -1,15 +1,4 @@
|
|||||||
include: package:very_good_analysis/analysis_options.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
public_member_api_docs: false
|
|
||||||
lines_longer_than_80_chars: false
|
|
||||||
use_setters_to_change_properties: false
|
|
||||||
avoid_positional_boolean_parameters: false
|
|
||||||
avoid_bool_literals_in_conditional_expressions: false
|
|
||||||
|
|
||||||
analyzer:
|
# Additional information about this file can be found at
|
||||||
exclude:
|
# https://dart.dev/guides/language/analysis-options
|
||||||
- "**/*.g.dart"
|
|
||||||
- "**/*.freezed.dart"
|
|
||||||
- "test/"
|
|
||||||
- "integration_test/"
|
|
||||||
|
18
flake.lock
18
flake.lock
@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1667395993,
|
"lastModified": 1649676176,
|
||||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -17,16 +17,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1667610399,
|
"lastModified": 1650034868,
|
||||||
"narHash": "sha256-XZd0f4ZWAY0QOoUSdiNWj/eFiKb4B9CJPtl9uO9SYY4=",
|
"narHash": "sha256-OAaf5BdWKGXTXvYnbvJuoQjSWnVKgt1cIOChF0MFt2o=",
|
||||||
"owner": "NixOS",
|
"owner": "PapaTutuWawa",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1dd8696f96db47156e1424a49578fe7dd4ce99a4",
|
"rev": "13a5646d450052b88067cab37b198f8a2737e431",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "PapaTutuWawa",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable-flutter-2.13.0-0.1.pre",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
11
flake.nix
11
flake.nix
@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
description = "moxlib";
|
description = "moxlib";
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:PapaTutuWawa/nixpkgs/nixos-unstable-flutter-2.13.0-0.1.pre";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = {
|
config.android_sdk.accept_license = true;
|
||||||
android_sdk.accept_license = true;
|
|
||||||
allowUnfree = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
android = pkgs.androidenv.composeAndroidPackages {
|
android = pkgs.androidenv.composeAndroidPackages {
|
||||||
# TODO: Find a way to pin these
|
# TODO: Find a way to pin these
|
||||||
@ -29,11 +26,11 @@
|
|||||||
useGoogleAPIs = false;
|
useGoogleAPIs = false;
|
||||||
useGoogleTVAddOns = false;
|
useGoogleTVAddOns = false;
|
||||||
};
|
};
|
||||||
pinnedJDK = pkgs.jdk;
|
pinnedJDK = pkgs.jdk11;
|
||||||
in {
|
in {
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
flutter pinnedJDK android.platform-tools dart # Flutter
|
flutterPackages.beta pinnedJDK android.platform-tools flutterPackages.dart-beta # Flutter
|
||||||
gitlint jq # Code hygiene
|
gitlint jq # Code hygiene
|
||||||
ripgrep # General utilities
|
ripgrep # General utilities
|
||||||
];
|
];
|
||||||
|
99
lib/automaton.dart
Normal file
99
lib/automaton.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
class NoTransitionPossibleException implements Exception {
|
||||||
|
@override
|
||||||
|
String errMsg() => "The transition graph allows no transition";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deterministic finite automaton. [T] is the state type while
|
||||||
|
/// [I] is the input type.
|
||||||
|
/// Edges of the node must be added with [addTransition]. If a trap state
|
||||||
|
/// is required, it can be set in the constructor.
|
||||||
|
class DeterministicFiniteAutomaton<T, I> {
|
||||||
|
/// The current state of the DFA
|
||||||
|
T _state;
|
||||||
|
/// The edges of the DFA: State x Input -> State
|
||||||
|
Map<T, Map<I, T>> _transitions;
|
||||||
|
/// Trap state
|
||||||
|
T? trapState;
|
||||||
|
|
||||||
|
/// The argument is the initial state
|
||||||
|
DeterministicFiniteAutomaton(this._state, { this.trapState }) : _transitions = {};
|
||||||
|
|
||||||
|
T get state => _state;
|
||||||
|
|
||||||
|
void addTransition(T oldState, I input, T newState) {
|
||||||
|
assert(oldState != trapState);
|
||||||
|
// These are handled implicitly if no transition has been found
|
||||||
|
assert(newState != trapState);
|
||||||
|
|
||||||
|
if (!_transitions.containsKey(oldState)) {
|
||||||
|
_transitions[oldState] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_transitions[oldState]![input] = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition the DFA based on its current state and the input [input].
|
||||||
|
void onInput(I input) {
|
||||||
|
final newState = _transitions[_state]?[input];
|
||||||
|
if (newState == null) {
|
||||||
|
// Go to the trap state if we can
|
||||||
|
if (trapState != null) {
|
||||||
|
_state = trapState!;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw NoTransitionPossibleException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_state = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns where [input] would take the automaton to. Returns null if no transition
|
||||||
|
/// is possible, ignoring trap transitions.
|
||||||
|
T? peekTransition(I input) {
|
||||||
|
if (!_transitions.containsKey(_state) || !_transitions[_state]!.containsKey(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _transitions[_state]![input]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef MealyAutomatonCallback<T, I> = void Function(T oldState, I input);
|
||||||
|
class MealyAutomaton<T, I> {
|
||||||
|
/// The base automaton
|
||||||
|
final DeterministicFiniteAutomaton<T, I> _automaton;
|
||||||
|
/// Mapping of State x Input -> Output callback
|
||||||
|
Map<T, Map<I, MealyAutomatonCallback<T, I>>> _outputs;
|
||||||
|
/// Trap state
|
||||||
|
MealyAutomatonCallback<T, I>? trapCallback;
|
||||||
|
|
||||||
|
// TODO: Assert that trapState != null implies trapCallback != null.
|
||||||
|
MealyAutomaton(T initialState, { T? trapState, this.trapCallback })
|
||||||
|
: _outputs = {},
|
||||||
|
_automaton = DeterministicFiniteAutomaton(initialState, trapState: trapState);
|
||||||
|
|
||||||
|
T get state => _automaton.state;
|
||||||
|
|
||||||
|
void addTransition(T oldState, I input, T newState, MealyAutomatonCallback<T, I> callback) {
|
||||||
|
_automaton.addTransition(oldState, input, newState);
|
||||||
|
|
||||||
|
if (!_outputs.containsKey(oldState)) {
|
||||||
|
_outputs[oldState] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_outputs[oldState]![input] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onInput(I input) {
|
||||||
|
final _state = _automaton.state;
|
||||||
|
if (_automaton.peekTransition(input) == null && trapCallback == null) {
|
||||||
|
throw new NoTransitionPossibleException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final callback = _outputs[_state]?[input] ?? trapCallback!;
|
||||||
|
|
||||||
|
_automaton.onInput(input);
|
||||||
|
callback(_state, input);
|
||||||
|
}
|
||||||
|
}
|
@ -1,43 +1,39 @@
|
|||||||
import 'dart:async';
|
import "dart:async";
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:meta/meta.dart';
|
import "package:synchronized/synchronized.dart";
|
||||||
import 'package:synchronized/synchronized.dart';
|
import "package:uuid/uuid.dart";
|
||||||
import 'package:uuid/uuid.dart';
|
import "package:logging/logging.dart";
|
||||||
|
import "package:meta/meta.dart";
|
||||||
|
|
||||||
/// Interface to allow arbitrary data to be sent as long as it can be
|
/// Interface to allow arbitrary data to be sent as long as it can be
|
||||||
/// JSON serialized/deserialized.
|
/// JSON serialized/deserialized.
|
||||||
class JsonImplementation {
|
class JsonImplementation {
|
||||||
JsonImplementation();
|
JsonImplementation();
|
||||||
|
|
||||||
// ignore: avoid_unused_constructor_parameters
|
Map<String, dynamic> toJson() => {};
|
||||||
factory JsonImplementation.fromJson(Map<String, dynamic> json) {
|
factory JsonImplementation.fromJson(Map<String, dynamic> json) {
|
||||||
return JsonImplementation();
|
return JsonImplementation();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper class that adds an ID to the data packet to be sent.
|
/// Wrapper class that adds an ID to the data packet to be sent.
|
||||||
class DataWrapper<T extends JsonImplementation> {
|
class DataWrapper<T extends JsonImplementation> {
|
||||||
const DataWrapper(
|
|
||||||
this.id,
|
|
||||||
this.data,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The id of the data packet.
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
/// The actual data.
|
|
||||||
final T data;
|
final T data;
|
||||||
|
|
||||||
|
const DataWrapper(
|
||||||
|
this.id,
|
||||||
|
this.data
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
"id": id,
|
||||||
'data': data.toJson()
|
"data": data.toJson()
|
||||||
};
|
};
|
||||||
|
|
||||||
static DataWrapper fromJson<T extends JsonImplementation>(Map<String, dynamic> json) => DataWrapper<T>(
|
static DataWrapper fromJson<T extends JsonImplementation>(Map<String, dynamic> json) => DataWrapper<T>(
|
||||||
json['id']! as String,
|
json["id"]! as String,
|
||||||
json['data']! as T,
|
json["data"]! as T
|
||||||
);
|
);
|
||||||
|
|
||||||
DataWrapper reply(T newData) => DataWrapper(id, newData);
|
DataWrapper reply(T newData) => DataWrapper(id, newData);
|
||||||
@ -53,23 +49,16 @@ abstract class AwaitableDataSender<
|
|||||||
S extends JsonImplementation,
|
S extends JsonImplementation,
|
||||||
R extends JsonImplementation
|
R extends JsonImplementation
|
||||||
> {
|
> {
|
||||||
|
final Lock _lock;
|
||||||
|
final Map<String, Completer<R>> _awaitables;
|
||||||
|
final Uuid _uuid;
|
||||||
|
final Logger _log;
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
AwaitableDataSender();
|
AwaitableDataSender() : _awaitables = {}, _uuid = const Uuid(), _lock = Lock(), _log = Logger("AwaitableDataSender");
|
||||||
|
|
||||||
/// A mapping of ID to completer for pending requests.
|
|
||||||
final Map<String, Completer<R>> _awaitables = {};
|
|
||||||
|
|
||||||
/// Critical section for accessing [AwaitableDataSender._awaitables].
|
|
||||||
final Lock _lock = Lock();
|
|
||||||
|
|
||||||
/// A UUID object for generating UUIDs.
|
|
||||||
final Uuid _uuid = const Uuid();
|
|
||||||
|
|
||||||
/// A logger.
|
|
||||||
final Logger _log = Logger('AwaitableDataSender');
|
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
Map<String, Completer<R>> getAwaitables() => _awaitables;
|
Map<String, Completer> getAwaitables() => _awaitables;
|
||||||
|
|
||||||
/// Called after an awaitable has been added.
|
/// Called after an awaitable has been added.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@ -83,12 +72,11 @@ abstract class AwaitableDataSender<
|
|||||||
/// Future will be returned that can be used to await a response. If it
|
/// Future will be returned that can be used to await a response. If it
|
||||||
/// is false, then null will be imediately resolved.
|
/// is false, then null will be imediately resolved.
|
||||||
Future<R?> sendData(S data, { bool awaitable = true, @visibleForTesting String? id }) async {
|
Future<R?> sendData(S data, { bool awaitable = true, @visibleForTesting String? id }) async {
|
||||||
// ignore: no_leading_underscores_for_local_identifiers
|
|
||||||
final _id = id ?? _uuid.v4();
|
final _id = id ?? _uuid.v4();
|
||||||
var future = Future<R?>.value();
|
Future<R?> future = Future.value(null);
|
||||||
_log.fine('sendData: Waiting to acquire lock...');
|
_log.fine("sendData: Waiting to acquire lock...");
|
||||||
await _lock.synchronized(() async {
|
await _lock.synchronized(() async {
|
||||||
_log.fine('sendData: Done');
|
_log.fine("sendData: Done");
|
||||||
if (awaitable) {
|
if (awaitable) {
|
||||||
_awaitables[_id] = Completer();
|
_awaitables[_id] = Completer();
|
||||||
onAdd();
|
onAdd();
|
||||||
@ -97,15 +85,15 @@ abstract class AwaitableDataSender<
|
|||||||
await sendDataImpl(
|
await sendDataImpl(
|
||||||
DataWrapper<S>(
|
DataWrapper<S>(
|
||||||
_id,
|
_id,
|
||||||
data,
|
data
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (awaitable) {
|
if (awaitable) {
|
||||||
future = _awaitables[_id]!.future;
|
future = _awaitables[_id]!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine('sendData: Releasing lock...');
|
_log.fine("sendData: Releasing lock...");
|
||||||
});
|
});
|
||||||
|
|
||||||
return future;
|
return future;
|
||||||
@ -114,21 +102,24 @@ abstract class AwaitableDataSender<
|
|||||||
/// Should be called when a [DataWrapper] has been received. Will resolve
|
/// Should be called when a [DataWrapper] has been received. Will resolve
|
||||||
/// the promise received from [sendData].
|
/// the promise received from [sendData].
|
||||||
Future<bool> onData(DataWrapper<R> data) async {
|
Future<bool> onData(DataWrapper<R> data) async {
|
||||||
_log.fine('onData: Waiting to acquire lock...');
|
bool found = false;
|
||||||
final completer = await _lock.synchronized(() async {
|
Completer? completer;
|
||||||
_log.fine('onData: Done');
|
_log.fine("onData: Waiting to acquire lock...");
|
||||||
final c = _awaitables[data.id];
|
await _lock.synchronized(() async {
|
||||||
if (c != null) {
|
_log.fine("onData: Done");
|
||||||
|
completer = _awaitables[data.id];
|
||||||
|
if (completer != null) {
|
||||||
_awaitables.remove(data.id);
|
_awaitables.remove(data.id);
|
||||||
return c;
|
found = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine('onData: Releasing lock');
|
_log.fine("onData: Releasing lock");
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
completer?.complete(data.data);
|
if (found) {
|
||||||
|
completer!.complete(data.data);
|
||||||
|
}
|
||||||
|
|
||||||
return completer != null;
|
return found;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
/// A wrapper around List<T>.firstWhere that does not throw but instead just
|
|
||||||
/// returns true if [test] returns true for an element or false if [test] never
|
|
||||||
/// returned true.
|
|
||||||
bool listContains<T>(List<T> list, bool Function(T element) test) {
|
|
||||||
return firstWhereOrNull<T>(list, test) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper around [List<T>.firstWhere] that does not throw but instead just
|
|
||||||
/// return null if [test] never returned true
|
|
||||||
T? firstWhereOrNull<T>(List<T> list, bool Function(T element) test) {
|
|
||||||
try {
|
|
||||||
return list.firstWhere(test);
|
|
||||||
} catch(e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
library moxlib;
|
library moxlib;
|
||||||
|
|
||||||
export 'awaitabledatasender.dart';
|
export "awaitabledatasender.dart";
|
||||||
export 'lists.dart';
|
export "automaton.dart";
|
||||||
export 'math.dart';
|
export "math.dart";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: moxlib
|
name: moxlib
|
||||||
description: A collection of code for sharing between various moxxy libraries. Not inteded for outside use.
|
description: A collection of code for sharing between various moxxy libraries. Not inteded for outside use.
|
||||||
version: 0.1.5
|
version: 0.1.4
|
||||||
homepage: https://codeberg.org/moxxy/moxlib
|
homepage: https://codeberg.org/moxxy/moxlib
|
||||||
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
|
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
|
|
||||||
@ -9,11 +9,10 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
logging: ^1.0.2
|
logging: ^1.0.2
|
||||||
meta: ^1.7.0
|
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
uuid: ^3.0.5
|
uuid: ^3.0.5
|
||||||
|
meta: ^1.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
test: ^1.20.1
|
test: ^1.20.1
|
||||||
very_good_analysis: ^3.0.1
|
|
||||||
|
79
test/automaton_test.dart
Normal file
79
test/automaton_test.dart
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import "package:moxlib/automaton.dart";
|
||||||
|
|
||||||
|
import "package:test/test.dart";
|
||||||
|
|
||||||
|
enum States {
|
||||||
|
a, b, c, trap
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test("Test a simple DFA", () {
|
||||||
|
final automaton = DeterministicFiniteAutomaton<States, int>(States.a);
|
||||||
|
automaton.addTransition(States.a, 1, States.b);
|
||||||
|
automaton.addTransition(States.b, 2, States.c);
|
||||||
|
automaton.addTransition(States.c, 3, States.a);
|
||||||
|
|
||||||
|
expect(automaton.state, States.a);
|
||||||
|
automaton.onInput(1);
|
||||||
|
expect(automaton.state, States.b);
|
||||||
|
automaton.onInput(2);
|
||||||
|
expect(automaton.state, States.c);
|
||||||
|
automaton.onInput(3);
|
||||||
|
expect(automaton.state, States.a);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test a simple DFA with a trap state", () {
|
||||||
|
final automaton = DeterministicFiniteAutomaton<States, int>(States.a, trapState: States.trap);
|
||||||
|
automaton.addTransition(States.a, 1, States.b);
|
||||||
|
automaton.addTransition(States.b, 2, States.c);
|
||||||
|
automaton.addTransition(States.c, 3, States.a);
|
||||||
|
|
||||||
|
expect(automaton.state, States.a);
|
||||||
|
automaton.onInput(1);
|
||||||
|
expect(automaton.state, States.b);
|
||||||
|
automaton.onInput(2);
|
||||||
|
expect(automaton.state, States.c);
|
||||||
|
automaton.onInput(4);
|
||||||
|
expect(automaton.state, States.trap);
|
||||||
|
|
||||||
|
// Transitioning away from the trap state should not be possible
|
||||||
|
automaton.onInput(5);
|
||||||
|
expect(automaton.state, States.trap);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test a simple Mealy Automaton", () {
|
||||||
|
bool called = false;
|
||||||
|
final callback = (state, input) {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
final automaton = MealyAutomaton<States, int>(States.a);
|
||||||
|
|
||||||
|
automaton.addTransition(States.a, 1, States.b, callback);
|
||||||
|
|
||||||
|
automaton.onInput(1);
|
||||||
|
|
||||||
|
expect(automaton.state, States.b);
|
||||||
|
expect(called, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test a simple Mealy Automaton with a trap state", () {
|
||||||
|
bool called = false;
|
||||||
|
bool trapCalled = false;
|
||||||
|
final callback = (state, input) {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
final trapCallback = (state, input) {
|
||||||
|
trapCalled = true;
|
||||||
|
};
|
||||||
|
final automaton = MealyAutomaton<States, int>(States.a, trapState: States.trap, trapCallback: trapCallback);
|
||||||
|
|
||||||
|
automaton.addTransition(States.a, 1, States.b, callback);
|
||||||
|
|
||||||
|
automaton.onInput(1);
|
||||||
|
expect(called, true);
|
||||||
|
|
||||||
|
automaton.onInput(1);
|
||||||
|
expect(automaton.state, States.trap);
|
||||||
|
expect(trapCalled, true);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user