368 lines
14 KiB
TypeScript
368 lines
14 KiB
TypeScript
import {
|
|
Value,
|
|
Dictionary,
|
|
decode, decodeWithAnnotations, encode, canonicalEncode,
|
|
DecodeError, ShortPacket,
|
|
Bytes, Record,
|
|
annotate,
|
|
strip, peel,
|
|
preserves,
|
|
stringify,
|
|
fromJS,
|
|
Constants,
|
|
Encoder,
|
|
GenericEmbedded,
|
|
EncoderState,
|
|
EmbeddedType,
|
|
DecoderState,
|
|
Decoder,
|
|
Embedded,
|
|
embed,
|
|
genericEmbeddedTypeDecode,
|
|
genericEmbeddedTypeEncode,
|
|
parse,
|
|
} from '../src/index';
|
|
const { Tag } = Constants;
|
|
import './test-utils';
|
|
|
|
import * as fs from 'fs';
|
|
|
|
const _discard = Symbol.for('discard');
|
|
const _capture = Symbol.for('capture');
|
|
const _observe = Symbol.for('observe');
|
|
const Discard = Record.makeConstructor<{}, GenericEmbedded>()(_discard, []);
|
|
const Capture = Record.makeConstructor<{pattern: Value<GenericEmbedded>}, GenericEmbedded>()(_capture, ['pattern']);
|
|
const Observe = Record.makeConstructor<{pattern: Value<GenericEmbedded>}, GenericEmbedded>()(_observe, ['pattern']);
|
|
|
|
describe('record constructors', () => {
|
|
it('should have constructorInfo', () => {
|
|
expect(Discard.constructorInfo.label).toEqual(Symbol.for('discard'));
|
|
expect(Capture.constructorInfo.label).toEqual(Symbol.for('capture'));
|
|
expect(Observe.constructorInfo.label).toEqual(Symbol.for('observe'));
|
|
expect(Discard.constructorInfo.arity).toEqual(0);
|
|
expect(Capture.constructorInfo.arity).toEqual(1);
|
|
expect(Observe.constructorInfo.arity).toEqual(1);
|
|
});
|
|
})
|
|
|
|
describe('RecordConstructorInfo', () => {
|
|
const C1 = Record.makeConstructor<{x: number, y: number}>()([1], ['x', 'y']);
|
|
const C2 = Record.makeConstructor<{z: number, w: number}>()([1], ['z', 'w']);
|
|
it('instance comparison should ignore embedded and fieldname differences', () => {
|
|
expect(C1(9,9)).is(C2(9,9));
|
|
expect(C1(9,9)).not.is(C2(9,8));
|
|
});
|
|
it('comparison based on embedded equality should not work', () => {
|
|
expect(C1.constructorInfo).not.toBe(C2.constructorInfo);
|
|
});
|
|
it('comparison based on .equals should work', () => {
|
|
expect(C1.constructorInfo).toEqual(C2.constructorInfo);
|
|
});
|
|
});
|
|
|
|
describe('records', () => {
|
|
it('should have correct getConstructorInfo', () => {
|
|
expect(Record.constructorInfo(Discard())).toEqual(Discard.constructorInfo);
|
|
expect(Record.constructorInfo(Capture(Discard()))).toEqual(Capture.constructorInfo);
|
|
expect(Record.constructorInfo(Observe(Capture(Discard())))).toEqual(Observe.constructorInfo);
|
|
});
|
|
});
|
|
|
|
describe('parsing from subarray', () => {
|
|
it('should maintain alignment of nextbytes', () => {
|
|
const u = Uint8Array.of(1, 1, 1, 1, 0xb1, 0x03, 0x33, 0x33, 0x33);
|
|
const bs = Bytes.from(u.subarray(4));
|
|
expect(decode(bs)).is("333");
|
|
});
|
|
});
|
|
|
|
describe('reusing buffer space', () => {
|
|
it('should be done safely, even with nested dictionaries', () => {
|
|
expect(canonicalEncode(fromJS(['aaa', Dictionary.fromJS({a: 1}), 'zzz'])).toHex()).is(
|
|
`b5
|
|
b103616161
|
|
b7
|
|
b10161 b00101
|
|
84
|
|
b1037a7a7a
|
|
84`.replace(/\s+/g, ''));
|
|
});
|
|
});
|
|
|
|
describe('encoding and decoding embeddeds', () => {
|
|
class LookasideEmbeddedType implements EmbeddedType<object> {
|
|
readonly objects: object[];
|
|
|
|
constructor(objects: object[]) {
|
|
this.objects = objects;
|
|
}
|
|
|
|
decode(d: DecoderState): object {
|
|
return this.fromValue(new Decoder<GenericEmbedded>(d).next());
|
|
}
|
|
|
|
encode(e: EncoderState, v: object): void {
|
|
new Encoder(e).push(this.toValue(v));
|
|
}
|
|
|
|
equals(a: object, b: object): boolean {
|
|
return Object.is(a, b);
|
|
}
|
|
|
|
fromValue(v: Value<GenericEmbedded>): object {
|
|
if (typeof v !== 'number' || v < 0 || v >= this.objects.length) {
|
|
throw new Error("Unknown embedded target");
|
|
}
|
|
return this.objects[v];
|
|
}
|
|
|
|
toValue(v: object): number {
|
|
let i = this.objects.indexOf(v);
|
|
if (i !== -1) return i;
|
|
this.objects.push(v);
|
|
return this.objects.length - 1;
|
|
}
|
|
}
|
|
|
|
it('should encode using embeddedId when no function has been supplied', () => {
|
|
const A1 = embed({a: 1});
|
|
const A2 = embed({a: 1});
|
|
const bs1 = canonicalEncode(A1);
|
|
const bs2 = canonicalEncode(A2);
|
|
const bs3 = canonicalEncode(A1);
|
|
expect(bs1.get(0)).toBe(Tag.Embedded);
|
|
expect(bs2.get(0)).toBe(Tag.Embedded);
|
|
expect(bs3.get(0)).toBe(Tag.Embedded);
|
|
// Can't really check the value assigned to the object. But we
|
|
// can check that it's different to a similar object!
|
|
expect(bs1).not.is(bs2);
|
|
expect(bs1).is(bs3);
|
|
});
|
|
it('should refuse to decode embeddeds when no function has been supplied', () => {
|
|
expect(() => decode(Bytes.from([Tag.Embedded, Tag.False])))
|
|
.toThrow("Embeddeds not permitted at this point in Preserves document");
|
|
});
|
|
it('should encode properly', () => {
|
|
const objects: object[] = [];
|
|
const pt = new LookasideEmbeddedType(objects);
|
|
const A = embed({a: 1});
|
|
const B = embed({b: 2});
|
|
expect(encode([A, B], { embeddedEncode: pt })).is(
|
|
Bytes.from([Tag.Sequence,
|
|
Tag.Embedded, Tag.SignedInteger, 0,
|
|
Tag.Embedded, Tag.SignedInteger, 1, 1,
|
|
Tag.End]));
|
|
expect(objects).toEqual([A.embeddedValue, B.embeddedValue]);
|
|
});
|
|
it('should decode properly', () => {
|
|
const objects: object[] = [];
|
|
const pt = new LookasideEmbeddedType(objects);
|
|
const X: Embedded<object> = embed({x: 123});
|
|
const Y: Embedded<object> = embed({y: 456});
|
|
objects.push(X.embeddedValue);
|
|
objects.push(Y.embeddedValue);
|
|
expect(decode(Bytes.from([
|
|
Tag.Sequence,
|
|
Tag.Embedded, Tag.SignedInteger, 0,
|
|
Tag.Embedded, Tag.SignedInteger, 1, 1,
|
|
Tag.End
|
|
]), { embeddedDecode: pt })).is([X, Y]);
|
|
});
|
|
it('should store embeddeds embedded in map keys correctly', () => {
|
|
const A1a = {a: 1};
|
|
const A1: Embedded<object> = embed(A1a);
|
|
const A2: Embedded<object> = embed({a: 1});
|
|
const m = new Dictionary<object, number>();
|
|
m.set([A1], 1);
|
|
m.set([A2], 2);
|
|
expect(m.get(A1)).toBeUndefined();
|
|
expect(m.get([A1])).toBe(1);
|
|
expect(m.get([A2])).toBe(2);
|
|
expect(m.get([embed({a: 1})])).toBeUndefined();
|
|
A1a.a = 3;
|
|
expect(m.get([A1])).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('integer text parsing', () => {
|
|
it('should work for zero', () => {
|
|
expect(parse('0')).is(0);
|
|
});
|
|
|
|
it('should work for smallish positive integers', () => {
|
|
expect(parse('60000')).is(60000);
|
|
});
|
|
it('should work for smallish negative integers', () => {
|
|
expect(parse('-60000')).is(-60000);
|
|
});
|
|
|
|
it('should work for largeish positive integers', () => {
|
|
expect(parse('1234567812345678123456781234567'))
|
|
.is(BigInt("1234567812345678123456781234567"));
|
|
});
|
|
it('should work for largeish negative integers', () => {
|
|
expect(parse('-1234567812345678123456781234567'))
|
|
.is(BigInt("-1234567812345678123456781234567"));
|
|
});
|
|
|
|
it('should work for larger positive integers', () => {
|
|
expect(parse('12345678123456781234567812345678'))
|
|
.is(BigInt("12345678123456781234567812345678"));
|
|
});
|
|
it('should work for larger negative integers', () => {
|
|
expect(parse('-12345678123456781234567812345678'))
|
|
.is(BigInt("-12345678123456781234567812345678"));
|
|
});
|
|
});
|
|
|
|
describe('integer binary encoding', () => {
|
|
it('should work for zero integers', () => {
|
|
expect(encode(0)).is(Bytes.fromHex('b000'));
|
|
});
|
|
it('should work for zero bigints', () => {
|
|
expect(encode(BigInt(0))).is(Bytes.fromHex('b000'));
|
|
});
|
|
|
|
it('should work for smallish positive integers', () => {
|
|
expect(encode(60000)).is(Bytes.fromHex('b00300ea60'));
|
|
});
|
|
it('should work for smallish negative integers', () => {
|
|
expect(encode(-60000)).is(Bytes.fromHex('b003ff15a0'));
|
|
});
|
|
|
|
it('should work for largeish positive integers', () => {
|
|
expect(encode(BigInt("1234567812345678123456781234567")))
|
|
.is(Bytes.fromHex('b00d0f951a8f2b4b049d518b923187'));
|
|
});
|
|
it('should work for largeish negative integers', () => {
|
|
expect(encode(BigInt("-1234567812345678123456781234567")))
|
|
.is(Bytes.fromHex('b00df06ae570d4b4fb62ae746dce79'));
|
|
});
|
|
|
|
it('should work for larger positive integers', () => {
|
|
expect(encode(BigInt("12345678123456781234567812345678")))
|
|
.is(Bytes.fromHex('b00e009bd30997b0ee2e252f73b5ef4e'));
|
|
});
|
|
it('should work for larger negative integers', () => {
|
|
expect(encode(BigInt("-12345678123456781234567812345678")))
|
|
.is(Bytes.fromHex('b00eff642cf6684f11d1dad08c4a10b2'));
|
|
});
|
|
});
|
|
|
|
describe('common test suite', () => {
|
|
const samples_bin = fs.readFileSync(__dirname + '/../../../../../tests/samples.bin');
|
|
const samples = decodeWithAnnotations(samples_bin, { embeddedDecode: genericEmbeddedTypeDecode });
|
|
|
|
const TestCases = Record.makeConstructor<{
|
|
cases: Dictionary<GenericEmbedded>
|
|
}>()(Symbol.for('TestCases'), ['cases']);
|
|
type TestCases = ReturnType<typeof TestCases>;
|
|
|
|
function encodeBinary(v: Value<GenericEmbedded>): Bytes {
|
|
return encode(v, { canonical: true, embeddedEncode: genericEmbeddedTypeEncode });
|
|
}
|
|
function looseEncodeBinary(v: Value<GenericEmbedded>): Bytes {
|
|
return encode(v, { canonical: false, includeAnnotations: true, embeddedEncode: genericEmbeddedTypeEncode });
|
|
}
|
|
function annotatedBinary(v: Value<GenericEmbedded>): Bytes {
|
|
return encode(v, { canonical: true, includeAnnotations: true, embeddedEncode: genericEmbeddedTypeEncode });
|
|
}
|
|
function decodeBinary(bs: Bytes): Value<GenericEmbedded> {
|
|
return decode(bs, { includeAnnotations: true, embeddedDecode: genericEmbeddedTypeDecode });
|
|
}
|
|
function encodeText(v: Value<GenericEmbedded>): string {
|
|
return stringify(v, { includeAnnotations: true, embeddedWrite: genericEmbeddedTypeEncode });
|
|
}
|
|
function decodeText(s: string): Value<GenericEmbedded> {
|
|
return parse(s, { includeAnnotations: true, embeddedDecode: genericEmbeddedTypeDecode });
|
|
}
|
|
|
|
type Variety = 'normal' | 'nondeterministic';
|
|
|
|
function runTestCase(variety: Variety,
|
|
tName: string,
|
|
binary: Bytes,
|
|
annotatedValue: Value<GenericEmbedded>)
|
|
{
|
|
describe(tName, () => {
|
|
const stripped = strip(annotatedValue);
|
|
it('should round-trip, canonically', () =>
|
|
expect(decodeBinary(encodeBinary(annotatedValue))).is(stripped));
|
|
it('should go back, stripped', () =>
|
|
expect(strip(decodeBinary(binary))).is(stripped));
|
|
it('should go back', () =>
|
|
expect(decodeBinary(binary)).is(annotatedValue));
|
|
it('should round-trip, with annotations', () =>
|
|
expect(decodeBinary(annotatedBinary(annotatedValue))).is(annotatedValue));
|
|
it('should round-trip as text, stripped', () =>
|
|
expect(decodeText(encodeText(stripped))).is(stripped));
|
|
it('should round-trip as text, with annotations', () =>
|
|
expect(decodeText(encodeText(annotatedValue))).is(annotatedValue));
|
|
it('should go forward', () =>
|
|
expect(annotatedBinary(annotatedValue)).is(binary));
|
|
if (variety === 'normal') {
|
|
it('should go forward, loosely', () =>
|
|
expect(looseEncodeBinary(annotatedValue)).is(binary));
|
|
}
|
|
});
|
|
}
|
|
|
|
const tests = (peel(TestCases._.cases(peel(samples) as TestCases)) as
|
|
Dictionary<GenericEmbedded>);
|
|
tests.forEach((t0: Value<GenericEmbedded>, tName0: Value<GenericEmbedded>) => {
|
|
const tName = Symbol.keyFor(strip(tName0) as symbol)!;
|
|
const t = peel(t0) as Record<symbol, any, GenericEmbedded>;
|
|
switch (t.label) {
|
|
case Symbol.for('Test'):
|
|
runTestCase('normal', tName, strip(t[0]) as Bytes, t[1]);
|
|
break;
|
|
case Symbol.for('NondeterministicTest'):
|
|
runTestCase('nondeterministic', tName, strip(t[0]) as Bytes, t[1]);
|
|
break;
|
|
case Symbol.for('DecodeError'):
|
|
describe(tName, () => {
|
|
it('should fail with DecodeError', () => {
|
|
expect(() => decodeBinary(strip(t[0]) as Bytes))
|
|
.toThrowFilter(e =>
|
|
DecodeError.isDecodeError(e) &&
|
|
!ShortPacket.isShortPacket(e));
|
|
});
|
|
});
|
|
break;
|
|
case Symbol.for('DecodeEOF'): // fall through
|
|
case Symbol.for('DecodeShort'):
|
|
describe(tName, () => {
|
|
it('should fail with ShortPacket', () => {
|
|
expect(() => decodeBinary(strip(t[0]) as Bytes))
|
|
.toThrowFilter(e => ShortPacket.isShortPacket(e));
|
|
});
|
|
});
|
|
break;
|
|
case Symbol.for('ParseError'):
|
|
describe(tName, () => {
|
|
it('should fail with DecodeError', () => {
|
|
expect(() => parse(strip(t[0]) as string))
|
|
.toThrowFilter(e =>
|
|
DecodeError.isDecodeError(e) &&
|
|
!ShortPacket.isShortPacket(e));
|
|
});
|
|
});
|
|
break;
|
|
case Symbol.for('ParseEOF'):
|
|
case Symbol.for('ParseShort'):
|
|
describe(tName, () => {
|
|
it('should fail with ShortPacket', () => {
|
|
expect(() => parse(strip(t[0]) as string))
|
|
.toThrowFilter(e => ShortPacket.isShortPacket(e));
|
|
});
|
|
});
|
|
break;
|
|
default:{
|
|
const e = new Error(preserves`Unsupported test kind ${t}`);
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
}
|
|
});
|
|
});
|