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>()(_capture, ['pattern']); const Observe = Record.makeConstructor<{pattern: Value}, 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 { readonly objects: object[]; constructor(objects: object[]) { this.objects = objects; } decode(d: DecoderState): object { return this.fromValue(new Decoder(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): 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 = embed({x: 123}); const Y: Embedded = 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 = embed(A1a); const A2: Embedded = embed({a: 1}); const m = new Dictionary(); 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('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 }>()(Symbol.for('TestCases'), ['cases']); type TestCases = ReturnType; function encodeBinary(v: Value): Bytes { return encode(v, { canonical: true, embeddedEncode: genericEmbeddedTypeEncode }); } function looseEncodeBinary(v: Value): Bytes { return encode(v, { canonical: false, includeAnnotations: true, embeddedEncode: genericEmbeddedTypeEncode }); } function annotatedBinary(v: Value): Bytes { return encode(v, { canonical: true, includeAnnotations: true, embeddedEncode: genericEmbeddedTypeEncode }); } function decodeBinary(bs: Bytes): Value { return decode(bs, { includeAnnotations: true, embeddedDecode: genericEmbeddedTypeDecode }); } function encodeText(v: Value): string { return stringify(v, { includeAnnotations: true, embeddedWrite: genericEmbeddedTypeEncode }); } function decodeText(s: string): Value { return parse(s, { includeAnnotations: true, embeddedDecode: genericEmbeddedTypeDecode }); } type Variety = 'normal' | 'nondeterministic'; function runTestCase(variety: Variety, tName: string, binary: Bytes, annotatedValue: Value) { 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); tests.forEach((t0: Value, tName0: Value) => { const tName = Symbol.keyFor(strip(tName0) as symbol)!; const t = peel(t0) as Record; 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; } } }); });