blob: e512e78ded95bf2ecf2f48be3decc6f85275b830 [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 for canonicalize.js
*/
var test = require('prova');
var BigInt = require('./../../src/vdl/big-int.js');
var Complex = require('./../../src/vdl/complex.js');
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 canonicalize = require('./../../src/vdl/canonicalize.js');
var stringify = require('./../../src/vdl/stringify.js');
require('../../src/vom/native-types');
var Time = require('../../src/gen-vdl/v.io/v23/vdlroot/time').Time;
var makeError = require('../../src/verror/make-errors');
var actions = require('../../src/verror/actions');
// A helper function that shallow copies an object into an object with the
// JSValue prototype. It makes the test cases a lot more readable.
function JS(obj) {
var JSValue = Registry.lookupOrCreateConstructor(Types.JSVALUE);
var jsval = Object.create(JSValue.prototype);
Object.keys(obj).forEach(function(key) {
jsval[key] = obj[key];
});
return jsval;
}
// Test basic JSValue canonicalization. Pure JSValues are used.
// TODO(alexfandrianto): It would be good to test a JSValue inside another type.
// For example, []JSValue or a struct with JSValues.
test('canonicalize JSValue - basic functionality', function(t) {
var tests = [
{
name: 'null',
input: null,
output: null,
outputDeep: JS({
'null': {}
})
},
{
name: 'number',
input: 4,
output: 4,
outputDeep: JS({
'number': {
val: 4
}
})
},
{
name: 'string',
input: 'fadasa',
output: 'fadasa',
outputDeep: JS({
'string': {
val: 'fadasa'
}
})
},
{
name: 'list',
input: [3, false, null, 'abc', undefined],
output: [3, false, null, 'abc', null],
outputDeep: JS({
'list': {
val: [
{
val: JS({
'number': { val: 3 }
})
},
{
val: JS({
'boolean': { val: false }
})
},
{
val: JS({
'null': {}
})
},
{
val: JS({
'string': { val: 'abc' }
})
},
{
val: JS({
'null': {}
})
}
]
}
})
},
{
name: 'map',
input: new Map([
[345, '345'],
[null, null]
]),
output: new Map([
[345, '345'],
[null, null]
]),
outputDeep: JS({
'map': {
val: [
{
key: {
val: JS({ 'number': { val: 345 } })
},
value: {
val: JS({ 'string': { val: '345' } })
}
},
{
key: {
val: JS({ 'null': {} })
},
value: {
val: JS({ 'null': {} })
}
}
]
}
})
},
{
name: 'object',
input: { name: '', servers: [], mT: false },
output: { name: '', servers: [], mT: false },
outputDeep: JS({
'object': {
val: [
{
key: {
val: 'name'
},
value: {
val: JS({ 'string': { val: '' } })
}
},
{
key: {
val: 'servers'
},
value: {
val: JS({ 'list': { val: [] } })
}
},
{
key: {
val: 'mT'
},
value: {
val: JS({ 'boolean': { val: false } })
}
}
]
}
})
}
];
for (var i = 0; i < tests.length; i++) {
var name = tests[i].name;
var input = tests[i].input;
var expected = tests[i].output;
var expectedDeep = tests[i].outputDeep;
var type = Types.JSVALUE;
// The input canonicalizes to the expected output.
var output = canonicalize.reduce(input, type);
t.deepEqual(output, expected, name + ' - canon match');
// Canonicalize is idempotent.
var output2 = canonicalize.reduce(output, type);
t.deepEqual(output2, output, name + ' - idempotent');
// The deep wrapped output should also match the expected deep output.
var outputDeep = canonicalize.fill(input, type);
t.deepEqual(outputDeep, expectedDeep, name + ' - deep');
// This is also idempotent.
var outputDeep2 = canonicalize.fill(outputDeep, type);
t.deepEqual(outputDeep2, outputDeep, name + ' - deep idempotent');
// DeepWrap(output) === outputDeep
var outputToDeep = canonicalize.fill(output, type);
t.deepEqual(outputToDeep, outputDeep, name + ' - shallow to deep');
// Unwrap(outputDeep) === output
var outputDeepToShallow = canonicalize.reduce(outputDeep, type);
t.deepEqual(outputDeepToShallow, output, name + ' - deep to shallow');
// The type of the deep output must match. (Shallow lacks type.)
var expectedTypeStr = stringify(type);
var outputDeepTypeStr = stringify(outputDeep._type);
t.equal(outputDeepTypeStr, expectedTypeStr,
name + ' - top-level type match');
}
t.end();
});
test('canonicalize JSValue - mixed JSValue and non-JSValue functionality',
function(t) {
var Float32 = Registry.lookupOrCreateConstructor(Types.FLOAT32);
var tests = [
{
name: 'list w/ typed values',
input: [3, false, null, 'abc', undefined, new Float32(3.14)],
output: [3, false, null, 'abc', null, new Float32(3.14)],
outputDeep: JS({
'list': {
val: [
{
val: JS({
'number': { val: 3 }
})
},
{
val: JS({
'boolean': { val: false }
})
},
{
val: JS({
'null': {}
})
},
{
val: JS({
'string': { val: 'abc' }
})
},
{
val: JS({
'null': {}
})
},
{
val: new Float32(3.14)// any with wrapped float32
}
]
}
})
},
];
for (var i = 0; i < tests.length; i++) {
var name = tests[i].name;
var input = tests[i].input;
var expected = tests[i].output;
var expectedDeep = tests[i].outputDeep;
var type = Types.JSVALUE;
// The input canonicalizes to the expected output.
var output = canonicalize.reduce(input, type);
t.deepEqual(output, expected, name);
// Canonicalize is idempotent.
var output2 = canonicalize.reduce(output, type);
t.deepEqual(output2, output, name + ' - idempotent');
// The deep wrapped output should also match the expected deep output.
var outputDeep = canonicalize.fill(input, type);
t.deepEqual(outputDeep, expectedDeep, name + ' - deep');
// This is also idempotent.
var outputDeep2 = canonicalize.fill(outputDeep, type);
t.deepEqual(outputDeep2, outputDeep, name + ' - deep idempotent');
// DeepWrap(output) === outputDeep
var outputToDeep = canonicalize.fill(output, type);
t.deepEqual(outputToDeep, outputDeep, ' - shallow to deep');
// Unwrap(outputDeep) === output
var outputDeepToShallow = canonicalize.reduce(outputDeep, type);
t.deepEqual(outputDeepToShallow, output, ' - deep to shallow');
// The type of the deep output must match. (Shallow lacks type.)
var expectedTypeStr = stringify(type);
var outputDeepTypeStr = stringify(outputDeep._type);
t.equal(outputDeepTypeStr, expectedTypeStr,
name + ' - top-level type match');
}
t.end();
});
test('canonicalize struct - basic functionality', function(t) {
var OptStringType = new Type({
kind: Kind.OPTIONAL,
elem: Types.STRING
});
var OptStr = Registry.lookupOrCreateConstructor(OptStringType);
var AnyListType = new Type({
kind: Kind.LIST,
elem: Types.ANY
});
var BoolListType = new Type({
kind: Kind.LIST,
elem: Types.BOOL
});
var ComplicatedStringStructType = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'JSValueString',
type: Types.ANY
},
{
name: 'WrappedString',
type: Types.STRING
},
{
name: 'NativeString',
type: Types.STRING
},
{
name: 'AnyString',
type: Types.ANY
},
{
name: 'NullOptionalAny',
type: OptStringType
},
{
name: 'OptionalToString',
type: OptStringType
},
{
name: 'UndefinedToZeroString',
type: Types.STRING
},
{
name: 'UndefinedToZeroStringAny',
type: Types.STRING
}
]
});
var ComplicatedBoolAnyListType = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'BoolToAny',
type: BoolListType
},
{
name: 'BoolToBool',
type: BoolListType
},
{
name: 'AnyToBool',
type: AnyListType
},
{
name: 'AnyToAny',
type: AnyListType
}
]
});
var Bool = Registry.lookupOrCreateConstructor(Types.BOOL);
var Str = Registry.lookupOrCreateConstructor(Types.STRING);
var ComplicatedStringStruct = Registry.lookupOrCreateConstructor(
ComplicatedStringStructType);
var ComplicatedBoolAnyList = Registry.lookupOrCreateConstructor(
ComplicatedBoolAnyListType);
var tests = [
{
name: 'empty object, no fields',
inputObject: {},
inputFields: [],
outputObject: {},
outputObjectDeep: {}
},
{
name: 'object w/ private properties, no fields',
inputObject: {_private: 'I persist!'},
inputFields: [],
outputObject: {_private: 'I persist!'},
outputObjectDeep: {_private: 'I persist!'}
},
{
name: 'normal object, no extra fields',
inputObject: {
a: 4,
b: 'can',
e: 'plan'
},
inputFields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
},
{
name: 'E',
type: Types.ANY
},
],
outputObject: {
a: 4,
b: 'can',
e: 'plan' // JSValue in ANY has no wrapping in shallow mode.
},
outputObjectDeep: {
a: { val: 4 },
b: { val: 'can' },
e: { // any
val: { // INFERRED: JSValue(string).
string: { val: 'plan' }
}
}
}
},
{
name: 'empty object, some fields',
inputObject: {},
inputFields: [
{
name: 'Man',
type: Types.ANY
},
{
name: 'Ban',
type: Types.BOOL
},
{
name: 'Dan',
type: Types.COMPLEX64
}
],
outputObject: {
man: null,
ban: false,
dan: new Complex(0, 0)
},
outputObjectDeep: {
man: { val: null },
ban: { val: false },
dan: { val: new Complex(0, 0) }
}
},
{
name: 'struct with internal string/any',
inputObject: new ComplicatedStringStruct({
jSValueString: 'go as JSValue',
wrappedString: new Str('overly wrapped input'),
nativeString: 'true string',
anyString: new Str('string any'),
nullOptionalAny: null,
optionalToString: new OptStr('non-empty string'),
undefinedToZeroString: undefined,
undefinedToZeroStringAny: undefined
}),
inputFields: [
{
name: 'JSValueString',
type: Types.ANY
},
{
name: 'WrappedString',
type: Types.ANY
},
{
name: 'NativeString',
type: Types.ANY
},
{
name: 'AnyString',
type: Types.STRING
},
{
name: 'NullOptionalAny',
type: Types.ANY
},
{
name: 'OptionalToString',
type: Types.STRING
},
{
name: 'UndefinedToZeroString',
type: Types.STRING
},
{
name: 'UndefinedToZeroStringAny',
type: Types.ANY
}
],
outputObject: {
jSValueString: 'go as JSValue',
wrappedString: new Str('overly wrapped input'),
nativeString: new Str('true string'),
anyString: 'string any',
nullOptionalAny: {
val: null
},
optionalToString: 'non-empty string',
undefinedToZeroString: '',
undefinedToZeroStringAny: new Str('')
},
outputObjectDeep: {
jSValueString: {
val: {
string: {
val: 'go as JSValue'
}
}
},
wrappedString: {
val: new Str('overly wrapped input')
},
nativeString: {
val: new Str('true string')
},
anyString: new Str('string any'),
nullOptionalAny: {
val: {
val: null
}
},
optionalToString: new Str('non-empty string'),
undefinedToZeroString: new Str(''),
undefinedToZeroStringAny: {
val: new Str('')
}
}
},
{
name: 'struct with internal []any and []bool',
inputObject: new ComplicatedBoolAnyList({
boolToAny: [true, false, true],
boolToBool: [false, false],
anyToBool: [new Bool(true), new Bool(true), new Bool(false)],
anyToAny: [new Bool(true)]
}),
inputFields: [
{
name: 'BoolToAny',
type: AnyListType
},
{
name: 'BoolToBool',
type: BoolListType
},
{
name: 'AnyToBool',
type: BoolListType
},
{
name: 'AnyToAny',
type: AnyListType
},
],
outputObject: {
boolToAny: [new Bool(true), new Bool(false), new Bool(true)],
boolToBool: [false, false],
anyToBool: [true, true, false],
anyToAny: [new Bool(true)]
},
outputObjectDeep: {
boolToAny: {
val: [
{ val: new Bool(true) },
{ val: new Bool(false) },
{ val: new Bool(true) }
]
},
boolToBool: {
val: [new Bool(false), new Bool(false)]
},
anyToBool: {
val: [new Bool(true), new Bool(true), new Bool(false)]
},
anyToAny: {
val: [{ val: new Bool(true) }]
},
}
},
{
name: 'simple zero values',
inputObject: {},
inputFields: [
{
name: 'Enum',
type: {
kind: Kind.ENUM,
labels: ['Sunday', 'Monday', 'Tuesday']
}
},
{
name: 'Optional',
type: {
kind: Kind.OPTIONAL,
elem: Types.STRING
}
},
{
name: 'String',
type: Types.STRING
},
{
name: 'Array',
type: {
kind: Kind.ARRAY,
elem: Types.BOOL,
len: 3
}
},
{
name: 'List',
type: {
kind: Kind.LIST,
elem: Types.BOOL
}
},
{
name: 'Set',
type: {
kind: Kind.SET,
key: Types.UINT64
}
},
{
name: 'Map',
type: {
kind: Kind.MAP,
key: Types.STRING,
elem: Types.STRING
}
},
{
name: 'TypeObject',
type: Types.TYPEOBJECT
}
],
outputObject: {
'enum': 'Sunday',
'optional': null,
'string': '',
'array': [false, false, false],
'list': [],
'set': new Set(),
'map': new Map(),
'typeObject': Types.ANY
},
outputObjectDeep: {
'enum': { val: 'Sunday' },
'optional': { val: null },
'string': { val: '' },
'array': {
val: [
{ val: false },
{ val: false },
{ val: false }
]
},
'list': {
val: []
},
'set': {
val: new Set()
},
'map': {
val: new Map()
},
'typeObject': Types.ANY
}
},
{
name: 'byte slice',
inputObject: {},
inputFields: [
{
name: 'ByteSlice',
type: {
kind: Kind.LIST,
elem: Types.BYTE
}
}
],
outputObject: {
'byteSlice': new Uint8Array()
},
outputObjectDeep: {
'byteSlice': {
val: new Uint8Array()
}
}
},
{
name: 'byte array',
inputObject: {},
inputFields: [
{
name: 'ByteArray',
type: {
kind: Kind.ARRAY,
elem: Types.BYTE,
len: 4
}
}
],
outputObject: {
'byteArray': new Uint8Array([0, 0, 0, 0])
},
outputObjectDeep: {
'byteArray': {
val: new Uint8Array([0, 0, 0, 0])
}
}
},
{
name: 'recursive canonicalize - struct, union',
inputObject: {},
inputFields: [
{
name: 'Struct',
type: {
kind: Kind.STRUCT,
fields: [
{
name: 'A',
type: Types.BOOL
},
{
name: 'B',
type: Types.UINT64
}
]
}
},
{
name: 'Union',
type: {
kind: Kind.UNION,
fields: [
{
name: 'A',
type: Types.BOOL
},
{
name: 'B',
type: Types.UINT64
}
]
}
}
],
outputObject: {
'struct': {
a: false,
b: new BigInt(0, new Uint8Array())
},
'union': {
a: false
}
},
outputObjectDeep: {
'struct': {
a: { val: false },
b: { val: new BigInt(0, new Uint8Array()) }
},
'union': {
a: { val: false }
}
}
}
];
for (var i = 0; i < tests.length; i++) {
var type = new Type({
kind: Kind.STRUCT,
fields: tests[i].inputFields
});
runNativeWireTest(tests[i], type, t);
}
t.end();
});
function runNativeWireTest(test, type, t) {
var name = test.name;
var input = test.inputObject;
var expected = test.outputObject;
var expectedDeep = test.outputObjectDeep;
// The input object and its fields canonicalize to the expected output.
var output = canonicalize.reduce(input, type);
t.deepEqual(output, expected, name);
// Canonicalize is idempotent.
var output2 = canonicalize.reduce(output, type);
t.deepEqual(output2, output, name + ' - idempotent');
// The deep wrapped output should also match the expected deep output.
var outputDeep = canonicalize.fill(input, type);
t.deepEqual(outputDeep, expectedDeep, name + ' - deep');
// This is also idempotent.
var outputDeep2 = canonicalize.fill(outputDeep, type);
t.deepEqual(outputDeep2, outputDeep, name + ' - deep idempotent');
// DeepWrap(output) === outputDeep
var outputToDeep = canonicalize.fill(output, type);
t.deepEqual(outputToDeep, outputDeep, ' - shallow to deep');
// Unwrap(outputDeep) === output
var outputDeepToShallow = canonicalize.reduce(outputDeep, type);
t.deepEqual(outputDeepToShallow, output, ' - deep to shallow');
}
test('canonicalize union - basic functionality', function(t) {
var tests = [
{
name: 'filled union A, some fields',
inputObject: {
a: 4
},
inputFields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
},
{
name: 'E',
type: Types.ANY
}
],
outputObject: {
a: 4
},
outputObjectDeep: {
a: {
val: 4
}
}
},
{
name: 'filled union E, some fields',
inputObject: {
e: [4, 'asdf']
},
inputFields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
},
{
name: 'E',
type: Types.ANY
}
],
outputObject: { // any with []JSValue
e: [4, 'asdf']
},
outputObjectDeep: {
e: { // any
val: { // INFERRED: JSValue(list)
list: {
val: [
{
val: { // any
number: { // JSValue(float64)
val: 4
}
}
},
{
val: { // any
string: { // JSValue(string)
val: 'asdf'
}
}
}
]
}
}
}
}
},
{
name: 'filled union with explicitly undefined fields',
inputObject: {
a: undefined,
b: 'and',
e: undefined
},
inputFields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
},
{
name: 'E',
type: Types.ANY
}
],
outputObject: {
b: 'and'
},
outputObjectDeep: {
b: { val: 'and' }
}
},
{
name: 'union with private properties',
inputObject: {
a: undefined,
b: 'foo',
_private1: 'I LIVE!',
_private2: 'ME TOO!'
},
inputFields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
},
{
name: 'E',
type: Types.ANY
}
],
outputObject: {
b: 'foo',
_private1: 'I LIVE!',
_private2: 'ME TOO!'
},
outputObjectDeep: {
b: { val: 'foo' },
_private1: 'I LIVE!',
_private2: 'ME TOO!'
}
}
];
for (var i = 0; i < tests.length; i++) {
var type = new Type({
kind: Kind.UNION,
fields: tests[i].inputFields
});
runNativeWireTest(tests[i], type, t);
}
t.end();
});
// Ensures that valid types don't error out when canonicalizing.
test('canonicalize type - basic functionality', function(t) {
var loopyList = {
kind: Kind.LIST
};
loopyList.elem = loopyList;
var expectedLoopyList = {
name: '',
kind: Kind.LIST
};
expectedLoopyList.elem = expectedLoopyList;
var tests = [
{
name: 'undefined type => any',
inputType: undefined,
outputType: Types.ANY
},
{
name: 'simple list',
inputType: {
kind: Kind.LIST,
elem: Types.INT16
},
outputType: {
name: '',
kind: Kind.LIST,
elem: Types.INT16
}
},
{
name: 'typeobject',
inputType: {
kind: Kind.TYPEOBJECT
},
outputType: Types.TYPEOBJECT
},
{
name: 'loopyList',
inputType: loopyList,
outputType: expectedLoopyList
}
];
for (var i = 0; i < tests.length; i++) {
var name = tests[i].name;
var input = tests[i].inputType;
var expected = tests[i].outputType;
// The input object and its fields canonicalize to the expected output.
// Since TypeObjects can be recursive, it's best to stringify them.
var output = canonicalize.type(input);
var outputStr = stringify(output);
var expectedStr = stringify(expected);
t.equal(outputStr, expectedStr, name);
// Canonicalize Type is idempotent.
var output2 = canonicalize.type(output);
var output2Str = stringify(output2);
t.equal(output2Str, expectedStr, name + ' - idempotent');
// Post-canonicalization, the type is still a TypeObject.
t.deepEqual(output._type, Types.TYPEOBJECT, name + ' - is TypeObject');
}
t.end();
});
// TODO(alexfandrianto): Add a general idempotency test since we always expect
// canonicalize and canonicalizeType to be idempotent when successful.
// TODO(alexfandrianto): Perhaps this test is not necessary anymore; we have
// other coverage, and it seems like it's just checking that deep wrap converts
// to shallow wrap.
test('canonicalize deep to shallow - basic functionality', function(t) {
var Int16 = Registry.lookupOrCreateConstructor(Types.INT16);
var Int64 = Registry.lookupOrCreateConstructor(Types.INT64);
var Uint32 = Registry.lookupOrCreateConstructor(Types.UINT32);
var Complex64 = Registry.lookupOrCreateConstructor(Types.COMPLEX64);
var Str = Registry.lookupOrCreateConstructor(Types.STRING);
var Uint32Uint32Map = Registry.lookupOrCreateConstructor({
kind: Kind.MAP,
name: '',
key: Types.INT32,
elem: Types.INT32
});
var KindNameStruct = Registry.lookupOrCreateConstructor({
kind: Kind.STRUCT,
name: '',
fields: [
{
name: 'Kind',
type: Types.UINT32
},
{
name: 'Name',
type: Types.STRING
}
]
});
var ABUnion = Registry.lookupOrCreateConstructor({
kind: Kind.UNION,
name: '',
fields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
}
]
});
var ABStruct = Registry.lookupOrCreateConstructor({
kind: Kind.STRUCT,
name: '',
fields: [
{
name: 'A',
type: Types.UINT32
},
{
name: 'B',
type: Types.STRING
}
]
});
var AnyStrStruct = Registry.lookupOrCreateConstructor({
kind: Kind.STRUCT,
name: '',
fields: [
{
name: 'Any',
type: Types.ANY
},
{
name: 'Normal',
type: Types.STRING
}
]
});
var tests = [
{
name: 'top-level only',
input: new Int16(5, true),
expected: new Int16(5)
},
{
name: 'wrapped big int',
input: new Int64(new BigInt(1, new Uint8Array([0x10, 0xff])), true),
expected: new Int64(new BigInt(1, new Uint8Array([0x10, 0xff])))
},
{
name: 'wrapped complex',
input: new Complex64(new Complex(4, 5), true),
expected: new Complex64(new Complex(4, 5))
},
{
name: 'map',
input: new Uint32Uint32Map(new Map([[3, 4], [6, 3]]), true),
expected: new Uint32Uint32Map(new Map([[3, 4], [6, 3]]))
},
{
name: 'fake typeobject',
input: new KindNameStruct({
kind: new Uint32(3, true),
name: new Str('Boolean', true)
}, true),
expected: {
kind: 3,
name: 'Boolean'
}
},
{
name: 'union',
input: new ABUnion({
b: new Str('abc', true),
}, true),
expected: {
b: 'abc'
}
},
{
name: 'struct',
input: new ABStruct({
a: new Uint32(3, true),
b: new Str('abc', true)
}, true),
expected: {
a: 3,
b: 'abc',
}
},
{
name: 'Struct with ANY',
input: new AnyStrStruct({
any: new Str('wrapped', true),
normal: new Str('shallow', true)
}, true),
expected: {
any: new Str('wrapped'),
normal: 'shallow'
}
}
];
for (var i = 0; i < tests.length; i++) {
testDeepWrapToUnwrap(t, tests[i]);
}
t.end();
});
// TODO(alexfandrianto): DeepWrapToUnwrap can be expanded to test more, just
// like the canonicalize struct and union tests. In fact, this tests basic
// canonicalization, since it includes more types than just struct/union.
// So the TODO is to convert this into a basic canonicalization test.
function testDeepWrapToUnwrap(t, test) {
var name = test.name;
var input = test.input;
var expected = test.expected;
// Canonicalize without wrapping deeply.
var output = canonicalize.reduce(input, input._type);
// Compare with stringify; the output/expected could be recursive.
var expectedStr = stringify(expected);
var outputStr = stringify(output);
t.equal(outputStr, expectedStr, name);
// The types must also match.
var type = input._type;
var expectedTypeStr = stringify(type);
var outputTypeStr = stringify(output._type);
t.equal(outputTypeStr, expectedTypeStr, name + ' - top-level type match');
}
// This test checks the successful cases of value to type conversion.
// For example, some structs can convert to maps, and non-null optionals convert
// to their base type.
// This test supplements the cross-language conversion tests cases in
// test-vom-compatible.js
test('canonicalize conversion - success', function(t) {
var AnyListType = new Type({
kind: Kind.LIST,
elem: Types.ANY
});
var OptStringType = new Type({
kind: Kind.OPTIONAL,
elem: Types.STRING
});
var StringListType = new Type({
kind: Kind.LIST,
elem: Types.STRING
});
var ByteListType = new Type({
kind: Kind.LIST,
elem: Types.BYTE
});
var MyEnumType = new Type({
kind: Kind.ENUM,
labels: ['M', 'A', 'G']
});
var IntSetType = new Type({
kind: Kind.SET,
key: Types.INT16
});
var FloatBoolMapType = new Type({
kind: Kind.MAP,
key: Types.FLOAT32,
elem: Types.BOOL
});
var StringSetType = new Type({
kind: Kind.SET,
key: Types.STRING
});
var StringyStructType = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'Ma',
type: Types.STRING
},
{
name: 'Bu',
type: ByteListType
},
{
name: 'Fu',
type: MyEnumType
}
]
});
var StringStringMapType = new Type({
kind: Kind.MAP,
key: Types.STRING,
elem: Types.STRING
});
var StringAnyMapType = new Type({
kind: Kind.MAP,
key: Types.STRING,
elem: Types.ANY
});
var Byte10ArrayType = new Type({
kind: Kind.ARRAY,
elem: Types.BYTE,
len: 10
});
var StructABCType = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'A',
type: Types.BOOL
},
{
name: 'B',
type: Types.STRING
},
{
name: 'C',
type: Types.UINT32
}
]
});
var StructCDBType = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'C',
type: Types.UINT32
},
{
name: 'D',
type: OptStringType
},
{
name: 'B',
type: Types.STRING
}
]
});
var Any = Registry.lookupOrCreateConstructor(Types.ANY);
var AnyList = Registry.lookupOrCreateConstructor(AnyListType);
var Bool = Registry.lookupOrCreateConstructor(Types.BOOL);
var Str = Registry.lookupOrCreateConstructor(Types.STRING);
var StrList = Registry.lookupOrCreateConstructor(StringListType);
var OptStr = Registry.lookupOrCreateConstructor(OptStringType);
var IntSet = Registry.lookupOrCreateConstructor(IntSetType);
var FloatBoolMap = Registry.lookupOrCreateConstructor(FloatBoolMapType);
var ByteList = Registry.lookupOrCreateConstructor(ByteListType);
var Byte10Array = Registry.lookupOrCreateConstructor(Byte10ArrayType);
var MyEnum = Registry.lookupOrCreateConstructor(MyEnumType);
var StructABC = Registry.lookupOrCreateConstructor(StructABCType);
var StructCDB = Registry.lookupOrCreateConstructor(StructCDBType);
var StringSet = Registry.lookupOrCreateConstructor(StringSetType);
var StringStringMap = Registry.lookupOrCreateConstructor(StringStringMapType);
var StringAnyMap = Registry.lookupOrCreateConstructor(StringAnyMapType);
var StringyStruct = Registry.lookupOrCreateConstructor(StringyStructType);
var tests = [
{
name: 'Any(String) to String',
inValue: new Any(new Str('fff')),
outValue: new Str('fff'),
targetType: Types.STRING
},
{
name: 'String to Any(String)',
inValue: new Str('fff'),
outValue: new Any(new Str('fff')),
targetType: Types.ANY
},
{
name: '[]Any to []String',
// Note: 'jsval' is a JSValue that happens to convert to a string.
// This cannot always be expected to work, however.
inValue: new AnyList([new Str('fff'), 'jsval']),
outValue: new StrList(['fff', 'jsval']),
targetType: StringListType
},
{
name: '[]string to []any',
inValue: new StrList(['fff', 'not jsval']),
outValue: new AnyList([new Str('fff'), new Str('not jsval')]),
targetType: AnyListType
},
{
name: 'OptString to String',
inValue: new OptStr('abc'),
outValue: new Str('abc'),
targetType: Types.STRING
},
{
name: 'String to ByteArray',
inValue: '1234567',
outValue: new Byte10Array(
new Uint8Array([49, 50, 51, 52, 53, 54, 55, 0, 0, 0])
),
targetType: Byte10ArrayType
},
{
name: 'Set to Map',
inValue: new IntSet(new Set([4, -5, 8])),
outValue: new FloatBoolMap(new Map([[4, true], [-5, true], [8, true]])),
targetType: FloatBoolMapType
},
{
name: 'Map to Set',
inValue: new FloatBoolMap(new Map([[4, false], [-5, true], [8, true]])),
outValue: new IntSet(new Set([-5, 8])),
targetType: IntSetType
},
{
name: 'StructABC to StructCDB',
inValue: new StructABC({
a: true,
b: 'boom',
c: 5
}),
outValue: new StructCDB({
b: 'boom',
c: 5,
d: undefined
}),
targetType: StructCDBType
},
{
name: 'StructCDB to StructABC',
inValue: new StructCDB({
d: null,
b: 'doom',
c: 6
}),
outValue: new StructABC({
a: undefined,
b: 'doom',
c: 6
}),
targetType: StructABCType
},
{
name: 'set[string] to map[string]any',
inValue: new StringSet(
new Set(['me', 'di', 'a'])
),
outValue: new StringAnyMap(
new Map([
['me', new Bool(true)],
['di', new Bool(true)],
['a', new Bool(true)]
])
),
targetType: StringAnyMapType
},
{
name: 'struct with string-y fields to map[string]any',
inValue: new StringyStruct({
ma: 'Pool', // string
bu: 'imp', // bytelist 105 109 112
fu: 'A' // autoconverts to enum
}),
outValue: new StringAnyMap(new Map([
['Ma', new Str('Pool')],
['Bu', new ByteList(new Uint8Array([105, 109, 112]))],
['Fu', new MyEnum('A')]
])),
targetType: StringAnyMapType
},
{
name: 'struct with string-y fields to map[string]string',
inValue: new StringyStruct({
ma: 'Pool',
bu: 'imp',
fu: 'A'
}),
outValue: new StringStringMap(new Map([
['Ma', 'Pool'],
['Bu', 'imp'],
['Fu', 'A']
])),
targetType: StringStringMapType
},
{
name: 'map[string]any to struct with stringy-fields',
inValue: new StringAnyMap(new Map([
['Ma', 'V'], // JSValue happens to be string-compatible.
['Bu', new ByteList(new Uint8Array([79]))],
['Fu', new MyEnum('M')]
])),
outValue: new StringyStruct({
ma: 'V',
bu: 'O', // bytelist 79
fu: 'M' // enum with M
}),
targetType: StringyStructType
},
{
name: 'map[string]any to set[string]',
inValue: new StringAnyMap(new Map([
['Z', true], // JSValue that happens to be bool-compatible
['o', new Bool(true)],
['nga', false], // Will not appear since it's false
['dine', new Bool(true)]
])),
outValue: new StringSet(new Set([
'Z', 'o', 'dine'
])),
targetType: StringSetType
}
];
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var reduced = canonicalize.reduce(test.inValue, test.targetType);
var outValue = test.outValue;
t.deepEqual(
stringify(reduced),
stringify(outValue),
test.name + ' converts correctly'
);
}
t.end();
});
test('canonicalize error', function(t) {
var E = makeError('MyId', actions.NO_RETRY, '', [
Types.STRING, Types.INT32 ]);
// There are two different values of native errors we expect. The first is
// the value that the developer will pass in. It's paramList will not have
// any wrapped elements. The second is the result of the conversion from the
// wire format to the native format. In this form, the individual values of
// in the paramList will be wrapped since they are of type any. This is
// strictly correct, but cumbersome. This is probably ok since we want to
// strongly discourage the use of the paramList programmatically.
var err = new E(null, 'foo', 32);
err.msg = 'My awesome message!!!';
Object.defineProperty(err, 'message', { value: err.msg });
var wrappedErr = err.clone();
wrappedErr.paramList = wrappedErr.paramList.map(function(v) {
return { val: v };
});
var VerrorConstructor = Registry.lookupOrCreateConstructor(Types.ERROR.elem);
var Str = Registry.lookupOrCreateConstructor(Types.STRING);
var Int32 = Registry.lookupOrCreateConstructor(Types.INT32);
var wrappedMessage = new VerrorConstructor({
id: 'MyId',
retryCode: actions.NO_RETRY,
msg: 'My awesome message!!!',
paramList: [new Str('app'), new Str('op'), new Str('foo'), new Int32(32)]
}, true);
var wrappedMessageWithLangId = new VerrorConstructor({
id: 'MyId',
retryCode: actions.NO_RETRY,
msg: 'My awesome message!!!',
paramList: [new Str('app'), new Str('op'), new Str('foo'), new Int32(32)]
}, true);
// When we convert from native type to wire type we transfer the _
wrappedMessageWithLangId._langId = 'en-US';
var tests = [
{
name: 'err, deepWrap = false',
inValue: err,
deepWrap: false,
outValue: { val: wrappedErr }, // any(error)
}, {
name: 'err, deepWrap = true',
inValue: err,
deepWrap: true,
outValue: { val: wrappedMessageWithLangId }, // any(error) deep
}, {
name: 'wrappedMessage, deepWrap = false',
inValue: wrappedMessage,
deepWrap: false,
outValue: { val: wrappedErr }, // any(error)
}, {
name: 'wrappedMessage, deepWrap = true',
inValue: wrappedMessage,
deepWrap: true,
outValue: { val: wrappedMessage }, // any(error) deep
},
{
name: '?err, deepWrap = false',
inValue: err,
type: Types.ERROR,
deepWrap: false,
outValue: { val: wrappedErr } // optional(error)
},
{
name: '?err, deepWrap = true',
inValue: err,
type: Types.ERROR,
deepWrap: true,
outValue: { val: wrappedMessageWithLangId } // optional(error) deep
}
];
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var type = test.type || Types.ANY;
var canon = canonicalize.value(test.inValue, type, test.deepWrap);
var outValue = test.outValue;
t.deepEqual(
stringify(canon),
stringify(outValue),
test.name);
}
t.end();
});
test('canonicalize time (to any)', function(t) {
var d = new Date(1999,11,30,23,59,59);
var conv = Date.parse('0001-01-01') / 1000;
var millis = d.getTime();
var timeStruct = new Time({
seconds: millis / 1000 - conv,
nanos: 0,
}, true);
var tests = [{
name: 'date deepWrap = true',
inValue: d,
deepWrap: true,
outValue: {
val: timeStruct
}
}, {
name: 'date deepWrap = false',
inValue: d,
deepWrap: false,
outValue: { val: d }
}, {
name: 'time.Time deepWrap = true',
inValue: timeStruct,
deepWrap: true,
outValue: {
val: timeStruct
},
},{
name: 'time.Time deepWrap = false',
inValue: timeStruct,
deepWrap: false,
outValue: {
val: d
}
}];
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var canon = canonicalize.value(test.inValue, Types.ANY, test.deepWrap);
var outValue = test.outValue;
t.deepEqual(
stringify(canon),
stringify(outValue),
test.name);
}
t.end();
});
// Tests the combination of native and vdl values.
test('canonicalize native and vdl', function(t) {
var TimeType = Time.prototype._type;
var TimeArray = new Type({
kind: Kind.ARRAY,
elem: TimeType,
len: 3
});
var TimeErrStruct = new Type({
kind: Kind.STRUCT,
fields: [
{
name: 'Time',
type: TimeType
},
{
name: 'Err',
type: Types.ERROR
}
]
});
// The canonical error (input) and its wrapped paramList form.
var CanonError = makeError(
'cerrID',
actions.RETRY_BACKOFF,
'Canonical Error',
[ Types.STRING, Types.INT32 ]
);
var cError = new CanonError(null, 'blue', -1); // no ctx, string, int32
var cErrorN = cError.clone(); // The reduced cError params should be wrapped.
cErrorN.paramList = cError.paramList.map(function(p) {
return { val: p };
});
// Additional constants
var MILLI_TO_NANO = 1000*1000;
var zeroDateOffset = Date.parse('0001-01-01');
var tests = [
{
name: 'TimeArray',
type: TimeArray,
inputObject: [
new Date(zeroDateOffset),
new Date(zeroDateOffset+100),
new Date(zeroDateOffset-2001)
],
outputObject: {
val: [
new Date(zeroDateOffset),
new Date(zeroDateOffset+100),
new Date(zeroDateOffset-2001)
]
},
outputObjectDeep: {
val: [
{
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 0 }
},
{
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 100*MILLI_TO_NANO }
},
{
seconds: { val: BigInt.fromNativeNumber(-3) },
nanos: { val: 999*MILLI_TO_NANO }
}
]
}
},
{
name: 'TimeArray (empty)',
type: TimeArray,
inputObject: undefined,
outputObject: {
val: [new Date(zeroDateOffset), new Date(zeroDateOffset),
new Date(zeroDateOffset)]
},
outputObjectDeep: {
val: [
{
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 0 }
},
{
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 0 }
},
{
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 0 }
},
]
}
},
{
name: 'TimeErrStruct (empty)',
type: TimeErrStruct,
inputObject: undefined,
outputObject: {
time: new Date(zeroDateOffset),
err: null,
},
outputObjectDeep: {
time: {
seconds: { val: BigInt.fromNativeNumber(0) },
nanos: { val: 0 }
},
err: { val: null }
}
},
{
name: 'TimeErrStruct',
type: TimeErrStruct,
inputObject: {
time: new Date(zeroDateOffset+4024),
err: cError
},
outputObject: {
time: new Date(zeroDateOffset+4024),
err: cErrorN
},
outputObjectDeep: {
time: {
seconds: { val: BigInt.fromNativeNumber(4) },
nanos: { val: 24 * MILLI_TO_NANO }
},
err: { // optional error
val: { // error
_langId: 'en-US',
id: { val: 'cerrID' },
retryCode: { val: actions.RETRY_BACKOFF },
msg: { val: 'Canonical Error' },
paramList: {
val: [
{ // any(string)
val: { val: 'app' }
},
{ // any(string)
val: { val: 'op' }
},
{ // any(string)
val: { val: 'blue' }
},
{ // any(int32)
val: { val: -1 }
}
]
}
}
}
}
}
];
for (var i = 0; i < tests.length; i++) {
runNativeWireTest(tests[i], tests[i].type, t);
}
t.end();
});
// This test checks the failure cases of value to type conversion.
// For example, some maps fail to convert to sets, and null optional values
// cannot convert to their base type.
// This test supplements the cross-language conversion tests cases in
// test-vom-compatible.js
test('canonicalize conversion - failure', function(t) {
var OptStringType = new Type({
kind: Kind.OPTIONAL,
elem: Types.STRING
});
var IntListType = new Type({
kind: Kind.LIST,
elem: Types.INT32
});
var Int3ArrType = new Type({
kind: Kind.ARRAY,
elem: Types.INT32,
len: 3
});
var IntSetType = new Type({
kind: Kind.SET,
key: Types.INT16
});
var Str = Registry.lookupOrCreateConstructor(Types.STRING);
var OptStr = Registry.lookupOrCreateConstructor(OptStringType);
var IntList = Registry.lookupOrCreateConstructor(IntListType);
var tests = [
{
name: 'number larger than MAX_FLOAT32',
inValue: 1e40,
targetType: Types.FLOAT32,
expectedErr: 'is too large'
},
{
name: 'imag smaller than MAX_FLOAT32 in Complex64',
inValue: { real: 0, imag: -1e40 },
targetType: Types.COMPLEX64,
expectedErr: 'is too small'
},
{
name: 'negative, real Complex to uint',
inValue: new Complex(-4, 0),
targetType: Types.UINT16,
expectedErr: 'value cannot be negative'
},
{
name: 'null OptString to String',
inValue: new OptStr(null),
targetType: Types.STRING,
expectedErr: 'value is null for non-optional type'
},
{
name: 'String to Bool',
inValue: new Str('not a boolean'),
targetType: Types.BOOL,
expectedErr: 'not compatible'
},
{
name: 'String to Bool - native',
inValue: 'not a boolean',
targetType: Types.BOOL,
expectedErr: 'value is not a boolean'
},
{
name: 'large list to smaller array',
inValue: new IntList([3, 4, 8, 1]),
targetType: Int3ArrType,
expectedErr: 'exceeds type length 3'
},
{
name: 'large list to smaller array - native',
inValue: [3, 4, 8, 1],
targetType: Int3ArrType,
expectedErr: 'exceeds type length 3'
},
{
name: 'map to set',
inValue: new Map([[4, 'not a bool'], [5, true]]),
targetType: IntSetType,
expectedErr: 'this Map value cannot convert to Set'
}
];
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
t.throws(
canonicalize.reduce.bind(null, test.inValue, test.targetType),
new RegExp('.*' + (test.expectedErr || '') + '.*'),
test.name + ' fails to convert'
);
}
t.end();
});