import { Value, Dictionary, decode, decodeWithAnnotations, encode, encodeWithAnnotations, canonicalEncode, DecodeError, ShortPacket, Bytes, Record, annotate, strip, peel, preserves, fromJS, Constants, tuple, } from '../src/index'; const { Tag } = Constants; import './test-utils'; import * as fs from 'fs'; class Pointer { v: Value; constructor(v: Value) { this.v = v; } equals(other: any, is: (a: any, b: any) => boolean) { return Object.is(other.constructor, this.constructor) && is(this.v, other.v); } } function decodePointer(v: Value): Pointer { return new Pointer(strip(v)); } function encodePointer(w: Pointer): Value { return w.v; } const Discard = Record.makeConstructor()(Symbol.for('discard'), []); const Capture = Record.makeConstructor()(Symbol.for('capture'), ['pattern']); const Observe = Record.makeConstructor()(Symbol.for('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()(tuple(1), ['x', 'y']); const C2 = Record.makeConstructor()(tuple(1), ['z', 'w']); it('instance comparison should ignore pointer and fieldname differences', () => { expect(C1(9,9)).is(C2(9,9)); expect(C1(9,9)).not.is(C2(9,8)); }); it('comparison based on pointer 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(Discard().getConstructorInfo()).toEqual(Discard.constructorInfo); expect(Capture(Discard()).getConstructorInfo()).toEqual(Capture.constructorInfo); expect(Observe(Capture(Discard())).getConstructorInfo()).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 pointers', () => { it('should encode using pointerId when no function has been supplied', () => { const A1 = ({a: 1}); const A2 = ({a: 1}); const bs1 = canonicalEncode(A1); const bs2 = canonicalEncode(A2); const bs3 = canonicalEncode(A1); expect(bs1.get(0)).toBe(Tag.Pointer); expect(bs2.get(0)).toBe(Tag.Pointer); expect(bs3.get(0)).toBe(Tag.Pointer); // 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 pointers when no function has been supplied', () => { expect(() => decode(Bytes.from([Tag.Pointer, Tag.SmallInteger_lo]))) .toThrow('No decodePointer function supplied'); }); it('should encode properly', () => { const objects: object[] = []; const A = {a: 1}; const B = {b: 2}; expect(encode( [A, B], { encodePointer(v: object): Value { objects.push(v); return objects.length - 1; } })).is(Bytes.from([Tag.Sequence, Tag.Pointer, Tag.SmallInteger_lo, Tag.Pointer, Tag.SmallInteger_lo + 1, Tag.End])); expect(objects).is([A, B]); }); it('should decode properly', () => { const X = {x: 123}; const Y = {y: 456}; const objects: object[] = [X, Y]; expect(decode(Bytes.from([ Tag.Sequence, Tag.Pointer, Tag.SmallInteger_lo, Tag.Pointer, Tag.SmallInteger_lo + 1, Tag.End ]), { decodePointer(v: Value): object { if (typeof v !== 'number' || v < 0 || v >= objects.length) { throw new Error("Unknown pointer target"); } return objects[v]; } })).is([X, Y]); }); it('should store pointers embedded in map keys correctly', () => { const A1 = ({a: 1}); const A2 = ({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([{a: 1}])).toBeUndefined(); A1.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, { decodePointer }); const TestCases = Record.makeConstructor()(Symbol.for('TestCases'), ['cases']); function DS(bs: Bytes) { return decode(bs, { decodePointer }); } function D(bs: Bytes) { return decodeWithAnnotations(bs, { decodePointer }); } function E(v: Value) { return encodeWithAnnotations(v, { encodePointer }); } 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(Symbol.for('R'), [Symbol.for('f')]) }, annotation6: { forward: Record(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 Dictionary, Pointer>; 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; } } }); });