mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Implement meshcore-open route reload
This commit is contained in:
@@ -4,7 +4,10 @@ import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/models/path_selection.dart';
|
||||
import 'package:meshcore_sar_app/providers/helpers/message_retry_manager.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Contact _buildContact() {
|
||||
return Contact(
|
||||
@@ -39,6 +42,10 @@ Message _buildDirectMessage(String id) {
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('MessagesProvider retransmission', () {
|
||||
test('direct messages become sent before delivery ACK arrives', () {
|
||||
final provider = MessagesProvider();
|
||||
@@ -171,7 +178,11 @@ void main() {
|
||||
);
|
||||
expect(retryCalls, 0);
|
||||
|
||||
async.elapse(const Duration(seconds: 4));
|
||||
async.elapse(const Duration(milliseconds: 998));
|
||||
async.flushMicrotasks();
|
||||
expect(retryCalls, 0);
|
||||
|
||||
async.elapse(const Duration(milliseconds: 1));
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(retryCalls, 1);
|
||||
@@ -237,68 +248,89 @@ void main() {
|
||||
expect(provider.messages.single.retryAttempt, 0);
|
||||
});
|
||||
|
||||
test('repeated max-retry failures request path reset', () async {
|
||||
test(
|
||||
'final router fallback runs after all normal retries are exhausted',
|
||||
() async {
|
||||
final provider = MessagesProvider();
|
||||
final fallbackCalls = <String>[];
|
||||
provider.onFinalRouterFallbackCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
fallbackCalls.add(messageId);
|
||||
return true;
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm5',
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
|
||||
provider.markMessageFailed('m5');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(fallbackCalls, ['m5']);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sending,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('final router fallback is not retried twice', () async {
|
||||
final provider = MessagesProvider();
|
||||
final contact = _buildContact();
|
||||
final resetRequests = <(String, int)>[];
|
||||
provider.onDirectPathFailedCallback =
|
||||
({required contact, required failureStreak}) async {
|
||||
resetRequests.add((contact.advName, failureStreak));
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm5',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m5');
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm6',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
provider.updateMessageRouteSelection(
|
||||
'm6',
|
||||
PathSelection.flood(),
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
|
||||
provider.markMessageFailed('m6');
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(resetRequests, [('Teammate', 2)]);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('successful delivery clears path failure streak', () async {
|
||||
final provider = MessagesProvider();
|
||||
final contact = _buildContact();
|
||||
final resetRequests = <int>[];
|
||||
provider.onDirectPathFailedCallback =
|
||||
({required contact, required failureStreak}) async {
|
||||
resetRequests.add(failureStreak);
|
||||
};
|
||||
test(
|
||||
'final permanent failure callback runs after router fallback failure',
|
||||
() async {
|
||||
final provider = MessagesProvider();
|
||||
final failedMessageIds = <String>[];
|
||||
provider.onFinalDirectMessageFailureCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
failedMessageIds.add(messageId);
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm7',
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
provider.updateMessageRouteSelection(
|
||||
'm7',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m7');
|
||||
PathSelection.flood(),
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
|
||||
provider.addSentMessage(_buildDirectMessage('m8'), contact: contact);
|
||||
provider.markMessageSent('m8', 123, 10);
|
||||
provider.markMessageDelivered(123, 150);
|
||||
provider.markMessageFailed('m7');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm9',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m9');
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(resetRequests, isEmpty);
|
||||
});
|
||||
expect(failedMessageIds, ['m7']);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.failed,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
31
test/services/messaging_route_preferences_test.dart
Normal file
31
test/services/messaging_route_preferences_test.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/services/messaging_route_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('route preference defaults are disabled', () async {
|
||||
expect(
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
||||
isFalse,
|
||||
);
|
||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isFalse);
|
||||
});
|
||||
|
||||
test('route preferences persist changes', () async {
|
||||
await MessagingRoutePreferences.setAutoRouteRotationEnabled(true);
|
||||
await MessagingRoutePreferences.setClearPathOnMaxRetry(true);
|
||||
|
||||
expect(
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
||||
isTrue,
|
||||
);
|
||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isTrue);
|
||||
});
|
||||
}
|
||||
115
test/services/nearest_router_selector_test.dart
Normal file
115
test/services/nearest_router_selector_test.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/services/nearest_router_selector.dart';
|
||||
|
||||
Contact _buildRepeater({
|
||||
required int seed,
|
||||
required String name,
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required int lastAdvert,
|
||||
int outPathLen = -1,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
|
||||
type: ContactType.repeater,
|
||||
flags: 0,
|
||||
outPathLen: outPathLen,
|
||||
outPath: Uint8List(0),
|
||||
advName: name,
|
||||
lastAdvert: lastAdvert,
|
||||
advLat: (latitude * 1e6).round(),
|
||||
advLon: (longitude * 1e6).round(),
|
||||
lastMod: lastAdvert,
|
||||
);
|
||||
}
|
||||
|
||||
Position _position(double latitude, double longitude) {
|
||||
return Position(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: 1,
|
||||
altitude: 0,
|
||||
altitudeAccuracy: 1,
|
||||
heading: 0,
|
||||
headingAccuracy: 1,
|
||||
speed: 0,
|
||||
speedAccuracy: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('selector chooses nearest eligible repeater', () {
|
||||
final selector = NearestRouterSelector();
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final recipient = _buildRepeater(
|
||||
seed: 90,
|
||||
name: 'Recipient',
|
||||
latitude: 46.05,
|
||||
longitude: 14.50,
|
||||
lastAdvert: now,
|
||||
).copyWith(type: ContactType.chat);
|
||||
|
||||
final selected = selector.select(
|
||||
senderPosition: _position(46.0569, 14.5058),
|
||||
repeaters: [
|
||||
_buildRepeater(
|
||||
seed: 1,
|
||||
name: 'Far',
|
||||
latitude: 46.10,
|
||||
longitude: 14.60,
|
||||
lastAdvert: now,
|
||||
outPathLen: 1,
|
||||
),
|
||||
_buildRepeater(
|
||||
seed: 2,
|
||||
name: 'Near',
|
||||
latitude: 46.0570,
|
||||
longitude: 14.5060,
|
||||
lastAdvert: now - 5,
|
||||
outPathLen: 1,
|
||||
),
|
||||
],
|
||||
recipient: recipient,
|
||||
);
|
||||
|
||||
expect(selected?.advName, 'Near');
|
||||
});
|
||||
|
||||
test('selector skips stale repeaters', () {
|
||||
final selector = NearestRouterSelector();
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final staleAdvert = now - (11 * 60);
|
||||
final recipient = _buildRepeater(
|
||||
seed: 91,
|
||||
name: 'Recipient',
|
||||
latitude: 46.05,
|
||||
longitude: 14.50,
|
||||
lastAdvert: now,
|
||||
).copyWith(type: ContactType.chat);
|
||||
|
||||
final selected = selector.select(
|
||||
senderPosition: _position(46.0569, 14.5058),
|
||||
repeaters: [
|
||||
_buildRepeater(
|
||||
seed: 3,
|
||||
name: 'Stale',
|
||||
latitude: 46.0570,
|
||||
longitude: 14.5060,
|
||||
lastAdvert: staleAdvert,
|
||||
outPathLen: 1,
|
||||
),
|
||||
],
|
||||
recipient: recipient,
|
||||
);
|
||||
|
||||
expect(selected, isNull);
|
||||
});
|
||||
}
|
||||
130
test/services/path_history_service_test.dart
Normal file
130
test/services/path_history_service_test.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/path_selection.dart';
|
||||
import 'package:meshcore_sar_app/services/path_history_service.dart';
|
||||
|
||||
Contact _buildContact({
|
||||
required int seed,
|
||||
required List<int> pathBytes,
|
||||
required int hopCount,
|
||||
required int hashSize,
|
||||
}) {
|
||||
final encoded = ((hashSize - 1) << 6) | (hopCount & 0x3F);
|
||||
final outPath = Uint8List(ContactRouteCodec.maxPathBytes)
|
||||
..setRange(0, pathBytes.length, pathBytes);
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: ContactRouteCodec.toSignedDescriptor(encoded),
|
||||
outPath: outPath,
|
||||
advName: 'Contact $seed',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('auto rotation ranks best paths before flood', () async {
|
||||
final service = PathHistoryService();
|
||||
final contact = _buildContact(
|
||||
seed: 0,
|
||||
pathBytes: [0xAA, 0xBB],
|
||||
hopCount: 2,
|
||||
hashSize: 1,
|
||||
);
|
||||
final best = PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList([0xAA, 0xBB]),
|
||||
hopCount: 2,
|
||||
hashSize: 1,
|
||||
);
|
||||
final second = PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList([0xCC, 0xDD]),
|
||||
hopCount: 2,
|
||||
hashSize: 1,
|
||||
);
|
||||
|
||||
await service.initialize();
|
||||
await service.recordLearnedPath(contact);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
best,
|
||||
success: true,
|
||||
roundTripTimeMs: 120,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
best,
|
||||
success: true,
|
||||
roundTripTimeMs: 110,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
second,
|
||||
success: true,
|
||||
roundTripTimeMs: 200,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
second,
|
||||
success: false,
|
||||
);
|
||||
|
||||
final first = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
final third = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
final secondPick = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
|
||||
expect(first.mode, PathSelectionMode.directHistorical);
|
||||
expect(first.canonicalPath, 'AA,BB');
|
||||
expect(third.mode, PathSelectionMode.directHistorical);
|
||||
expect(third.canonicalPath, 'CC,DD');
|
||||
expect(secondPick.mode, PathSelectionMode.flood);
|
||||
});
|
||||
|
||||
test('no history falls back to flood', () async {
|
||||
final service = PathHistoryService();
|
||||
final contact = Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: -1,
|
||||
outPath: Uint8List(0),
|
||||
advName: 'No Route',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
|
||||
final selection = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
|
||||
expect(selection.mode, PathSelectionMode.flood);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user