Compare commits

...

14 Commits

Author SHA1 Message Date
83be3c8826 ci: Use pubcached also for the tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-22 19:51:08 +02:00
48ff0784f4 ci: Only notify on failures
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-22 19:47:40 +02:00
9d9e6ddc1e ci: Fix the CI by using pubcached
Also, notify of CI status
2023-08-22 19:46:30 +02:00
5e867e30ee feat: Move the Result type into moxlib
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
Now, `import 'package:moxlib/moxlib.dart';` is enough.
2023-06-17 21:16:41 +02:00
3d3b047097 feat: Lint and format 2023-06-17 21:14:17 +02:00
7e55015948 feat: Remove list helpers
Use the collection package instead.
2023-06-17 21:06:29 +02:00
2a469e10e8 feat: Lint using very_good_analysis
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-14 14:53:33 +01:00
9bdff5ae01 release: Bump version 2022-11-05 13:21:28 +01:00
c1087fe12e feat: Add listContains and firstWhereOrNull 2022-11-05 13:21:17 +01:00
96edecdf2a ci: Remove dependency on Flutter (Take 2)
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-09-28 14:14:29 +02:00
086e41f2bb ci: Remove dependency on Flutter
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-09-28 14:13:29 +02:00
cad98aab54 ci: Fix duplicate pipeline naming
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-09-28 14:12:20 +02:00
db88155e95 ci: Add Woodpecker CI 2022-09-28 14:10:57 +02:00
b07e526b6a release: Bump 2022-08-20 15:08:28 +02:00
14 changed files with 265 additions and 409 deletions

24
.woodpecker.yml Normal file
View File

@@ -0,0 +1,24 @@
pipeline:
lint:
image: dart:3.0.7
commands:
# Proxy requests to pub.dev using pubcached
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart analyze --fatal-infos --fatal-warnings
test:
image: dart:3.0.7
commands:
# Proxy requests to pub.dev using pubcached
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart test
notify:
image: git.polynom.me/papatutuwawa/woodpecker-xmpp
settings:
xmpp_tls: 1
xmpp_is_muc: 1
xmpp_recipient: moxxy-build@muc.moxxy.org
xmpp_alias: 2Bot
secrets: [ xmpp_jid, xmpp_password, xmpp_server ]
when:
status:
- failure

View File

@@ -1,4 +1,8 @@
include: package:flutter_lints/flutter.yaml include: package:very_good_analysis/analysis_options.yaml
linter:
# Additional information about this file can be found at rules:
# https://dart.dev/guides/language/analysis-options 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

18
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1649676176, "lastModified": 1656065134,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", "narHash": "sha256-oc6E6ByIw3oJaIyc67maaFcnjYOz1mMcOtHxbEf9NwQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", "rev": "bee6a7250dd1b01844a2de7e02e4df7d8a0a206c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,16 +17,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1650034868, "lastModified": 1692311226,
"narHash": "sha256-OAaf5BdWKGXTXvYnbvJuoQjSWnVKgt1cIOChF0MFt2o=", "narHash": "sha256-mRzNup0PIUD6YxbrYvjzL7f+1oaOGy9nmGCV3AZkQus=",
"owner": "PapaTutuWawa", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "13a5646d450052b88067cab37b198f8a2737e431", "rev": "ef8288935ba859fc3b30632fa6e04705f81b9c2a",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "PapaTutuWawa", "owner": "NixOS",
"ref": "nixos-unstable-flutter-2.13.0-0.1.pre", "ref": "nixpkgs-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@@ -1,43 +1,21 @@
{ {
description = "moxlib"; description = "moxlib";
inputs = { inputs = {
nixpkgs.url = "github:PapaTutuWawa/nixpkgs/nixos-unstable-flutter-2.13.0-0.1.pre"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
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.android_sdk.accept_license = true;
}; };
android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these
#toolsVersion = "26.1.1";
#platformToolsVersion = "31.0.3";
#buildToolsVersions = [ "31.0.0" ];
#includeEmulator = true;
#emulatorVersion = "30.6.3";
platformVersions = [ "28" ];
includeSources = false;
includeSystemImages = true;
systemImageTypes = [ "default" ];
abiVersions = [ "x86_64" ];
includeNDK = false;
useGoogleAPIs = false;
useGoogleTVAddOns = false;
};
pinnedJDK = pkgs.jdk11;
in { in {
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutterPackages.beta pinnedJDK android.platform-tools flutterPackages.dart-beta # Flutter dart # Dart
gitlint jq # Code hygiene gitlint jq # Code hygiene
ripgrep # General utilities ripgrep # General utilities
]; ];
ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
JAVA_HOME = pinnedJDK;
ANDROID_AVD_HOME = (toString ./.) + "/.android/avd";
}; };
}); });
} }

