mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-13 04:12:00 +10:00
373 lines
12 KiB
Dart
373 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
|
import 'package:meshcore_open/services/app_settings_service.dart';
|
|
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
|
import 'package:meshcore_open/models/contact.dart';
|
|
import 'package:meshcore_open/l10n/app_localizations.dart';
|
|
|
|
class _FakeMeshCoreConnector extends MeshCoreConnector {
|
|
final StreamController<Uint8List> _receivedFramesController =
|
|
StreamController<Uint8List>.broadcast();
|
|
|
|
@override
|
|
Stream<Uint8List> get receivedFrames => _receivedFramesController.stream;
|
|
|
|
@override
|
|
Uint8List? get selfPublicKey =>
|
|
Uint8List.fromList(List.generate(32, (i) => i));
|
|
|
|
@override
|
|
double? get selfLatitude => 48.8566;
|
|
|
|
@override
|
|
double? get selfLongitude => 2.3522;
|
|
|
|
@override
|
|
List<Contact> get allContactsUnfiltered => [];
|
|
|
|
@override
|
|
List<Contact> get contacts => [];
|
|
|
|
void emitFrame(Uint8List frame) {
|
|
_receivedFramesController.add(frame);
|
|
}
|
|
|
|
final List<Uint8List> sentFrames = [];
|
|
|
|
@override
|
|
Future<void> sendFrame(
|
|
Uint8List frame, {
|
|
String? channelSendQueueId,
|
|
bool expectsGenericAck = false,
|
|
}) async {
|
|
sentFrames.add(frame);
|
|
}
|
|
}
|
|
|
|
Widget _buildTestApp({
|
|
required MeshCoreConnector connector,
|
|
required Widget child,
|
|
}) {
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<MeshCoreConnector>.value(value: connector),
|
|
ChangeNotifierProvider<AppSettingsService>(
|
|
create: (_) => AppSettingsService(),
|
|
),
|
|
Provider<MapTileCacheService>(create: (_) => MapTileCacheService()),
|
|
],
|
|
child: MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('PathTraceMapScreen parses response frames correctly', (
|
|
tester,
|
|
) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Test Trace',
|
|
path: Uint8List.fromList([0x12, 0x34]),
|
|
pathHashByteWidth: 1,
|
|
);
|
|
|
|
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
|
|
await tester.pump();
|
|
|
|
// Verify a send request was made
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
|
|
// Extract the tag sent in the request (bytes 1-4)
|
|
final tag = sentFrame.sublist(1, 5);
|
|
|
|
// Emit respCodeSent (6)
|
|
final respCodeSentFrame = Uint8List(10);
|
|
respCodeSentFrame[0] = respCodeSent;
|
|
respCodeSentFrame[1] = 0; // reserved / is_flood
|
|
respCodeSentFrame.setRange(2, 6, tag);
|
|
// timeout milliseconds = 5000 (0x1388)
|
|
respCodeSentFrame[6] = 0x88;
|
|
respCodeSentFrame[7] = 0x13;
|
|
respCodeSentFrame[8] = 0x00;
|
|
respCodeSentFrame[9] = 0x00;
|
|
|
|
connector.emitFrame(respCodeSentFrame);
|
|
await tester.pump();
|
|
|
|
// Now emit PUSH_CODE_TRACE_DATA (0x89)
|
|
// Structure:
|
|
// Offset 0: pushCodeTraceData (0x89)
|
|
// Offset 1: reserved (0)
|
|
// Offset 2: pathLength (2)
|
|
// Offset 3: flag (0)
|
|
// Offset 4..7: tag
|
|
// Offset 8..11: auth (0)
|
|
// Offset 12..13: pathBytes [0x12, 0x34]
|
|
// Offset 14..15: SNR bytes [12, 16] -> to be mapped to signed 8 bit Snr/4
|
|
final pushTraceFrame = Uint8List(16);
|
|
pushTraceFrame[0] = pushCodeTraceData;
|
|
pushTraceFrame[1] = 0; // reserved
|
|
pushTraceFrame[2] = 2; // pathLength
|
|
pushTraceFrame[3] = 0; // flag
|
|
pushTraceFrame.setRange(4, 8, tag);
|
|
// auth bytes (8..11) = 0
|
|
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
|
|
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
|
|
|
|
connector.emitFrame(pushTraceFrame);
|
|
// pump multiple times to handle async tasks
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify it doesn't show "Path trace not available" or similar
|
|
expect(find.text('Path trace not available.'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'PathTraceMapScreen parses multi-byte encoded path trace response correctly',
|
|
(tester) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Test Trace Multi-byte',
|
|
path: Uint8List.fromList([0x12, 0x34]),
|
|
pathHashByteWidth: 2,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(connector: connector, child: screen),
|
|
);
|
|
await tester.pump();
|
|
|
|
// Verify a send request was made
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
|
|
// Extract the tag sent in the request (bytes 1-4)
|
|
final tag = sentFrame.sublist(1, 5);
|
|
|
|
// Emit respCodeSent (6)
|
|
final respCodeSentFrame = Uint8List(10);
|
|
respCodeSentFrame[0] = respCodeSent;
|
|
respCodeSentFrame[1] = 0;
|
|
respCodeSentFrame.setRange(2, 6, tag);
|
|
respCodeSentFrame[6] = 0x88;
|
|
respCodeSentFrame[7] = 0x13;
|
|
respCodeSentFrame[8] = 0x00;
|
|
respCodeSentFrame[9] = 0x00;
|
|
|
|
connector.emitFrame(respCodeSentFrame);
|
|
await tester.pump();
|
|
|
|
// Now emit PUSH_CODE_TRACE_DATA (0x89)
|
|
// Structure:
|
|
// Offset 0: pushCodeTraceData (0x89)
|
|
// Offset 1: reserved (0)
|
|
// Offset 2: pathLength (65) -> mode 1 (width 2), 1 hop
|
|
// Offset 3: flag (0)
|
|
// Offset 4..7: tag
|
|
// Offset 8..11: auth (0)
|
|
// Offset 12..13: pathBytes [0x12, 0x34]
|
|
// Offset 14..15: SNR bytes [12, 16]
|
|
final pushTraceFrame = Uint8List(16);
|
|
pushTraceFrame[0] = pushCodeTraceData;
|
|
pushTraceFrame[1] = 0; // reserved
|
|
pushTraceFrame[2] = 65; // pathLength encoded (mode 1, 1 hop -> 2 bytes)
|
|
pushTraceFrame[3] = 0; // flag
|
|
pushTraceFrame.setRange(4, 8, tag);
|
|
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
|
|
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
|
|
|
|
connector.emitFrame(pushTraceFrame);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify it parsed correctly (should not show the unavailable message)
|
|
expect(find.text('Path trace not available.'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('PathTraceMapScreen buildPath: Direct repeater ping (0-hop)', (
|
|
tester,
|
|
) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
final target = Contact(
|
|
publicKey: Uint8List.fromList([0xAA, 0xBB, 0xCC]),
|
|
name: 'Repeater Node',
|
|
type: advTypeRepeater,
|
|
path: Uint8List(0),
|
|
pathLength: 0,
|
|
lastSeen: DateTime.now(),
|
|
);
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Direct Repeater Ping',
|
|
path: Uint8List.fromList([0xAA]),
|
|
targetContact: target,
|
|
flipPathAround: true,
|
|
pathHashByteWidth: 1,
|
|
);
|
|
|
|
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
|
|
await tester.pump();
|
|
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
final payload = sentFrame.sublist(10);
|
|
expect(payload, Uint8List.fromList([0xAA]));
|
|
});
|
|
|
|
testWidgets('PathTraceMapScreen buildPath: 1-hop chat contact trace', (
|
|
tester,
|
|
) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
final target = Contact(
|
|
publicKey: Uint8List.fromList([0xCC, 0xDD, 0xEE]),
|
|
name: 'Chat User',
|
|
type: advTypeChat,
|
|
path: Uint8List.fromList([0xBB]),
|
|
pathLength: 1,
|
|
lastSeen: DateTime.now(),
|
|
);
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: '1-Hop Chat Trace',
|
|
path: Uint8List.fromList([0xBB]),
|
|
targetContact: target,
|
|
flipPathAround: true,
|
|
pathHashByteWidth: 1,
|
|
);
|
|
|
|
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
|
|
await tester.pump();
|
|
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
final payload = sentFrame.sublist(10);
|
|
expect(payload, Uint8List.fromList([0xBB, 0xCC, 0xBB]));
|
|
});
|
|
|
|
testWidgets('PathTraceMapScreen buildPath: Multi-hop chat contact trace', (
|
|
tester,
|
|
) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
final target = Contact(
|
|
publicKey: Uint8List.fromList([0x33, 0x44, 0x55]),
|
|
name: 'Chat User Multi-hop',
|
|
type: advTypeChat,
|
|
path: Uint8List.fromList([0x11, 0x22]),
|
|
pathLength: 2,
|
|
lastSeen: DateTime.now(),
|
|
);
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Multi-Hop Chat Trace',
|
|
path: Uint8List.fromList([0x11, 0x22]),
|
|
targetContact: target,
|
|
flipPathAround: true,
|
|
pathHashByteWidth: 1,
|
|
);
|
|
|
|
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
|
|
await tester.pump();
|
|
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
final payload = sentFrame.sublist(10);
|
|
expect(payload, Uint8List.fromList([0x11, 0x22, 0x33, 0x22, 0x11]));
|
|
});
|
|
|
|
testWidgets(
|
|
'PathTraceMapScreen buildPath: Map trace with null target contact',
|
|
(tester) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Map Trace No Target',
|
|
path: Uint8List.fromList([0x11, 0x22]),
|
|
targetContact: null,
|
|
flipPathAround: true,
|
|
pathHashByteWidth: 1,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
_buildTestApp(connector: connector, child: screen),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
expect(sentFrame[0], cmdSendTracePath);
|
|
final payload = sentFrame.sublist(10);
|
|
expect(payload, Uint8List.fromList([0x11, 0x22, 0x11]));
|
|
},
|
|
);
|
|
|
|
testWidgets('PathTraceMapScreen parses raw byte length fallback correctly', (
|
|
tester,
|
|
) async {
|
|
final connector = _FakeMeshCoreConnector();
|
|
|
|
final screen = PathTraceMapScreen(
|
|
title: 'Test Raw Fallback',
|
|
path: Uint8List.fromList([0x12, 0x34]),
|
|
pathHashByteWidth: 2,
|
|
);
|
|
|
|
await tester.pumpWidget(_buildTestApp(connector: connector, child: screen));
|
|
await tester.pump();
|
|
|
|
expect(connector.sentFrames.length, 1);
|
|
final sentFrame = connector.sentFrames.first;
|
|
final tag = sentFrame.sublist(1, 5);
|
|
|
|
// Emit respCodeSent (6)
|
|
final respCodeSentFrame = Uint8List(10);
|
|
respCodeSentFrame[0] = respCodeSent;
|
|
respCodeSentFrame[1] = 0;
|
|
respCodeSentFrame.setRange(2, 6, tag);
|
|
respCodeSentFrame[6] = 0x88;
|
|
respCodeSentFrame[7] = 0x13;
|
|
respCodeSentFrame[8] = 0x00;
|
|
respCodeSentFrame[9] = 0x00;
|
|
|
|
connector.emitFrame(respCodeSentFrame);
|
|
await tester.pump();
|
|
|
|
// Now emit PUSH_CODE_TRACE_DATA (0x89)
|
|
// Structure:
|
|
// Offset 2: pathLength (2) -> raw byte length count, mode is 0!
|
|
// Offset 12..13: pathBytes [0x12, 0x34]
|
|
final pushTraceFrame = Uint8List(16);
|
|
pushTraceFrame[0] = pushCodeTraceData;
|
|
pushTraceFrame[1] = 0; // reserved
|
|
pushTraceFrame[2] = 2; // pathLength as raw byte length (2 bytes)
|
|
pushTraceFrame[3] = 0; // flag
|
|
pushTraceFrame.setRange(4, 8, tag);
|
|
pushTraceFrame.setRange(12, 14, [0x12, 0x34]); // pathBytes
|
|
pushTraceFrame.setRange(14, 16, [12, 16]); // SNR bytes
|
|
|
|
connector.emitFrame(pushTraceFrame);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify it parsed correctly (should not show the unavailable message)
|
|
expect(find.text('Path trace not available.'), findsNothing);
|
|
});
|
|
}
|