flutter unit/integration tests

This commit is contained in:
Christien Rioux 2024-03-16 23:57:46 -04:00
parent d586748333
commit 6a8c0830d2
18 changed files with 1357 additions and 520 deletions

20
Cargo.lock generated
View File

@ -6172,9 +6172,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if 1.0.0",
"serde",
@ -6184,9 +6184,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
@ -6211,9 +6211,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -6221,9 +6221,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
@ -6234,9 +6234,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "wasm-bindgen-test"

View File

@ -0,0 +1,4 @@
#!/bin/bash
pushd example 2>/dev/null
flutter run integration_test/app_test.dart $@
popd 2>/dev/null

View File

@ -2,39 +2,72 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'fixtures.dart';
import 'test_crypto.dart';
import 'test_routing_context.dart';
import 'test_table_db.dart';
import 'test_veilid_config.dart';
import 'test_dht.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final fixture = DefaultFixture();
group('VeilidConfig', () {
final fixture = DefaultFixture();
setUp(fixture.setUp);
tearDown(fixture.tearDown);
test('test VeilidConfig defaults', testVeilidConfigDefaults);
group('Unstarted Tests', () {
test('veilid config defaults', testVeilidConfigDefaults);
});
// group('end-to-end test', () {
// testWidgets('tap on the floating action button, verify counter',
// (tester) async {
// // Load app widget.
// await tester.pumpWidget(const MyApp());
group('Started Tests', () {
setUpAll(fixture.setUp);
tearDownAll(fixture.tearDown);
// // Verify the counter starts at 0.
// expect(find.text('0'), findsOneWidget);
group('Crypto Tests', () {
test('best cryptosystem', testBestCryptoSystem);
test('get cryptosystem', testGetCryptoSystem);
test('get cryptosystem invalid', testGetCryptoSystemInvalid);
test('hash and verify password', testHashAndVerifyPassword);
});
// // Finds the floating action button to tap on.
// final fab = find.byKey(const Key('increment'));
group('Table DB Tests', () {
test('delete table db nonexistent', testDeleteTableDbNonExistent);
test('open delete table db', testOpenDeleteTableDb);
test('open twice table db', testOpenTwiceTableDb);
test('open twice table db store load', testOpenTwiceTableDbStoreLoad);
test('open twice table db store delete load',
testOpenTwiceTableDbStoreDeleteLoad);
test('resize table db', testResizeTableDb);
});
// // Emulate a tap on the floating action button.
// await tester.tap(fab);
group('Attached Tests', () {
setUpAll(fixture.attach);
tearDownAll(fixture.detach);
// // Trigger a frame.
// await tester.pumpAndSettle();
group('Routing Contexts', () {
test('routing contexts', testRoutingContexts);
test('app message loopback',
() => testAppMessageLoopback(fixture.updateStream));
test('app call loopback',
() => testAppCallLoopback(fixture.updateStream));
test('app message loopback big packets',
() => testAppMessageLoopbackBigPackets(fixture.updateStream));
test('app call loopback big packets',
() => testAppCallLoopbackBigPackets(fixture.updateStream));
});
// // Verify the counter increments by 1.
// expect(find.text('1'), findsOneWidget);
// });
// });
group('Veilid DHT', () {
test('get dht value unopened', testGetDHTValueUnopened);
test('open dht record nonexistent no writer',
testOpenDHTRecordNonexistentNoWriter);
test('close dht record nonexistent', testCloseDHTRecordNonexistent);
test('delete dht record nonexistent', testDeleteDHTRecordNonexistent);
test(
'create delete dht record simple', testCreateDeleteDHTRecordSimple);
test('create delete dht record no close',
testCreateDeleteDHTRecordNoClose);
test('get dht value nonexistent', testGetDHTValueNonexistent);
test('set get dht value', testSetGetDHTValue);
test('open writer dht value', testOpenWriterDHTValue);
test('inspect dht record', testInspectDHTRecord);
});
});
});
}

View File

@ -7,15 +7,17 @@ import 'package:veilid/veilid.dart';
class DefaultFixture {
DefaultFixture();
StreamSubscription<VeilidUpdate>? _updateSubscription;
Stream<VeilidUpdate>? _updateStream;
StreamSubscription<VeilidUpdate>? _veilidUpdateSubscription;
Stream<VeilidUpdate>? _veilidUpdateStream;
final StreamController<VeilidUpdate> _updateStreamController =
StreamController.broadcast();
static final _fixtureMutex = Mutex();
Future<void> setUp() async {
await _fixtureMutex.acquire();
assert(_updateStream == null, 'should not set up fixture twice');
assert(_veilidUpdateStream == null, 'should not set up fixture twice');
final Map<String, dynamic> platformConfigJson;
if (kIsWeb) {
@ -53,16 +55,26 @@ class DefaultFixture {
}
Veilid.instance.initializeVeilidCore(platformConfigJson);
final defaultConfig = await getDefaultVeilidConfig(
isWeb: kIsWeb, programName: 'Veilid Tests');
var config = await getDefaultVeilidConfig(
isWeb: kIsWeb,
programName: 'Veilid Tests',
// ignore: avoid_redundant_argument_values, do_not_use_environment
bootstrap: const String.fromEnvironment('BOOTSTRAP'),
// ignore: avoid_redundant_argument_values, do_not_use_environment
networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'),
);
final updateStream =
_updateStream = await Veilid.instance.startupVeilidCore(defaultConfig);
if (_updateStream == null) {
throw Exception('failed to start up veilid core');
}
config =
config.copyWith(tableStore: config.tableStore.copyWith(delete: true));
config = config.copyWith(
protectedStore: config.protectedStore.copyWith(delete: true));
config =
config.copyWith(blockStore: config.blockStore.copyWith(delete: true));
_updateSubscription = updateStream.listen((update) {
final us =
_veilidUpdateStream = await Veilid.instance.startupVeilidCore(config);
_veilidUpdateSubscription = us.listen((update) {
if (update is VeilidLog) {
} else if (update is VeilidUpdateAttachment) {
} else if (update is VeilidUpdateConfig) {
@ -70,21 +82,56 @@ class DefaultFixture {
} else if (update is VeilidAppMessage) {
} else if (update is VeilidAppCall) {
} else if (update is VeilidUpdateValueChange) {
} else if (update is VeilidUpdateRouteChange) {
} else {
throw Exception('unexpected update: $update');
}
_updateStreamController.sink.add(update);
});
}
Future<void> tearDown() async {
assert(_updateStream != null, 'should not tearDown without setUp');
Stream<VeilidUpdate> get updateStream => _updateStreamController.stream;
final cancelFut = _updateSubscription?.cancel();
Future<void> attach() async {
await Veilid.instance.attach();
// Wait for attached state
while (true) {
final state = await Veilid.instance.getVeilidState();
var done = false;
if (state.attachment.publicInternetReady) {
switch (state.attachment.state) {
case AttachmentState.detached:
break;
case AttachmentState.attaching:
break;
case AttachmentState.detaching:
break;
default:
done = true;
break;
}
}
if (done) {
break;
}
await Future.delayed(const Duration(seconds: 1));
}
}
Future<void> detach() async {
await Veilid.instance.detach();
}
Future<void> tearDown() async {
assert(_veilidUpdateStream != null, 'should not tearDown without setUp');
final cancelFut = _veilidUpdateSubscription?.cancel();
await Veilid.instance.shutdownVeilidCore();
await cancelFut;
_updateSubscription = null;
_updateStream = null;
_veilidUpdateSubscription = null;
_veilidUpdateStream = null;
_fixtureMutex.release();
}

View File

@ -0,0 +1,34 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:veilid/veilid.dart';
Future<void> testBestCryptoSystem() async {
final cs = await Veilid.instance.bestCryptoSystem();
expect(await cs.defaultSaltLength(), equals(16));
}
Future<void> testGetCryptoSystem() async {
final cs = await Veilid.instance.getCryptoSystem(cryptoKindVLD0);
expect(await cs.defaultSaltLength(), equals(16));
}
Future<void> testGetCryptoSystemInvalid() async {
await expectLater(
() async => await Veilid.instance.getCryptoSystem(cryptoKindNONE),
throwsA(isA<VeilidAPIException>()));
}
Future<void> testHashAndVerifyPassword() async {
final cs = await Veilid.instance.bestCryptoSystem();
final nonce = await cs.randomNonce();
final salt = nonce.decode();
// Password match
final phash = await cs.hashPassword(utf8.encode("abc123"), salt);
expect(await cs.verifyPassword(utf8.encode("abc123"), phash), isTrue);
// Password mismatch
await cs.hashPassword(utf8.encode("abc1234"), salt);
expect(await cs.verifyPassword(utf8.encode("abc1235"), phash), isFalse);
}

View File

@ -0,0 +1,250 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:veilid/veilid.dart';
final bogusKey =
TypedKey.fromString("VLD0:qD10lHHPD1_Qr23_Qy-1JnxTht12eaWwENVG_m2v7II");
Future<void> testGetDHTValueUnopened() async {
final rc = await Veilid.instance.routingContext();
try {
await expectLater(
() async => await rc.getDHTValue(bogusKey, 0, forceRefresh: false),
throwsA(isA<VeilidAPIException>()));
} finally {
rc.close();
}
}
Future<void> testOpenDHTRecordNonexistentNoWriter() async {
final rc = await Veilid.instance.routingContext();
try {
await expectLater(() async => await rc.openDHTRecord(bogusKey),
throwsA(isA<VeilidAPIException>()));
} finally {
rc.close();
}
}
Future<void> testCloseDHTRecordNonexistent() async {
final rc = await Veilid.instance.routingContext();
try {
await expectLater(() async => await rc.closeDHTRecord(bogusKey),
throwsA(isA<VeilidAPIException>()));
} finally {
rc.close();
}
}
Future<void> testDeleteDHTRecordNonexistent() async {
final rc = await Veilid.instance.routingContext();
try {
await expectLater(() async => await rc.deleteDHTRecord(bogusKey),
throwsA(isA<VeilidAPIException>()));
} finally {
rc.close();
}
}
Future<void> testCreateDeleteDHTRecordSimple() async {
final rc = await Veilid.instance.routingContext();
try {
final rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
await rc.closeDHTRecord(rec.key);
await rc.deleteDHTRecord(rec.key);
} finally {
rc.close();
}
}
Future<void> testCreateDeleteDHTRecordNoClose() async {
final rc = await Veilid.instance.routingContext();
try {
final rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
await rc.deleteDHTRecord(rec.key);
} finally {
rc.close();
}
}
Future<void> testGetDHTValueNonexistent() async {
final rc = await Veilid.instance.routingContext();
try {
final rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
expect(await rc.getDHTValue(rec.key, 0), isNull);
await rc.deleteDHTRecord(rec.key);
} finally {
rc.close();
}
}
Future<void> testSetGetDHTValue() async {
final rc = await Veilid.instance.routingContext();
try {
final rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 2));
expect(await rc.setDHTValue(rec.key, 0, utf8.encode("BLAH BLAH BLAH")),
isNull);
final vd2 = await rc.getDHTValue(rec.key, 0);
expect(vd2, isNotNull);
final vd3 = await rc.getDHTValue(rec.key, 0, forceRefresh: true);
expect(vd3, isNotNull);
final vd4 = await rc.getDHTValue(rec.key, 1);
expect(vd4, isNull);
expect(vd2, equals(vd3));
await rc.deleteDHTRecord(rec.key);
} finally {
rc.close();
}
}
Future<void> testOpenWriterDHTValue() async {
final rc = await Veilid.instance.routingContext();
try {
var rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 2));
final key = rec.key;
final owner = rec.owner;
final secret = rec.ownerSecret!;
final cs = await Veilid.instance.getCryptoSystem(rec.key.kind);
expect(await cs.validateKeyPair(owner, secret), isTrue);
final otherKeyPair = await cs.generateKeyPair();
final va = utf8.encode("Qwertyuiop Asdfghjkl Zxcvbnm");
final vb = utf8.encode("1234567890");
final vc = utf8.encode("!@#\$%^&*()");
// Test subkey writes
expect(await rc.setDHTValue(key, 1, va), isNull);
var vdtemp = await rc.getDHTValue(key, 1);
expect(vdtemp, isNotNull);
expect(vdtemp!.data, equals(va));
expect(vdtemp.seq, equals(0));
expect(vdtemp.writer, equals(owner));
expect(await rc.getDHTValue(key, 0), isNull);
expect(await rc.setDHTValue(key, 0, vb), isNull);
expect(
await rc.getDHTValue(key, 0, forceRefresh: true),
equals(ValueData(
data: vb,
seq: 0,
writer: owner,
)));
expect(
await rc.getDHTValue(key, 1, forceRefresh: true),
equals(ValueData(
data: va,
seq: 0,
writer: owner,
)));
// Equal value should not trigger sequence number update
expect(await rc.setDHTValue(key, 1, va), isNull);
// Different value should trigger sequence number update
expect(await rc.setDHTValue(key, 1, vb), isNull);
// Now that we initialized some subkeys
// and verified they stored correctly
// Delete things locally and reopen and see if we can write
// with the same writer key
//
await rc.closeDHTRecord(key);
await rc.deleteDHTRecord(key);
rec = await rc.openDHTRecord(key,
writer: KeyPair(key: owner, secret: secret));
expect(rec, isNotNull);
expect(rec.key, equals(key));
expect(rec.owner, equals(owner));
expect(rec.ownerSecret, equals(secret));
expect(rec.schema, isA<DHTSchemaDFLT>());
expect(rec.schema.oCnt, equals(2));
// Verify subkey 1 can be set before it is get but newer is available online
vdtemp = await rc.setDHTValue(key, 1, vc);
expect(vdtemp, isNotNull);
expect(vdtemp!.data, equals(vb));
expect(vdtemp.seq, equals(1));
expect(vdtemp.writer, equals(owner));
// Verify subkey 1 can be set a second time and it updates because seq is newer
expect(await rc.setDHTValue(key, 1, vc), isNull);
// Verify the network got the subkey update with a refresh check
vdtemp = await rc.getDHTValue(key, 1, forceRefresh: true);
expect(vdtemp, isNotNull);
expect(vdtemp!.data, equals(vc));
expect(vdtemp.seq, equals(2));
expect(vdtemp.writer, equals(owner));
// Delete things locally and reopen and see if we can write
// with a different writer key (should fail)
await rc.closeDHTRecord(key);
await rc.deleteDHTRecord(key);
rec = await rc.openDHTRecord(key, writer: otherKeyPair);
expect(rec, isNotNull);
expect(rec.key, equals(key));
expect(rec.owner, equals(owner));
expect(rec.ownerSecret, isNull);
expect(rec.schema, isA<DHTSchemaDFLT>());
expect(rec.schema.oCnt, equals(2));
// Verify subkey 1 can NOT be set because we have the wrong writer
await expectLater(() async => await rc.setDHTValue(key, 1, va),
throwsA(isA<VeilidAPIException>()));
// Verify subkey 0 can NOT be set because we have the wrong writer
await expectLater(() async => await rc.setDHTValue(key, 0, va),
throwsA(isA<VeilidAPIException>()));
// Verify subkey 0 can be set because override with the right writer
expect(
await rc.setDHTValue(key, 0, va,
writer: KeyPair(key: owner, secret: secret)),
isNull);
// Clean up
await rc.closeDHTRecord(key);
await rc.deleteDHTRecord(key);
} finally {
rc.close();
}
}
Future<void> testInspectDHTRecord() async {
final rc = await Veilid.instance.routingContext();
try {
var rec = await rc.createDHTRecord(const DHTSchema.dflt(oCnt: 2));
expect(await rc.setDHTValue(rec.key, 0, utf8.encode("BLAH BLAH BLAH")),
isNull);
final rr = await rc.inspectDHTRecord(rec.key);
expect(rr.subkeys, equals([ValueSubkeyRange.make(0, 1)]));
expect(rr.localSeqs, equals([0, 0xFFFFFFFF]));
expect(rr.networkSeqs, equals([]));
final rr2 =
await rc.inspectDHTRecord(rec.key, scope: DHTReportScope.syncGet);
expect(rr2.subkeys, equals([ValueSubkeyRange.make(0, 1)]));
expect(rr2.localSeqs, equals([0, 0xFFFFFFFF]));
expect(rr2.networkSeqs, equals([0, 0xFFFFFFFF]));
await rc.closeDHTRecord(rec.key);
await rc.deleteDHTRecord(rec.key);
} finally {
rc.close();
}
}

View File

@ -0,0 +1,263 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter_test/flutter_test.dart';
import 'package:veilid/veilid.dart';
Future<void> testRoutingContexts() async {
{
final rc = await Veilid.instance.routingContext();
rc.close();
}
{
final rc = await Veilid.instance.routingContext();
final rcp = rc.withDefaultSafety();
rcp.close();
rc.close();
}
{
final rc = await Veilid.instance.routingContext();
final rcp = rc.withSequencing(Sequencing.ensureOrdered);
rcp.close();
rc.close();
}
{
final rc = await Veilid.instance.routingContext();
final rcp = rc.withSafety(const SafetySelectionSafe(
safetySpec: SafetySpec(
hopCount: 2,
stability: Stability.lowLatency,
sequencing: Sequencing.noPreference)));
rcp.close();
rc.close();
}
{
final rc = await Veilid.instance.routingContext();
final rcp = rc.withSafety(
const SafetySelectionUnsafe(sequencing: Sequencing.preferOrdered));
rcp.close();
rc.close();
}
}
Future<void> testAppMessageLoopback(Stream<VeilidUpdate> updateStream) async {
final appMessageQueue = StreamController<VeilidAppMessage>();
final appMessageSubscription = updateStream.listen((update) {
if (update is VeilidAppMessage) {
appMessageQueue.sink.add(update);
}
});
try {
await Veilid.instance.debug("purge routes");
// make a routing context that uses a safety route
final rc = await Veilid.instance.routingContext();
try {
// make a new local private route
final prl = await Veilid.instance.newPrivateRoute();
try {
// import it as a remote route as well so we can send to it
final prr = await Veilid.instance.importRemotePrivateRoute(prl.blob);
try {
// send an app message to our own private route
final message = utf8.encode("abcd1234");
await rc.appMessage(prr, message);
// we should get the same message back
final update = await appMessageQueue.stream.first;
expect(update.message, equals(message));
expect(update.routeId, isNotNull);
} finally {
await Veilid.instance.releasePrivateRoute(prr);
}
} finally {
await Veilid.instance.releasePrivateRoute(prl.routeId);
}
} finally {
rc.close();
}
} finally {
await appMessageSubscription.cancel();
}
}
Future<void> testAppCallLoopback(Stream<VeilidUpdate> updateStream) async {
final appCallQueue = StreamController<VeilidAppCall>();
final appMessageSubscription = updateStream.listen((update) {
if (update is VeilidAppCall) {
appCallQueue.sink.add(update);
}
});
try {
await Veilid.instance.debug("purge routes");
// make a routing context that uses a safety route
final rc = await Veilid.instance.routingContext();
try {
// make a new local private route
final prl = await Veilid.instance.newPrivateRoute();
try {
// import it as a remote route as well so we can send to it
final prr = await Veilid.instance.importRemotePrivateRoute(prl.blob);
try {
// send an app call to our own private route
final message = utf8.encode("abcd1234");
final appCallFuture = rc.appCall(prr, message);
// we should get the same call back
final update = await appCallQueue.stream.first;
final appcallid = update.callId;
expect(update.message, equals(message));
expect(update.routeId, isNotNull);
// now we reply to the request
final reply = utf8.encode("qwer5678");
await Veilid.instance.appCallReply(appcallid, reply);
// now we should get the reply from the call
final result = await appCallFuture;
expect(result, equals(reply));
} finally {
await Veilid.instance.releasePrivateRoute(prr);
}
} finally {
await Veilid.instance.releasePrivateRoute(prl.routeId);
}
} finally {
rc.close();
}
} finally {
await appMessageSubscription.cancel();
}
}
Future<void> testAppMessageLoopbackBigPackets(
Stream<VeilidUpdate> updateStream) async {
final appMessageQueue = StreamController<VeilidAppMessage>();
final appMessageSubscription = updateStream.listen((update) {
if (update is VeilidAppMessage) {
appMessageQueue.sink.add(update);
}
});
final sentMessages = <String>{};
final random = Random.secure();
final cs = await Veilid.instance.bestCryptoSystem();
try {
await Veilid.instance.debug("purge routes");
// make a routing context that uses a safety route
final rc = await Veilid.instance.routingContext();
try {
// make a new local private route
final prl = await Veilid.instance.newPrivateRoute();
try {
// import it as a remote route as well so we can send to it
final prr = await Veilid.instance.importRemotePrivateRoute(prl.blob);
try {
for (var i = 0; i < 5; i++) {
// send an app message to our own private route
final message = await cs.randomBytes(random.nextInt(32768));
await rc.appMessage(prr, message);
sentMessages.add(base64Url.encode(message));
}
final appMessageQueueIterator =
StreamIterator(appMessageQueue.stream);
// we should get the same messages back
for (var i = 0; i < sentMessages.length; i++) {
if (await appMessageQueueIterator.moveNext()) {
final update = appMessageQueueIterator.current;
expect(sentMessages.contains(base64Url.encode(update.message)),
isTrue);
} else {
fail("not enough messages in the queue");
}
}
} finally {
await Veilid.instance.releasePrivateRoute(prr);
}
} finally {
await Veilid.instance.releasePrivateRoute(prl.routeId);
}
} finally {
rc.close();
}
} finally {
await appMessageSubscription.cancel();
}
}
Future<void> testAppCallLoopbackBigPackets(
Stream<VeilidUpdate> updateStream) async {
final appCallQueue = StreamController<VeilidAppCall>();
final appMessageSubscription = updateStream.listen((update) {
if (update is VeilidAppCall) {
appCallQueue.sink.add(update);
}
});
final appCallQueueHandler = () async {
await for (final update in appCallQueue.stream) {
await Veilid.instance.appCallReply(update.callId, update.message);
}
}();
final sentMessages = <String>{};
final random = Random.secure();
final cs = await Veilid.instance.bestCryptoSystem();
try {
await Veilid.instance.debug("purge routes");
// make a routing context that uses a safety route
final rc = (await Veilid.instance.routingContext())
.withSequencing(Sequencing.ensureOrdered, closeSelf: true);
try {
// make a new local private route
final prl = await Veilid.instance
.newCustomPrivateRoute(Stability.reliable, Sequencing.ensureOrdered);
try {
// import it as a remote route as well so we can send to it
final prr = await Veilid.instance.importRemotePrivateRoute(prl.blob);
try {
for (var i = 0; i < 5; i++) {
// send an app message to our own private route
final message = await cs.randomBytes(random.nextInt(32768));
final outmessage = await rc.appCall(prr, message);
expect(message, equals(outmessage));
}
final appMessageQueueIterator = StreamIterator(appCallQueue.stream);
// we should get the same messages back
for (var i = 0; i < sentMessages.length; i++) {
if (await appMessageQueueIterator.moveNext()) {
final update = appMessageQueueIterator.current;
expect(sentMessages.contains(base64Url.encode(update.message)),
isTrue);
} else {
fail("not enough messages in the queue");
}
}
} finally {
await Veilid.instance.releasePrivateRoute(prr);
}
} finally {
await Veilid.instance.releasePrivateRoute(prl.routeId);
}
} finally {
rc.close();
}
} finally {
await appMessageSubscription.cancel();
}
await appCallQueue.close();
await appCallQueueHandler;
}

View File

@ -0,0 +1,133 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:veilid/veilid.dart';
const testDb = "__dart_test_db";
const testNonexistentDb = "__dart_test_nonexistent_db";
Future<void> testDeleteTableDbNonExistent() async {
expect(await Veilid.instance.deleteTableDB(testNonexistentDb), isFalse);
}
Future<void> testOpenDeleteTableDb() async {
// delete test db if it exists
await Veilid.instance.deleteTableDB(testDb);
final tdb = await Veilid.instance.openTableDB(testDb, 1);
try {
expect(() async => await Veilid.instance.deleteTableDB(testDb),
throwsA(isA<VeilidAPIException>()));
} finally {
tdb.close();
}
expect(await Veilid.instance.deleteTableDB(testDb), isTrue);
}
Future<void> testOpenTwiceTableDb() async {
// delete test db if it exists
await Veilid.instance.deleteTableDB(testDb);
final tdb = await Veilid.instance.openTableDB(testDb, 1);
final tdb2 = await Veilid.instance.openTableDB(testDb, 1);
// delete should fail because open
await expectLater(() async => await Veilid.instance.deleteTableDB(testDb),
throwsA(isA<VeilidAPIException>()));
tdb.close();
// delete should fail because open
await expectLater(() async => await Veilid.instance.deleteTableDB(testDb),
throwsA(isA<VeilidAPIException>()));
tdb2.close();
// delete should now succeed
expect(await Veilid.instance.deleteTableDB(testDb), isTrue);
}
Future<void> testOpenTwiceTableDbStoreLoad() async {
// delete test db if it exists
await Veilid.instance.deleteTableDB(testDb);
final tdb = await Veilid.instance.openTableDB(testDb, 1);
try {
final tdb2 = await Veilid.instance.openTableDB(testDb, 1);
try {
// store into first db copy
await tdb.store(0, utf8.encode("asdf"), utf8.encode("1234"));
// load from second db copy
expect(
await tdb2.load(0, utf8.encode("asdf")), equals(utf8.encode("1234")));
} finally {
tdb2.close();
}
} finally {
tdb.close();
}
// delete should now succeed
expect(await Veilid.instance.deleteTableDB(testDb), isTrue);
}
Future<void> testOpenTwiceTableDbStoreDeleteLoad() async {
// delete test db if it exists
await Veilid.instance.deleteTableDB(testDb);
final tdb = await Veilid.instance.openTableDB(testDb, 1);
try {
final tdb2 = await Veilid.instance.openTableDB(testDb, 1);
try {
// store into first db copy
await tdb.store(0, utf8.encode("asdf"), utf8.encode("1234"));
// delete from second db copy and clean up
await tdb2.delete(0, utf8.encode("asdf"));
} finally {
tdb2.close();
}
// load from first db copy
expect(await tdb.load(0, utf8.encode("asdf")), isNull);
} finally {
tdb.close();
}
// delete should now succeed
expect(await Veilid.instance.deleteTableDB(testDb), isTrue);
}
Future<void> testResizeTableDb() async {
// delete test db if it exists
await Veilid.instance.deleteTableDB(testDb);
final tdb = await Veilid.instance.openTableDB(testDb, 1);
try {
// reopen the db with more columns should fail if it is already open
await expectLater(() async => await Veilid.instance.openTableDB(testDb, 2),
throwsA(isA<VeilidAPIException>()));
} finally {
tdb.close();
}
final tdb2 = await Veilid.instance.openTableDB(testDb, 2);
try {
// write something to second column
await tdb2.store(1, utf8.encode("qwer"), utf8.encode("5678"));
// reopen the db with fewer columns
final tdb3 = await Veilid.instance.openTableDB(testDb, 1);
try {
// Should fail access to second column
await expectLater(() async => await tdb3.load(1, utf8.encode("qwer")),
throwsA(isA<VeilidAPIException>()));
// Should succeed with access to second column
expect(
await tdb2.load(1, utf8.encode("qwer")), equals(utf8.encode("5678")));
} finally {
tdb3.close();
}
} finally {
tdb2.close();
}
// delete should now succeed
expect(await Veilid.instance.deleteTableDB(testDb), isTrue);
}

View File

@ -240,6 +240,31 @@ class RouteBlob with _$RouteBlob {
_$RouteBlobFromJson(json as Map<String, dynamic>);
}
//////////////////////////////////////
/// Inspect
@freezed
class DHTRecordReport with _$DHTRecordReport {
const factory DHTRecordReport({
required List<ValueSubkeyRange> subkeys,
required List<int> localSeqs,
required List<int> networkSeqs,
}) = _DHTRecordReport;
factory DHTRecordReport.fromJson(dynamic json) =>
_$DHTRecordReportFromJson(json as Map<String, dynamic>);
}
enum DHTReportScope {
local,
syncGet,
syncSet,
updateGet,
updateSet;
factory DHTReportScope.fromJson(dynamic j) =>
DHTReportScope.values.byName((j as String).toCamelCase());
String toJson() => name.toPascalCase();
}
//////////////////////////////////////
/// VeilidRoutingContext
@ -247,9 +272,11 @@ abstract class VeilidRoutingContext {
void close();
// Modifiers
VeilidRoutingContext withDefaultSafety();
VeilidRoutingContext withSafety(SafetySelection safetySelection);
VeilidRoutingContext withSequencing(Sequencing sequencing);
VeilidRoutingContext withDefaultSafety({bool closeSelf = false});
VeilidRoutingContext withSafety(SafetySelection safetySelection,
{bool closeSelf = false});
VeilidRoutingContext withSequencing(Sequencing sequencing,
{bool closeSelf = false});
Future<SafetySelection> safety();
// App call/message
@ -269,4 +296,7 @@ abstract class VeilidRoutingContext {
Future<Timestamp> watchDHTValues(TypedKey key,
{List<ValueSubkeyRange>? subkeys, Timestamp? expiration, int? count});
Future<bool> cancelDHTWatch(TypedKey key, {List<ValueSubkeyRange>? subkeys});
Future<DHTRecordReport> inspectDHTRecord(TypedKey key,
{List<ValueSubkeyRange>? subkeys,
DHTReportScope scope = DHTReportScope.local});
}

View File

@ -1355,3 +1355,210 @@ abstract class _RouteBlob implements RouteBlob {
_$$RouteBlobImplCopyWith<_$RouteBlobImpl> get copyWith =>
throw _privateConstructorUsedError;
}
DHTRecordReport _$DHTRecordReportFromJson(Map<String, dynamic> json) {
return _DHTRecordReport.fromJson(json);
}
/// @nodoc
mixin _$DHTRecordReport {
List<ValueSubkeyRange> get subkeys => throw _privateConstructorUsedError;
List<int> get localSeqs => throw _privateConstructorUsedError;
List<int> get networkSeqs => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DHTRecordReportCopyWith<DHTRecordReport> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DHTRecordReportCopyWith<$Res> {
factory $DHTRecordReportCopyWith(
DHTRecordReport value, $Res Function(DHTRecordReport) then) =
_$DHTRecordReportCopyWithImpl<$Res, DHTRecordReport>;
@useResult
$Res call(
{List<ValueSubkeyRange> subkeys,
List<int> localSeqs,
List<int> networkSeqs});
}
/// @nodoc
class _$DHTRecordReportCopyWithImpl<$Res, $Val extends DHTRecordReport>
implements $DHTRecordReportCopyWith<$Res> {
_$DHTRecordReportCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? subkeys = null,
Object? localSeqs = null,
Object? networkSeqs = null,
}) {
return _then(_value.copyWith(
subkeys: null == subkeys
? _value.subkeys
: subkeys // ignore: cast_nullable_to_non_nullable
as List<ValueSubkeyRange>,
localSeqs: null == localSeqs
? _value.localSeqs
: localSeqs // ignore: cast_nullable_to_non_nullable
as List<int>,
networkSeqs: null == networkSeqs
? _value.networkSeqs
: networkSeqs // ignore: cast_nullable_to_non_nullable
as List<int>,
) as $Val);
}
}
/// @nodoc
abstract class _$$DHTRecordReportImplCopyWith<$Res>
implements $DHTRecordReportCopyWith<$Res> {
factory _$$DHTRecordReportImplCopyWith(_$DHTRecordReportImpl value,
$Res Function(_$DHTRecordReportImpl) then) =
__$$DHTRecordReportImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<ValueSubkeyRange> subkeys,
List<int> localSeqs,
List<int> networkSeqs});
}
/// @nodoc
class __$$DHTRecordReportImplCopyWithImpl<$Res>
extends _$DHTRecordReportCopyWithImpl<$Res, _$DHTRecordReportImpl>
implements _$$DHTRecordReportImplCopyWith<$Res> {
__$$DHTRecordReportImplCopyWithImpl(
_$DHTRecordReportImpl _value, $Res Function(_$DHTRecordReportImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? subkeys = null,
Object? localSeqs = null,
Object? networkSeqs = null,
}) {
return _then(_$DHTRecordReportImpl(
subkeys: null == subkeys
? _value._subkeys
: subkeys // ignore: cast_nullable_to_non_nullable
as List<ValueSubkeyRange>,
localSeqs: null == localSeqs
? _value._localSeqs
: localSeqs // ignore: cast_nullable_to_non_nullable
as List<int>,
networkSeqs: null == networkSeqs
? _value._networkSeqs
: networkSeqs // ignore: cast_nullable_to_non_nullable
as List<int>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$DHTRecordReportImpl implements _DHTRecordReport {
const _$DHTRecordReportImpl(
{required final List<ValueSubkeyRange> subkeys,
required final List<int> localSeqs,
required final List<int> networkSeqs})
: _subkeys = subkeys,
_localSeqs = localSeqs,
_networkSeqs = networkSeqs;
factory _$DHTRecordReportImpl.fromJson(Map<String, dynamic> json) =>
_$$DHTRecordReportImplFromJson(json);
final List<ValueSubkeyRange> _subkeys;
@override
List<ValueSubkeyRange> get subkeys {
if (_subkeys is EqualUnmodifiableListView) return _subkeys;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_subkeys);
}
final List<int> _localSeqs;
@override
List<int> get localSeqs {
if (_localSeqs is EqualUnmodifiableListView) return _localSeqs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_localSeqs);
}
final List<int> _networkSeqs;
@override
List<int> get networkSeqs {
if (_networkSeqs is EqualUnmodifiableListView) return _networkSeqs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_networkSeqs);
}
@override
String toString() {
return 'DHTRecordReport(subkeys: $subkeys, localSeqs: $localSeqs, networkSeqs: $networkSeqs)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DHTRecordReportImpl &&
const DeepCollectionEquality().equals(other._subkeys, _subkeys) &&
const DeepCollectionEquality()
.equals(other._localSeqs, _localSeqs) &&
const DeepCollectionEquality()
.equals(other._networkSeqs, _networkSeqs));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_subkeys),
const DeepCollectionEquality().hash(_localSeqs),
const DeepCollectionEquality().hash(_networkSeqs));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$DHTRecordReportImplCopyWith<_$DHTRecordReportImpl> get copyWith =>
__$$DHTRecordReportImplCopyWithImpl<_$DHTRecordReportImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$DHTRecordReportImplToJson(
this,
);
}
}
abstract class _DHTRecordReport implements DHTRecordReport {
const factory _DHTRecordReport(
{required final List<ValueSubkeyRange> subkeys,
required final List<int> localSeqs,
required final List<int> networkSeqs}) = _$DHTRecordReportImpl;
factory _DHTRecordReport.fromJson(Map<String, dynamic> json) =
_$DHTRecordReportImpl.fromJson;
@override
List<ValueSubkeyRange> get subkeys;
@override
List<int> get localSeqs;
@override
List<int> get networkSeqs;
@override
@JsonKey(ignore: true)
_$$DHTRecordReportImplCopyWith<_$DHTRecordReportImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -109,3 +109,23 @@ Map<String, dynamic> _$$RouteBlobImplToJson(_$RouteBlobImpl instance) =>
'route_id': instance.routeId,
'blob': const Uint8ListJsonConverter().toJson(instance.blob),
};
_$DHTRecordReportImpl _$$DHTRecordReportImplFromJson(
Map<String, dynamic> json) =>
_$DHTRecordReportImpl(
subkeys: (json['subkeys'] as List<dynamic>)
.map(ValueSubkeyRange.fromJson)
.toList(),
localSeqs:
(json['local_seqs'] as List<dynamic>).map((e) => e as int).toList(),
networkSeqs:
(json['network_seqs'] as List<dynamic>).map((e) => e as int).toList(),
);
Map<String, dynamic> _$$DHTRecordReportImplToJson(
_$DHTRecordReportImpl instance) =>
<String, dynamic>{
'subkeys': instance.subkeys.map((e) => e.toJson()).toList(),
'local_seqs': instance.localSeqs,
'network_seqs': instance.networkSeqs,
};