View File

@@ -1,99 +0,0 @@
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);
}
}

View File

@@ -1,125 +0,0 @@
import "dart:async";
import "package:synchronized/synchronized.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
/// JSON serialized/deserialized.
class JsonImplementation {
JsonImplementation();
Map<String, dynamic> toJson() => {};
factory JsonImplementation.fromJson(Map<String, dynamic> json) {
return JsonImplementation();
}
}
/// Wrapper class that adds an ID to the data packet to be sent.
class DataWrapper<T extends JsonImplementation> {
final String id;
final T data;
const DataWrapper(
this.id,
this.data
);
Map<String, dynamic> toJson() => {
"id": id,
"data": data.toJson()
};
static DataWrapper fromJson<T extends JsonImplementation>(Map<String, dynamic> json) => DataWrapper<T>(
json["id"]! as String,
json["data"]! as T
);
DataWrapper reply(T newData) => DataWrapper(id, newData);
}
/// This class is useful in contexts where data is sent between two parties, e.g. the
/// UI and the background service and a correlation between requests and responses is
/// to be enabled.
///
/// awaiting [sendData] will return a [Future] that will resolve to the reresponse when
/// received via [onData].
abstract class AwaitableDataSender<
S extends JsonImplementation,
R extends JsonImplementation
> {
final Lock _lock;
final Map<String, Completer<R>> _awaitables;
final Uuid _uuid;
final Logger _log;
@mustCallSuper
AwaitableDataSender() : _awaitables = {}, _uuid = const Uuid(), _lock = Lock(), _log = Logger("AwaitableDataSender");
@visibleForTesting
Map<String, Completer> getAwaitables() => _awaitables;
/// Called after an awaitable has been added.
@visibleForTesting
void onAdd() {}
/// NOTE: Must be overwritten by the actual implementation
@visibleForOverriding
Future<void> sendDataImpl(DataWrapper data);
/// Sends [data] using [sendDataImpl]. If [awaitable] is true, then a
/// Future will be returned that can be used to await a response. If it
/// is false, then null will be imediately resolved.
Future<R?> sendData(S data, { bool awaitable = true, @visibleForTesting String? id }) async {
final _id = id ?? _uuid.v4();
Future<R?> future = Future.value(null);
_log.fine("sendData: Waiting to acquire lock...");
await _lock.synchronized(() async {
_log.fine("sendData: Done");
if (awaitable) {
_awaitables[_id] = Completer();
onAdd();
}
await sendDataImpl(
DataWrapper<S>(
_id,
data
)
);
if (awaitable) {
future = _awaitables[_id]!.future;
}
_log.fine("sendData: Releasing lock...");
});
return future;
}
/// Should be called when a [DataWrapper] has been received. Will resolve
/// the promise received from [sendData].
Future<bool> onData(DataWrapper<R> data) async {
bool found = false;
Completer? completer;
_log.fine("onData: Waiting to acquire lock...");
await _lock.synchronized(() async {
_log.fine("onData: Done");
completer = _awaitables[data.id];
if (completer != null) {
_awaitables.remove(data.id);
found = true;
}
_log.fine("onData: Releasing lock");
});
if (found) {
completer!.complete(data.data);
}
return found;
}
}

View File

@@ -1,5 +1,5 @@
library moxlib; library moxlib;
export "awaitabledatasender.dart"; export 'src/awaitabledatasender.dart';
export "automaton.dart"; export 'src/math.dart';
export "math.dart"; export 'src/result.dart';

View File

