blob: 16325bf3be47394b1587319c92425861b6b0c8b6 [file] [log] [blame]
// Copyright 2022 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('Transfer client', () => {
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(
sessionId: number,
offset: number,
data: string,
remainingBytes: number
): Chunk {
const chunk = new Chunk();
chunk.setSessionId(sessionId);
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<ProgressStats> = [];
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.setSessionId(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.setSessionId(4);
chunk.setOffset(0);
chunk.setPendingBytes(32);
chunk.setMaxChunkSizeBytes(8);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(4);
chunk.setOffset(0);
chunk.setPendingBytes(32);
chunk.setMaxChunkSizeBytes(8);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(4);
chunk.setOffset(0);
chunk.setPendingBytes(8);
chunk.setMaxChunkSizeBytes(8);
const chunk2 = new Chunk();
chunk2.setSessionId(4);
chunk2.setOffset(8);
chunk2.setPendingBytes(8);
chunk2.setMaxChunkSizeBytes(8);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(4);
chunk.setOffset(0);
chunk.setPendingBytes(8);
chunk.setMaxChunkSizeBytes(4);
chunk.setType(Chunk.Type.PARAMETERS_RETRANSMIT);
chunk.setWindowEndOffset(8);
const chunk2 = new Chunk();
chunk2.setSessionId(4);
chunk2.setOffset(4);
chunk2.setPendingBytes(8);
chunk2.setType(Chunk.Type.PARAMETERS_CONTINUE);
chunk2.setWindowEndOffset(12);
const chunk3 = new Chunk();
chunk3.setSessionId(4);
chunk3.setOffset(8);
chunk3.setPendingBytes(8);
chunk3.setType(Chunk.Type.PARAMETERS_CONTINUE);
chunk3.setWindowEndOffset(16);
const chunk4 = new Chunk();
chunk4.setSessionId(4);
chunk4.setOffset(12);
chunk4.setPendingBytes(8);
chunk4.setType(Chunk.Type.PARAMETERS_CONTINUE);
chunk4.setWindowEndOffset(20);
const chunk5 = new Chunk();
chunk5.setSessionId(4);
chunk5.setOffset(16);
chunk5.setPendingBytes(8);
chunk5.setType(Chunk.Type.PARAMETERS_CONTINUE);
chunk5.setWindowEndOffset(24);
const chunk6 = new Chunk();
chunk6.setSessionId(4);
chunk6.setOffset(20);
chunk6.setPendingBytes(8);
chunk6.setType(Chunk.Type.PARAMETERS_CONTINUE);
chunk6.setWindowEndOffset(28);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(4);
chunk.setOffset(0);
chunk.setPendingBytes(8);
chunk.setMaxChunkSizeBytes(8);
const chunk2 = new Chunk();
chunk2.setSessionId(4);
chunk2.setOffset(8);
chunk2.setPendingBytes(8);
chunk2.setMaxChunkSizeBytes(8);
const completeChunk = new Chunk();
completeChunk.setSessionId(4);
completeChunk.setStatus(Status.OK);
enqueueServerResponses(service.method('Write')!, [
[chunk],
[chunk2],
[completeChunk],
]);
const progress: Array<ProgressStats> = [];
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.setSessionId(4);
chunk1.setOffset(0);
chunk1.setPendingBytes(8);
chunk1.setMaxChunkSizeBytes(8);
const chunk2 = new Chunk();
chunk2.setSessionId(4);
chunk2.setOffset(8);
chunk2.setPendingBytes(8);
chunk2.setMaxChunkSizeBytes(8);
const chunk3 = new Chunk();
chunk3.setSessionId(4);
chunk3.setOffset(4); // Rewind
chunk3.setPendingBytes(8);
chunk3.setMaxChunkSizeBytes(8);
const chunk4 = new Chunk();
chunk4.setSessionId(4);
chunk4.setOffset(12); // Rewind
chunk4.setPendingBytes(16);
chunk4.setMaxChunkSizeBytes(16);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(4);
chunk1.setOffset(0);
chunk1.setPendingBytes(8);
chunk1.setMaxChunkSizeBytes(8);
const chunk2 = new Chunk();
chunk2.setSessionId(4);
chunk2.setOffset(100); // larger offset than data
chunk2.setPendingBytes(8);
chunk2.setMaxChunkSizeBytes(8);
const completeChunk = new Chunk();
completeChunk.setSessionId(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.setSessionId(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.setSessionId(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.setSessionId(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.setSessionId(22);
expectedChunk1.setResourceId(22);
expectedChunk1.setType(Chunk.Type.TRANSFER_START);
const expectedChunk2 = new Chunk();
expectedChunk2.setSessionId(22);
expectedChunk2.setData(textEncoder.encode('01234'));
expectedChunk2.setType(Chunk.Type.TRANSFER_DATA);
const lastChunk = new Chunk();
lastChunk.setSessionId(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.setSessionId(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]);
});
});
});