Files
abawo-bt-app/test/service/firmware_update_service_test.dart

201 lines
5.7 KiB
Dart

import 'dart:async';
import 'package:abawo_bt_app/model/shifter_types.dart';
import 'package:abawo_bt_app/service/firmware_update_service.dart';
import 'package:anyhow/anyhow.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('FirmwareUpdateService', () {
test('completes happy path with START, data frames, and FINISH', () async {
final transport = _FakeFirmwareUpdateTransport();
final service = FirmwareUpdateService(
transport: transport,
defaultWindowSize: 4,
defaultAckTimeout: const Duration(milliseconds: 100),
);
final image = List<int>.generate(130, (index) => index & 0xFF);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 7,
);
expect(result.isOk(), isTrue);
expect(transport.controlWrites.length, 2);
expect(
transport.controlWrites.first.first, universalShifterDfuOpcodeStart);
expect(transport.controlWrites.last, [universalShifterDfuOpcodeFinish]);
expect(transport.dataWrites.length, greaterThanOrEqualTo(3));
expect(service.currentProgress.state, DfuUpdateState.completed);
expect(service.currentProgress.sentBytes, image.length);
await service.dispose();
await transport.dispose();
});
test('rewinds to ack+1 and retransmits after ACK stall', () async {
final transport = _FakeFirmwareUpdateTransport(dropFirstSequence: 1);
final service = FirmwareUpdateService(
transport: transport,
defaultWindowSize: 3,
defaultAckTimeout: const Duration(milliseconds: 100),
maxNoProgressRetries: 4,
);
final image = List<int>.generate(190, (index) => index & 0xFF);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 9,
);
expect(result.isOk(), isTrue);
expect(transport.dataWrites.length, greaterThan(4));
expect(transport.sequenceWriteCount(1), greaterThan(1));
expect(service.currentProgress.state, DfuUpdateState.completed);
await service.dispose();
await transport.dispose();
});
test('cancel sends ABORT and reports aborted state', () async {
final firstFrameSent = Completer<void>();
final transport = _FakeFirmwareUpdateTransport(
onDataWrite: (frame) {
if (!firstFrameSent.isCompleted) {
firstFrameSent.complete();
}
},
suppressDataAcks: true,
);
final service = FirmwareUpdateService(
transport: transport,
defaultWindowSize: 1,
defaultAckTimeout: const Duration(milliseconds: 500),
);
final future = service.startUpdate(
imageBytes: List<int>.generate(90, (index) => index & 0xFF),
sessionId: 11,
);
await firstFrameSent.future.timeout(const Duration(seconds: 1));
await service.cancelUpdate();
final result = await future;
expect(result.isErr(), isTrue);
expect(result.unwrapErr().toString(), contains('canceled'));
expect(transport.controlWrites.last, [universalShifterDfuOpcodeAbort]);
expect(service.currentProgress.state, DfuUpdateState.aborted);
await service.dispose();
await transport.dispose();
});
});
}
class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
_FakeFirmwareUpdateTransport({
this.dropFirstSequence,
this.onDataWrite,
this.suppressDataAcks = false,
});
final int? dropFirstSequence;
final void Function(List<int> frame)? onDataWrite;
final bool suppressDataAcks;
final StreamController<List<int>> _ackController =
StreamController<List<int>>.broadcast();
final List<List<int>> controlWrites = <List<int>>[];
final List<List<int>> dataWrites = <List<int>>[];
final Set<int> _droppedOnce = <int>{};
int _lastAck = 0xFF;
int _expectedSequence = 0;
@override
Future<Result<DfuPreflightResult>> runPreflight({
required int requestedMtu,
}) async {
return Ok(
DfuPreflightResult.ready(
requestedMtu: requestedMtu,
negotiatedMtu: 128,
),
);
}
@override
Stream<List<int>> subscribeToAck() => _ackController.stream;
@override
Future<Result<void>> writeControl(List<int> payload) async {
controlWrites.add(List<int>.from(payload, growable: false));
final opcode = payload.isEmpty ? -1 : payload.first;
if (opcode == universalShifterDfuOpcodeStart) {
_lastAck = 0xFF;
_expectedSequence = 0;
scheduleMicrotask(() {
_ackController.add([0xFF]);
});
}
if (opcode == universalShifterDfuOpcodeAbort) {
_lastAck = 0xFF;
_expectedSequence = 0;
}
return Ok(null);
}
@override
Future<Result<void>> writeDataFrame(List<int> frame) async {
dataWrites.add(List<int>.from(frame, growable: false));
onDataWrite?.call(frame);
if (suppressDataAcks) {
return Ok(null);
}
final sequence = frame.first;
final shouldDrop = dropFirstSequence != null &&
sequence == dropFirstSequence &&
!_droppedOnce.contains(sequence);
if (shouldDrop) {
_droppedOnce.add(sequence);
scheduleMicrotask(() {
_ackController.add([_lastAck]);
});
return Ok(null);
}
if (sequence == _expectedSequence) {
_lastAck = sequence;
_expectedSequence = (_expectedSequence + 1) & 0xFF;
}
scheduleMicrotask(() {
_ackController.add([_lastAck]);
});
return Ok(null);
}
int sequenceWriteCount(int sequence) {
var count = 0;
for (final frame in dataWrites) {
if (frame.first == sequence) {
count += 1;
}
}
return count;
}
Future<void> dispose() async {
await _ackController.close();
}
}