import { Value, Dictionary, decode, decodeWithAnnotations, encode, encodeWithAnnotations, canonicalEncode, DecodeError, ShortPacket, Bytes, Record, annotate, strip, peel, preserves, fromJS, Constants, Encoder, GenericEmbedded, EncoderState, EmbeddedType, DecoderState, Decoder, Embedded, embed, genericEmbeddedTypeDecode, genericEmbeddedTypeEncode, } 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 91 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.SmallInteger_lo]))) .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.SmallInteger_lo, Tag.Embedded, Tag.SmallInteger_lo + 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.SmallInteger_lo, Tag.Embedded, Tag.SmallInteger_lo + 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 DS(bs: Bytes) { return decode(bs, { embeddedDecode: genericEmbeddedTypeDecode }); } function D(bs: Bytes) { return decodeWithAnnotations(bs, { embeddedDecode: genericEmbeddedTypeDecode }); } function E(v: Value) { return encodeWithAnnotations(v, { embeddedEncode: genericEmbeddedTypeEncode }); } interface ExpectedValues { [testName: string]: ({ value: Value; } | { forward: Value; back: Value; }); } const expectedValues: ExpectedValues = { annotation1: { forward: annotate(9, "abc"), back: 9 }, annotation2: { forward: annotate([[], annotate([], "x")], "abc", "def"), back: [[], []] }, annotation3: { forward: annotate(5, annotate(2, 1), annotate(4, 3)), back: 5 }, annotation5: { forward: annotate( Record(Symbol.for('R'), [annotate(Symbol.for('f'), Symbol.for('af'))]), Symbol.for('ar')), back: Record, any>(Symbol.for('R'), [Symbol.for('f')]) }, annotation6: { forward: Record, any>( annotate(Symbol.for('R'), Symbol.for('ar')), [annotate(Symbol.for('f'), Symbol.for('af'))]), back: Record(Symbol.for('R'), [Symbol.for('f')]) }, annotation7: { forward: annotate([], Symbol.for('a'), Symbol.for('b'), Symbol.for('c')), back: [] }, list1: { forward: [1, 2, 3, 4], back: [1, 2, 3, 4] }, record2: { value: Observe(Record(Symbol.for("speak"), [ Discard(), Capture(Discard()) ])) }, }; type Variety = 'normal' | 'nondeterministic' | 'decode'; function runTestCase(variety: Variety, tName: string, binaryForm: Bytes, annotatedTextForm: Value) { describe(tName, () => { const textForm = strip(annotatedTextForm); const {forward, back} = (function () { const entry = expectedValues[tName] ?? {value: textForm}; if ('value' in entry) { return {forward: entry.value, back: entry.value}; } else if ('forward' in entry && 'back' in entry) { return entry; } else { throw new Error('Invalid expectedValues entry for ' + tName); } })(); it('should match the expected value', () => expect(textForm).is(back)); it('should round-trip', () => expect(DS(E(textForm))).is(back)); it('should go forward', () => expect(DS(E(forward))).is(back)); it('should go back', () => expect(DS(binaryForm)).is(back)); it('should go back with annotations', () => expect(D(E(annotatedTextForm))).is(annotatedTextForm)); if (variety !== 'decode' && variety !== 'nondeterministic') { it('should encode correctly', () => expect(E(forward)).is(binaryForm)); it('should encode correctly with annotations', () => expect(E(annotatedTextForm)).is(binaryForm)); } }); } 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('DecodeTest'): runTestCase('decode', tName, strip(t[0]) as Bytes, t[1]); break; case Symbol.for('DecodeError'): describe(tName, () => { it('should fail with DecodeError', () => { expect(() => D(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(() => D(strip(t[0]) as Bytes)) .toThrowFilter(e => ShortPacket.isShortPacket(e)); }); }); break; case Symbol.for('ParseError'): case Symbol.for('ParseEOF'): case Symbol.for('ParseShort'): /* Skipped for now, until we have an implementation of text syntax */ break; default:{ const e = new Error(preserves`Unsupported test kind ${t}`); console.error(e); throw e; } } }); });