blob: 90608ae880d8aa74e3f4d900fa549144b584ec0d [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.
var test = require('prova');
var mercury = require('mercury');
var _ = require('lodash');
var proxyquire = require('proxyquireify')(require);
var mockLRUCache = require('./mocks/lru-cache');
var sampleWorld = require('../../../../src/services/sample-world');
test.timeout(60 * 1000);
// @noCallThru ensures this completely overrdies the original config
// instead of inheriting the properties that are not defined here from
// the original dependency
var vanadiumConfigForTest = {
'@noCallThru': true
};
// The V23_NAMESPACE environment variable is set by servicerunner in the "make
// test" target. That environment variable is picked up by the "envify" prova
// transform and used to set process.env.V23_NAMESPACE.
var globalRoot = process.env.V23_NAMESPACE;
// Require namespaceService but using test specific mocks and configs
var namespaceService =
proxyquire('../../../../src/services/namespace/service.js', {
'../../vanadium-config': vanadiumConfigForTest,
'lru-cache': function() {
return mockLRUCache;
},
'vanadium': require('vanadium'),
});
var sw;
function initSampleWorld(t) {
if (!sw) {
var prefix = '';
sw = sampleWorld.create(prefix);
}
return sw.catch(function(err) {
t.fail(err);
t.end();
// Throw again so the promise is not resolved and test does not continue
// to run.
throw err;
});
}
test('getChildren of default namespace root', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getChildren().
then(function assertResult(result) {
assertIsImmutable(t, result);
// Wait until we finish, we expect 2 top level items: cottage, house
result.events.on('end', function validate() {
mercury.watch(result, function(children) {
assertCottage(children[0]);
assertHouse(children[1]);
t.end();
});
});
result.events.on('globError', function(error) {
t.notOk(error, 'did not expect any globs errors');
t.end();
});
}).catch(t.end);
function assertCottage(item) {
assertServer(t, item, {
name: 'cottage',
objectName: 'cottage',
isLeaf: false,
isMounttable: true
});
}
function assertHouse(item) {
assertServer(t, item, {
name: 'house',
objectName: 'house',
isLeaf: false,
isMounttable: true
});
}
}
});
test('getChildren of cottage/lawn', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getChildren('cottage/lawn').
then(function assertResult(result) {
assertIsImmutable(t, result);
// Wait until we finish, we expect 3 items back, front and
// master-sprinkler
result.events.on('end', function validate() {
mercury.watch(result, function(children) {
assertBack(children[0]);
assertSprinkler(children[2]);
t.end();
});
});
result.events.on('globError', function(error) {
t.notOk(error, 'did not expect any globs errors');
t.end();
});
}).catch(t.end);
function assertSprinkler(item) {
assertServer(t, item, {
name: 'master-sprinkler',
objectName: 'cottage/lawn/master-sprinkler',
isLeaf: true
});
}
function assertBack(item) {
assertSubtableName(t, item, {
name: 'back',
objectName: 'cottage/lawn/back'
});
}
}
});
test('getChildren of rooted ' + globalRoot + '/house/kitchen', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getChildren(globalRoot + '/house/kitchen').
then(function assertResult(result) {
assertIsImmutable(t, result);
// Wait until we finish, we expect 2 items, lights
// and smoke-detector
result.events.on('end', function validate() {
mercury.watch(result, function(children) {
assertLightSwitch(children[0]);
assertSmokeDetector(children[1]);
t.end();
});
});
result.events.on('globError', function(error) {
t.notOk(error, 'did not expect any globs errors');
t.end();
});
}).catch(t.end);
function assertLightSwitch(item) {
assertServer(t, item, {
name: 'lights',
objectName: globalRoot + '/house/kitchen/lights',
isLeaf: true
});
}
function assertSmokeDetector(item) {
assertServer(t, item, {
name: 'smoke-detector',
objectName: globalRoot + '/house/kitchen/smoke-detector',
isLeaf: true
});
}
}
});
// The HOUSE_MOUNTTABLE environment variable is set by run-tests.sh. That
// environment variable is picked up by the "envify" prova transform and used to
// set process.env.HOUSE_MOUNTTABLE.
var hostPortRoot = process.env.HOUSE_MOUNTTABLE;
test('getChildren of rooted ' + hostPortRoot + '/kitchen', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getChildren(hostPortRoot + '/kitchen').
then(function assertResult(result) {
assertIsImmutable(t, result);
// Wait until we finish, we expect 2 items, lights
// and smoke-detector
result.events.on('end', function validate() {
mercury.watch(result, function(children) {
assertLightSwitch(children[0]);
assertSmokeDetector(children[1]);
t.end();
});
});
result.events.on('globError', function(error) {
t.notOk(error, 'did not expect any globs errors');
t.end();
});
}).catch(t.end);
function assertLightSwitch(item) {
assertServer(t, item, {
name: 'lights',
objectName: hostPortRoot + '/kitchen/lights',
isLeaf: true
});
}
function assertSmokeDetector(item) {
assertServer(t, item, {
name: 'smoke-detector',
objectName: hostPortRoot + '/kitchen/smoke-detector',
isLeaf: true
});
}
}
});
test('getChildren of non-existing mounttable', function(t) {
initSampleWorld(t).then(run);
function run() {
// TODO(aghassemi) why does namespace library return empty results instead
// of error when globbing rooted names that don't exist?
namespaceService.getChildren('/DoesNotExist:666/What/Ever').
then(function assertResult(result) {
result.events.on('end', function validate() {
// Expect empty results
mercury.watch(result, function(children) {
t.deepEqual(children, []);
t.end();
});
});
result.events.on('globError', function(error) {
// we do actually expect a glob error in this case
t.ok(error);
});
}).catch(t.end);
}
});
test('getNamespaceItem of leaf server', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getNamespaceItem('cottage/lawn/master-sprinkler').
then(function assertItem(itemObs) {
assertIsImmutable(t, itemObs);
var item = itemObs();
assertServer(t, item, {
name: 'master-sprinkler',
objectName: 'cottage/lawn/master-sprinkler',
isLeaf: true
});
t.end();
}).catch(t.end);
}
});
test('getNamespaceItem of subtable', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getNamespaceItem('cottage/lawn/back').
then(function assertItem(itemObs) {
assertIsImmutable(t, itemObs);
var item = itemObs();
assertSubtableName(t, item, {
name: 'back',
objectName: 'cottage/lawn/back'
});
t.end();
}).catch(t.end);
}
});
test('getNamespaceItem of mounttable leaf server', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.getNamespaceItem('cottage').
then(function assertItem(itemObs) {
assertIsImmutable(t, itemObs);
var item = itemObs();
assertServer(t, item, {
name: 'cottage',
objectName: 'cottage',
isLeaf: false,
isMounttable: true
});
t.end();
}).catch(t.end);
}
});
test('search uses caching', function(t) {
initSampleWorld(t).then(run);
function run() {
mockLRUCache.reset();
namespaceService.search('house', '*').
then(function assertNoCacheHit() {
t.notOk(mockLRUCache.wasCacheHit('glob|house/*'),
'first glob call is not a cache hit');
// Call second time, there should have been a cache hit
return namespaceService.search('house', '*');
}).then(function assertCacheHit() {
t.ok(mockLRUCache.wasCacheHit('glob|house/*'),
'second glob call is a cache hit');
// Call glob with same name, different query
return namespaceService.search('house', 'foo*');
}).then(function assertNoCacheHit() {
t.notOk(mockLRUCache.wasCacheHit('glob|house/foo*'),
'third glob call with different query is not a cache hit');
t.end();
}).catch(t.end);
}
});
test('getSignature uses caching', function(t) {
initSampleWorld(t).then(run);
function run() {
mockLRUCache.reset();
namespaceService.getSignature('house/alarm').then(function() {
t.notOk(mockLRUCache.wasCacheHit('getSignature|house/alarm'),
'first getSignature call is not a cache hit');
// Call a second time
return namespaceService.getSignature('house/alarm');
}).then(function() {
t.ok(mockLRUCache.wasCacheHit('getSignature|house/alarm'),
'second getSignature call is a cache hit');
// Call a different name
return namespaceService.getSignature('house/kitchen/smoke-detector');
}).then(function() {
t.notOk(mockLRUCache.wasCacheHit(
'getSignature|house/kitchen/smoke-detector'
), 'third getSignature call to a different name is not a cache hit');
t.end();
}).catch(t.end);
}
});
test('test clearCache', function(t) {
initSampleWorld(t).then(run);
function run() {
mockLRUCache.reset();
// cache bunch of things under house first, then clear cache and validate
Promise.all([
namespaceService.getSignature('house/alarm'),
namespaceService.search('house', '*'),
namespaceService.search('house', ''),
namespaceService.search('house-2', ''),
namespaceService.getRemoteBlessings('house/alarm')
])
.then(function verifyItemsAreCached() {
t.ok(mockLRUCache.has('getSignature|house/alarm'),
'signature is cached');
t.ok(mockLRUCache.has('glob|house/*'),
'glob is cached');
t.ok(mockLRUCache.has('glob|house'),
'glob is cached');
t.ok(mockLRUCache.has('glob|house-2'),
'glob is cached');
t.ok(mockLRUCache.has('getRemoteBlessings|house/alarm'),
'getRemoteBlessings is cached');
})
.then(function clearCacheForHouse() {
namespaceService.clearCache('house');
}).then(function verifyCacheWasCleared() {
t.notOk(mockLRUCache.has('getSignature|house/alarm'),
'signature cache cleared');
t.notOk(mockLRUCache.has('glob|house/*'),
'glob cache cleared');
t.notOk(mockLRUCache.has('glob|house'),
'glob cache cleared');
t.notOk(mockLRUCache.has('getRemoteBlessings|house/alarm'),
'getRemoteBlessings cache cleared');
t.ok(mockLRUCache.has('glob|house-2'),
'house-2 should not be impacted');
t.end();
}).then(function deleteEverything() {
return namespaceService.clearCache();
}).then(function verifyAllIsGone() {
t.notOk(mockLRUCache.has('glob|house-2'),
'house-2 should be gone now');
}).catch(t.end);
}
});
// Make RPC: good inputs => no error
var okRPCs = {
'no input': ['house/alarm', 'status', []],
'bool input': ['house/living-room/lights', 'flipSwitch', [true]],
'int input': ['cottage/smoke-detector', 'sensitivity', [2]],
'float input': ['house/alarm', 'delayArm', [2.5]],
'string input': ['cottage/pool/speaker', 'playSong', ['Happy Birthday']],
'slice input': ['house/master-bedroom/speaker', 'addSongs', [
['A', 'B']
]],
'2+ inputs': ['cottage/pool/heater', 'start', [70, 5]],
};
_.forOwn(okRPCs, function run(params, inputType) {
test(
'makeRPC accepts good input - ' + inputType,
testMakeRPCNoError.bind(null, params)
);
});
// Make RPC: bad inputs => error
var badRPCs = {
//TODO(aghassemi) re-enable after #483
//'no service': ['mansion/smoke-detector', 'status', []],
'no method': ['cottage/pool/speaker', 'status', []],
'no input': ['cottage/lights', 'flipSwitch', null],
'bad type': ['cottage/lights', 'flipSwitch', ['notBool']],
'lacks input': ['cottage/pool/heater', 'start', [80]],
'invalid input': ['house/living-room/blast-speaker', 'playSong', ['notThere']]
};
_.forOwn(badRPCs, function run(params, inputType) {
test(
'makeRPC errors on bad input - ' + inputType,
testMakeRPCHasError.bind(null, params)
);
});
// Make RPC: outputs have the expected # of outputs
test('makeRPC returns output properly', function(t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.makeRPC('cottage/alarm', 'panic', []).then(
function got0Outputs(res) { // 0 outputs: has no result.
t.ok(res === undefined, '0 outputs => is undefined');
return namespaceService.makeRPC('house/alarm', 'status', []);
}
).then( // 1 output: (Non-array/slice output) is not an Array.
function got1Output(res) {
t.notOk(res instanceof Array, '1 output => not an Array');
return namespaceService.makeRPC('cottage/smoke-detector', 'test', []);
}
).then( // 1 output: Delayed return. Also not an array.
function got1OutputDelayed(res) {
t.notOk(res instanceof Array, '1 output => not an Array');
return namespaceService.makeRPC('cottage/pool/heater', 'status', []);
}
).then( // 2 outputs: Is an Array of the correct length.
function got2Outputs(res) {
var ok = res instanceof Array && res.length === 2;
t.ok(ok, '2 outputs => length 2 Array');
t.end();
}
).catch(t.end);
}
});
/*
* Runs a test to ensure the makeRPC call terminates without error.
*/
function testMakeRPCNoError(args, t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.makeRPC.apply(null, args).then(function(result) {
t.pass('completed without error');
t.end();
}).catch(function(err) {
t.end(err);
});
}
}
/*
* Runs a test to ensure the makeRPC call terminates with an error.
*/
function testMakeRPCHasError(args, t) {
initSampleWorld(t).then(run);
function run() {
namespaceService.makeRPC.apply(null, args).then(function(result) {
t.fail('should not have completed without error');
t.end();
}).catch(function(err) {
t.pass('correctly returned an error');
t.end();
});
}
}
/*
* Test helpers
*/
function assertServer(t, item, vals) {
assertMountedName(t, item, vals.name);
assertObjectName(t, item, vals.objectName);
assertIsServer(t, item);
if (vals.isLeaf === true) {
assertIsLeaf(t, item);
} else if (vals.isLeaf === false) {
assertIsNotLeaf(t, item);
}
t.equals(!!item.isMounttable, !!vals.isMounttable,
item.mountedName + ': is a mounttable');
}
function assertSubtableName(t, item, vals) {
assertMountedName(t, item, vals.name);
assertObjectName(t, item, vals.objectName);
assertIsNotLeaf(t, item);
t.ok(item.hasMountPoint, ': is subtable node');
}
function assertMountedName(t, item, val) {
t.ok(item.mountedName, item.mountedName + ': has a mounted name');
t.equal(item.mountedName, val, item.mountedName + ': mounted name matches');
}
function assertObjectName(t, item, val) {
t.ok(item.objectName, item.mountedName + ': has an object name');
t.equal(item.objectName, val, item.mountedName + ': object name matches');
}
function assertIsServer(t, item) {
t.ok(item.hasServer, item.mountedName + ': is a server');
}
function assertIsLeaf(t, item) {
t.equal(item.isLeaf, true, item.mountedName + ': is leaf');
}
function assertIsNotLeaf(t, item) {
t.equal(item.isLeaf, false, item.mountedName + ': is not leaf');
}
function assertIsImmutable(t, observable) {
t.ok(observable.set === undefined, 'is immutable');
}
//TODO(aghassemi)
//Tests for:
// Recursive glob
// Glob with some keyword
// Ensuring array is updated when nodes get mounted and unmounted (when we use
// watchGlob)
// getEndpoints()
// getPermissions()
// resolveToMounttable()
// getRemoteBlessings()