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

@ -67,6 +67,19 @@ void main() {
expect(frames[1].bytes.length, universalShifterDfuFrameSizeBytes); expect(frames[1].bytes.length, universalShifterDfuFrameSizeBytes);
expect(frames[1].bytes.sublist(1, 18), image.sublist(63, 80)); expect(frames[1].bytes.sublist(1, 18), image.sublist(63, 80));
}); });
test('uses deterministic wrapping sequence numbers from custom start', () {
final image = List<int>.generate(
3 * universalShifterDfuFramePayloadSizeBytes,
(index) => index & 0xFF);
final frames = DfuProtocol.buildDataFrames(image, startSequence: 0xFE);
expect(frames.length, 3);
expect(frames[0].sequence, 0xFE);
expect(frames[1].sequence, 0xFF);
expect(frames[2].sequence, 0x00);
});
}); });
group('DfuProtocol sequence and ACK helpers', () { group('DfuProtocol sequence and ACK helpers', () {

View File

@ -66,6 +66,32 @@ void main() {
await transport.dispose(); 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 { test('cancel sends ABORT and reports aborted state', () async {
final firstFrameSent = Completer<void>(); final firstFrameSent = Completer<void>();
final transport = _FakeFirmwareUpdateTransport( final transport = _FakeFirmwareUpdateTransport(
@ -201,6 +227,45 @@ void main() {
await service.dispose(); await service.dispose();
await transport.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>> controlWrites = <List<int>>[];
final List<List<int>> dataWrites = <List<int>>[]; final List<List<int>> dataWrites = <List<int>>[];
final List<int> ackNotifications = <int>[];
final List<String> postFinishSteps = <String>[]; final List<String> postFinishSteps = <String>[];
final Set<int> _droppedOnce = <int>{}; final Set<int> _droppedOnce = <int>{};
int _lastAck = 0xFF; int _lastAck = 0xFF;
@ -254,9 +320,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
if (opcode == universalShifterDfuOpcodeStart) { if (opcode == universalShifterDfuOpcodeStart) {
_lastAck = 0xFF; _lastAck = 0xFF;
_expectedSequence = 0; _expectedSequence = 0;
scheduleMicrotask(() { _scheduleAck(0xFF);
_ackController.add([0xFF]);
});
} }
if (opcode == universalShifterDfuOpcodeAbort) { if (opcode == universalShifterDfuOpcodeAbort) {
@ -283,9 +347,7 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
if (shouldDrop) { if (shouldDrop) {
_droppedOnce.add(sequence); _droppedOnce.add(sequence);
scheduleMicrotask(() { _scheduleAck(_lastAck);
_ackController.add([_lastAck]);
});
return Ok(null); return Ok(null);
} }
@ -294,13 +356,19 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
_expectedSequence = (_expectedSequence + 1) & 0xFF; _expectedSequence = (_expectedSequence + 1) & 0xFF;
} }
scheduleMicrotask(() { _scheduleAck(_lastAck);
_ackController.add([_lastAck]);
});
return Ok(null); return Ok(null);
} }
void _scheduleAck(int sequence) {
final ack = sequence & 0xFF;
ackNotifications.add(ack);
scheduleMicrotask(() {
_ackController.add([ack]);
});
}
@override @override
Future<Result<void>> waitForExpectedResetDisconnect({ Future<Result<void>> waitForExpectedResetDisconnect({
required Duration timeout, required Duration timeout,