import { describe, it, expect } from 'vitest'; import { Value, Dictionary, decode, decodeWithAnnotations, encode, canonicalEncode, DecodeError, ShortPacket, Bytes, Record, strip, peel, preserves, stringify, fromJS, Constants, Encoder, GenericEmbedded, EncoderState, EmbeddedType, DecoderState, Decoder, genericEmbeddedTypeDecode, genericEmbeddedTypeEncode, parse, Embedded, KeyedDictionary, IdentityEmbeddedTypeEncode, } 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)).isPreserves(C2(9,9)); expect(C1(9,9)).not.isPreserves(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)).isPreserves("333"); }); }); describe('reusing buffer space', () => { it('should be done safely, even with nested dictionaries', () => { expect(canonicalEncode(fromJS(['aaa', KeyedDictionary.fromJS({a: 1}), 'zzz'])).toHex()).isPreserves( `b5 b103616161 b7 b10161 b00101 84 b1037a7a7a 84`.replace(/\s+/g, '')); }); }); describe('encoding and decoding embeddeds', () => { class LookasideEmbeddedType implements EmbeddedType> { readonly objects: Embedded[]; constructor(objects: Embedded[]) { this.objects = objects; } decode(d: DecoderState): Embedded { return this.fromValue(new Decoder(d).next()); } encode(e: EncoderState, v: Embedded): void { new Encoder(e).push(this.toValue(v)); } fromValue(v: Value): Embedded { if (typeof v !== 'number' || v < 0 || v >= this.objects.length) { throw new Error(`Unknown embedded target: ${stringify(v)}`); } return this.objects[v]; } toValue(v: Embedded): 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 IdentityEmbeddedTypeEncode when explicitly requested', () => { const A1 = new Embedded({a: 1}); const A2 = new Embedded({a: 1}); const identityEncoder = new IdentityEmbeddedTypeEncode(); const bs1 = canonicalEncode(A1, { embeddedEncode: identityEncoder }); const bs2 = canonicalEncode(A2, { embeddedEncode: identityEncoder }); const bs3 = canonicalEncode(A1, { embeddedEncode: identityEncoder }); 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.isPreserves(bs2); expect(bs1).isPreserves(bs3); }); it('should refuse to encode embeddeds when no function has been supplied', () => { expect(() => canonicalEncode(new Embedded({a : 1}))) .toThrow("Embeddeds not permitted encoding Preserves document"); }); 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: Embedded[] = []; const pt = new LookasideEmbeddedType(objects); const A = new Embedded({a: 1}); const B = new Embedded({b: 2}); expect(encode([A, B], { embeddedEncode: pt })).isPreserves( Bytes.from([Tag.Sequence, Tag.Embedded, Tag.SignedInteger, 0, Tag.Embedded, Tag.SignedInteger, 1, 1, Tag.End])); expect(objects).toEqual([A, B]); }); it('should decode properly', () => { const objects: Embedded[] = []; const pt = new LookasideEmbeddedType(objects); const X = new Embedded({x: 123}); const Y = new Embedded({y: 456}); objects.push(X); objects.push(Y); expect(decode(Bytes.from([ Tag.Sequence, Tag.Embedded, Tag.SignedInteger, 0, Tag.Embedded, Tag.SignedInteger, 1, 1, Tag.End ]), { embeddedDecode: pt })).isPreserves([X, Y]); }); it('should store embeddeds embedded in map keys correctly', () => { const A1a = new Embedded({a: 1}); const A1 = A1a; const A2 = new Embedded({a: 1}); const m = new KeyedDictionary, Value>, number>(); 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(); A1a.value.a = 3; expect(m.get([A1])).toBe(1); }); }); describe('integer text parsing', () => { it('should work for zero', () => { expect(parse('0')).isPreserves(0); }); it('should work for smallish positive integers', () => { expect(parse('60000')).isPreserves(60000); }); it('should work for smallish negative integers', () => { expect(parse('-60000')).isPreserves(-60000); }); it('should work for largeish positive integers', () => { expect(parse('1234567812345678123456781234567')) .isPreserves(BigInt("1234567812345678123456781234567")); }); it('should work for largeish negative integers', () => { expect(parse('-1234567812345678123456781234567')) .isPreserves(BigInt("-1234567812345678123456781234567")); }); it('should work for larger positive integers', () => { expect(parse('12345678123456781234567812345678')) .isPreserves(BigInt("12345678123456781234567812345678")); }); it('should work for larger negative integers', () => { expect(parse('-12345678123456781234567812345678')) .isPreserves(BigInt("-12345678123456781234567812345678")); }); }); describe('integer binary encoding', () => { it('should work for zero integers', () => { expect(encode(0)).isPreserves(Bytes.fromHex('b000')); }); it('should work for zero bigints', () => { expect(encode(BigInt(0))).isPreserves(Bytes.fromHex('b000')); }); it('should work for smallish positive integers', () => { expect(encode(60000)).isPreserves(Bytes.fromHex('b00300ea60')); }); it('should work for smallish negative integers', () => { expect(encode(-60000)).isPreserves(Bytes.fromHex('b003ff15a0')); }); it('should work for largeish positive integers', () => { expect(encode(BigInt("1234567812345678123456781234567"))) .isPreserves(Bytes.fromHex('b00d0f951a8f2b4b049d518b923187')); }); it('should work for largeish negative integers', () => { expect(encode(BigInt("-1234567812345678123456781234567"))) .isPreserves(Bytes.fromHex('b00df06ae570d4b4fb62ae746dce79')); }); it('should work for larger positive integers', () => { expect(encode(BigInt("12345678123456781234567812345678"))) .isPreserves(Bytes.fromHex('b00e009bd30997b0ee2e252f73b5ef4e')); }); it('should work for larger negative integers', () => { expect(encode(BigInt("-12345678123456781234567812345678"))) .isPreserves(Bytes.fromHex('b00eff642cf6684f11d1dad08c4a10b2')); }); }); 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))).isPreserves(stripped)); it('should go back, stripped', () => expect(strip(decodeBinary(binary))).isPreserves(stripped)); it('should go back', () => expect(decodeBinary(binary)).isPreserves(annotatedValue)); it('should round-trip, with annotations', () => expect(decodeBinary(annotatedBinary(annotatedValue))).isPreserves(annotatedValue)); it('should round-trip as text, stripped', () => expect(decodeText(encodeText(stripped))).isPreserves(stripped)); it('should round-trip as text, with annotations', () => expect(decodeText(encodeText(annotatedValue))).isPreserves(annotatedValue)); it('should go forward', () => expect(annotatedBinary(annotatedValue)).isPreserves(binary)); if (variety === 'normal') { it('should go forward, loosely', () => expect(looseEncodeBinary(annotatedValue)).isPreserves(binary)); } }); } const tests = (peel(TestCases._.cases(peel(samples) as TestCases)) as Dictionary); Dictionary.asMap(tests).forEach((t0, tName0) => { 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; } } }); });