From e5d98cdd84cf6cc07f78dce65bdb6dda633bef36 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 22 Jan 2021 16:22:54 -0600 Subject: [PATCH] add proper error handling for bad device id --- src/sagas/flash-firmware.test.ts | 158 +++++++++++++++++++++++++++++++ src/sagas/flash-firmware.ts | 23 ++--- 2 files changed, 165 insertions(+), 16 deletions(-) diff --git a/src/sagas/flash-firmware.test.ts b/src/sagas/flash-firmware.test.ts index d38ce423..c0afb14d 100644 --- a/src/sagas/flash-firmware.test.ts +++ b/src/sagas/flash-firmware.test.ts @@ -538,6 +538,78 @@ describe('flashFirmware', () => { await saga.end(); }); + test('unsupported device', async () => { + const metadata: FirmwareMetadata = { + 'metadata-version': '1.0.0', + 'device-id': HubType.MoveHub, + 'checksum-type': 'sum', + 'firmware-version': '1.2.3', + 'max-firmware-size': 1024, + 'mpy-abi-version': 5, + 'mpy-cross-options': ['-mno-unicode'], + 'user-mpy-offset': 100, + }; + + const zip = new JSZip(); + zip.file('firmware-base.bin', new Uint8Array(64)); + zip.file('firmware.metadata.json', JSON.stringify(metadata)); + zip.file('main.py', 'print("test")'); + zip.file('ReadMe_OSS.txt', 'test'); + + jest.spyOn(window, 'fetch').mockResolvedValueOnce( + new Response(await zip.generateAsync({ type: 'blob' })), + ); + + const saga = new AsyncSaga( + flashFirmware, + { + bootloader: { connection: BootloaderConnectionState.Disconnected }, + settings: { flashCurrentProgram: false }, + }, + { + nextMessageId: createCountFunc(), + }, + ); + + // saga is triggered by this action + + saga.put(flashFirmwareAction()); + + // first step is to connect to the hub bootloader + + let action = await saga.take(); + expect(action).toEqual(connect()); + + saga.updateState({ + bootloader: { connection: BootloaderConnectionState.Connected }, + }); + saga.put(didConnect()); + + // then find out what kind of hub it is + + action = await saga.take(); + expect(action).toEqual(infoRequest(0)); + + // received an unknown hub type ID + + saga.put(didRequest(0)); + saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, 0 as HubType)); + + // should raise an error that we don't have any firmware for this hub + + action = await saga.take(); + expect(action).toStrictEqual( + didFailToFinish(FailToFinishReasonType.NoFirmware), + ); + + // should request to disconnect after failure + + action = await saga.take(); + expect(action).toEqual(disconnect()); + + await saga.end(); + }); + test('erase response is failed', async () => { const metadata: FirmwareMetadata = { 'metadata-version': '1.0.0', @@ -1077,6 +1149,92 @@ describe('flashFirmware', () => { await saga.end(); }); + + test('connected device type does not match firmware device type', async () => { + const metadata: FirmwareMetadata = { + 'metadata-version': '1.0.0', + 'device-id': HubType.MoveHub, + 'checksum-type': 'sum', + 'firmware-version': '1.2.3', + 'max-firmware-size': 1024, + 'mpy-abi-version': 5, + 'mpy-cross-options': ['-mno-unicode'], + 'user-mpy-offset': 100, + }; + + const zip = new JSZip(); + zip.file('firmware-base.bin', new Uint8Array(64)); + zip.file('firmware.metadata.json', JSON.stringify(metadata)); + zip.file('main.py', 'print("test")'); + zip.file('ReadMe_OSS.txt', 'test'); + + const saga = new AsyncSaga( + flashFirmware, + { + bootloader: { connection: BootloaderConnectionState.Disconnected }, + settings: { flashCurrentProgram: false }, + }, + { + nextMessageId: createCountFunc(), + }, + ); + + // saga is triggered by this action + + saga.put( + flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })), + ); + + // the first step is to compile main.py to .mpy + + let action = await saga.take(); + expect(action).toMatchInlineSnapshot(` + Object { + "options": Array [ + "-mno-unicode", + ], + "script": "print(\\"test\\")", + "type": "mpy.action.compile", + } + `); + + const mpySize = 20; + const mpyBinaryData = new Uint8Array(mpySize); + saga.put(didCompile(mpyBinaryData)); + + // then connect to the hub bootloader + + action = await saga.take(); + expect(action).toEqual(connect()); + + saga.updateState({ + bootloader: { connection: BootloaderConnectionState.Connected }, + }); + saga.put(didConnect()); + + // then find out what kind of hub it is + + action = await saga.take(); + expect(action).toEqual(infoRequest(0)); + + // connected hub type does not match firmware hub type + saga.put(didRequest(0)); + saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.CityHub)); + + // should raise an error that we don't have any firmware for this hub + + action = await saga.take(); + expect(action).toStrictEqual( + didFailToFinish(FailToFinishReasonType.DeviceMismatch), + ); + + // should request to disconnect after failure + + action = await saga.take(); + expect(action).toEqual(disconnect()); + + await saga.end(); + }); }); test('user supplied main.py', async () => { diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index 2fd6dfde..9f47b47d 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -77,7 +77,7 @@ const firmwareZipMap = new Map([ * Disconnects the BLE if we are connected and cancels the task (including the * parent task). */ -function* disconnectAndCancel(): SagaGenerator { +function* disconnectAndCancel(): SagaGenerator { const connection = yield* select((s: RootState) => s.bootloader.connection); if (connection === BootloaderConnectionState.Connected) { @@ -85,12 +85,6 @@ function* disconnectAndCancel(): SagaGenerator { } yield* cancel(); - - // HACK: cancel effect doesn't return, so we need this to make typescript - // happy about the never return type. - - // istanbul ignore next: not reachable - throw undefined; } function* waitForDidRequest(id: number): SagaGenerator { @@ -312,22 +306,19 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { }); if (deviceId !== undefined && info.hubType !== deviceId) { - throw Error(`Connected to ${info.hubType} but firmware is for ${deviceId}`); + yield* put(didFailToFinish(FailToFinishReasonType.DeviceMismatch)); + yield* disconnectAndCancel(); } if (firmware === undefined) { const firmwarePath = firmwareZipMap.get(info.hubType); if (firmwarePath === undefined) { - yield* put( - notification.add( - 'error', - "Sorry, we don't have firmware for this hub yet.", - ), - ); - yield* put(disconnectRequest(nextMessageId())); - return; + yield* put(didFailToFinish(FailToFinishReasonType.NoFirmware)); + yield* disconnectAndCancel(); } + defined(firmwarePath); + const response = yield* call(() => fetch(firmwarePath)); if (!response.ok) { yield* put(notification.add('error', 'Failed to fetch firmware.'));