306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
import {
|
|
Value,
|
|
Dictionary,
|
|
decode, decodeWithAnnotations, encode, encodeWithAnnotations, canonicalEncode,
|
|
DecodeError, ShortPacket,
|
|
Bytes, Record,
|
|
annotate,
|
|
strip, peel,
|
|
preserves,
|
|
fromJS,
|
|
Constants,
|
|
} from '../src/index';
|
|
const { Tag } = Constants;
|
|
import './test-utils';
|
|
|
|
import * as fs from 'fs';
|
|
|
|
class Pointer {
|
|
v: Value<Pointer>;
|
|
|
|
constructor(v: Value<Pointer>) {
|
|
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>): Pointer {
|
|
return new Pointer(strip(v));
|
|
}
|
|
|
|
function encodePointer(w: Pointer): Value<Pointer> {
|
|
return w.v;
|
|
}
|
|
|
|
const _discard = Symbol.for('discard');
|
|
const _capture = Symbol.for('capture');
|
|
const _observe = Symbol.for('observe');
|
|
const Discard = Record.makeConstructor<{}, Pointer>()(_discard, []);
|
|
const Capture = Record.makeConstructor<{pattern: Value<Pointer>}, Pointer>()(_capture, ['pattern']);
|
|
const Observe = Record.makeConstructor<{pattern: Value<Pointer>}, Pointer>()(_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 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(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 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<object> {
|
|
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>): 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<{cases: Dictionary<Value<Pointer>, Pointer>}>()(Symbol.for('TestCases'), ['cases']);
|
|
type TestCases = ReturnType<typeof TestCases>;
|
|
|
|
function DS(bs: Bytes) {
|
|
return decode(bs, { decodePointer });
|
|
}
|
|
function D(bs: Bytes) {
|
|
return decodeWithAnnotations(bs, { decodePointer });
|
|
}
|
|
function E(v: Value<Pointer>) {
|
|
return encodeWithAnnotations(v, { encodePointer });
|
|
}
|
|
|
|
interface ExpectedValues {
|
|
[testName: string]: { value: Value<Pointer> } | { forward: Value<Pointer>, back: Value<Pointer> };
|
|
}
|
|
|
|
const expectedValues: ExpectedValues = {
|
|
annotation1: { forward: annotate<Pointer>(9, "abc"),
|
|
back: 9 },
|
|
annotation2: { forward: annotate<Pointer>([[], annotate<Pointer>([], "x")], "abc", "def"),
|
|
back: [[], []] },
|
|
annotation3: { forward: annotate<Pointer>(5,
|
|
annotate<Pointer>(2, 1),
|
|
annotate<Pointer>(4, 3)),
|
|
back: 5 },
|
|
annotation5: {
|
|
forward: annotate<Pointer>(
|
|
Record<symbol, any, Pointer>(Symbol.for('R'),
|
|
[annotate<Pointer>(Symbol.for('f'),
|
|
Symbol.for('af'))]),
|
|
Symbol.for('ar')),
|
|
back: Record<Value<Pointer>, any, Pointer>(Symbol.for('R'), [Symbol.for('f')])
|
|
},
|
|
annotation6: {
|
|
forward: Record<Value<Pointer>, any, Pointer>(
|
|
annotate<Pointer>(Symbol.for('R'),
|
|
Symbol.for('ar')),
|
|
[annotate<Pointer>(Symbol.for('f'),
|
|
Symbol.for('af'))]),
|
|
back: Record<symbol, any, Pointer>(Symbol.for('R'), [Symbol.for('f')])
|
|
},
|
|
annotation7: {
|
|
forward: annotate<Pointer>([], 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<Pointer>) {
|
|
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<Value<Pointer>, Pointer>;
|
|
tests.forEach((t0: Value<Pointer>, tName0: Value<Pointer>) => {
|
|
const tName = Symbol.keyFor(strip(tName0) as symbol)!;
|
|
const t = peel(t0) as Record<symbol, any, Pointer>;
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
});
|