import { Value, Dictionary, decode, decodeWithAnnotations, encodeWithAnnotations, canonicalEncode, DecodeError, ShortPacket, Bytes, Record, annotate, strip, peel, preserves, fromJS, } from '../src/index'; import './test-utils'; import * as fs from 'fs'; const Discard = Record.makeConstructor('discard', []); const Capture = Record.makeConstructor('capture', ['pattern']); const Observe = Record.makeConstructor('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.makeBasicConstructor([1], ['x', 'y']); const C2 = Record.makeBasicConstructor([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', {a: 1}, 'zzz'])).toHex()).is( `b5 b103616161 b7 b10161 91 84 b1037a7a7a 84`.replace(/\s+/g, '')); }); }); describe('common test suite', () => { const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin'); const samples = decodeWithAnnotations(samples_bin); const TestCases = Record.makeConstructor('TestCases', ['cases']); function DS(bs: Bytes) { return decode(bs); } function D(bs: Bytes) { return decodeWithAnnotations(bs); } function E(v: Value) { return encodeWithAnnotations(v); } const 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(new Record(Symbol.for('R'), [annotate(Symbol.for('f'), Symbol.for('af'))]), Symbol.for('ar')), back: new Record(Symbol.for('R'), [Symbol.for('f')]) }, annotation6: { forward: new Record(annotate(Symbol.for('R'), Symbol.for('ar')), [annotate(Symbol.for('f'), Symbol.for('af'))]), back: new 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(new 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; 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; } } }); });