// Copyright 2021 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. /* eslint-env browser, jasmine */ import 'jasmine'; import { Channel, Client, decode, MethodStub, ServiceClient, } from '@pigweed/pw_rpc'; import {Status} from '@pigweed/pw_status'; import { PacketType, RpcPacket, } from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb'; import {ProtoCollection} from 'transfer_proto_collection/generated/ts_proto_collection'; import {Chunk} from 'transfer_proto_tspb/transfer_proto_tspb_pb/pw_transfer/transfer_pb'; import {Manager} from './client'; import {ProgressStats} from './transfer'; const DEFAULT_TIMEOUT_S = 0.3; describe('Encoder', () => { const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); let client: Client; let service: ServiceClient; let sentChunks: Chunk[]; let packetsToSend: Uint8Array[][]; beforeEach(() => { const lib = new ProtoCollection(); const channels: Channel[] = [new Channel(1, handleRequest)]; client = Client.fromProtoSet(channels, lib); service = client.channel(1)!.service('pw.transfer.Transfer')!; sentChunks = []; packetsToSend = []; }); function handleRequest(data: Uint8Array): void { const packet = decode(data); if (packet.getType() !== PacketType.CLIENT_STREAM) { return; } const chunk = Chunk.deserializeBinary(packet.getPayload_asU8()); sentChunks.push(chunk); if (packetsToSend.length > 0) { const responses = packetsToSend.shift()!; for (const response of responses) { client.processPacket(response); } } } function receivedData(): Uint8Array { let length = 0; sentChunks.forEach((chunk: Chunk) => { length += chunk.getData().length; }); const data = new Uint8Array(length); let offset = 0; sentChunks.forEach((chunk: Chunk) => { data.set(chunk.getData() as Uint8Array, offset); offset += chunk.getData().length; }); return data; } function enqueueServerError(method: MethodStub, error: Status): void { const packet = new RpcPacket(); packet.setType(PacketType.SERVER_ERROR); packet.setChannelId(1); packet.setServiceId(service.id); packet.setMethodId(method.id); packet.setStatus(error); packetsToSend.push([packet.serializeBinary()]); } function enqueueServerResponses(method: MethodStub, responses: Chunk[][]) { for (const responseGroup of responses) { const serializedGroup = []; for (const response of responseGroup) { const packet = new RpcPacket(); packet.setType(PacketType.SERVER_STREAM); packet.setChannelId(1); packet.setServiceId(service.id); packet.setMethodId(method.id); packet.setStatus(Status.OK); packet.setPayload(response.serializeBinary()); serializedGroup.push(packet.serializeBinary()); } packetsToSend.push(serializedGroup); } } function buildChunk( transferId: number, offset: number, data: string, remainingBytes: number ): Chunk { const chunk = new Chunk(); chunk.setTransferId(transferId); chunk.setOffset(offset); chunk.setData(textEncoder.encode(data)); chunk.setRemainingBytes(remainingBytes); return chunk; } it('read transfer basic', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = buildChunk(3, 0, 'abc', 0); enqueueServerResponses(service.method('Read')!, [[chunk1]]); const data = await manager.read(3); expect(textDecoder.decode(data)).toEqual('abc'); expect(sentChunks).toHaveSize(2); expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue(); expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK); }); it('read transfer multichunk', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = buildChunk(3, 0, 'abc', 3); const chunk2 = buildChunk(3, 3, 'def', 0); enqueueServerResponses(service.method('Read')!, [[chunk1, chunk2]]); const data = await manager.read(3); expect(data).toEqual(textEncoder.encode('abcdef')); expect(sentChunks).toHaveSize(2); expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue(); expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK); }); it('read transfer progress callback', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = buildChunk(3, 0, 'abc', 3); const chunk2 = buildChunk(3, 3, 'def', 0); enqueueServerResponses(service.method('Read')!, [[chunk1, chunk2]]); const progress: Array = []; const data = await manager.read(3, (stats: ProgressStats) => { progress.push(stats); }); expect(textDecoder.decode(data)).toEqual('abcdef'); expect(sentChunks).toHaveSize(2); expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue(); expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK); expect(progress).toEqual([ new ProgressStats(3, 3, 6), new ProgressStats(6, 6, 6), ]); }); it('read transfer retry bad offset', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = buildChunk(3, 0, '123', 6); const chunk2 = buildChunk(3, 1, '456', 3); // Incorrect offset; expecting 3 const chunk3 = buildChunk(3, 3, '456', 3); const chunk4 = buildChunk(3, 6, '789', 0); enqueueServerResponses(service.method('Read')!, [ [chunk1, chunk2], [chunk3, chunk4], ]); const data = await manager.read(3); expect(data).toEqual(textEncoder.encode('123456789')); expect(sentChunks).toHaveSize(3); expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue(); expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK); }); it('read transfer retry timeout', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = buildChunk(3, 0, 'xyz', 0); enqueueServerResponses(service.method('Read')!, [[], [chunk]]); const data = await manager.read(3); expect(textDecoder.decode(data)).toEqual('xyz'); // Two transfer parameter requests should have been sent. expect(sentChunks).toHaveSize(3); expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue(); expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK); }); it('read transfer timeout', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); await manager .read(27) .then(() => { fail('Unexpected completed promise'); }) .catch(error => { expect(error.id).toEqual(27); expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]); expect(sentChunks).toHaveSize(4); }); }); it('read transfer error', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setStatus(Status.NOT_FOUND); chunk.setTransferId(31); enqueueServerResponses(service.method('Read')!, [[chunk]]); await manager .read(31) .then(() => { fail('Unexpected completed promise'); }) .catch(error => { expect(error.id).toEqual(31); expect(Status[error.status]).toEqual(Status[Status.NOT_FOUND]); }); }); it('read transfer server error', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); enqueueServerError(service.method('Read')!, Status.NOT_FOUND); await manager .read(31) .then(data => { fail('Unexpected completed promise'); }) .catch(error => { expect(error.id).toEqual(31); expect(Status[error.status]).toEqual(Status[Status.INTERNAL]); }); }); it('write transfer basic', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(4); chunk.setOffset(0); chunk.setPendingBytes(32); chunk.setMaxChunkSizeBytes(8); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk], [completeChunk], ]); await manager.write(4, textEncoder.encode('hello')); expect(sentChunks).toHaveSize(2); expect(receivedData()).toEqual(textEncoder.encode('hello')); }); it('write transfer max chunk size', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(4); chunk.setOffset(0); chunk.setPendingBytes(32); chunk.setMaxChunkSizeBytes(8); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk], [completeChunk], ]); await manager.write(4, textEncoder.encode('hello world')); expect(sentChunks).toHaveSize(3); expect(receivedData()).toEqual(textEncoder.encode('hello world')); expect(sentChunks[1].getData()).toEqual(textEncoder.encode('hello wo')); expect(sentChunks[2].getData()).toEqual(textEncoder.encode('rld')); }); it('write transfer multiple parameters', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(4); chunk.setOffset(0); chunk.setPendingBytes(8); chunk.setMaxChunkSizeBytes(8); const chunk2 = new Chunk(); chunk2.setTransferId(4); chunk2.setOffset(8); chunk2.setPendingBytes(8); chunk2.setMaxChunkSizeBytes(8); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk], [chunk2], [completeChunk], ]); await manager.write(4, textEncoder.encode('data to write')); expect(sentChunks).toHaveSize(3); expect(receivedData()).toEqual(textEncoder.encode('data to write')); expect(sentChunks[1].getData()).toEqual(textEncoder.encode('data to ')); expect(sentChunks[2].getData()).toEqual(textEncoder.encode('write')); }); it('write transfer parameters update', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(4); chunk.setOffset(0); chunk.setPendingBytes(8); chunk.setMaxChunkSizeBytes(4); chunk.setType(Chunk.Type.PARAMETERS_RETRANSMIT); chunk.setWindowEndOffset(8); const chunk2 = new Chunk(); chunk2.setTransferId(4); chunk2.setOffset(4); chunk2.setPendingBytes(8); chunk2.setType(Chunk.Type.PARAMETERS_CONTINUE); chunk2.setWindowEndOffset(12); const chunk3 = new Chunk(); chunk3.setTransferId(4); chunk3.setOffset(8); chunk3.setPendingBytes(8); chunk3.setType(Chunk.Type.PARAMETERS_CONTINUE); chunk3.setWindowEndOffset(16); const chunk4 = new Chunk(); chunk4.setTransferId(4); chunk4.setOffset(12); chunk4.setPendingBytes(8); chunk4.setType(Chunk.Type.PARAMETERS_CONTINUE); chunk4.setWindowEndOffset(20); const chunk5 = new Chunk(); chunk5.setTransferId(4); chunk5.setOffset(16); chunk5.setPendingBytes(8); chunk5.setType(Chunk.Type.PARAMETERS_CONTINUE); chunk5.setWindowEndOffset(24); const chunk6 = new Chunk(); chunk6.setTransferId(4); chunk6.setOffset(20); chunk6.setPendingBytes(8); chunk6.setType(Chunk.Type.PARAMETERS_CONTINUE); chunk6.setWindowEndOffset(28); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk], [chunk2], [chunk3], [chunk4], [chunk5], [chunk6], [completeChunk], ]); await manager.write(4, textEncoder.encode('hello this is a message')); expect(receivedData()).toEqual( textEncoder.encode('hello this is a message') ); expect(sentChunks[1].getData()).toEqual(textEncoder.encode('hell')); expect(sentChunks[2].getData()).toEqual(textEncoder.encode('o th')); expect(sentChunks[3].getData()).toEqual(textEncoder.encode('is i')); expect(sentChunks[4].getData()).toEqual(textEncoder.encode('s a ')); expect(sentChunks[5].getData()).toEqual(textEncoder.encode('mess')); expect(sentChunks[6].getData()).toEqual(textEncoder.encode('age')); }); it('write transfer progress callback', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(4); chunk.setOffset(0); chunk.setPendingBytes(8); chunk.setMaxChunkSizeBytes(8); const chunk2 = new Chunk(); chunk2.setTransferId(4); chunk2.setOffset(8); chunk2.setPendingBytes(8); chunk2.setMaxChunkSizeBytes(8); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk], [chunk2], [completeChunk], ]); const progress: Array = []; await manager.write( 4, textEncoder.encode('data to write'), (stats: ProgressStats) => { progress.push(stats); } ); expect(sentChunks).toHaveSize(3); expect(receivedData()).toEqual(textEncoder.encode('data to write')); expect(sentChunks[1].getData()).toEqual(textEncoder.encode('data to ')); expect(sentChunks[2].getData()).toEqual(textEncoder.encode('write')); console.log(progress); expect(progress).toEqual([ new ProgressStats(8, 0, 13), new ProgressStats(13, 8, 13), new ProgressStats(13, 13, 13), ]); }); it('write transfer rewind', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = new Chunk(); chunk1.setTransferId(4); chunk1.setOffset(0); chunk1.setPendingBytes(8); chunk1.setMaxChunkSizeBytes(8); const chunk2 = new Chunk(); chunk2.setTransferId(4); chunk2.setOffset(8); chunk2.setPendingBytes(8); chunk2.setMaxChunkSizeBytes(8); const chunk3 = new Chunk(); chunk3.setTransferId(4); chunk3.setOffset(4); // Rewind chunk3.setPendingBytes(8); chunk3.setMaxChunkSizeBytes(8); const chunk4 = new Chunk(); chunk4.setTransferId(4); chunk4.setOffset(12); // Rewind chunk4.setPendingBytes(16); chunk4.setMaxChunkSizeBytes(16); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk1], [chunk2], [chunk3], [chunk4], [completeChunk], ]); await manager.write(4, textEncoder.encode('pigweed data transfer')); expect(sentChunks).toHaveSize(5); expect(sentChunks[1].getData()).toEqual(textEncoder.encode('pigweed ')); expect(sentChunks[2].getData()).toEqual(textEncoder.encode('data tra')); expect(sentChunks[3].getData()).toEqual(textEncoder.encode('eed data')); expect(sentChunks[4].getData()).toEqual(textEncoder.encode(' transfer')); }); it('write transfer bad offset', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk1 = new Chunk(); chunk1.setTransferId(4); chunk1.setOffset(0); chunk1.setPendingBytes(8); chunk1.setMaxChunkSizeBytes(8); const chunk2 = new Chunk(); chunk2.setTransferId(4); chunk2.setOffset(100); // larger offset than data chunk2.setPendingBytes(8); chunk2.setMaxChunkSizeBytes(8); const completeChunk = new Chunk(); completeChunk.setTransferId(4); completeChunk.setStatus(Status.OK); enqueueServerResponses(service.method('Write')!, [ [chunk1], [chunk2], [completeChunk], ]); await manager .write(4, textEncoder.encode('small data')) .then(() => { fail('Unexpected succesful promise'); }) .catch(error => { expect(error.id).toEqual(4); expect(Status[error.status]).toEqual(Status[Status.OUT_OF_RANGE]); }); }); it('write transfer error', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(21); chunk.setStatus(Status.UNAVAILABLE); enqueueServerResponses(service.method('Write')!, [[chunk]]); await manager .write(21, textEncoder.encode('no write')) .then(() => { fail('Unexpected succesful promise'); }) .catch(error => { expect(error.id).toEqual(21); expect(Status[error.status]).toEqual(Status[Status.UNAVAILABLE]); }); }); it('write transfer server error', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(21); chunk.setStatus(Status.NOT_FOUND); enqueueServerError(service.method('Write')!, Status.NOT_FOUND); await manager .write(21, textEncoder.encode('server error')) .then(() => { fail('Unexpected succesful promise'); }) .catch(error => { expect(error.id).toEqual(21); expect(Status[error.status]).toEqual(Status[Status.INTERNAL]); }); }); it('write transfer timeout after initial chunk', async () => { const manager = new Manager(service, 0.001, 4, 2); await manager .write(22, textEncoder.encode('no server response!')) .then(() => { fail('unexpected succesful write'); }) .catch(error => { expect(sentChunks).toHaveSize(3); // Initial chunk + two retries. expect(error.id).toEqual(22); expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]); }); }); it('write transfer timeout after intermediate chunk', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S, 4, 2); const chunk = new Chunk(); chunk.setTransferId(22); chunk.setPendingBytes(10); chunk.setMaxChunkSizeBytes(5); enqueueServerResponses(service.method('Write')!, [[chunk]]); await manager .write(22, textEncoder.encode('0123456789')) .then(() => { fail('unexpected succesful write'); }) .catch(error => { const expectedChunk1 = new Chunk(); expectedChunk1.setTransferId(22); expectedChunk1.setType(Chunk.Type.TRANSFER_START); const expectedChunk2 = new Chunk(); expectedChunk2.setTransferId(22); expectedChunk2.setData(textEncoder.encode('01234')); expectedChunk2.setType(Chunk.Type.TRANSFER_DATA); const lastChunk = new Chunk(); lastChunk.setTransferId(22); lastChunk.setData(textEncoder.encode('56789')); lastChunk.setOffset(5); lastChunk.setRemainingBytes(0); lastChunk.setType(Chunk.Type.TRANSFER_DATA); const expectedChunks = [ expectedChunk1, expectedChunk2, lastChunk, lastChunk, // retry 1 lastChunk, // retry 2 ]; expect(sentChunks).toEqual(expectedChunks); expect(error.id).toEqual(22); expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]); }); }); it('write zero pending bytes is internal error', async () => { const manager = new Manager(service, DEFAULT_TIMEOUT_S); const chunk = new Chunk(); chunk.setTransferId(23); chunk.setPendingBytes(0); enqueueServerResponses(service.method('Write')!, [[chunk]]); await manager .write(23, textEncoder.encode('no write')) .then(() => { fail('Unexpected succesful promise'); }) .catch(error => { expect(error.id).toEqual(23); expect(Status[error.status]).toEqual(Status[Status.INTERNAL]); }); }); });