"use strict"; const assert = require('assert'); const Immutable = require('immutable'); const Preserves = require('../src/index.js'); const { is, List, Set, Map, Decoder, Encoder, decode, decodeWithAnnotations, encode, DecodeError, EncodeError, ShortPacket, Bytes, Record, Single, Double, annotate, stripAnnotations, PreserveOn, } = Preserves; const fs = require('fs'); const util = require('util'); const Discard = Record.makeConstructor('discard', []); const Capture = Record.makeConstructor('capture', ['pattern']); const Observe = Record.makeConstructor('observe', ['pattern']); describe('record constructors', () => { it('should have constructorInfo', () => { assert.strictEqual(Discard.constructorInfo.label, Symbol.for('discard')); assert.strictEqual(Capture.constructorInfo.label, Symbol.for('capture')); assert.strictEqual(Observe.constructorInfo.label, Symbol.for('observe')); assert.strictEqual(Discard.constructorInfo.arity, 0); assert.strictEqual(Capture.constructorInfo.arity, 1); assert.strictEqual(Observe.constructorInfo.arity, 1); }); }) describe('RecordConstructorInfo', () => { const C1 = Record.makeBasicConstructor(Immutable.List([1]), ['x', 'y']); const C2 = Record.makeBasicConstructor(Immutable.List([1]), ['z', 'w']); it('instance comparison should ignore pointer and fieldname differences', () => { assert(is(C1(9,9), C2(9,9))); assert(!is(C1(9,9), C2(9,8))); }); it('comparison based on pointer equality should not work', () => { assert.notStrictEqual(C1.constructorInfo, C2.constructorInfo); }); it('comparison based on .equals should work', () => { assert(is(C1.constructorInfo, C2.constructorInfo)); }); }); describe('records', () => { it('should have correct getConstructorInfo', () => { assert(Discard().getConstructorInfo().equals(Discard.constructorInfo)); assert(Capture(Discard()).getConstructorInfo().equals(Capture.constructorInfo)); assert(Observe(Capture(Discard())).getConstructorInfo().equals(Observe.constructorInfo)); assert(is(Observe(Capture(Discard())).getConstructorInfo(), Observe.constructorInfo)); }); }); class SimpleStream { constructor(t, n, items) { this.t = t; this.n = n; this.items = items; } [PreserveOn](e) { e.encodestream(this.t, this.n, this.items); } } class StringStream extends SimpleStream { constructor(items) { super(1, 1, items); }} class BytesStream extends SimpleStream { constructor(items) { super(1, 2, items); }} class SymbolStream extends SimpleStream { constructor(items) { super(1, 3, items); }} class RecordStream extends SimpleStream { constructor(items) { super(2, 0, items); }} // Not needed -- an ordinary array will do! // class SequenceStream extends SimpleStream { constructor(items) { super(2, 1, items); }} class SetStream extends SimpleStream { constructor(items) { super(2, 2, items); }} class DictionaryStream extends SimpleStream { constructor(items) { super(2, 3, items); }} describe('common test suite', () => { const samples_bin = fs.readFileSync(__dirname + '/../../../tests/samples.bin'); const samples = decodeWithAnnotations(samples_bin); const TestCases = Record.makeConstructor('TestCases', ['mapping', 'cases']); const ExpectedPlaceholderMapping = Record.makeConstructor('ExpectedPlaceholderMapping', ['table']); const expectedPlaceholderMapping = TestCases._mapping(samples.peel()).strip(); const placeholders_decode = ExpectedPlaceholderMapping._table(expectedPlaceholderMapping); const placeholders_encode = placeholders_decode.mapEntries((e) => [e[1],e[0]]); function DS(bs) { return decode(bs, {placeholders: placeholders_decode}); } function D(bs) { return decodeWithAnnotations(bs, {placeholders: placeholders_decode}); } function E(v) { return encode(v, {placeholders: placeholders_encode}); } const expectedValues = { annotation1: { forward: annotate(9, "abc"), back: 9 }, annotation2: { forward: annotate(List([List(), annotate(List(), "x")]), "abc", "def"), back: List([List(), List()]) }, 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(List(), Symbol.for('a'), Symbol.for('b'), Symbol.for('c')), back: List() }, bytes1: { forward: new BytesStream([Bytes('he'), Bytes('ll'), Bytes('o')]), back: Bytes('hello') }, list1: { forward: [1, 2, 3, 4], back: List([1, 2, 3, 4]) }, list2: { forward: [ new StringStream([Bytes('abc')]), new StringStream([Bytes('def')]), ], back: List(["abc", "def"]) }, list3: { forward: [List(["a", 1]), List(["b", 2]), List(["c", 3])], back: List([List(["a", 1]), List(["b", 2]), List(["c", 3])]) }, record2: { value: Observe(new Record(Symbol.for("speak"), [ Discard(), Capture(Discard()) ])) }, string0a: { forward: new StringStream([]), back: '' }, string1: { forward: new StringStream([Bytes('he'), Bytes('ll'), Bytes('o')]), back: 'hello' }, string2: { forward: new StringStream([Bytes('he'), Bytes('llo')]), back: 'hello' }, symbol1: { forward: new SymbolStream([Bytes('he'), Bytes('ll'), Bytes('o')]), back: Symbol.for('hello') }, }; function runTestCase(variety, tName, binaryForm, annotatedTextForm) { describe(tName, () => { const textForm = annotatedTextForm.strip(); 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', () => assert(is(textForm, back))); it('should round-trip', () => assert(is(DS(E(textForm)), back))); it('should go forward', () => assert(is(DS(E(forward)), back))); it('should go back', () => assert(is(DS(binaryForm), back))); it('should go back with annotations', () => assert(is(D(E(annotatedTextForm)), annotatedTextForm))); if (variety !== 'decode' && variety !== 'nondeterministic') { it('should encode correctly', () => assert(is(E(forward), binaryForm), E(forward) + ' ' + binaryForm)); } if (variety !== 'decode' && variety !== 'nondeterministic' && variety !== 'streaming') { it('should encode correctly with annotations', () => assert(is(E(annotatedTextForm), binaryForm), E(annotatedTextForm) + ' ' + binaryForm)); } }); } const tests = TestCases._cases(samples.peel()).peel(); tests.forEach((t0, tName0) => { const tName = Symbol.keyFor(tName0.strip()); const t = t0.peel(); switch (t.label) { case Symbol.for('Test'): runTestCase('normal', tName, t.get(0).strip(), t.get(1)); break; case Symbol.for('StreamingTest'): runTestCase('streaming', tName, t.get(0).strip(), t.get(1)); break; case Symbol.for('NondeterministicTest'): runTestCase('nondeterministic', tName, t.get(0).strip(), t.get(1)); break; case Symbol.for('DecodeTest'): runTestCase('decode', tName, t.get(0).strip(), t.get(1)); break; case Symbol.for('DecodeError'): describe(tName, () => { it('should fail with DecodeError', () => { try { D(t.get(0).strip()); assert.fail("but it didn't"); } catch (e) { assert(e instanceof DecodeError); assert(!(e instanceof ShortPacket)); } }); }); break; case Symbol.for('DecodeShort'): describe(tName, () => { it('should fail with ShortPacket', () => { try { D(t.get(0).strip()); assert.fail("but it didn't"); } catch (e) { assert(e instanceof ShortPacket); } }); }); break; case Symbol.for('ParseError'): case Symbol.for('ParseShort'): /* Skipped for now, until we have an implementation of text syntax */ break; default:{ const e = new Error('Unsupported test kind'); e.irritant = t; e.testKind = t.label; console.error(e); throw e; } } }); }); describe('parsing from subarray', () => { it('should maintain alignment of nextbytes', () => { const u = Uint8Array.of(57, 57, 57, 57, 83, 51, 51, 51); const bs = Bytes.from(u.subarray(4)); assert.strictEqual(new Decoder(bs).next(), "333"); }); });