View File

@ -94,6 +94,10 @@ typedef _RoutingContextWatchDHTValuesDart = void Function(
// id: u32, key: FfiStr, subkeys: FfiStr)
typedef _RoutingContextCancelDHTWatchDart = void Function(
int, int, Pointer<Utf8>, Pointer<Utf8>);
// fn routing_context_inspect_dht_record(port: i64,
// id: u32, key: FfiStr, subkeys: FfiStr, scope: FfiStr)
typedef _RoutingContextInspectDHTRecordDart = void Function(
int, int, Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>);
// fn new_private_route(port: i64)
typedef _NewPrivateRouteDart = void Function(int);
@ -534,26 +538,40 @@ class VeilidRoutingContextFFI extends VeilidRoutingContext {
}
@override
VeilidRoutingContextFFI withDefaultSafety() {
VeilidRoutingContextFFI withDefaultSafety({bool closeSelf = false}) {
_ctx.ensureValid();
final newId = _ctx.ffi._routingContextWithDefaultSafety(_ctx.id!);
return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
final out = VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
if (closeSelf) {
close();
}
return out;
}
@override
VeilidRoutingContextFFI withSafety(SafetySelection safetySelection) {
VeilidRoutingContextFFI withSafety(SafetySelection safetySelection,
{bool closeSelf = false}) {
_ctx.ensureValid();
final newId = _ctx.ffi._routingContextWithSafety(
_ctx.id!, jsonEncode(safetySelection).toNativeUtf8());
return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
final out = VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
if (closeSelf) {
close();
}
return out;
}
@override
VeilidRoutingContextFFI withSequencing(Sequencing sequencing) {
VeilidRoutingContextFFI withSequencing(Sequencing sequencing,
{bool closeSelf = false}) {
_ctx.ensureValid();
final newId = _ctx.ffi._routingContextWithSequencing(
_ctx.id!, jsonEncode(sequencing).toNativeUtf8());
return VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
final out = VeilidRoutingContextFFI._(_Ctx(newId, _ctx.ffi));
if (closeSelf) {
close();
}
return out;
}
@override
@ -717,6 +735,26 @@ class VeilidRoutingContextFFI extends VeilidRoutingContext {
final cancelled = await processFuturePlain<bool>(recvPort.first);
return cancelled;
}
@override
Future<DHTRecordReport> inspectDHTRecord(TypedKey key,
{List<ValueSubkeyRange>? subkeys,
DHTReportScope scope = DHTReportScope.local}) async {
subkeys ??= [];
_ctx.ensureValid();
final nativeKey = jsonEncode(key).toNativeUtf8();
final nativeSubkeys = jsonEncode(subkeys).toNativeUtf8();
final nativeScope = jsonEncode(scope).toNativeUtf8();
final recvPort = ReceivePort('routing_context_inspect_dht_record');
final sendPort = recvPort.sendPort;
_ctx.ffi._routingContextInspectDHTRecord(
sendPort.nativePort, _ctx.id!, nativeKey, nativeSubkeys, nativeScope);
final report =
await processFutureJson(DHTRecordReport.fromJson, recvPort.first);
return report;
}
}
class _TDBT {
@ -1259,6 +1297,11 @@ class VeilidFFI extends Veilid {
Void Function(Int64, Uint32, Pointer<Utf8>, Pointer<Utf8>),
_RoutingContextCancelDHTWatchDart>(
'routing_context_cancel_dht_watch'),
_routingContextInspectDHTRecord = dylib.lookupFunction<
Void Function(
Int64, Uint32, Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>),
_RoutingContextInspectDHTRecordDart>(
'routing_context_inspect_dht_record'),
_newPrivateRoute =
dylib.lookupFunction<Void Function(Int64), _NewPrivateRouteDart>(
'new_private_route'),
@ -1402,9 +1445,14 @@ class VeilidFFI extends Veilid {
_DefaultVeilidConfigDart>('default_veilid_config') {
// Get veilid_flutter initializer
final initializeVeilidFlutter = _dylib.lookupFunction<
Void Function(Pointer<_DartPostCObject>),
void Function(Pointer<_DartPostCObject>)>('initialize_veilid_flutter');
initializeVeilidFlutter(NativeApi.postCObject);
Void Function(Pointer<_DartPostCObject>, Pointer<Utf8>),
void Function(Pointer<_DartPostCObject>,
Pointer<Utf8>)>('initialize_veilid_flutter');
initializeVeilidFlutter(
NativeApi.postCObject,
// ignore: avoid_redundant_argument_values, do_not_use_environment
const String.fromEnvironment('VEILID_CRASH_PATH').toNativeUtf8(),
); <