diff --git a/packages/client/lib/RESP/decoder.spec.ts b/packages/client/lib/RESP/decoder.spec.ts index e4e447fab6..5f0267911a 100644 --- a/packages/client/lib/RESP/decoder.spec.ts +++ b/packages/client/lib/RESP/decoder.spec.ts @@ -1,226 +1,286 @@ -// import { strict as assert } from 'node:assert'; -// import { SinonSpy, spy } from 'sinon'; -// import { Decoder, RESP_TYPES } from './decoder'; -// import { ErrorReply } from '../errors'; -// import { TypeMapping } from './types'; +import { strict as assert } from 'node:assert'; +import { SinonSpy, spy } from 'sinon'; +import { Decoder, RESP_TYPES } from './decoder'; +import { BlobError, ErrorReply, SimpleError } from '../errors'; +import { TypeMapping } from './types'; -// function createDecoderAndSpies(typeMapping: TypeMapping = {}) { -// const typeMappingSpy = spy(() => typeMapping), -// onReplySpy = spy(), -// onErrorReplySpy = spy(), -// onPushSpy = spy(); +interface Test { + toWrite: Buffer; + typeMapping?: TypeMapping; + replies?: Array; + errorReplies?: Array; + pushReplies?: Array; +} -// return { -// decoder: new Decoder({ -// getTypeMapping: typeMappingSpy, -// onReply: onReplySpy, -// onErrorReply: onErrorReplySpy, -// onPush: onPushSpy -// }), -// typeMappingSpy, -// onReplySpy, -// onErrorReplySpy, -// onPushSpy -// }; -// } +function test(name: string, config: Test) { + describe(name, () => { + it('single chunk', () => { + const setup = setupTest(config); + setup.decoder.write(config.toWrite); + assertCalls(config, setup); + }); -// function writeChunks(stream: Decoder, buffer: Buffer) { -// let i = 0; -// while (i < buffer.length) { -// stream.write(buffer.subarray(i, ++i)); -// } -// } + it('byte by byte', () => { + const setup = setupTest(config); + for (let i = 0; i < config.toWrite.length; i++) { + setup.decoder.write(config.toWrite.subarray(i, i + 1)); + } + assertCalls(config, setup); + }); + }) +} -// type Replies = Array>; +function setupTest(config: Test) { + const onReplySpy = spy(), + onErrorReplySpy = spy(), + onPushSpy = spy(); -// function generateTests(toWrite: Buffer, tests: Array): void { -// for (const test of tests) { -// describe(test.name, () => { -// generateTests(toWrite, test); -// }); -// } -// } + return { + decoder: new Decoder({ + getTypeMapping: () => config.typeMapping ?? {}, + onReply: onReplySpy, + onErrorReply: onErrorReplySpy, + onPush: onPushSpy + }), + onReplySpy, + onErrorReplySpy, + onPushSpy + }; +} -// interface Test { -// typeMapping?: TypeMapping; -// replies?: Replies; -// errorReplies?: Replies; -// pushReplies?: Replies; -// } +function assertCalls(config: Test, spies: ReturnType) { + assertSpyCalls(spies.onReplySpy, config.replies); + assertSpyCalls(spies.onErrorReplySpy, config.errorReplies); + assertSpyCalls(spies.onPushSpy, config.pushReplies); +} -// function genetareTypeTests(toWrite: Buffer, { typeMapping, replies, errorReplies, pushReplies }: Test) { -// const total = (replies?.length ?? 0) + (errorReplies?.length ?? 0) + (pushReplies?.length ?? 0); -// it('single chunk', () => { -// const { decoder, typeMappingSpy, onReplySpy, onErrorReplySpy, onPushSpy } = createDecoderAndSpies(typeMapping); -// decoder.write(toWrite); -// assert.equal(typeMappingSpy.callCount, total); -// testReplies(onReplySpy, replies); -// testReplies(onErrorReplySpy, errorReplies); -// testReplies(onPushSpy, pushReplies); -// }); +function assertSpyCalls(spy: SinonSpy, replies?: Array) { + if (!replies) { + assert.equal(spy.callCount, 0); + return; + } -// it('multiple chunks', () => { -// const { decoder, typeMappingSpy, onReplySpy, onErrorReplySpy, onPushSpy } = createDecoderAndSpies(typeMapping); -// writeChunks(decoder, toWrite); -// assert.equal(typeMappingSpy.callCount, total); -// testReplies(onReplySpy, replies); -// testReplies(onErrorReplySpy, errorReplies); -// testReplies(onPushSpy, pushReplies); -// }); -// } + assert.equal(spy.callCount, replies.length); + for (const [i, reply] of replies.entries()) { + assert.deepEqual( + spy.getCall(i).args, + [reply] + ); + } +} -// function testReplies(spy: SinonSpy, replies?: Replies): void { -// if (!replies) { -// assert.equal(spy.callCount, 0); -// return; -// } - -// assert.equal(spy.callCount, replies.length); -// for (const [i, reply] of replies.entries()) { -// assert.deepEqual( -// spy.getCall(i).args, -// reply -// ); -// } -// } - -// describe('RESP2Parser', () => { -// describe('Null', () => { -// genetareTypeTests(Buffer.from('_\r\n'), { -// replies: [[null]] -// }); -// }); - -// describe('Boolean', () => { -// genetareTypeTests(Buffer.from('#t\r\n'), { -// replies: [[null]] -// }); -// }); - -// describe('Number', () => { -// generateTests(Buffer.from(':-1\r\n')) -// describe('as number', () => { -// describe('-1', () => { -// generateTests({ -// toWrite: , -// replies: [[-1]] -// }); -// }); +describe.only('RESP Decoder', () => { + test('Null', { + toWrite: Buffer.from('_\r\n'), + replies: [null] + }); -// describe('0', () => { -// generateTests({ -// toWrite: Buffer.from(':0\r\n'), -// replies: [[0]] -// }); -// }); + describe('Boolean', () => { + test('true', { + toWrite: Buffer.from('#t\r\n'), + replies: [true] + }); -// describe('+1', () => { -// generateTests({ -// toWrite: Buffer.from(':+1\r\n'), -// replies: [[1]] -// }); -// }); -// }); -// }); - -// describe('Simple String', () => { -// describe('as strings', () => { -// generateTests({ -// toWrite: Buffer.from('+OK\r\n'), -// replies: [['OK']] -// }); -// }); + test('false', { + toWrite: Buffer.from('#f\r\n'), + replies: [false] + }); + }); -// describe('as buffers', () => { -// generateTests({ -// toWrite: Buffer.from('+OK\r\n'), -// typeMapping: { -// [RESP_TYPES.SIMPLE_STRING]: Buffer -// }, -// replies: [[Buffer.from('OK')]] -// }); -// }); -// }); + describe('Number', () => { + test('0', { + toWrite: Buffer.from(':0\r\n'), + replies: [0] + }); -// describe('Error', () => { -// generateTests({ -// toWrite: Buffer.from('-ERR\r\n'), -// errorReplies: [[new ErrorReply('ERR')]] -// }); -// }); + test('1', { + toWrite: Buffer.from(':+1\r\n'), + replies: [1] + }); - + test('+1', { + toWrite: Buffer.from(':+1\r\n'), + replies: [1] + }); -// describe('Bulk String', () => { -// describe('null', () => { -// generateTests({ -// toWrite: Buffer.from('$-1\r\n'), -// returnStringsAsBuffers: false, -// replies: [[null]] -// }); -// }); + test('-1', { + toWrite: Buffer.from(':-1\r\n'), + replies: [-1] + }); + }); -// describe('as strings', () => { -// generateTests({ -// toWrite: Buffer.from('$2\r\naa\r\n'), -// returnStringsAsBuffers: false, -// replies: [['aa']] -// }); -// }); + describe('BigNumber', () => { + test('0', { + toWrite: Buffer.from('(0\r\n'), + replies: [0n] + }); -// describe('as buffers', () => { -// generateTests({ -// toWrite: Buffer.from('$2\r\naa\r\n'), -// returnStringsAsBuffers: true, -// replies: [[Buffer.from('aa')]] -// }); -// }); -// }); + test('1', { + toWrite: Buffer.from('(1\r\n'), + replies: [1n] + }); -// describe('Array', () => { -// describe('null', () => { -// generateTests({ -// toWrite: Buffer.from('*-1\r\n'), -// returnStringsAsBuffers: false, -// replies: [[null]] -// }); -// }); + test('+1', { + toWrite: Buffer.from('(+1\r\n'), + replies: [1n] + }); -// const arrayBuffer = Buffer.from( -// '*5\r\n' + -// '+OK\r\n' + -// '-ERR\r\n' + -// ':0\r\n' + -// '$1\r\na\r\n' + -// '*0\r\n' -// ); + test('-1', { + toWrite: Buffer.from('(-1\r\n'), + replies: [-1n] + }); + }); -// describe('as strings', () => { -// generateTests({ -// toWrite: arrayBuffer, -// returnStringsAsBuffers: false, -// replies: [[[ -// 'OK', -// new ErrorReply('ERR'), -// 0, -// 'a', -// [] -// ]]] -// }); -// }); + describe('Double', () => { + test('0', { + toWrite: Buffer.from(',0\r\n'), + replies: [0] + }); -// describe('as buffers', () => { -// generateTests({ -// toWrite: arrayBuffer, -// returnStringsAsBuffers: true, -// replies: [[[ -// Buffer.from('OK'), -// new ErrorReply('ERR'), -// 0, -// Buffer.from('a'), -// [] -// ]]] -// }); -// }); -// }); -// }); + test('1', { + toWrite: Buffer.from(',1\r\n'), + replies: [1] + }); + + test('+1', { + toWrite: Buffer.from(',+1\r\n'), + replies: [1] + }); + + test('-1', { + toWrite: Buffer.from(',-1\r\n'), + replies: [-1] + }); + + test('1.1', { + toWrite: Buffer.from(',1.1\r\n'), + replies: [1.1] + }); + + test('nan', { + toWrite: Buffer.from(',nan\r\n'), + replies: [NaN] + }); + + test('inf', { + toWrite: Buffer.from(',inf\r\n'), + replies: [Infinity] + }); + + test('+inf', { + toWrite: Buffer.from(',+inf\r\n'), + replies: [Infinity] + }); + + test('-inf', { + toWrite: Buffer.from(',-inf\r\n'), + replies: [-Infinity] + }); + + test('1e1', { + toWrite: Buffer.from(',1e1\r\n'), + replies: [1e1] + }); + + test('-1.1E1', { + toWrite: Buffer.from(',-1.1E1\r\n'), + replies: [-1.1E1] + }); + }); + + test('SimpleString', { + toWrite: Buffer.from('+OK\r\n'), + replies: ['OK'] + }); + + describe('BlobString', () => { + test("''", { + toWrite: Buffer.from('$0\r\n\r\n'), + replies: [''] + }); + + test("'1234567890'", { + toWrite: Buffer.from('$10\r\n1234567890\r\n'), + replies: ['1234567890'] + }); + + test('null (RESP2 backwards compatibility)', { + toWrite: Buffer.from('$-1\r\n'), + replies: [null] + }); + }); + + describe('VerbatimString', () => { + test("''", { + toWrite: Buffer.from('=4\r\ntxt:\r\n'), + replies: [''] + }); + + test("'123456'", { + toWrite: Buffer.from('=10\r\ntxt:123456\r\n'), + replies: ['123456'] + }); + }); + + test('SimpleError', { + toWrite: Buffer.from('-ERROR\r\n'), + errorReplies: [new SimpleError('ERROR')] + }); + + test('BlobError', { + toWrite: Buffer.from('!5\r\nERROR\r\n'), + errorReplies: [new BlobError('ERROR')] + }); + + describe('Array', () => { + test('[]', { + toWrite: Buffer.from('*0\r\n'), + replies: [[]] + }); + + test('[0..9]', { + toWrite: Buffer.from(`*10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + }); + + test('null (RESP2 backwards compatibility)', { + toWrite: Buffer.from('*-1\r\n'), + replies: [null] + }); + }); + + describe('Set', () => { + test('empty', { + toWrite: Buffer.from('~0\r\n'), + replies: [[]] + }); + + test('of 0..9', { + toWrite: Buffer.from(`*10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + }); + }); + + describe('Map', () => { + test('{}', { + toWrite: Buffer.from('%0\r\n'), + replies: [Object.create(null)] + }); + + test("{ '0'..'9': }", { + toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`), + replies: [Object.create(null, { + 0: { value: '0', enumerable: true }, + 1: { value: '1', enumerable: true }, + 2: { value: '2', enumerable: true }, + 3: { value: '3', enumerable: true }, + 4: { value: '4', enumerable: true }, + 5: { value: '5', enumerable: true }, + 6: { value: '6', enumerable: true }, + 7: { value: '7', enumerable: true }, + 8: { value: '8', enumerable: true }, + 9: { value: '9', enumerable: true } + })] + }); + }); +}); diff --git a/packages/client/lib/RESP/decoder.ts b/packages/client/lib/RESP/decoder.ts index 7e2a488587..7c48f1f783 100644 --- a/packages/client/lib/RESP/decoder.ts +++ b/packages/client/lib/RESP/decoder.ts @@ -257,7 +257,7 @@ export class Decoder { private _maybeDecodeNumberValue(isNegative, chunk) { const cb = this._decodeUnsingedNumber.bind(this, 0); return ++this._cursor === chunk.length ? - this._decodeNumberValue.bind(isNegative, cb) : + this._decodeNumberValue.bind(this, isNegative, cb) : this._decodeNumberValue(isNegative, cb, chunk); } @@ -306,8 +306,8 @@ export class Decoder { private _maybeDecodeBigNumberValue(isNegative, chunk) { const cb = this._decodeUnsingedBigNumber.bind(this, 0n); - return this._cursor === chunk.length ? - this._decodeBigNumberValue.bind(isNegative, cb) : + return ++this._cursor === chunk.length ? + this._decodeBigNumberValue.bind(this, isNegative, cb) : this._decodeBigNumberValue(isNegative, cb, chunk); } @@ -357,7 +357,7 @@ export class Decoder { private _maybeDecodeDoubleInteger(isNegative, chunk) { return ++this._cursor === chunk.length ? this._decodeDoubleInteger.bind(this, isNegative, 0) : - this._decodeDoubleInteger(true, 0, chunk); + this._decodeDoubleInteger(isNegative, 0, chunk); } private _decodeDoubleInteger(isNegative, integer, chunk) { @@ -376,14 +376,17 @@ export class Decoder { switch (byte) { case ASCII['.']: this._cursor = cursor + 1; // skip . - return cursor < chunk.length ? + return this._cursor < chunk.length ? this._decodeDoubleDecimal(isNegative, 0, integer, chunk) : this._decodeDoubleDecimal.bind(this, isNegative, 0, integer); case ASCII.E: case ASCII.e: - this._cursor = cursor + 1; // skip e - return this._decodeDoubleExponent(isNegative ? -integer : integer, chunk); + this._cursor = cursor + 1; // skip E/e + const i = isNegative ? -integer : integer; + return this._cursor < chunk.length ? + this._decodeDoubleExponent(i, chunk) : + this._decodeDoubleExponent.bind(this, i); case ASCII['\r']: this._cursor = cursor + 2; // skip \r\n @@ -414,11 +417,11 @@ export class Decoder { switch (byte) { case ASCII.E: case ASCII.e: - this._cursor = cursor + 1; // skip e + this._cursor = cursor + 1; // skip E/e const d = isNegative ? -double : double; return this._cursor === chunk.length ? - this._decodeDoubleExponent.bind(this, d, false, 0) : - this._decodeDoubleExponent(d, false, 0, chunk); + this._decodeDoubleExponent.bind(this, d) : + this._decodeDoubleExponent(d, chunk); case ASCII['\r']: this._cursor = cursor + 2; // skip \r\n