@@ -0,0 +1,136 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
import 'package:uuid/uuid.dart';
/// Interface to allow arbitrary data to be sent as long as it can be
/// JSON serialized/deserialized.
class JsonImplementation {
JsonImplementation();
// ignore: avoid_unused_constructor_parameters
factory JsonImplementation.fromJson(Map<String, dynamic> json) {
return JsonImplementation();
}
Map<String, dynamic> toJson() => {};
}
/// Wrapper class that adds an ID to the data packet to be sent.
class DataWrapper<T extends JsonImplementation> {
const DataWrapper(
this.id,
this.data,
);
/// The id of the data packet.
final String id;
/// The actual data.
final T data;
Map<String, dynamic> toJson() => {'id': id, 'data': data.toJson()};
static DataWrapper fromJson<T extends JsonImplementation>(
Map<String, dynamic> json,
) =>
DataWrapper<T>(
json['id']! as String,
json['data']! as T,
);
DataWrapper reply(T newData) => DataWrapper(id, newData);
}
/// This class is useful in contexts where data is sent between two parties, e.g. the
/// UI and the background service and a correlation between requests and responses is
/// to be enabled.
///
/// awaiting [sendData] will return a [Future] that will resolve to the reresponse when
/// received via [onData].
abstract class AwaitableDataSender<S extends JsonImplementation,
R extends JsonImplementation> {
@mustCallSuper
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
Map<String, Completer<R>> getAwaitables() => _awaitables;
/// Called after an awaitable has been added.
@visibleForTesting
void onAdd() {}
/// NOTE: Must be overwritten by the actual implementation
@visibleForOverriding
Future<void> sendDataImpl(DataWrapper data);
/// Sends [data] using [sendDataImpl]. If [awaitable] is true, then a
/// Future will be returned that can be used to await a response. If it
/// is false, then null will be imediately resolved.
Future<R?> sendData(
S data, {
bool awaitable = true,
@visibleForTesting String? id,
}) async {
// ignore: no_leading_underscores_for_local_identifiers
final _id = id ?? _uuid.v4();
var future = Future<R?>.value();
_log.fine('sendData: Waiting to acquire lock...');
await _lock.synchronized(() async {
_log.fine('sendData: Done');
if (awaitable) {
_awaitables[_id] = Completer();
onAdd();
}
await sendDataImpl(
DataWrapper<S>(
_id,
data,
),
);
if (awaitable) {
future = _awaitables[_id]!.future;
}
_log.fine('sendData: Releasing lock...');
});
return future;
}
/// Should be called when a [DataWrapper] has been received. Will resolve
/// the promise received from [sendData].
Future<bool> onData(DataWrapper<R> data) async {
_log.fine('onData: Waiting to acquire lock...');
final completer = await _lock.synchronized(() async {
_log.fine('onData: Done');
final c = _awaitables[data.id];
if (c != null) {
_awaitables.remove(data.id);
return c;
}
_log.fine('onData: Releasing lock');
return null;
});
completer?.complete(data.data);
return completer != null;
}
}

24
lib/src/result.dart Normal file
View File

@@ -0,0 +1,24 @@
/// Holds a value of either [T] or [V].
class Result<T, V> {
/// Constructs a result. [_data] must be either of type [T] or [V].
const Result(this._data)
: assert(
_data is T || _data is V,
'Invalid data type $_data: Must be either $T or $V',
);
final dynamic _data;
/// Returns true if the data contained within is of type [S]. If not, returns false.
bool isType<S>() => _data is S;
/// Returns the data contained within cast to [S]. Before doing this call, it's recommended
/// to check isType<S>() first.
S get<S>() {
assert(_data is S, 'Data is not $S');
return _data as S;
}
/// Returns the runtime type of the data.
Object get dataRuntimeType => _data.runtimeType;
}

View File

@@ -1,21 +1,19 @@
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 intended for outside use.
version: 0.1.3 version: 0.2.0
homepage: https://codeberg.org/moxxy/moxlib homepage: https://codeberg.org/moxxy/moxlib
publish_to: https://pub.polynom.me publish_to: https://git.polynom.me/api/packages/Moxxy/pub
environment: environment:
sdk: ">=2.17.0-266.1.beta <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.17.0"
dependencies: dependencies:
flutter: logging: ^1.0.2
sdk: flutter meta: ^1.7.0
logging: 1.0.2 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: ^5.0.0

View File

@@ -1,79 +0,0 @@
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);
});
}

View File

