test(dfu): cover retry failures and sequence wrap cases

This commit is contained in:
2026-03-04 18:09:48 +01:00
parent 2ac68e09ab
commit bdcd200a62
2 changed files with 90 additions and 9 deletions

View File

@ -66,6 +66,32 @@ void main() {
await transport.dispose();
});
test('fails after bounded retries when ACK progress times out', () async {
final transport = _FakeFirmwareUpdateTransport(suppressDataAcks: true);
final service = FirmwareUpdateService(
transport: transport,
defaultWindowSize: 1,
defaultAckTimeout: const Duration(milliseconds: 40),
maxNoProgressRetries: 2,
);
final image = List<int>.generate(90, (index) => index & 0xFF);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 10,
);
expect(result.isErr(), isTrue);
expect(result.unwrapErr().toString(), contains('Upload stalled'));
expect(result.unwrapErr().toString(), contains('after 3 retries'));
expect(transport.controlWrites.last, [universalShifterDfuOpcodeAbort]);
expect(transport.sequenceWriteCount(0), 3);
expect(service.currentProgress.state, DfuUpdateState.failed);
await service.dispose();
await transport.dispose();
});
test('cancel sends ABORT and reports aborted state', () async {
final firstFrameSent = Completer<void>();
final transport = _FakeFirmwareUpdateTransport(
@ -201,6 +227,45 @@ void main() {
await service.dispose();
await transport.dispose();
});
test('handles deterministic ACK sequence wrap-around across 0xFF->0x00',
() async {
const frameCount = 260;
final transport = _FakeFirmwareUpdateTransport();
final service = FirmwareUpdateService(
transport: transport,
defaultWindowSize: 16,
defaultAckTimeout: const Duration(milliseconds: 100),
);
final image = List<int>.generate(
frameCount * universalShifterDfuFramePayloadSizeBytes,
(index) => index & 0xFF,
);
final result = await service.startUpdate(
imageBytes: image,
sessionId: 16,
);
expect(result.isOk(), isTrue);
var ffToZeroTransitions = 0;
for (var i = 1; i < transport.ackNotifications.length; i++) {
if (transport.ackNotifications[i - 1] == 0xFF &&
transport.ackNotifications[i] == 0x00) {
ffToZeroTransitions += 1;
}
}
expect(ffToZeroTransitions, greaterThanOrEqualTo(2));
expect(service.currentProgress.lastAckedSequence, 0x03);
expect(service.currentProgress.sentBytes, image.length);
expect(service.currentProgress.state, DfuUpdateState.completed);
await service.dispose();
await transport.dispose();
});
});
}
@ -226,6 +291,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
final List<List<int>> controlWrites = <List<int>>[];
final List<List<int>> dataWrites = <List<int>>[];
final List<int> ackNotifications = <int>[];
final List<String> postFinishSteps = <String>[];
final Set<int> _droppedOnce = <int>{};
int _lastAck = 0xFF;
@ -254,9 +320,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
if (opcode == universalShifterDfuOpcodeStart) {
_lastAck = 0xFF;
_expectedSequence = 0;
scheduleMicrotask(() {
_ackController.add([0xFF]);
});
_scheduleAck(0xFF);
}
if (opcode == universalShifterDfuOpcodeAbort) {
@ -283,9 +347,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
if (shouldDrop) {
_droppedOnce.add(sequence);
scheduleMicrotask(() {
_ackController.add([_lastAck]);
});
_scheduleAck(_lastAck);
return Ok(null);
}
@ -294,13 +356,19 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
_expectedSequence = (_expectedSequence + 1) & 0xFF;
}
scheduleMicrotask(() {
_ackController.add([_lastAck]);
});
_scheduleAck(_lastAck);
return Ok(null);
}
void _scheduleAck(int sequence) {
final ack = sequence & 0xFF;
ackNotifications.add(ack);
scheduleMicrotask(() {
_ackController.add([ack]);
});
}
@override
Future<Result<void>> waitForExpectedResetDisconnect({
required Duration timeout,