SMP protocol commands encoding/decoding (#111)
* SMP protocol commands encoding/decoding * change "var" to type * Parser `word` method now returns null if the word is empty * refactor Parser `word` method * move Parser `end` getter * add linter rules, move linter options to root * remove omit_local_variable_types linter rule * ci: only build haskell on changes
This commit is contained in:
parent
fec99d526a
commit
a39cd2990f
@ -7,6 +7,9 @@ on:
|
||||
- v5
|
||||
tags:
|
||||
- "v*"
|
||||
paths:
|
||||
- haskell/**
|
||||
- .github/workflows/build_haskell.yml
|
||||
pull_request:
|
||||
|
||||
jobs:
|
@ -23,7 +23,8 @@ linter:
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
prefer_double_quotes: true
|
||||
constant_identifier_names: false
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
44
packages/analysis_options.yaml
Normal file
44
packages/analysis_options.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_double_quotes: true
|
||||
constant_identifier_names: false
|
||||
always_declare_return_types: true
|
||||
avoid_dynamic_calls: true
|
||||
avoid_empty_else: true
|
||||
avoid_relative_lib_imports: true
|
||||
avoid_shadowing_type_parameters: true
|
||||
avoid_slow_async_io: true
|
||||
avoid_types_as_parameter_names: true
|
||||
await_only_futures: true
|
||||
camel_case_extensions: true
|
||||
camel_case_types: true
|
||||
cancel_subscriptions: true
|
||||
curly_braces_in_flow_control_structures: true
|
||||
directives_ordering: true
|
||||
empty_catches: true
|
||||
hash_and_equals: true
|
||||
iterable_contains_unrelated_type: true
|
||||
list_remove_unrelated_type: true
|
||||
no_adjacent_strings_in_list: true
|
||||
no_duplicate_case_values: true
|
||||
package_api_docs: true
|
||||
package_prefixed_library_names: true
|
||||
prefer_generic_function_type_aliases: true
|
||||
prefer_is_empty: true
|
||||
prefer_is_not_empty: true
|
||||
prefer_iterable_whereType: true
|
||||
prefer_typing_uninitialized_variables: true
|
||||
sort_child_properties_last: true
|
||||
test_types_in_equals: true
|
||||
throw_in_finally: true
|
||||
unawaited_futures: true
|
||||
unnecessary_import: true
|
||||
unnecessary_null_aware_assignments: true
|
||||
unnecessary_statements: true
|
||||
unnecessary_type_check: true
|
||||
unrelated_type_equality_checks: true
|
||||
unsafe_html: true
|
||||
use_full_hex_values_for_flutter_colors: true
|
||||
valid_regexps: true
|
10
packages/simplexmq/.gitignore
vendored
Normal file
10
packages/simplexmq/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build outputs.
|
||||
build/
|
||||
|
||||
# Omit committing pubspec.lock for library packages; see
|
||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
pubspec.lock
|
3
packages/simplexmq/CHANGELOG.md
Normal file
3
packages/simplexmq/CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
39
packages/simplexmq/README.md
Normal file
39
packages/simplexmq/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
6
packages/simplexmq/example/simplexmq_example.dart
Normal file
6
packages/simplexmq/example/simplexmq_example.dart
Normal file
@ -0,0 +1,6 @@
|
||||
// import 'package:simplexmq/simplexmq.dart';
|
||||
|
||||
// void main() {
|
||||
// var awesome = Awesome();
|
||||
// print('awesome: ${awesome.isAwesome}');
|
||||
// }
|
8
packages/simplexmq/lib/simplexmq.dart
Normal file
8
packages/simplexmq/lib/simplexmq.dart
Normal file
@ -0,0 +1,8 @@
|
||||
/// Support for doing something awesome.
|
||||
///
|
||||
/// More dartdocs go here.
|
||||
library simplexmq;
|
||||
|
||||
export "src/protocol.dart";
|
||||
|
||||
// TODO: Export any libraries intended for clients of this package.
|
132
packages/simplexmq/lib/src/buffer.dart
Normal file
132
packages/simplexmq/lib/src/buffer.dart
Normal file
@ -0,0 +1,132 @@
|
||||
import "dart:typed_data";
|
||||
|
||||
Uint8List encodeAscii(String s) => Uint8List.fromList(s.codeUnits);
|
||||
|
||||
String decodeAscii(Uint8List b) => String.fromCharCodes(b);
|
||||
|
||||
Uint8List concat(Uint8List b1, Uint8List b2) {
|
||||
final a = Uint8List(b1.length + b2.length);
|
||||
a.setAll(0, b1);
|
||||
a.setAll(b1.length, b2);
|
||||
return a;
|
||||
}
|
||||
|
||||
T fold<T, E>(List<E> xs, T Function(T, E) combine, T initial) {
|
||||
T res = initial;
|
||||
for (final x in xs) {
|
||||
res = combine(res, x);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Uint8List concatN(List<Uint8List> bs) {
|
||||
final aLen = fold(bs, (int size, Uint8List b) => size + b.length, 0);
|
||||
final a = Uint8List(aLen);
|
||||
fold(bs, (int offset, Uint8List b) {
|
||||
a.setAll(offset, b);
|
||||
return offset + b.length;
|
||||
}, 0);
|
||||
return a;
|
||||
}
|
||||
|
||||
final charSpace = " ".codeUnitAt(0);
|
||||
final charEqual = "=".codeUnitAt(0);
|
||||
final empty = Uint8List(0);
|
||||
|
||||
Uint8List unwords(Uint8List b1, Uint8List b2) {
|
||||
final a = Uint8List(b1.length + b2.length + 1);
|
||||
a.setAll(0, b1);
|
||||
a[b1.length] = charSpace;
|
||||
a.setAll(b1.length + 1, b2);
|
||||
return a;
|
||||
}
|
||||
|
||||
Uint8List unwordsN(List<Uint8List> bs) {
|
||||
int i = bs.length;
|
||||
int size = bs.length - 1;
|
||||
while (i > 0) {
|
||||
size += bs[--i].length;
|
||||
}
|
||||
final a = Uint8List(size);
|
||||
|
||||
int offset = 0;
|
||||
for (i = 0; i < bs.length - 1; i++) {
|
||||
final b = bs[i];
|
||||
a.setAll(offset, b);
|
||||
offset += b.length;
|
||||
a[offset++] = charSpace;
|
||||
}
|
||||
a.setAll(offset, bs[i]);
|
||||
return a;
|
||||
}
|
||||
|
||||
final _base64chars = Uint8List.fromList(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
.codeUnits);
|
||||
|
||||
List<int?> __base64lookup() {
|
||||
final a = List<int?>.filled(256, null);
|
||||
for (int i = 0; i < _base64chars.length; i++) {
|
||||
a[_base64chars[i]] = i;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
final _base64lookup = __base64lookup();
|
||||
|
||||
Uint8List encode64(Uint8List a) {
|
||||
final len = a.length;
|
||||
final b64len = (len / 3).ceil() * 4;
|
||||
final b64 = Uint8List(b64len);
|
||||
|
||||
int j = 0;
|
||||
for (int i = 0; i < len; i += 3) {
|
||||
final e1 = i + 1 < len ? a[i + 1] : 0;
|
||||
final e2 = i + 2 < len ? a[i + 2] : 0;
|
||||
b64[j++] = _base64chars[a[i] >> 2];
|
||||
b64[j++] = _base64chars[((a[i] & 3) << 4) | (e1 >> 4)];
|
||||
b64[j++] = _base64chars[((e1 & 15) << 2) | (e2 >> 6)];
|
||||
b64[j++] = _base64chars[e2 & 63];
|
||||
}
|
||||
|
||||
if (len % 3 != 0) b64[b64len - 1] = charEqual;
|
||||
if (len % 3 == 1) b64[b64len - 2] = charEqual;
|
||||
|
||||
return b64;
|
||||
}
|
||||
|
||||
Uint8List? decode64(Uint8List b64) {
|
||||
int len = b64.length;
|
||||
if (len % 4 != 0) return null;
|
||||
int bLen = (len * 3) >> 2;
|
||||
|
||||
if (b64[len - 1] == charEqual) {
|
||||
len--;
|
||||
bLen--;
|
||||
if (b64[len - 1] == charEqual) {
|
||||
len--;
|
||||
bLen--;
|
||||
}
|
||||
}
|
||||
|
||||
final bytes = Uint8List(bLen);
|
||||
|
||||
int i = 0;
|
||||
int pos = 0;
|
||||
while (i < len) {
|
||||
final enc1 = _base64lookup[b64[i++]];
|
||||
final enc2 = i < len ? _base64lookup[b64[i++]] : 0;
|
||||
final enc3 = i < len ? _base64lookup[b64[i++]] : 0;
|
||||
final enc4 = i < len ? _base64lookup[b64[i++]] : 0;
|
||||
if (enc1 == null || enc2 == null || enc3 == null || enc4 == null) {
|
||||
return null;
|
||||
}
|
||||
bytes[pos++] = (enc1 << 2) | (enc2 >> 4);
|
||||
int p = pos++;
|
||||
if (p < bLen) bytes[p] = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
p = pos++;
|
||||
if (p < bLen) bytes[p] = ((enc3 & 3) << 6) | (enc4 & 63);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
136
packages/simplexmq/lib/src/parser.dart
Normal file
136
packages/simplexmq/lib/src/parser.dart
Normal file
@ -0,0 +1,136 @@
|
||||
import "dart:typed_data";
|
||||
import "buffer.dart";
|
||||
|
||||
typedef BinaryTags<T> = Map<T, Uint8List>;
|
||||
|
||||
int cc(String c) => c.codeUnitAt(0);
|
||||
|
||||
final char0 = cc("0");
|
||||
final char9 = cc("9");
|
||||
final charLowerA = cc("a");
|
||||
final charLowerZ = cc("z");
|
||||
final charUpperA = cc("A");
|
||||
final charUpperZ = cc("Z");
|
||||
final charPlus = cc("+");
|
||||
final charSlash = cc("/");
|
||||
|
||||
class Parser {
|
||||
final Uint8List _s;
|
||||
int _pos = 0;
|
||||
bool _fail = false;
|
||||
Parser(this._s);
|
||||
|
||||
bool get fail => _fail;
|
||||
bool get end => _pos >= _s.length;
|
||||
|
||||
// only calls `parse` if the parser did not previously fail
|
||||
T? _run<T>(T? Function() parse) {
|
||||
if (_fail || _pos >= _s.length) {
|
||||
_fail = true;
|
||||
return null;
|
||||
}
|
||||
final res = parse();
|
||||
if (res == null) _fail = true;
|
||||
return res;
|
||||
}
|
||||
|
||||
// takes a required number of bytes
|
||||
Uint8List? take(int len) => _run(() {
|
||||
final end = _pos + len;
|
||||
if (end > _s.length) return null;
|
||||
final res = _s.sublist(_pos, end);
|
||||
_pos = end;
|
||||
return res;
|
||||
});
|
||||
|
||||
// takes chars (> 0) while condition is true; function isAlphaNum or isDigit can be used
|
||||
Uint8List? takeWhile1(bool Function(int) f) => _run(() {
|
||||
final pos = _pos;
|
||||
while (f(_s[_pos])) {
|
||||
_pos++;
|
||||
}
|
||||
return _pos > pos ? _s.sublist(pos, _pos) : null;
|
||||
});
|
||||
|
||||
// takes the non-empty word until the first space or until the end of the string
|
||||
Uint8List? word() => _run(() {
|
||||
int pos = _s.indexOf(charSpace, _pos);
|
||||
if (pos == -1) pos = _s.length;
|
||||
if (pos <= _pos) return null;
|
||||
final res = _s.sublist(_pos, pos);
|
||||
_pos = pos;
|
||||
return res;
|
||||
});
|
||||
|
||||
bool? str(Uint8List s) => _run(() {
|
||||
for (int i = 0, j = _pos; i < s.length; i++, j++) {
|
||||
if (s[i] != _s[j]) return null;
|
||||
}
|
||||
_pos += s.length;
|
||||
return true;
|
||||
});
|
||||
|
||||
// takes space
|
||||
bool? space() => _run(() {
|
||||
if (_s[_pos] == charSpace) {
|
||||
_pos++;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
int? decimal() => _run(() {
|
||||
final s = takeWhile1(isDigit);
|
||||
if (s == null) return null;
|
||||
int n = 0;
|
||||
for (int i = 0; i < s.length; i++) {
|
||||
n *= 10;
|
||||
n += s[i] - char0;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
DateTime? datetime() => _run(() {
|
||||
final s = word();
|
||||
if (s != null) return DateTime.tryParse(decodeAscii(s));
|
||||
});
|
||||
|
||||
// takes base-64 encoded string and returns decoded binary
|
||||
Uint8List? base64() => _run(() {
|
||||
bool tryCharEqual() {
|
||||
final ok = _pos < _s.length && _s[_pos] == charEqual;
|
||||
if (ok) _pos++;
|
||||
return ok;
|
||||
}
|
||||
|
||||
final pos = _pos;
|
||||
int c;
|
||||
do {
|
||||
c = _s[_pos];
|
||||
} while ((isAlphaNum(c) || c == charPlus || c == charSlash) &&
|
||||
++_pos < _s.length);
|
||||
|
||||
if (tryCharEqual()) tryCharEqual();
|
||||
return _pos > pos ? decode64(_s.sublist(pos, _pos)) : null;
|
||||
});
|
||||
|
||||
// takes one of the binary tags and returns its key
|
||||
T? someStr<T>(BinaryTags<T> ts) => _run(() {
|
||||
outer:
|
||||
for (final t in ts.entries) {
|
||||
final s = t.value;
|
||||
for (int i = 0, j = _pos; i < s.length; i++, j++) {
|
||||
if (s[i] != _s[j]) continue outer;
|
||||
}
|
||||
_pos += s.length;
|
||||
return t.key;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
bool isDigit(int c) => c >= char0 && c <= char9;
|
||||
|
||||
bool isAlphaNum(int c) =>
|
||||
(c >= char0 && c <= char9) ||
|
||||
(c >= charLowerA && c <= charLowerZ) ||
|
||||
(c >= charUpperA && c <= charUpperZ);
|
268
packages/simplexmq/lib/src/protocol.dart
Normal file
268
packages/simplexmq/lib/src/protocol.dart
Normal file
@ -0,0 +1,268 @@
|
||||
import "dart:typed_data";
|
||||
import "buffer.dart";
|
||||
import "parser.dart";
|
||||
|
||||
abstract class SMPCommand {
|
||||
Uint8List serialize();
|
||||
}
|
||||
|
||||
abstract class ClientCommand extends SMPCommand {}
|
||||
|
||||
abstract class BrokerCommand extends SMPCommand {}
|
||||
|
||||
final rsaPrefix = encodeAscii("rsa:");
|
||||
|
||||
Uint8List serializePubKey(Uint8List rcvPubKey) =>
|
||||
concat(rsaPrefix, encode64(rcvPubKey));
|
||||
|
||||
final Uint8List cNEW = encodeAscii("NEW");
|
||||
final Uint8List cSUB = encodeAscii("SUB");
|
||||
final Uint8List cKEY = encodeAscii("KEY");
|
||||
final Uint8List cACK = encodeAscii("ACK");
|
||||
final Uint8List cOFF = encodeAscii("OFF");
|
||||
final Uint8List cDEL = encodeAscii("DEL");
|
||||
final Uint8List cSEND = encodeAscii("SEND");
|
||||
final Uint8List cPING = encodeAscii("PING");
|
||||
final Uint8List cIDS = encodeAscii("IDS");
|
||||
final Uint8List cMSG = encodeAscii("MSG");
|
||||
final Uint8List cEND = encodeAscii("END");
|
||||
final Uint8List cOK = encodeAscii("OK");
|
||||
final Uint8List cERR = encodeAscii("ERR");
|
||||
final Uint8List cPONG = encodeAscii("PONG");
|
||||
|
||||
enum SMPCmdTag {
|
||||
NEW,
|
||||
SUB,
|
||||
KEY,
|
||||
ACK,
|
||||
OFF,
|
||||
DEL,
|
||||
SEND,
|
||||
PING,
|
||||
IDS,
|
||||
MSG,
|
||||
END,
|
||||
OK,
|
||||
ERR,
|
||||
PONG,
|
||||
}
|
||||
|
||||
final BinaryTags<SMPCmdTag> smpCmdTags = {
|
||||
SMPCmdTag.NEW: cNEW,
|
||||
SMPCmdTag.SUB: cSUB,
|
||||
SMPCmdTag.KEY: cKEY,
|
||||
SMPCmdTag.ACK: cACK,
|
||||
SMPCmdTag.OFF: cOFF,
|
||||
SMPCmdTag.DEL: cDEL,
|
||||
SMPCmdTag.SEND: cSEND,
|
||||
SMPCmdTag.PING: cPING,
|
||||
SMPCmdTag.IDS: cIDS,
|
||||
SMPCmdTag.MSG: cMSG,
|
||||
SMPCmdTag.END: cEND,
|
||||
SMPCmdTag.OK: cOK,
|
||||
SMPCmdTag.ERR: cERR,
|
||||
SMPCmdTag.PONG: cPONG,
|
||||
};
|
||||
|
||||
class NEW extends ClientCommand {
|
||||
final Uint8List rcvPubKey;
|
||||
NEW(this.rcvPubKey);
|
||||
@override
|
||||
Uint8List serialize() => unwords(cNEW, serializePubKey(rcvPubKey));
|
||||
}
|
||||
|
||||
class SUB extends ClientCommand {
|
||||
@override
|
||||
Uint8List serialize() => cSUB;
|
||||
}
|
||||
|
||||
class KEY extends ClientCommand {
|
||||
final Uint8List sndPubKey;
|
||||
KEY(this.sndPubKey);
|
||||
@override
|
||||
Uint8List serialize() => unwords(cKEY, serializePubKey(sndPubKey));
|
||||
}
|
||||
|
||||
class ACK extends ClientCommand {
|
||||
@override
|
||||
Uint8List serialize() => cACK;
|
||||
}
|
||||
|
||||
class OFF extends ClientCommand {
|
||||
@override
|
||||
Uint8List serialize() => cOFF;
|
||||
}
|
||||
|
||||
class DEL extends ClientCommand {
|
||||
@override
|
||||
Uint8List serialize() => cDEL;
|
||||
}
|
||||
|
||||
List<Uint8List> serializeMsg(Uint8List msg) =>
|
||||
[encodeAscii(msg.length.toString()), msg, empty];
|
||||
|
||||
class SEND extends ClientCommand {
|
||||
final Uint8List msgBody;
|
||||
SEND(this.msgBody);
|
||||
@override
|
||||
Uint8List serialize() => unwordsN([cSEND, ...serializeMsg(msgBody)]);
|
||||
}
|
||||
|
||||
class PING extends ClientCommand {
|
||||
@override
|
||||
Uint8List serialize() => cPING;
|
||||
}
|
||||
|
||||
class IDS extends BrokerCommand {
|
||||
final Uint8List rcvId;
|
||||
final Uint8List sndId;
|
||||
IDS(this.rcvId, this.sndId) : super();
|
||||
@override
|
||||
Uint8List serialize() => unwordsN([cIDS, encode64(rcvId), encode64(sndId)]);
|
||||
}
|
||||
|
||||
class MSG extends BrokerCommand {
|
||||
final Uint8List msgId;
|
||||
final DateTime ts;
|
||||
final Uint8List msgBody;
|
||||
MSG(this.msgId, this.ts, this.msgBody);
|
||||
@override
|
||||
Uint8List serialize() => unwordsN([
|
||||
cMSG,
|
||||
encode64(msgId),
|
||||
encodeAscii(ts.toIso8601String()),
|
||||
...serializeMsg(msgBody)
|
||||
]);
|
||||
}
|
||||
|
||||
class END extends BrokerCommand {
|
||||
@override
|
||||
Uint8List serialize() => cEND;
|
||||
}
|
||||
|
||||
class OK extends BrokerCommand {
|
||||
@override
|
||||
Uint8List serialize() => cOK;
|
||||
}
|
||||
|
||||
enum ErrorType { BLOCK, CMD, AUTH, QUOTA, NO_MSG, INTERNAL }
|
||||
|
||||
final BinaryTags<ErrorType> errorTags = {
|
||||
ErrorType.BLOCK: encodeAscii("BLOCK"),
|
||||
ErrorType.CMD: encodeAscii("CMD"),
|
||||
ErrorType.AUTH: encodeAscii("AUTH"),
|
||||
ErrorType.QUOTA: encodeAscii("QUOTA"),
|
||||
ErrorType.NO_MSG: encodeAscii("NO_MSG"),
|
||||
ErrorType.INTERNAL: encodeAscii("INTERNAL"),
|
||||
};
|
||||
|
||||
enum CmdErrorType { PROHIBITED, KEY_SIZE, SYNTAX, NO_AUTH, HAS_AUTH, NO_QUEUE }
|
||||
|
||||
final BinaryTags<CmdErrorType> cmdErrorTags = {
|
||||
CmdErrorType.PROHIBITED: encodeAscii("PROHIBITED"),
|
||||
CmdErrorType.KEY_SIZE: encodeAscii("KEY_SIZE"),
|
||||
CmdErrorType.SYNTAX: encodeAscii("SYNTAX"),
|
||||
CmdErrorType.NO_AUTH: encodeAscii("NO_AUTH"),
|
||||
CmdErrorType.HAS_AUTH: encodeAscii("HAS_AUTH"),
|
||||
CmdErrorType.NO_QUEUE: encodeAscii("NO_QUEUE"),
|
||||
};
|
||||
|
||||
class ERR extends BrokerCommand {
|
||||
final ErrorType err;
|
||||
final CmdErrorType? cmdErr;
|
||||
ERR(this.err)
|
||||
: cmdErr = err == ErrorType.CMD
|
||||
? throw ArgumentError("CMD error should be created with ERR.CMD")
|
||||
: null;
|
||||
ERR.cmd(this.cmdErr) : err = ErrorType.CMD;
|
||||
@override
|
||||
Uint8List serialize() {
|
||||
final _err = errorTags[err]!;
|
||||
return cmdErr == null
|
||||
? unwords(cERR, _err)
|
||||
: unwordsN([cERR, _err, cmdErrorTags[cmdErr!]!]);
|
||||
}
|
||||
}
|
||||
|
||||
class PONG extends BrokerCommand {
|
||||
@override
|
||||
Uint8List serialize() => cPONG;
|
||||
}
|
||||
|
||||
final Map<SMPCmdTag, SMPCommand? Function(Parser p)> smpCmdParsers = {
|
||||
SMPCmdTag.NEW: (p) {
|
||||
p.space();
|
||||
final key = pubKeyP(p);
|
||||
if (key != null) return NEW(key);
|
||||
},
|
||||
SMPCmdTag.SUB: (_) => SUB(),
|
||||
SMPCmdTag.KEY: (p) {
|
||||
p.space();
|
||||
final key = pubKeyP(p);
|
||||
if (key != null) return KEY(key);
|
||||
},
|
||||
SMPCmdTag.ACK: (_) => ACK(),
|
||||
SMPCmdTag.OFF: (_) => OFF(),
|
||||
SMPCmdTag.DEL: (_) => DEL(),
|
||||
SMPCmdTag.SEND: (p) {
|
||||
p.space();
|
||||
final msg = messageP(p);
|
||||
if (msg != null) return SEND(msg);
|
||||
},
|
||||
SMPCmdTag.PING: (_) => PING(),
|
||||
SMPCmdTag.IDS: (p) {
|
||||
p.space();
|
||||
final rId = p.base64();
|
||||
p.space();
|
||||
final sId = p.base64();
|
||||
if (rId != null && sId != null) return IDS(rId, sId);
|
||||
},
|
||||
SMPCmdTag.MSG: (p) {
|
||||
p.space();
|
||||
final msgId = p.base64();
|
||||
p.space();
|
||||
final ts = p.datetime();
|
||||
p.space();
|
||||
final msg = messageP(p);
|
||||
if (msgId != null && ts != null && msg != null) return MSG(msgId, ts, msg);
|
||||
},
|
||||
SMPCmdTag.END: (_) => END(),
|
||||
SMPCmdTag.OK: (_) => OK(),
|
||||
SMPCmdTag.ERR: (p) {
|
||||
p.space();
|
||||
final err = p.someStr(errorTags);
|
||||
if (err == ErrorType.CMD) {
|
||||
p.space();
|
||||
final cmdErr = p.someStr(cmdErrorTags);
|
||||
if (cmdErr != null) return ERR.cmd(cmdErr);
|
||||
} else if (err != null) {
|
||||
return ERR(err);
|
||||
}
|
||||
},
|
||||
SMPCmdTag.PONG: (_) => PONG(),
|
||||
};
|
||||
|
||||
SMPCommand? smpCommandP(Parser p) {
|
||||
final cmd = p.someStr(smpCmdTags);
|
||||
return p.fail ? null : smpCmdParsers[cmd]!(p);
|
||||
}
|
||||
|
||||
SMPCommand? parseSMPCommand(Uint8List s) {
|
||||
final p = Parser(s);
|
||||
final cmd = smpCommandP(p);
|
||||
if (cmd != null && p.end) return cmd;
|
||||
}
|
||||
|
||||
Uint8List? pubKeyP(Parser p) {
|
||||
p.str(rsaPrefix);
|
||||
return p.base64();
|
||||
}
|
||||
|
||||
Uint8List? messageP(Parser p) {
|
||||
final len = p.decimal();
|
||||
p.space();
|
||||
Uint8List? msg;
|
||||
if (len != null) msg = p.take(len);
|
||||
p.space();
|
||||
return p.fail ? null : msg;
|
||||
}
|
15
packages/simplexmq/pubspec.yaml
Normal file
15
packages/simplexmq/pubspec.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
name: simplexmq
|
||||
description: A starting point for Dart libraries or applications.
|
||||
version: 0.0.1
|
||||
# homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.14.0 <3.0.0'
|
||||
|
||||
|
||||
# dependencies:
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^1.0.0
|
||||
test: ^1.16.0
|
66
packages/simplexmq/test/buffer_test.dart
Normal file
66
packages/simplexmq/test/buffer_test.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import "dart:typed_data";
|
||||
import "package:simplexmq/src/buffer.dart";
|
||||
import "package:test/test.dart";
|
||||
|
||||
final hello123 = Uint8List.fromList([104, 101, 108, 108, 111, 49, 50, 51]);
|
||||
|
||||
class Base64Test {
|
||||
final String description;
|
||||
final String binary;
|
||||
final String base64;
|
||||
|
||||
Base64Test(this.binary, this.base64) : description = binary;
|
||||
Base64Test.withDescription(this.description, this.binary, this.base64);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group("ascii encoding/decoding", () {
|
||||
test("encodeAscii", () {
|
||||
expect(encodeAscii("hello123"), hello123);
|
||||
});
|
||||
|
||||
test("decodeAscii", () {
|
||||
expect(decodeAscii(hello123), "hello123");
|
||||
});
|
||||
});
|
||||
|
||||
group("base-64 encoding/decoding", () {
|
||||
String allBinaryChars() {
|
||||
final a = Uint8List(256);
|
||||
for (var i = 0; i < 256; i++) {
|
||||
a[i] = i;
|
||||
}
|
||||
return decodeAscii(a);
|
||||
}
|
||||
|
||||
final base64tests = [
|
||||
Base64Test("\x12\x34\x56\x78", "EjRWeA=="),
|
||||
Base64Test("hello123", "aGVsbG8xMjM="),
|
||||
Base64Test("Hello world", "SGVsbG8gd29ybGQ="),
|
||||
Base64Test("Hello worlds!", "SGVsbG8gd29ybGRzIQ=="),
|
||||
Base64Test("May", "TWF5"),
|
||||
Base64Test("Ma", "TWE="),
|
||||
Base64Test("M", "TQ=="),
|
||||
Base64Test.withDescription(
|
||||
"all binary chars",
|
||||
allBinaryChars(),
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
|
||||
),
|
||||
];
|
||||
|
||||
test("encode64", () {
|
||||
for (final t in base64tests) {
|
||||
expect(encode64(encodeAscii(t.binary)), encodeAscii(t.base64));
|
||||
}
|
||||
});
|
||||
|
||||
test("decode64", () {
|
||||
for (final t in base64tests) {
|
||||
expect(decode64(encodeAscii(t.base64)), encodeAscii(t.binary));
|
||||
}
|
||||
expect(decode64(encodeAscii("TWE")), null);
|
||||
expect(decode64(encodeAscii("TWE==")), null);
|
||||
expect(decode64(encodeAscii("TW!=")), null);
|
||||
});
|
||||
});
|
||||
}
|
105
packages/simplexmq/test/protocol_test.dart
Normal file
105
packages/simplexmq/test/protocol_test.dart
Normal file
@ -0,0 +1,105 @@
|
||||
import "dart:typed_data";
|
||||
import "package:simplexmq/src/buffer.dart";
|
||||
import "package:simplexmq/src/protocol.dart";
|
||||
import "package:test/test.dart";
|
||||
|
||||
void main() {
|
||||
group("Parsing & serializing SMP commands", () {
|
||||
group("valid commands", () {
|
||||
Null Function() parseSerialize(SMPCommand cmd) => () {
|
||||
final s = cmd.serialize();
|
||||
expect(parseSMPCommand(s)?.serialize(), s);
|
||||
expect(parseSMPCommand(concat(s, Uint8List.fromList([charSpace]))),
|
||||
null);
|
||||
};
|
||||
|
||||
test("NEW", parseSerialize(NEW(encodeAscii("rsa:1234"))));
|
||||
test("SUB", parseSerialize(SUB()));
|
||||
test("KEY", parseSerialize(KEY(encodeAscii("rsa:1234"))));
|
||||
test("ACK", parseSerialize(ACK()));
|
||||
test("OFF", parseSerialize(OFF()));
|
||||
test("DEL", parseSerialize(DEL()));
|
||||
test("SEND", parseSerialize(SEND(encodeAscii("hello"))));
|
||||
test("PING", parseSerialize(PING()));
|
||||
test("IDS", parseSerialize(IDS(encodeAscii("abc"), encodeAscii("def"))));
|
||||
test(
|
||||
"MSG",
|
||||
parseSerialize(MSG(encodeAscii("fgh"), DateTime.now().toUtc(),
|
||||
encodeAscii("hello"))));
|
||||
test("END", parseSerialize(END()));
|
||||
test("OK", parseSerialize(OK()));
|
||||
test("ERR", parseSerialize(ERR(ErrorType.AUTH)));
|
||||
test("ERR CMD", parseSerialize(ERR.cmd(CmdErrorType.SYNTAX)));
|
||||
test("PONG", parseSerialize(PONG()));
|
||||
});
|
||||
|
||||
group("invalid commands", () {
|
||||
void Function() parseFailure(String s) =>
|
||||
() => expect(parseSMPCommand(encodeAscii(s)), null);
|
||||
|
||||
void Function() parseSuccess(String s) =>
|
||||
() => expect(parseSMPCommand(encodeAscii(s)) is SMPCommand, true);
|
||||
|
||||
group("NEW", () {
|
||||
test("ok", parseSuccess("NEW rsa:abcd"));
|
||||
test("no key", parseFailure("NEW"));
|
||||
test("invalid base64", parseFailure("NEW rsa:abc"));
|
||||
test("double space", parseFailure("NEW rsa:abcd"));
|
||||
});
|
||||
|
||||
group("KEY", () {
|
||||
test("ok", parseSuccess("KEY rsa:abcd"));
|
||||
test("no key", parseFailure("KEY"));
|
||||
test("invalid base64", parseFailure("KEY rsa:abc"));
|
||||
test("double space", parseFailure("KEY rsa:abcd"));
|
||||
});
|
||||
|
||||
group("SEND", () {
|
||||
test("ok", parseSuccess("SEND 5 hello "));
|
||||
test("no size", parseFailure("SEND hello "));
|
||||
test("incorrect size", parseFailure("SEND 6 hello "));
|
||||
test("no trailing space", parseFailure("SEND 5 hello"));
|
||||
test("double space 1", parseFailure("SEND 5 hello "));
|
||||
test("double space 2", parseFailure("SEND 5 hello "));
|
||||
});
|
||||
|
||||
group("IDS", () {
|
||||
test("ok", parseSuccess("IDS abcd efgh"));
|
||||
test("no IDs", parseFailure("IDS"));
|
||||
test("only one ID", parseFailure("IDS abcd"));
|
||||
test("invalid base64 1", parseFailure("IDS abc efgh"));
|
||||
test("invalid base64 2", parseFailure("IDS abcd efg"));
|
||||
test("double space 1", parseFailure("IDS abcd efgh"));
|
||||
test("double space 2", parseFailure("IDS abcd efgh"));
|
||||
});
|
||||
|
||||
group("MSG", () {
|
||||
final String ts = "2021-10-03T10:50:59.895Z";
|
||||
test("ok", parseSuccess("MSG abcd $ts 5 hello "));
|
||||
test("invalid base64", parseFailure("MSG abc $ts 5 hello "));
|
||||
test("invalid timestamp 1",
|
||||
parseFailure("MSG abc 2021-10-03T10:50:59.895 5 hello "));
|
||||
test("invalid timestamp 2",
|
||||
parseFailure("MSG abc 2021-14-03T10:50:59.895Z 5 hello "));
|
||||
test("no size", parseFailure("MSG abcd $ts hello "));
|
||||
test("incorrect size", parseFailure("MSG abcd $ts 6 hello "));
|
||||
test("no trailing space", parseFailure("MSG abcd $ts 5 hello"));
|
||||
test("double space 1", parseFailure("MSG abcd $ts 5 hello "));
|
||||
test("double space 2", parseFailure("MSG abcd $ts 5 hello "));
|
||||
test("double space 3", parseFailure("MSG abcd $ts 5 hello "));
|
||||
test("double space 4", parseFailure("MSG abcd $ts 5 hello "));
|
||||
});
|
||||
|
||||
group("ERR", () {
|
||||
test("ok 1", parseSuccess("ERR AUTH"));
|
||||
test("ok 2", parseSuccess("ERR CMD SYNTAX"));
|
||||
test("unknown error", parseFailure("ERR HELLO"));
|
||||
test("unknown CMD error", parseFailure("ERR CMD HELLO"));
|
||||
test("bad sub-error", parseFailure("ERR AUTH SYNTAX"));
|
||||
test("double space 1", parseFailure("ERR AUTH"));
|
||||
test("double space 2", parseFailure("ERR CMD SYNTAX"));
|
||||
test("double space 3", parseFailure("ERR CMD SYNTAX"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
240
pubspec.lock
240
pubspec.lock
@ -1,6 +1,27 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "27.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -29,6 +50,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -43,6 +71,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -57,6 +106,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -74,6 +130,48 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -81,6 +179,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -95,6 +200,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -102,11 +228,74 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.10"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -142,6 +331,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.17.10"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -149,6 +345,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -163,5 +366,40 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.12.0 <3.0.0"
|
||||
dart: ">=2.14.0 <3.0.0"
|
||||
|
@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
sdk: ">=2.14.0 <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@ -45,6 +45,7 @@ dev_dependencies:
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
test: ^1.17.10
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
@ -5,26 +5,26 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import 'package:simplex_chat/main.dart';
|
||||
import "package:simplex_chat/main.dart";
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
expect(find.text("0"), findsOneWidget);
|
||||
expect(find.text("1"), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
// Tap the "+" icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text("0"), findsNothing);
|
||||
expect(find.text("1"), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user