blob: 2cb8c860145146c542486177e6f45d7ea1b7a1ac [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/**
* @fileoverview Tests of VOM encoding and decoding.
*/
var test = require('tape');
var kind = require('./../../src/vdl/kind.js');
var registry = require('./../../src/vdl/registry.js');
var Type = require('./../../src/vdl/type.js');
var types = require('./../../src/vdl/types.js');
var typeUtil = require('./../../src/vdl/type-util.js');
var stringify = require('./../../src/vdl/stringify.js');
var canonicalize = require('./../../src/vdl/canonicalize.js');
var Deferred = require('../../src/lib/deferred');
var ByteMessageWriter = require(
'./../../src/vom/byte-message-writer.js');
var ByteArrayMessageReader = require(
'./../../src/vom/byte-array-message-reader.js');
var Encoder = require('./../../src/vom/encoder.js');
var Decoder = require('./../../src/vom/decoder.js');
var linkedListNodeType = {
kind: kind.STRUCT,
name: 'LinkedListNode',
fields: [
{
name: 'Value',
type: types.ANY
},
{
name: 'Next'
}
]
};
linkedListNodeType.fields[1].type = {
kind: kind.OPTIONAL,
elem: linkedListNodeType
};
var treeNodeType = new Type({
kind: kind.STRUCT,
name: 'TreeNodeType',
fields: [
{
name: 'Value',
type: types.ANY
},
{
name: 'Left'
},
{
name: 'Right'
}
]
});
var nextTreeNodeType = new Type({
kind: kind.OPTIONAL,
elem: treeNodeType
});
treeNodeType.fields[1].type = nextTreeNodeType;
treeNodeType.fields[2].type = nextTreeNodeType;
// Define a type with _type on the prototype to test type look up in one of.
function NamedUintConstructor(val) {
this.val = val;
}
NamedUintConstructor.prototype._type = {
kind: kind.UINT32,
name: 'namedUint32'
};
NamedUintConstructor.prototype._wrappedType = true;
[
{
n: 'Decode(Encode(Byte))',
v: 1,
expectedOutput: {
val: 1
},
t: types.BYTE
},
{
n: 'Decode(Encode(Uint16))',
v: 1000,
expectedOutput: {
val: 1000
},
t: types.UINT16
},
{
n: 'Decode(Encode(Float32))',
v: 0.3,
expectedOutput: {
val: 0.3
},
t: types.FLOAT32
},
{
n: 'Decode(Encode(Int32))',
v: -1,
expectedOutput: {
val: -1
},
t: types.INT32
},
{
n: 'Decode(Encode(Complex64))',
v: {
real: 1.9,
imag: -0.4
},
expectedOutput: {
val: {
real: 1.9,
imag: -0.4
}
},
t: types.COMPLEX64
},
{
n: 'Decode(Encode(String))',
v: 'a string',
expectedOutput: {
val: 'a string'
},
t: types.STRING
},
{
n: 'Decode(Encode(Bool))',
v: true,
expectedOutput: {
val: true
},
t: types.BOOL
},
{
n: 'Decode(Encode(typeObject))',
v: {
kind: kind.LIST,
name: 'A list',
elem: types.STRING
},
expectedOutput: {
kind: kind.LIST,
name: 'A list',
elem: types.STRING
},
t: types.TYPEOBJECT
},
{
n: 'Decode(Encode(Struct{X: typeObject(nil)}))',
v: {
x: undefined
},
expectedOutput: {
x: types.ANY
},
t: {
kind: kind.STRUCT,
fields: [
{
name: 'X',
type: types.TYPEOBJECT
}
]
}
},
{
n: 'Decode(Encode(Struct w/ Private field))',
v: {
x: 'val',
_privateField: 99
},
expectedOutput: {
x: 'val'
},
t: {
kind: kind.STRUCT,
fields: [
{
name: 'X',
type: types.STRING
}
]
}
},
{
n: 'Decode(Encode(Object w/ Private field [no type]))',
v: {
x: 'val',
_privateField: 99
},
expectedOutput: {
x: 'val'
}
},
{
n: 'Decode(Encode(List<Uint32>))',
v: [2, 3, 4],
expectedOutput: {
val: [2, 3, 4]
},
t: {
kind: kind.LIST,
elem: types.UINT32
}
},
{
n: 'Decode(Encode(Array<Int32>))',
v: [2, 3, 4],
expectedOutput: {
val: [2, 3, 4]
},
t: {
kind: kind.ARRAY,
elem: types.INT32,
len: 3
}
},
{
n: 'Decode(Encode(List<Byte>))',
v: new Uint8Array([0x80, 0x90]),
expectedOutput: {
val: new Uint8Array([0x80, 0x90])
},
t: {
kind: kind.LIST,
elem: types.BYTE
}
},
{
n: 'Decode(Encode(Array<Byte>))',
v: new Uint8Array([0x80, 0x90]),
expectedOutput: {
val: new Uint8Array([0x80, 0x90])
},
t: {
kind: kind.ARRAY,
elem: types.BYTE,
len: 2
}
},
{
n: 'Decode(Encode(Set<String> as Object))',
v: {
'b': true,
'a': true
},
expectedOutput: {
val: new Set(['B', 'A'])
},
t: {
kind: kind.SET,
key: types.STRING
},
},
{
n: 'Decode(Encode(Set<Uint32> as Set))',
v: new Set([3, 5]),
expectedOutput: {
val: new Set([3, 5])
},
t: {
kind: kind.SET,
key: types.UINT32
}
},
{
n: 'Decode(Encode(Map[String]String as Object))',
v: {
'key1': 'value1',
'key2': 'value2'
},
expectedOutput: {
val: new Map([
['Key1', 'value1'],
['Key2', 'value2']
]),
},
t: {
kind: kind.MAP,
key: types.STRING,
elem: types.STRING
},
},
{
n: 'Decode(Encode(Map[Uint16]Float32 as Map))',
v: new Map([
[3, 1.3],
[2, -5.6]
]),
expectedOutput: {
val: new Map([
[3, 1.3],
[2, -5.6]
])
},
t: {
kind: kind.MAP,
key: types.UINT16,
elem: types.FLOAT32
}
},
{
n: 'Decode(Encode(Struct as Object))',
v: {
field1: 8,
field2: 'str',
field3: [4, 5]
},
t: {
kind: kind.STRUCT,
name: 'testStruct',
fields: [
{
name: 'Field1',
type: types.UINT16
},
{
name: 'Field2',
type: types.STRING
},
{
name: 'Field3',
type: {
kind: kind.LIST,
elem: types.FLOAT64
}
}
]
}
},
{
n: 'Decode(Encode(Enum))',
v: 'alabel',
expectedOutput: {
val: 'alabel'
},
t: {
kind: kind.ENUM,
name: 'enumtype',
labels: ['alabel', 'blabel']
}
},
{
n: 'Decode(Encode(Optional String w/ value))',
v: 'optionalString',
expectedOutput: {
val: 'optionalString'
},
t: {
kind: kind.OPTIONAL,
elem: types.STRING
}
},
{
n: 'Decode(Encode(Optional String w/ null))',
v: null,
expectedOutput: {
val: null
},
t: {
kind: kind.OPTIONAL,
elem: types.STRING
}
},
{
n: 'Decode(Encode(Array<Byte>) w/o type)',
v: new Uint8Array([0x80, 0x90]),
expectedOutput: new Uint8Array([0x80, 0x90])
},
{
n: 'Decode(Encode(Bool w/o type))',
v: true,
expectedOutput: true
},
{
n: 'Decode(Encode(Map w/o type))',
v: new Map([
['x', [4, 5, 3]],
['y', 'test']
]),
expectedOutput: new Map([
['x', [4, 5, 3]],
['y', 'test']
])
},
{
n: 'Decode(Encode(Set w/o type))',
v: new Set([3, 4, 0]),
expectedOutput: new Set([3, 4, 0])
},
{
n: 'Decode(Encode(Map w/o type))',
v: new Map([
['string', false],
[2, ['mixed', 3, 'list']]
]),
expectedOutput: new Map([
['string', false],
[2, ['mixed', 3, 'list']]
])
},
{
n: 'Decode(Encode(Object w/o type))',
v: {
a: 'a',
b: 3,
c: true
},
expectedOutput: {
a: 'a',
b: 3,
c: true
}
},
{
n: 'Decode(Encode(List))',
v: [
{
aa: 2,
bb: 3
},
'something else'
],
expectedOutput: {
val: [
{
aa: 2,
bb: 3
},
'something else'
]
},
t: {
kind: kind.LIST,
elem: types.ANY
}
},
{
n: 'Decode(Encode(Union<String, Uint16> w/ Uint16))',
v: {
'uInt': 5
},
t: {
kind: kind.UNION,
name: 'unionName',
fields: [
{
name: 'StringInt',
type: types.STRING
},
{
name: 'UInt',
type: types.UINT16
}
]
}
},
{
n: 'Decode(Encode(Union<String, Bool> w/ String))',
v: {
'stringBool': 'str'
},
t: {
kind: kind.UNION,
name: 'unionName',
fields: [
{
name: 'StringBool',
type: types.STRING
},
{
name: 'Boolean',
type: types.BOOL
}
]
}
},
{
n: 'Decode(Encode(Union<String, Bool> w/ Bool))',
v: {
'boolean': true
},
t: {
kind: kind.UNION,
name: 'UnionName',
fields: [
{
name: 'StringBool',
type: types.STRING
},
{
name: 'Boolean',
type: types.BOOL
}
]
}
},
{
n: 'Decode(Encode(Union<Map[Uint16]Uint32, List<Float64>> w/ List))',
v: {
'list': [4,3,5]
},
t: {
kind: kind.UNION,
name: 'UnionName',
fields: [
{
name: 'Map',
type: {
kind: kind.MAP,
key: types.STRING,
elem: types.UINT32
}
},
{
name: 'List',
type: {
kind: kind.LIST,
elem: types.FLOAT64
}
}
]
}
},
{
n: 'Decode(Encode(Union<Map[Uint16]Uint32, List<Float64>> w/ Map))',
v: {
'map': { // This is a native object; the fields
'a': 9, // are capitalized upon conversion to Map.
'b': 10,
_type: {
kind: kind.MAP,
key: types.STRING,
elem: types.UINT32
}
}
},
t: {
kind: kind.UNION,
name: 'UnionName',
fields: [
{
name: 'Map',
type: {
kind: kind.MAP,
key: types.STRING,
elem: types.UINT32
}
},
{
name: 'List',
type: {
kind: kind.LIST,
elem: types.FLOAT64
}
}
]
},
expectedOutput: {
'map': new Map([
['A', 9],
['B', 10]
])
}
},
{
n: 'Decode(Encode(Linked List Nodes))',
v: {
value: 9,
next: {
value: 10,
next: {
value: 11,
next: null
}
}
},
expectedOutput: {
value: 9,
next: {
value: 10,
next: {
value: 11,
next: null
}
}
},
t: linkedListNodeType
},
{
n: 'Decode(Encode(Tree Nodes))',
v: {
value: 4,
left: {
value: 5,
left: null,
right: null
},
right: {
value: false,
left: {
value: true,
left: null,
right: null
},
right: null
}
},
expectedOutput: {
value: 4,
left: {
value: 5,
left: null,
right: null
},
right: {
value: false,
left: {
value: true,
left: null,
right: null
},
right: null
}
},
t: treeNodeType
},
{
n: 'Decode(Encode(Any))',
v: 5,
expectedOutput: { // any with JSValue(number)
val: 5
},
t: types.ANY
},
{
n: 'Decode(Encode(Any)) - wrapLike',
v: {
val: 5
},
expectedOutput: { // any with JSValue(object with field val: 5)
val: {
val: 5
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Any)) - INT32 wrap',
v: {
val: 5,
_type: types.INT32, // pretend this is on the prototype
_wrappedType: true // pretend this is on the prototype
},
expectedOutput: {
val: {
val: 5
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Any)) - Optional wrap',
v: {
val: 'not null',
_type: { // pretend this is on the prototype
kind: kind.OPTIONAL,
elem: types.STRING
},
_wrappedType: true // pretend this is on the prototype
},
expectedOutput: {
val: {
val: 'not null'
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Any)) - Struct wrap',
v: {
a: 'abc',
_type: { // pretend this is on the prototype
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.STRING
}
]
}
},
expectedOutput: {
val: {
a: 'abc'
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Any)) - Struct w/ Any wrap',
v: {
a: 'abc',
_type: { // pretend this is on the prototype
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.ANY
}
]
}
},
expectedOutput: {
val: {
a: 'abc'
}
},
t: types.ANY
},
{
n: 'Decode(Encode(JSValue w/ null))',
expectedOutput: null,
v: null,
t: types.JSVALUE
},
{
n: 'Decode(Encode(Any w/ null))',
expectedOutput: {
val: null
},
v: null, // guesses to be a null of type ANY
t: types.ANY
},
{
n: 'Decode(Encode(Any w/ null)) - wrapLike))',
expectedOutput: {
val: {
val: null
}
},
v: {
val: null // is not a null of type ANY; it's a struct
},
t: types.ANY
},
{
n: 'Decode(Encode(Any w/ null)) - ANY wrap',
expectedOutput: {
val: null
},
v: {
val: null,
_type: types.ANY, // pretend this is on the prototype
_wrappedType: true // pretend this is on the prototype
},
t: types.ANY
},
{
n: 'Decode(Encode(Any w/ null)) - OPTIONAL wrap',
expectedOutput: {
val: {
val: null
}
},
v: {
val: null,
_type: { // pretend this is on the prototype
kind: kind.OPTIONAL,
elem: types.STRING
},
_wrappedType: true // pretend this is on the prototype
},
t: types.ANY
},
{
n: 'Decode(Encode(Any w/ null)) - in a struct',
v: {
a: null,
_type: { // pretend this is on the prototype
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.ANY
}
]
}
},
expectedOutput: {
val: {
a: null
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Any w/ null)) - wrapped null in a struct',
v: {
a: {
val: null,
_type: types.ANY, // pretend this is on the prototype
_wrappedType: true // pretend this is on the prototype
},
_type: { // pretend this is on the prototype
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.ANY
}
]
}
},
expectedOutput: {
val: {
a: null
}
},
t: types.ANY
},
{
n: 'Decode(Encode(Map in Map))',
expectedOutput: {
val: new Map([
[
'testMethod',
new Map([
['numInArgs', 3],
['numOutArgs', 3],
['isStreaming', false]
])
],
[
'testMethod2',
new Map([
['numInArgs', 2],
['numOutArgs', 1],
['isStreaming', true]
])
]
])
},
v: new Map([
[
'testMethod',
new Map([
['numInArgs', 3],
['numOutArgs', 3],
['isStreaming', false]
])
],
[
'testMethod2',
new Map([
['numInArgs', 2],
['numOutArgs', 1],
['isStreaming', true]
])
]
]),
t: {
kind: kind.MAP,
key: types.STRING,
elem: {
kind: kind.MAP,
key: types.STRING,
elem: types.ANY,
},
},
},
{
n: 'Struct zero-values are filled post-decode',
v: {
c: true,
_type: {
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.UINT32
},
{
name: 'B',
type: types.STRING
},
{
name: 'C',
type: types.BOOL
}
]
}
},
expectedOutput: {
a: 0,
b: '',
c: true
}
},
{
n: 'Internal struct zero-values are filled post-decode',
v: [
{
c: true
},
{
a: 3
},
{
b: 'hello'
},
{
a: 42,
b: 'all here',
c: false
}
],
expectedOutput: {
val: [
{
a: 0,
b: '',
c: true
},
{
a: 3,
b: '',
c: false
},
{
a: 0,
b: 'hello',
c: false
},
{
a: 42,
b: 'all here',
c: false
}
]
},
t: {
kind: kind.LIST,
elem: {
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.UINT32
},
{
name: 'B',
type: types.STRING
},
{
name: 'C',
type: types.BOOL
}
]
}
}
},
{
n: 'native string',
v: 'hi',
expectedOutput: 'hi'
},
{
n: 'native number',
v: 4,
expectedOutput: 4
},
{
n: 'native bool',
v: true,
expectedOutput: true
},
{
n: 'native list',
v: [{}, 'f', true],
expectedOutput: [{}, 'f', true]
},
{
n: 'native object',
v: {
a: 'three',
A: 'THREE',
b: 3
},
expectedOutput: {
a: 'three',
A: 'THREE',
b: 3
}
},
{
n: 'native map',
v: new Map([
[null, 3],
['asdf', 'jkle']
]),
expectedOutput: new Map([
[null, 3],
['asdf', 'jkle']
])
},
{
n: 'native set',
v: new Set([null, 3, true, ['asdf', 'jkle']]),
expectedOutput: new Set([null, 3, true, ['asdf', 'jkle']])
},
{
n: 'typed string',
v: new (registry.lookupOrCreateConstructor(types.STRING))(''),
expectedOutput: new (registry.lookupOrCreateConstructor(types.STRING))('')
},
{
n: 'typed number',
v: new (registry.lookupOrCreateConstructor(types.INT16))(4),
expectedOutput: new (registry.lookupOrCreateConstructor(types.INT16))(4)
},
{
n: 'typed boolean',
v: new (registry.lookupOrCreateConstructor(types.BOOL))(true),
expectedOutput: new (registry.lookupOrCreateConstructor(types.BOOL))(true)
},
].forEach(function(testCase) {
test('encode and decode - ' + testCase.n, function(t) {
var promises = [];
function runTestCase(test, useCb) {
var messageWriter = new ByteMessageWriter();
var encoder = new Encoder(messageWriter);
encoder.encode(test.v, test.t); // encode to messageWriter
var messageReader = new ByteArrayMessageReader(messageWriter.getBytes());
var decoder = new Decoder(messageReader);
if (useCb) {
var def = new Deferred();
decoder.decode(function(err, result) {
def.resolve();
if (err) {
return t.fail('(cb) failed with ' + err.stack);
}
handleResult(result);
});
promises.push(def.promise);
} else {
promises.push(decoder.decode().then(handleResult, function(err) {
t.fail(' (promise) failed with ' + err.stack);
}));
}
function handleResult(result) {
var asyncType = useCb ? ' (cb)' : ' (promise)';
var resultStr = stringify(result);
var expected = test.expectedOutput || test.v;
var expectedStr = stringify(expected);
t.equals(resultStr, expectedStr, asyncType +
' - decode value match');
// Then validate that we were given a canonicalized value.
// Note that some results are native post-decode; if so, use
// types.JSVALUE.
var resultType = types.JSVALUE;
if (typeUtil.isTyped(result)) {
resultType = result._type;
}
t.deepEqual(
canonicalize.reduce(result, resultType),
expected,
asyncType + ' - decode value validation'
);
// If given a type, check that the decoded object's type matches it.
// TODO(bprosnitz) Even if test.t isn't defined, we should still know
// what the expected type ought to be.
if (test.t) {
var resultTypeStr = stringify(resultType);
var expectedTypeStr = stringify(canonicalize.type(test.t));
t.equals(resultTypeStr, expectedTypeStr, asyncType +
' - decode type match');
}
}
}
runTestCase(testCase, true);
runTestCase(testCase, false);
Promise.all(promises).then(function() {
t.end();
}, t.end);
});
});
var Str = registry.lookupOrCreateConstructor(types.STRING);
var IntList = registry.lookupOrCreateConstructor(new Type({
kind: kind.LIST,
elem: types.INT16
}));
[
{
n: 'converting null to non-optional type',
v: null,
t: types.UINT64
},
{
n: 'encoding float as int',
v: 3.5,
t: types.INT32
},
{
n: 'converting string to complex type',
v: 'a string cannot convert to Complex',
t: types.COMPLEX64
},
{
n: 'converting wrapped string to complex type',
v: new Str('a string cannot convert to Complex'),
t: types.COMPLEX64,
e: 'are not compatible'
},
{
n: 'using value as typeobject',
v: [3, 4, 90],
t: types.TYPEOBJECT
},
{
n: 'using wrapped value as typeobject',
v: new IntList([3, 4, 90]),
t: types.TYPEOBJECT,
e: 'are not compatible'
},
{
n: 'using label not in enum',
v: 'Thursday',
t: {
kind: kind.ENUM,
labels: ['Sunday', 'Monday', 'Tuesday']
}
},
{
n: 'array size mismatch',
v: [2, -4, 9, 34],
t: {
kind: kind.ARRAY,
elem: types.INT16,
len: 3
}
},
{
n: 'map does not convert to set',
v: new Map([
['a', true],
['b', true],
['c', true]
]),
t: {
kind: kind.SET,
key: types.STRING
}
},
{
n: 'object cannot be converted to non-string keyed sets',
v: {
a: true,
b: true,
c: true
},
t: {
kind: kind.SET,
key: types.FLOAT64
}
},
{
n: 'object cannot be converted to non-string keyed maps',
v: {
a: 3,
b: true,
c: 'asf'
},
t: {
kind: kind.MAP,
key: types.UINT32,
elem: types.ANY
}
},
{
n: 'extra struct entry', // TODO(alexfandrianto): Should we drop the field
v: { // instead of throwing an error?
a: 3,
b: 0,
c: 'asf',
d: 'KABOOM! This cannot be here!'
},
t: {
kind: kind.STRUCT,
fields: [
{
name: 'A',
type: types.INT16
},
{
name: 'B',
type: types.BYTE
},
{
name: 'C',
type: types.STRING
}
]
}
},
{
n: 'Union is not TwoOrMoreOf',
v: {
a: 3,
c: 'asf'
},
t: {
kind: kind.UNION,
fields: [
{
name: 'A',
type: types.INT16
},
{
name: 'B',
type: types.BYTE
},
{
name: 'C',
type: types.STRING
}
]
}
},
{
n: 'Union is not NoneOf',
v: {},
t: {
kind: kind.UNION,
fields: [
{
name: 'A',
type: types.INT16
},
{
name: 'B',
type: types.BYTE
},
{
name: 'C',
type: types.STRING
}
]
}
}
].forEach(function(testCase) {
test('encode error cases - ' + test.n, function(t) {
var messageWriter = new ByteMessageWriter();
var encoder = new Encoder(messageWriter);
t.throws(encoder.encode.bind(encoder, testCase.v, testCase.t),
new RegExp('.*' + (testCase.e || '') + '.*'));
t.end();
});
});