244 lines
7.9 KiB
Dart
244 lines
7.9 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:abawo_bt_app/model/firmware_file_selection.dart';
|
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
|
import 'package:abawo_bt_app/service/dfu_protocol.dart';
|
|
import 'package:abawo_bt_app/service/firmware_file_selection_service.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
group('FirmwareFileSelectionService', () {
|
|
test('prepares bootloader metadata for selected .bin firmware', () async {
|
|
final image = _validBootloaderImage();
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.BIN',
|
|
filePath: '/tmp/firmware.BIN',
|
|
fileBytes: image,
|
|
),
|
|
),
|
|
sessionIdGenerator: () => 0x1AB,
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
expect(result.isSuccess, isTrue);
|
|
|
|
final firmware = result.firmware!;
|
|
expect(firmware.fileName, 'firmware.BIN');
|
|
expect(firmware.filePath, '/tmp/firmware.BIN');
|
|
expect(firmware.fileBytes, image);
|
|
expect(firmware.metadata.totalLength, image.length);
|
|
expect(firmware.metadata.crc32, BootloaderDfuProtocol.crc32(image));
|
|
expect(firmware.metadata.appStart, universalShifterDfuAppStart);
|
|
expect(firmware.metadata.imageVersion, 0);
|
|
expect(firmware.metadata.sessionId, 0xAB);
|
|
expect(firmware.metadata.flags, universalShifterDfuFlagNone);
|
|
expect(firmware.metadata.vectorStackPointer, 0x20001000);
|
|
expect(firmware.metadata.vectorReset, 0x00030009);
|
|
});
|
|
|
|
test('returns canceled result when user dismisses picker', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(selection: null),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(result.isCanceled, isTrue);
|
|
expect(result.failure?.reason, FirmwareSelectionFailureReason.canceled);
|
|
});
|
|
|
|
test('rejects unsupported extension', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.hex',
|
|
fileBytes: _validBootloaderImage(),
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(result.failure?.reason,
|
|
FirmwareSelectionFailureReason.unsupportedExtension);
|
|
});
|
|
|
|
test('rejects empty payload', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: Uint8List(0),
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(result.failure?.reason, FirmwareSelectionFailureReason.emptyFile);
|
|
});
|
|
|
|
test('rejects images that are too small for a vector table', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(
|
|
result.failure?.reason, FirmwareSelectionFailureReason.imageTooSmall);
|
|
});
|
|
|
|
test('rejects images larger than the application slot', () async {
|
|
final image = Uint8List(universalShifterDfuAppSlotSizeBytes + 1);
|
|
_writeLeU32(image, 0, 0x20001000);
|
|
_writeLeU32(image, 4, 0x00030009);
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: image,
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(
|
|
result.failure?.reason, FirmwareSelectionFailureReason.imageTooLarge);
|
|
});
|
|
|
|
test('accepts image exactly at application slot size', () async {
|
|
final image = Uint8List(universalShifterDfuAppSlotSizeBytes);
|
|
_writeLeU32(image, 0, 0x20001000);
|
|
_writeLeU32(image, 4, 0x00030009);
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: image,
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isTrue);
|
|
expect(result.firmware?.metadata.totalLength,
|
|
universalShifterDfuAppSlotSizeBytes);
|
|
});
|
|
|
|
test('rejects images with invalid vector table', () async {
|
|
final image = _validBootloaderImage();
|
|
_writeLeU32(image, 0, 0x10001000);
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: image,
|
|
),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(result.failure?.reason,
|
|
FirmwareSelectionFailureReason.invalidVectorTable);
|
|
});
|
|
|
|
test('generates session id per run', () async {
|
|
var nextSession = 9;
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: _validBootloaderImage(),
|
|
),
|
|
),
|
|
sessionIdGenerator: () => nextSession++,
|
|
);
|
|
|
|
final first = await service.selectAndPrepareBootloaderDfu();
|
|
final second = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(first.firmware?.metadata.sessionId, 9);
|
|
expect(second.firmware?.metadata.sessionId, 10);
|
|
});
|
|
|
|
test('normalizes generated zero session id to one', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: FirmwarePickerSelection(
|
|
fileName: 'firmware.bin',
|
|
fileBytes: _validBootloaderImage(),
|
|
),
|
|
),
|
|
sessionIdGenerator: () => 0,
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isTrue);
|
|
expect(result.firmware?.metadata.sessionId, 1);
|
|
});
|
|
|
|
test('maps picker read failure to explicit validation error', () async {
|
|
final service = FirmwareFileSelectionService(
|
|
filePicker: _FakeFirmwareFilePicker(
|
|
selection: null,
|
|
error: const FormatException('broken pick payload'),
|
|
),
|
|
);
|
|
|
|
final result = await service.selectAndPrepareBootloaderDfu();
|
|
|
|
expect(result.isSuccess, isFalse);
|
|
expect(result.failure?.reason, FirmwareSelectionFailureReason.readFailed);
|
|
expect(result.failure?.message, contains('broken pick payload'));
|
|
});
|
|
});
|
|
}
|
|
|
|
Uint8List _validBootloaderImage() {
|
|
final image = Uint8List(16);
|
|
_writeLeU32(image, 0, 0x20001000);
|
|
_writeLeU32(image, 4, 0x00030009);
|
|
return image;
|
|
}
|
|
|
|
void _writeLeU32(Uint8List bytes, int offset, int value) {
|
|
final data = ByteData.sublistView(bytes);
|
|
data.setUint32(offset, value, Endian.little);
|
|
}
|
|
|
|
class _FakeFirmwareFilePicker implements FirmwareFilePicker {
|
|
_FakeFirmwareFilePicker({
|
|
required this.selection,
|
|
this.error,
|
|
});
|
|
|
|
final FirmwarePickerSelection? selection;
|
|
final Object? error;
|
|
|
|
@override
|
|
Future<FirmwarePickerSelection?> pickFirmwareFile() async {
|
|
if (error != null) {
|
|
throw error!;
|
|
}
|
|
return selection;
|
|
}
|
|
}
|