From 2d4470511275e8b762009a2582c612eaa3305402 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 22 Jan 2021 12:15:28 -0600 Subject: [PATCH] proper error handling for failure to connect --- src/sagas/flash-firmware.test.ts | 273 +++++++++++++++++++------------ src/sagas/flash-firmware.ts | 9 +- 2 files changed, 168 insertions(+), 114 deletions(-) diff --git a/src/sagas/flash-firmware.test.ts b/src/sagas/flash-firmware.test.ts index 89959dfe..6e39d2c8 100644 --- a/src/sagas/flash-firmware.test.ts +++ b/src/sagas/flash-firmware.test.ts @@ -18,11 +18,13 @@ import { flashFirmware as flashFirmwareAction, } from '../actions/flash-firmware'; import { + BootloaderConnectionFailureReason, BootloaderProgramRequestAction, checksumRequest, checksumResponse, connect, didConnect, + didFailToConnect, didRequest, eraseRequest, eraseResponse, @@ -44,143 +46,198 @@ afterEach(() => { }); describe('flashFirmware', () => { - test('normal flow', 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, - }; + describe('normal flow', () => { + test('success', 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 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' })), - ); + jest.spyOn(window, 'fetch').mockResolvedValueOnce( + new Response(await zip.generateAsync({ type: 'blob' })), + ); - const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc() }); + const saga = new AsyncSaga(flashFirmware, { + nextMessageId: createCountFunc(), + }); - saga.setState({ settings: { flashCurrentProgram: false } }); + saga.setState({ settings: { flashCurrentProgram: false } }); - // saga is triggered by this action + // saga is triggered by this action - saga.put(flashFirmwareAction()); + saga.put(flashFirmwareAction()); - // first step is to connect to the hub bootloader + // first step is to connect to the hub bootloader - let action = await saga.take(); - expect(action).toEqual(connect()); + let action = await saga.take(); + expect(action).toEqual(connect()); - saga.put(didConnect()); + saga.put(didConnect()); - // then find out what kind of hub it is + // then find out what kind of hub it is - action = await saga.take(); - expect(action).toEqual(infoRequest(0)); - - saga.put(didRequest(0)); - saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub)); - - // then compile main.py to .mpy - - 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 start flashing the firmware - - // should get didStart action just before starting to erase - action = await saga.take(); - expect(action).toEqual(didStart()); - - // erase first - - action = await saga.take(); - expect(action).toEqual(eraseRequest(1)); - - saga.put(didRequest(1)); - saga.put(eraseResponse(Result.OK)); - - // then write the new firmware - - const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8; - action = await saga.take(); - expect(action).toEqual(initRequest(2, totalFirmwareSize)); - - saga.put(didRequest(2)); - saga.put(initResponse(Result.OK)); - - const dummyPayload = new ArrayBuffer(0); - let id = 2; - for (let count = 1, offset = 0; ; count++, offset += 14) { action = await saga.take(); - expect(action).toEqual( - programRequest(++id, 0x08005000 + offset, dummyPayload), - ); - expect((action as BootloaderProgramRequestAction).payload.byteLength).toBe( - Math.min(14, totalFirmwareSize - offset), - ); + expect(action).toEqual(infoRequest(0)); + + saga.put(didRequest(0)); + saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub)); + + // then compile main.py to .mpy + + 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 start flashing the firmware + + // should get didStart action just before starting to erase + action = await saga.take(); + expect(action).toEqual(didStart()); + + // erase first + + action = await saga.take(); + expect(action).toEqual(eraseRequest(1)); + + saga.put(didRequest(1)); + saga.put(eraseResponse(Result.OK)); + + // then write the new firmware + + const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8; + action = await saga.take(); + expect(action).toEqual(initRequest(2, totalFirmwareSize)); + + saga.put(didRequest(2)); + saga.put(initResponse(Result.OK)); + + const dummyPayload = new ArrayBuffer(0); + let id = 2; + for (let count = 1, offset = 0; ; count++, offset += 14) { + action = await saga.take(); + expect(action).toEqual( + programRequest(++id, 0x08005000 + offset, dummyPayload), + ); + expect( + (action as BootloaderProgramRequestAction).payload.byteLength, + ).toBe(Math.min(14, totalFirmwareSize - offset)); + + saga.put(didRequest(id)); + + action = await saga.take(); + expect(action).toEqual(didProgress(offset / totalFirmwareSize)); + + // Have to be careful that a checksum request is not sent after + // last payload is sent, otherwise the hub gets confused. + + if (offset + 14 >= totalFirmwareSize) { + break; + } + + if (count % 10 === 0) { + action = await saga.take(); + expect(action).toEqual(checksumRequest(++id)); + + saga.put(didRequest(id)); + saga.put(checksumResponse(0)); + } + } + + // hub indicates success + + saga.put(programResponse(0, totalFirmwareSize)); + + action = await saga.take(); + expect(action).toEqual(didProgress(1)); + + // and finally reboot the hub + + action = await saga.take(); + expect(action).toEqual(rebootRequest(++id)); saga.put(didRequest(id)); + // then we are done + action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); + expect(action).toEqual(didFinish()); - // Have to be careful that a checksum request is not sent after - // last payload is sent, otherwise the hub gets confused. + await saga.end(); + }); - if (offset + 14 >= totalFirmwareSize) { - break; - } + test('fail to connect', 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, + }; - if (count % 10 === 0) { - action = await saga.take(); - expect(action).toEqual(checksumRequest(++id)); + 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'); - saga.put(didRequest(id)); - saga.put(checksumResponse(0)); - } - } + jest.spyOn(window, 'fetch').mockResolvedValueOnce( + new Response(await zip.generateAsync({ type: 'blob' })), + ); - // hub indicates success + const saga = new AsyncSaga(flashFirmware, { + nextMessageId: createCountFunc(), + }); - saga.put(programResponse(0, totalFirmwareSize)); + saga.setState({ settings: { flashCurrentProgram: false } }); - action = await saga.take(); - expect(action).toEqual(didProgress(1)); + // saga is triggered by this action - // and finally reboot the hub + saga.put(flashFirmwareAction()); - action = await saga.take(); - expect(action).toEqual(rebootRequest(++id)); + // first step is to connect to the hub bootloader - saga.put(didRequest(id)); + let action = await saga.take(); + expect(action).toEqual(connect()); - // then we are done + saga.put( + didFailToConnect(BootloaderConnectionFailureReason.GattServiceNotFound), + ); - action = await saga.take(); - expect(action).toEqual(didFinish()); + // it should fail here because of failure to connect - await saga.end(); + action = await saga.take(); + expect(action).toEqual( + didFailToStart(FailToStartReasonType.FailedToConnect), + ); + + await saga.end(); + }); }); describe('user supplied firmware.zip', () => { diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index a81c6f0b..231cf814 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -31,9 +31,8 @@ import { } from '../actions/flash-firmware'; import { BootloaderChecksumResponseAction, + BootloaderConnectionAction, BootloaderConnectionActionType, - BootloaderConnectionDidConnectAction, - BootloaderConnectionDidFailToConnectAction, BootloaderDidRequestAction, BootloaderDidRequestType, BootloaderEraseResponseAction, @@ -233,15 +232,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { } yield* put(connect()); - const connectResult = yield* take< - | BootloaderConnectionDidConnectAction - | BootloaderConnectionDidFailToConnectAction - >([ + const connectResult = yield* take([ BootloaderConnectionActionType.DidConnect, BootloaderConnectionActionType.DidFailToConnect, ]); if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) { + yield* put(didFailToStart(FailToStartReasonType.FailedToConnect)); return; }