Tests and many associated repairs to the JavaScript implementation

This commit is contained in:
Tony Garnock-Jones 2018-11-13 12:15:56 +00:00
parent 22e2934845
commit 41ab0cf4ac
3 changed files with 225 additions and 39 deletions

View File

@ -48,7 +48,7 @@ class Decoder {
// TODO: Bignums :-/
const v = this.nextbyte();
if (v < 128) return v;
return this.varint() * 128 + (v - 128);
return (this.varint() << 7) + (v - 128);
}
nextbytes(n) {
@ -86,7 +86,7 @@ class Decoder {
binarystream(arg, minor) {
const result = [];
while (!this.peekend(arg)) {
chunk = this.next();
const chunk = this.next();
if (!Buffer.isBuffer(chunk)) throw new DecodeError("Unexpected non-binary chunk");
result.push(chunk);
}
@ -104,7 +104,7 @@ class Decoder {
if (bs.length === 0) return 0;
let acc = bs[0];
if (acc & 0x80) acc -= 256;
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | b[i];
for (let i = 1; i < bs.length; i++) acc = (acc << 8) | bs[i];
return acc;
}
@ -122,7 +122,7 @@ class Decoder {
if (vs.length === 0) throw new DecodeError("Too few elements in encoded record");
return new Record(vs[0], vs.slice(1));
} else {
const label = shortForms[minor];
const label = this.shortForms[minor];
if (label === void 0) throw new DecodeError("Use of unconfigured short form " + minor);
return new Record(label, vs);
}
@ -132,11 +132,19 @@ class Decoder {
switch (minor) {
case 0: return List(vs);
case 1: return Set(vs);
case 2: return mapFromArray(vs);
case 2: return this.mapFromArray(vs);
case 3: throw new DecodeError("Invalid collection type");
}
}
mapFromArray(vs) {
return Map().withMutations((m) => {
for (let i = 0; i < vs.length; i += 2) {
m.set(vs[i], vs[i+1]);
}
});
}
next() {
const [major, minor, arg] = this.nextop();
switch (major) {
@ -146,8 +154,8 @@ class Decoder {
switch (arg) {
case 0: return false;
case 1: return true;
case 2: return Float(this.nextbytes(4).readFloatBE());
case 3: return this.nextbytes(8).readDoubleBE();
case 2: return new Single(this.nextbytes(4).readFloatBE());
case 3: return new Double(this.nextbytes(8).readDoubleBE());
}
case 1:
return (arg > 12) ? arg - 16 : arg;
@ -209,12 +217,11 @@ class Encoder {
}
varint(v) {
if (v < 128) {
this.emitbyte(v);
} else {
while (v >= 128) {
this.emitbyte((v % 128) + 128);
this.varint(Math.floor(v / 128));
v = Math.floor(v / 128);
}
this.emitbyte(v);
}
leadbyte(major, minor, arg) {
@ -232,7 +239,7 @@ class Encoder {
encodeint(v) {
// TODO: Bignums :-/
const plain_bitcount = Math.floor(Math.log2(Math.abs(v))) + 1;
const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : ~v)) + 1;
const signed_bitcount = plain_bitcount + 1;
const bytecount = (signed_bitcount + 7) >> 3;
this.header(1, 0, bytecount);
@ -261,45 +268,56 @@ class Encoder {
if (typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function') {
v[PreserveOn](this);
}
if (v === false) return this.leadbyte(0, 0, 0);
if (v === true) return this.leadbyte(0, 0, 1);
if (typeof v === 'number') {
if (v >= -3 && v <= 12) return this.leadbyte(0, 1, v >= 0 ? v : v + 16);
return this.encodeint(v);
else if (typeof v === 'boolean') {
this.leadbyte(0, 0, v ? 1 : 0);
}
if (typeof v === 'string') {
else if (typeof v === 'number') {
if (v >= -3 && v <= 12) {
this.leadbyte(0, 1, v >= 0 ? v : v + 16);
} else {
this.encodeint(v);
}
}
else if (typeof v === 'string') {
const bs = Buffer.from(v);
this.header(1, 1, bs.length);
return this.emitbytes(bs);
this.emitbytes(bs);
}
if (typeof v === 'symbol') {
else if (typeof v === 'symbol') {
const key = Symbol.keyFor(v);
if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v);
const bs = Buffer.from(key);
this.header(1, 3, bs.length);
return this.emitbytes(bs);
this.emitbytes(bs);
}
if (Buffer.isBuffer(v)) {
else if (Buffer.isBuffer(v)) {
this.header(1, 2, v.length);
return this.emitbytes(v);
this.emitbytes(v);
}
if (List.isList(v)) return this.encodecollection(0, v);
if (Set.isSet(v)) return this.encodecollection(1, v);
if (Map.isMap(v)) {
const kvs = [];
v.forEach((val, key) => { kvs.push(key); kvs.push(val); });
return this.encodecollection(2, kvs);
else if (List.isList(v)) {
this.encodecollection(0, v);
}
if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
return this.encodestream(3, 0, v);
else if (Set.isSet(v)) {
this.encodecollection(1, v);
}
throw new EncodeError("Cannot encode", v);
else if (Map.isMap(v)) {
this.encodecollection(2, List().withMutations((l) => {
v.forEach((val, key) => { l.push(key).push(val); });
}));
}
else if (typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function') {
this.encodestream(3, 0, v);
}
else {
throw new EncodeError("Cannot encode", v);
}
return this; // for chaining
}
}
class Record {
constructor(label, fields) {
this.label = label;
this.label = fromJS(label);
this.fields = fromJS(fields);
}
@ -332,11 +350,15 @@ class Record {
encoder.header(2, 3, this.fields.size + 1);
encoder.push(this.label);
}
for (const field in this.fields) { encoder.push(field); }
for (const field of this.fields) { encoder.push(field); }
}
}
Record.makeConstructor = function (label, fieldNames) {
Record.makeConstructor = function (labelSymbolText, fieldNames) {
return Record.makeBasicConstructor(Symbol.for(labelSymbolText), fieldNames);
};
Record.makeBasicConstructor = function (label, fieldNames) {
const arity = fieldNames.length;
const ctor = (...fields) => {
if (fields.length !== arity) {
@ -355,14 +377,15 @@ class Float {
this.value = value;
}
equals(other) {
return (other instanceof Float) && (other.value === this.value);
}
hashCode() {
return this.value | 0; // TODO: something better?
}
}
class Single extends Float {
equals(other) {
return (other instanceof Single) && (other.value === this.value);
}
[PreserveOn](encoder) {
encoder.leadbyte(0, 0, 2);
encoder.makeroom(4);
@ -371,6 +394,18 @@ class Float {
}
}
class Double extends Float {
equals(other) {
return (other instanceof Double) && (other.value === this.value);
}
[PreserveOn](encoder) {
encoder.leadbyte(0, 0, 3);
encoder.makeroom(8);
encoder.buffer.writeDoubleBE(this.value, encoder.index);
encoder.index += 8;
}
}
Object.assign(module.exports, {
PreserveOn,
DecodeError,
@ -379,4 +414,6 @@ Object.assign(module.exports, {
Encoder,
Record,
Float,
Single,
Double,
});

View File

@ -0,0 +1,43 @@
023f800000
033ff0000000000000
03fe3cb7b759bf0426
10
11
1c
1d
1e
1f
25626865626c6c6060616f35
25626865636c6c6f35
26626865626c6c6060616f36
27626865626c6c6060616f37
2c111213143c
2cc2516111c2516212c25163133c
410d
417f
4180
4181
41fc
420080
4200ff
420100
427fff
42feff
42ff00
42ff01
42ff02
42ff7f
43008000
4300ffff
43010000
43020000
5568656c6c6f
6568656c6c6f
7568656c6c6f
9180
a1b375737065616b809180
b5c5767469746c656476706572736f6e12757468696e6711416559426c61636b77656c6cb4746461746542071d1213524472
c411121314
c41e1f1011
c75568656c6c6f75746865726565776f726c64c0d00100
e8716111516201c31112136163e27a66697273742d6e616d6559456c697a6162657468e2777375726e616d6559426c61636b77656c6c

View File

@ -0,0 +1,106 @@
"use strict";
const chai = require('chai');
const expect = chai.expect;
chai.use(require('chai-immutable'));
const Immutable = require('immutable');
const { List, Set, Map, fromJS } = Immutable;
const Preserves = require('../src/index.js');
const { Decoder, Encoder, Record, Single, Double } = Preserves;
const fs = require('fs');
const util = require('util');
const shortForms = {
0: Symbol.for('discard'),
1: Symbol.for('capture'),
2: Symbol.for('observe'),
};
const Discard = Record.makeConstructor('discard', []);
const Capture = Record.makeConstructor('capture', ['pattern']);
const Observe = Record.makeConstructor('observe', ['pattern']);
describe('hex samples', () => {
const samples = fs.readFileSync(__dirname + '/samples.txt').toString().split(/\n/)
.filter((h) => h)
.map((h) => Buffer.from(h, 'hex'));
// As new samples are added to samples.txt, we will need to update this list:
const samplesExpected = [
{ expected: new Single(1), },
{ expected: new Double(1), },
{ expected: new Double(-1.202e+300), },
{ expected: 0, },
{ expected: 1, },
{ expected: 12, },
{ expected: -3, },
{ expected: -2, },
{ expected: -1, },
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
{ expected: Buffer.from("hello"), encodesTo: '6568656c6c6f', },
{ expected: List([1, 2, 3, 4]), encodesTo: 'c411121314', },
{ expected: fromJS([["a", 1], ["b", 2], ["c", 3]]), encodesTo: 'c3c2516111c2516212c2516313', },
{ expected: 13, },
{ expected: 127, },
{ expected: -128, },
{ expected: -127, },
{ expected: -4, },
{ expected: 128, },
{ expected: 255, },
{ expected: 256, },
{ expected: 32767, },
{ expected: -257, },
{ expected: -256, },
{ expected: -255, },
{ expected: -254, },
{ expected: -129, },
{ expected: 32768, },
{ expected: 65535, },
{ expected: 65536, },
{ expected: 131072, },
{ expected: "hello", },
{ expected: Buffer.from("hello"), },
{ expected: Symbol.for("hello"), },
{ expected: Capture(Discard()), },
{ expected: Observe(new Record(Symbol.for('speak'), [Discard(), Capture(Discard())])), },
{ expected:
new Record([Symbol.for('titled'), Symbol.for('person'), 2, Symbol.for('thing'), 1],
[101, "Blackwell", new Record(Symbol.for('date'), [1821, 2, 3]), "Dr"]), },
{ expected: List([1, 2, 3, 4]), },
{ expected: List([-2, -1, 0, 1]), },
{ expected:
fromJS(["hello", Symbol.for('there'), Buffer.from('world'), [], Set(), true, false]), },
{ expected:
Map()
.set(Symbol.for('a'), 1)
.set('b', true)
.set(fromJS([1, 2, 3]), Buffer.from('c'))
.set(Map().set(Symbol.for('first-name'), 'Elizabeth'),
Map().set(Symbol.for('surname'), 'Blackwell')), },
];
samples.forEach((s, sampleIndex) => {
it('[' + sampleIndex + '] ' + s.toString('hex') + ' should decode OK', () => {
const actual = new Decoder(s, { shortForms }).next();
const expected = samplesExpected[sampleIndex].expected;
expect(Immutable.is(actual, expected),
'[' + sampleIndex + '] actual ' + util.inspect(actual) +
', expected ' + util.inspect(expected))
.to.be.true;
});
});
samples.forEach((s, sampleIndex) => {
it('[' + sampleIndex + '] ' + s.toString('hex') + ' should encode OK', () => {
const entry = samplesExpected[sampleIndex];
const actual = entry.encodesTo || s;
const expected = new Encoder({ shortForms }).push(entry.expected).contents();
expect(actual.toString('hex')).to.equal(expected.toString('hex'));
});
});
});