feat(dfu): verify reconnect before reporting update success

This commit is contained in:
2026-03-03 17:11:47 +01:00
parent aafa9928ac
commit c581b4d92c
2 changed files with 289 additions and 0 deletions

View File

@ -27,6 +27,14 @@ void main() {
transport.controlWrites.first.first, universalShifterDfuOpcodeStart);
expect(transport.controlWrites.last, [universalShifterDfuOpcodeFinish]);
expect(transport.dataWrites.length, greaterThanOrEqualTo(3));
expect(
transport.postFinishSteps,
[
'waitForExpectedResetDisconnect',
'reconnectForVerification',
'verifyDeviceReachable',
],
);
expect(service.currentProgress.state, DfuUpdateState.completed);
expect(service.currentProgress.sentBytes, image.length);
@ -91,6 +99,108 @@ void main() {
await service.dispose();
await transport.dispose();
});
test('fails when reconnect does not succeed after expected reset',
() async {
final transport = _FakeFirmwareUpdateTransport(
reconnectError: 'simulated reconnect timeout',
);
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: 13,
);
expect(result.isErr(), isTrue);
expect(result.unwrapErr().toString(), contains('did not reconnect'));
expect(service.currentProgress.state, DfuUpdateState.failed);
expect(
transport.postFinishSteps,
[
'waitForExpectedResetDisconnect',
'reconnectForVerification',
],
);
await service.dispose();
await transport.dispose();
});
test('fails when expected reset disconnect is not observed', () async {
final transport = _FakeFirmwareUpdateTransport(
resetDisconnectError: 'simulated missing disconnect',
);
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: 15,
);
expect(result.isErr(), isTrue);
expect(
result.unwrapErr().toString(),
contains('expected post-FINISH reset disconnect'),
);
expect(service.currentProgress.state, DfuUpdateState.failed);
expect(
transport.postFinishSteps,
['waitForExpectedResetDisconnect'],
);
await service.dispose();
await transport.dispose();
});
test('fails when post-update status verification read fails', () async {
final transport = _FakeFirmwareUpdateTransport(
verificationError: 'simulated status read failure',
);
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: 14,
);
expect(result.isErr(), isTrue);
expect(
result.unwrapErr().toString(),
contains('post-update verification failed'),
);
expect(
result.unwrapErr().toString(),
contains('does not expose a version characteristic'),
);
expect(service.currentProgress.state, DfuUpdateState.failed);
expect(
transport.postFinishSteps,
[
'waitForExpectedResetDisconnect',
'reconnectForVerification',
'verifyDeviceReachable',
],
);
await service.dispose();
await transport.dispose();
});
});
}
@ -99,17 +209,24 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
this.dropFirstSequence,
this.onDataWrite,
this.suppressDataAcks = false,
this.resetDisconnectError,
this.reconnectError,
this.verificationError,
});
final int? dropFirstSequence;
final void Function(List<int> frame)? onDataWrite;
final bool suppressDataAcks;
final String? resetDisconnectError;
final String? reconnectError;
final String? verificationError;
final StreamController<List<int>> _ackController =
StreamController<List<int>>.broadcast();
final List<List<int>> controlWrites = <List<int>>[];
final List<List<int>> dataWrites = <List<int>>[];
final List<String> postFinishSteps = <String>[];
final Set<int> _droppedOnce = <int>{};
int _lastAck = 0xFF;
int _expectedSequence = 0;
@ -184,6 +301,39 @@ class _FakeFirmwareUpdateTransport implements FirmwareUpdateTransport {
return Ok(null);
}
@override
Future<Result<void>> waitForExpectedResetDisconnect({
required Duration timeout,
}) async {
postFinishSteps.add('waitForExpectedResetDisconnect');
if (resetDisconnectError != null) {
return bail(resetDisconnectError!);
}
return Ok(null);
}
@override
Future<Result<void>> reconnectForVerification({
required Duration timeout,
}) async {
postFinishSteps.add('reconnectForVerification');
if (reconnectError != null) {
return bail(reconnectError!);
}
return Ok(null);
}
@override
Future<Result<void>> verifyDeviceReachable({
required Duration timeout,
}) async {
postFinishSteps.add('verifyDeviceReachable');
if (verificationError != null) {
return bail(verificationError!);
}
return Ok(null);
}
int sequenceWriteCount(int sequence) {
var count = 0;
for (final frame in dataWrites) {