@@ -1,29 +1,23 @@
import "package:moxlib/awaitabledatasender.dart"; import 'package:moxlib/moxlib.dart';
import 'package:test/test.dart';
import "package:test/test.dart";
class TestDataType implements JsonImplementation { class TestDataType implements JsonImplementation {
final String data;
TestDataType(this.data); TestDataType(this.data);
@override factory TestDataType.fromJson(Map<String, dynamic> json) =>
Map<String, dynamic> toJson() => { TestDataType(json['data']! as String);
"data": data
};
factory TestDataType.fromJson(Map<String, dynamic> json) => TestDataType( final String data;
json["data"]!
); @override
Map<String, dynamic> toJson() => {'data': data};
} }
class FakeAwaitableDataSender< class FakeAwaitableDataSender<S extends JsonImplementation,
S extends JsonImplementation, R extends JsonImplementation> extends AwaitableDataSender<S, R> {
R extends JsonImplementation FakeAwaitableDataSender({this.onAddFunc}) : super();
> extends AwaitableDataSender<S, R> {
final void Function()? onAddFunc;
FakeAwaitableDataSender({ this.onAddFunc }) : super(); final void Function()? onAddFunc;
@override @override
Future<void> sendDataImpl(DataWrapper data) async {} Future<void> sendDataImpl(DataWrapper data) async {}
@@ -35,39 +29,41 @@ class FakeAwaitableDataSender<
} }
void main() { void main() {
test("Sending an event without awaiting it", () async { test('Sending an event without awaiting it', () async {
final handler = FakeAwaitableDataSender<TestDataType, TestDataType>(); final handler = FakeAwaitableDataSender<TestDataType, TestDataType>();
final result = await handler.sendData(TestDataType("hallo"), awaitable: false); final result =
await handler.sendData(TestDataType('hallo'), awaitable: false);
expect(result, null); expect(result, null);
expect(handler.getAwaitables().length, 0); expect(handler.getAwaitables().length, 0);
}); });
test("Sending an event without awaiting it", () async { test('Sending an event without awaiting it', () async {
final handler = FakeAwaitableDataSender<TestDataType, TestDataType>(); final handler = FakeAwaitableDataSender<TestDataType, TestDataType>();
const id = "abc123"; const id = 'abc123';
final result = handler.sendData(TestDataType("hallo"), awaitable: true, id: id); final result =
await handler.onData(DataWrapper(id, TestDataType("welt"))); handler.sendData(TestDataType('hallo'), awaitable: true, id: id);
await handler.onData(DataWrapper(id, TestDataType('welt')));
expect((await result)!.data, "welt"); expect((await result)!.data, 'welt');
expect(handler.getAwaitables().length, 0); expect(handler.getAwaitables().length, 0);
}); });
test("Queue multiple data packets and resolve in reverse order", () async { test('Queue multiple data packets and resolve in reverse order', () async {
int i = 0; var i = 0;
final handler = FakeAwaitableDataSender<TestDataType, TestDataType>( final handler = FakeAwaitableDataSender<TestDataType, TestDataType>(
onAddFunc: () { onAddFunc: () {
i++; i++;
expect(i <= 2, true); expect(i <= 2, true);
} },
); );
final a = handler.sendData(TestDataType("1"), id: "1"); final a = handler.sendData(TestDataType('1'), id: '1');
final b = handler.sendData(TestDataType("2"), id: "2"); final b = handler.sendData(TestDataType('2'), id: '2');
await handler.onData(DataWrapper("2", TestDataType("4"))); await handler.onData(DataWrapper('2', TestDataType('4')));
await handler.onData(DataWrapper("1", TestDataType("1"))); await handler.onData(DataWrapper('1', TestDataType('1')));
expect((await a)!.data, "1"); expect((await a)!.data, '1');
expect((await b)!.data, "4"); expect((await b)!.data, '4');
}); });
} }

View File

@@ -1,10 +1,9 @@
import "package:moxlib/math.dart"; import 'package:moxlib/moxlib.dart';
import 'package:test/test.dart';
import "package:test/test.dart";
void main() { void main() {
group("implies", () { group('implies', () {
test("Truth table test", () { test('Truth table test', () {
expect(implies(true, true), true); expect(implies(true, true), true);
expect(implies(true, false), false); expect(implies(true, false), false);
expect(implies(false, true), true); expect(implies(false, true), true);