thywill
Version:
A Node.js clustered framework for single page web applications based on asynchronous messaging.
1,112 lines (1,035 loc) • 36.7 kB
JavaScript
/**
* @fileOverview
* Various utility functions for testing Thywill.
*/
var childProcess = require('child_process');
var path = require('path');
var http = require('http');
var async = require('async');
var vows = require('vows');
var assert = require('assert');
var clone = require('clone');
var express = require('express');
var redis = require('redis');
var Client = require('work-already').Client;
var MemorySocketStore = require('socket.io/lib/stores/memory');
var RedisSocketStore = require('socket.io/lib/stores/redis');
var RedisSessionStore = require('connect-redis')(express);
var MemorySessionStore = require('connect/lib/middleware/session/memory');
var Thywill = require('thywill');
var Message = Thywill.getBaseClass('Message');
// ------------------------------------------------------------
// Relating to setting up headless Thywill instances without
// applications running in this process.
// ------------------------------------------------------------
exports.headless = {};
/**
* Utility function to create a client for a local Redis server.
* @return {object}
* A Redis client.
*/
function createRedisClient () {
var options = {};
var client = redis.createClient(6379, '127.0.0.1', options);
// This is fairly hacky, but needed to exercise the protection code. Create
// a dummy Thywill instance, stick a dummy log into it, and run the protect
// function on the client.
var thywill = new Thywill();
thywill.log = {
debug: function (message) {
console.log(message);
},
info: function (message) {
console.log(message);
},
warn: function (message) {
console.log(message);
},
error: function (message) {
console.log(message);
}
};
thywill.protectRedisClient(client);
return client;
}
/**
* Set up the config to start a Thywill server, based on the options provided.
*
* @param {Object} baseConfig
* The base configuration object, lacking any class instances, etc.
* @param {Object} options
* The various options.
* @return {Object}
* A cloned configuration object.
*/
function setupConfig (baseConfig, options) {
var config = clone(baseConfig);
// Create some Redis clients, if needed.
var redisClients;
function setupRedisClients () {
if (!redisClients) {
redisClients = {
pub: createRedisClient(),
sub: createRedisClient(),
other: createRedisClient()
};
}
}
// ------------------------------------------------------------
// Set up server.
// ------------------------------------------------------------
var port = config.thywill.ports[options.localClusterMemberId];
// If using Express, set it up.
if (config.clientInterface.implementation.name === 'socketIoExpressClientInterface') {
// Create servers.
var app = express();
config.clientInterface.server.app = app;
var server = http.createServer(app).listen(port);
config.clientInterface.server.server = server;
// Create a session store.
if (options.useRedisSessionStore) {
setupRedisClients();
config.clientInterface.sessions.store = new RedisSessionStore({
client: redisClients.other
});
} else {
config.clientInterface.sessions.store = new MemorySessionStore();
}
// Add minimal configuration to the Express application: just the cookie and
// session middleware.
app.use(express.cookieParser(config.clientInterface.sessions.cookieSecret));
app.use(express.session({
cookie: {
httpOnly: true
},
key: config.clientInterface.sessions.cookieKey,
secret: config.clientInterface.sessions.cookieSecret,
store: config.clientInterface.sessions.store
}));
// Middleware and routes might be added here or after Thywill launches. Either
// way is just fine and won't interfere with Thywill's use of Express to serve
// resources. e.g. adding a catch-all here is acceptable:
app.all('*', function (req, res, next) {
res.statusCode = 404;
res.send('No such resource.');
});
}
// Not using Express, so a vanilla http.Server with no session management is
// set up.
else {
config.clientInterface.server.server = http.createServer().listen(port);
}
// ------------------------------------------------------------
// Set up Socket.IO store.
// ------------------------------------------------------------
// Are we using Redis?
if (options.useRedisSocketStore) {
// Create a RedisStore for Socket.IO.
setupRedisClients();
config.clientInterface.socketConfig.global.store = new RedisSocketStore({
redisPub: redisClients.pub,
redisSub: redisClients.sub,
redisClient: redisClients.other
});
} else {
// Create a MemoryStore for Socket.IO.
config.clientInterface.socketConfig.global.store = new MemorySocketStore();
}
// ------------------------------------------------------------
// Set up cluster.
// ------------------------------------------------------------
config.cluster.localClusterMemberId = options.localClusterMemberId;
if (config.cluster.implementation.name === 'redisCluster') {
setupRedisClients();
config.cluster.communication.publishRedisClient = redisClients.pub;
config.cluster.communication.subscribeRedisClient = redisClients.sub;
}
// ------------------------------------------------------------
// Set up resourceManager.
// ------------------------------------------------------------
if (config.resourceManager.implementation.name === 'redisResourceManager') {
setupRedisClients();
config.resourceManager.redisClient = redisClients.other;
}
// ------------------------------------------------------------
// Set up channelManager.
// ------------------------------------------------------------
if (config.channelManager && config.channelManager.implementation.name === 'redisChannelManager') {
setupRedisClients();
config.channelManager.redisClient = redisClients.other;
}
return config;
}
/**
* Add a batch to a Vows suite that launches a Thywill instance without any
* applications.
*
* @param {Object} suite
* A Vows test suite instance.
* @param {Object} options
* The various options.
*/
exports.headless.addThywillLaunchBatch = function (suite, options) {
suite.thywills = suite.thywills || [];
// Config should have only clonable things in it - no class instances, etc.
var config = setupConfig(options.config, options);
suite.addBatch({
'Launch Thywill': {
topic: function () {
Thywill.launch(config, null, this.callback);
},
'successful launch': function (error, thywill) {
assert.isNull(error);
suite.thywills.push(thywill);
assert.isTrue(suite.thywills[0] instanceof Thywill);
}
}
});
};
/**
* Create a partially formed Vows suite that launches a Thywill instance
* without any applications as the first batch. This all happens in the
* local process.
*
* {
* // Thywill configuration, without any of the class instances filled in.
* config: object
* // Optionally name the local cluster member.
* localClusterMemberId: string
* // If true then Redis implementations are used for Socket.IO.
* UseRedisSocketStore: boolean
* // If true then Redis implementations are used for Express sessions.
* UseRedisSessionStore: boolean
* }
*
* @param {string} name
* The test suite name.
* @param {Object} options
* The various options.
* @return {Object}
* A Vows suite.
*/
exports.headless.singleInstanceVowsSuite = function (name, options) {
var suite = vows.describe(name);
options.localClusterMemberId = options.localClusterMemberId || 'alpha';
exports.headless.addThywillLaunchBatch(suite, options);
return suite;
};
/**
* Create a partially formed Vows suite that launches two Thywill instances
* without any applications in the initial batches. This all happens in the
* local process.
*
* @param {string} name
* The test suite name.
* @param {Object} options
* The various options.
* @return {Object}
* A Vows suite.
*/
exports.headless.clusterVowsSuite = function (name, options) {
var suite = vows.describe(name);
var optionsAlpha = clone(options);
optionsAlpha.localClusterMemberId = 'alpha';
exports.headless.addThywillLaunchBatch(suite, optionsAlpha);
var optionsBeta = clone(options);
optionsBeta.localClusterMemberId = 'beta';
exports.headless.addThywillLaunchBatch(suite, optionsBeta);
return suite;
};
/**
* Add batches to a test suite.
*
* @param {Object} suite
* A Vows test suite instance.
* @param {String} name
* The name of the file in this directory containing the batch-adding
* functions, for use in a require() call.
* @param {String} property
* The property that will contain the batch-adding function.
*/
exports.headless.addBatches = function (suite, name, property) {
var fn = require('./' + name)[property];
fn(suite);
};
// ------------------------------------------------------------------------
// Related to testing against application child processes.
// ------------------------------------------------------------------------
// On the use of child processes to test the applications:
//
// This is done because there are odd issues with the use of the Redis-based
// Socket.IO store when running multiple Thywill servers in the same process.
// It is quicker to work around that with child processes for end-to-end web
// and socket tests than to head on down the rabbit hole of figuring out
// exactly what the problem is.
//
// This issue only impacts socket.io connections, and only with two or more
// Thywill instances in the same process. When trying to connect, the
// connection hangs, as the connection event is only emitted in the general
// namespace, not in the application namespace as it should be. Again, this
// only happens when two or more Thywill instances run in the same process.
exports.application = {};
/**
* Launch one of the applications in a child process.
*
* Data has the form:
*
* {
* application: string
* port: number
* clusterMemberId: string
* }
*
* @param {object} data
* @param {Function} callback
*/
function launchApplicationInChildProcess (data, callback) {
var args = JSON.stringify(data);
args = new Buffer(args, 'utf8').toString('base64');
var child = childProcess.fork(
path.join(__dirname, 'launchApplicationInstance.js'),
[args],
{
// Pass over all of the environment.
env: process.ENV,
// Share stdout.
silent: false
}
);
// Helper functions added to the child process instance.
child.onUnexpectedExit = function (code, signal) {
throw new Error('Child process running application terminated with code: ' + code);
};
child.shutdown = function () {
this.removeListener('exit', this.onUnexpectedExit);
this.kill('SIGTERM');
};
// Make sure the child process dies along with this parent process insofar
// as is possible. SIGTERM listener shouldn't be needed here for Vows test
// processes, but leave it in anyway.
process.on('SIGTERM', function () {
child.shutdown();
});
process.on('exit', function () {
child.shutdown();
});
// Kind of ugly. TODO: replace with Domains or something better.
process.once('uncaughtException', function (error) {
child.shutdown();
// If this was the last of the listeners, then rethrow the error.
if (process.listeners('uncaughtException').length === 0) {
throw error;
}
});
// If the child process dies, then we have a problem.
child.on('exit', child.onUnexpectedExit);
// Listen for one message from the application child process - used to wait
// for its Thywill startup to be done.
child.once('message', function (message) {
if (message === 'complete') {
callback(null, child);
} else {
callback(message);
}
});
}
/**
* Obtain a Vows suite in which the first batches involve launching one or
* more child processes running one of the example applications, and then
* starting up work-already package clients - but not actually connecting
* via Socket.IO.
*
* @see exports.application.vowsSuite
*/
exports.application.vowsSuitePendingConnections = function (name, options) {
var suite = vows.describe(name);
if (!Array.isArray(options.processData)) {
options.processData = [options.processData];
}
// Add batches to launch the child processes.
options.processData.forEach(function (data, index, array) {
data.application = options.applicationName;
var batchContent = {
topic: function () {
launchApplicationInChildProcess(data, this.callback);
},
'child process launched': function (error, child) {
assert.isNull(error);
assert.isObject(child);
suite.childProcesses = suite.childProcesses || [];
suite.childProcessData = suite.childProcessData || [];
suite.childProcesses.push(child);
suite.childProcessData.push(data);
}
};
var batchName = 'Launch application: ' + options.applicationName + ' port: ' + data.port + ' clusterMemberId: ' + data.clusterMemberId;
var batch = {};
batch[batchName] = batchContent;
suite.addBatch(batch);
});
// Add batches to set up work-already clients and get them connected via
// Socket.IO to these child process Thywill instances.
options.processData.forEach(function (data, index, array) {
var batchContent, batchName, batch;
// 1) Create client.
batchContent = {
topic: function () {
return new Client({
server: {
host: 'localhost',
port: data.port,
protocol: 'http'
},
sockets: {
defaultNamespace: '/' + options.applicationName,
defaultTimeout: options.defaultTimeout
}
});
},
'client created': function (client) {
suite.clients = suite.clients || [];
suite.clients[index] = client;
}
};
batchName = 'Create work-already client for clusterMemberId: ' + data.clusterMemberId;
batch = {};
batch[batchName] = batchContent;
suite.addBatch(batch);
// 2) Get application page.
batchContent = {
topic: function () {
suite.clients[index].action('/' + options.applicationName + '/', this.callback);
},
'page fetched': function (error, page) {
assert.isNull(error);
assert.isObject(page);
assert.strictEqual(page.statusCode, 200);
assert.isString(page.body);
options.pageMatches.forEach(function (pageMatch, index, array) {
assert.include(page.body, pageMatch);
});
}
};
batchName = 'Load application page from clusterMemberId: ' + data.clusterMemberId;
batch = {};
batch[batchName] = batchContent;
suite.addBatch(batch);
});
return suite;
};
/**
* Obtain a Vows suite in which the first batches involve launching one or
* more child processes running one of the example applications, and then
* starting up work-already package clients and connecting via Socket.IO.
*
* The options object has the form:
*
* {
* // Name of the application to run.
* applicationName: string,
* // The default timeout for operations in milliseconds.
* defaultTimeout: number,
* // When the application page loads, these strings must be matched in it.
* pageMatches: [
* string,
* string,
* ...
* ]
* // Details on the child processes to be launched.
* processData: [
* {
* port: 10078,
* clusterMemberId: 'alpha'
* },
* {
* port: 10079,
* clusterMemberId: 'beta'
* },
* ...
* ]
* }
*
* @param {string} name
* The Vows suite name.
* @param {object} options
* Options for the setup.
* @return {object}
* A Vows suite.
*/
exports.application.vowsSuite = function(name, options) {
var suite = exports.application.vowsSuitePendingConnections(name, options);
// Add batches to connect via Socket.IO.
options.processData.forEach(function (data, index, array) {
var batchContent = {
topic: function () {
suite.clients[index].action({
type: 'connect',
socketConfig: {
'resource': options.applicationName + '/socket.io'
}
}, this.callback);
},
'connected': function (error, page) {
var client = suite.clients[index];
assert.isNull(error);
assert.isObject(client.page.sockets);
assert.isObject(client.page.sockets[client.config.sockets.defaultNamespace]);
}
};
var batchName = 'Connect via Socket.IO to clusterMemberId: ' + data.clusterMemberId;
var batch = {};
batch[batchName] = batchContent;
suite.addBatch(batch);
});
return suite;
};
/**
* Add a batch to ensure that work-already clients are shut down and child
* application processes go away, regardless of how the rest of the tests
* progress.
*
* @param {object} suite
* A Vows test suite instance.
*/
exports.application.closeVowsSuite = function (suite) {
suite.addBatch({
'Close clients': {
topic: function () {
if (Array.isArray(suite.clients) && suite.clients.length) {
async.forEach(suite.clients, function (client, asyncCallback) {
client.action({ type: 'unload' }, asyncCallback);
}, this.callback);
} else {
this.callback();
}
},
'clients unloaded': function (error) {
assert.typeOf(error, 'undefined');
}
}
});
suite.addBatch({
'Shut down application child processes': {
topic: function () {
if (Array.isArray(suite.childProcesses)) {
suite.childProcesses.forEach(function (child, index, array) {
// Use the helper method we added to the child.
child.shutdown();
});
}
return true;
},
'shutdown complete': function () {}
}
});
};
/**
* Perform an optional action with one child process Thywill instance, and wait
* for an emitted response from one or more instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to use for the action, or
* // undefined if no action is to be taken.
* actionIndex: number | undefined,
* // An action definition or undefined if no action is to be taken.
* action: object | undefined,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number|array,
* // The Message instance or message data expected in the response. This is
* // only checked if no vows are provided.
* responseMessage: mixed,
* // Optional vows functions to run tests, keyed by display name. Signatures
* // are function (error, results).
* vows: object
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addActionAndAwaitResponsesBatch = function (batchName, suite, options) {
if (!Array.isArray(options.responseIndexes)) {
options.responseIndexes = [options.responseIndexes];
}
// If the action is connect, and the actionIndex is included in the response
// indexes, switch it to connectAndAwaitEmit, because otherwise we'll
// probably miss the emitted response.
//
// This requires further if-statements further down in this method as well.
if (
options.action &&
options.action.type === 'connect' &&
options.responseIndexes.indexOf(options.actionIndex) !== -1
) {
options.action.type = 'connectAndAwaitEmit';
options.action.eventType = 'toClient';
}
// Put the default vow in place if no vows are provided. This checks the
// provided action.responseMessage against what is actually received.
options.vows = options.vows || {
'expected responses emitted': function (unusedError, results) {
var args = [
options.applicationId,
exports.wrapAsMessage(options.responseMessage).toObject()
];
// Strip out nulls, e.g. if waiting on index 0 and 2, then 1 will be
// empty.
results = results.filter(function (result, index, array) {
return result;
});
results.forEach(function (result, index, array) {
assert.isNull(result.error);
assert.deepEqual(result.socketEvent.args, args);
});
}
};
var batch = {};
var definition = {
topic: function () {
var self = this;
var results = [];
// Helper function; get all results before callback is called.
var addResult = function (index, error, socketEvent) {
results[index] = {
error: error,
socketEvent: socketEvent
};
var incomplete = options.responseIndexes.some(function (responseIndex, index, array) {
return (typeof results[responseIndex] !== 'object');
});
if (!incomplete) {
self.callback(null, results);
}
};
// Note the filtering - don't wait on emit if we're already running
// connectAndAwaitEmit, and the actionIndex is also one of the
// responseIndexes. We're already waiting on an emitted event in that
// case.
options.responseIndexes.filter(function (responseIndex, index, array) {
return (
!options.action ||
options.action.type !== 'connectAndAwaitEmit' ||
responseIndex !== options.actionIndex
);
}).forEach(function (responseIndex, index, array) {
suite.clients[responseIndex].action({
type: 'awaitEmit',
eventType: 'toClient'
}, function (error, socketEvent) {
addResult(responseIndex, error, socketEvent);
});
});
// If we're taking an action, then take it.
if (options.action) {
// If this is connectAndAwaitEmit, and the actionIndex is one of the
// responseIndexes, then we have to pass the result along.
if (
options.action.type === 'connectAndAwaitEmit' &&
options.responseIndexes.indexOf(options.actionIndex) !== -1
) {
suite.clients[options.actionIndex].action(options.action, function (error, socketEvent) {
addResult(options.actionIndex, error, socketEvent);
});
}
// Otherwise we discard the callback for the action as uninteresting
// unless it errors.
else {
suite.clients[options.actionIndex].action(options.action, function (error) {
if (error) {
console.error(error.stack || error.toString());
}
});
}
}
}
};
for (var prop in options.vows) {
definition[prop] = options.vows[prop];
}
batch[batchName] = definition;
suite.addBatch(batch);
};
/**
* Perform an optional action with one child process Thywill instance, and wait
* to confirm that there is no emitted response event of a specific type within
* the timeout period arriving from one or more instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to use for the action, or
* // undefined if no action is to be taken.
* actionIndex: number | undefined,
* // An action definition or undefined if no action is to be taken.
* action: object | undefined,
* // Which Thywill instances / client instances expect to have no response.
* responseIndexes: number|array,
* // An optional function that checks the message in the response. If it
* // returns true, then that message counts and the test failed. Otherwise
* // the message is ignored. If no function is provided, then every message
* // counts.
* matchResponseMessage: function (eventName, data ...) { return true; }
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addActionAndConfirmNoResponsesBatch = function (batchName, suite, options) {
if (!Array.isArray(options.responseIndexes)) {
options.responseIndexes = [options.responseIndexes];
}
// If the action is connect, and the actionIndex is included in the response
// indexes, switch it to connectAndConfirmNoMatchingEmit, because otherwise
// we'll probably miss any immediately emitted response.
//
// This requires further if-statements further down in this method as well.
if (
options.action &&
options.action.type === 'connect' &&
options.responseIndexes.indexOf(options.actionIndex) !== -1
) {
options.action.type = 'connectAndConfirmNoMatchingEmit';
options.action.eventType = 'toClient';
}
var batch = {};
var definition = {
topic: function () {
var self = this;
var results = [];
// Helper function; get all results before callback is called.
var addResult = function (index, error) {
results[index] = {
error: error
};
var incomplete = options.responseIndexes.some(function (responseIndex, index, array) {
return (typeof results[responseIndex] !== 'object');
});
if (!incomplete) {
self.callback(null, results);
}
};
// Note the filtering - don't confirm no emit if we're already running
// connectAndConfirmNoMatchingEmit, and the actionIndex is also one of the
// responseIndexes. We're already checking for no emitted event in that
// case.
options.responseIndexes.filter(function (responseIndex, index, array) {
return (
!options.action ||
options.action.type !== 'connectAndConfirmNoMatchingEmit' ||
responseIndex !== options.actionIndex
);
}).forEach(function (responseIndex, index, array) {
suite.clients[responseIndex].action({
type: 'confirmNoMatchingEmit',
eventType: 'toClient',
match: options.matchResponseMessage
}, function (error) {
addResult(responseIndex, error);
});
});
// If we're taking an action, then take it.
if (options.action) {
// If this is connectAndAwaitEmit, and the actionIndex is one of the
// responseIndexes, then we have to pass the result along.
if (
options.action.type === 'connectAndConfirmNoMatchingEmit' &&
options.responseIndexes.indexOf(options.actionIndex) !== -1
) {
suite.clients[options.actionIndex].action(options.action, function (error) {
addResult(options.actionIndex, error);
});
}
// Otherwise we discard the callback for the action as uninteresting
// unless it errors.
else {
suite.clients[options.actionIndex].action(options.action, function (error) {
if (error) {
console.error(error.stack || error.toString());
}
});
}
}
}
};
// The vow for this definition. Pretty simple, as we're looking for an
// absence of things.
definition['no responses emitted'] = function (unusedError, results) {
results.forEach(function (result, index, array) {
assert.isNull(result.error);
});
};
batch[batchName] = definition;
suite.addBatch(batch);
};
/**
* Send a message to a child process Thywill instance, and wait for a response
* on one or more instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to use for sending.
* actionIndex: number,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number | array,
* // The Message instance or message data to send.
* sendMessage: mixed,
* // The Message instance or message data expected in the response.
* responseMessage: mixed,
* // Optional vows functions to run tests, keyed by display name. Signatures
* // are function (error, results).
* vows: object
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addSendAndAwaitResponsesBatch = function (batchName, suite, options) {
var message = exports.wrapAsMessage(options.sendMessage);
options.action = {
type: 'emit',
args: ['fromClient', options.applicationId, message]
};
exports.application.addActionAndAwaitResponsesBatch(batchName, suite, options);
};
/**
* Send a message to a child process Thywill instance, and wait for a response
* on one or more instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to use for sending.
* actionIndex: number,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number | array,
* // The Message instance or message data to send.
* sendMessage: mixed,
* // An optional function that checks the message in the response. If it
* // returns true, then that message counts and the test failed. Otherwise
* // the message is ignored. If no function is provided, then every message
* // counts.
* matchResponseMessage: function (eventName, data ...) { return true; }
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addSendAndConfirmNoResponsesBatch = function (batchName, suite, options) {
var message = exports.wrapAsMessage(options.sendMessage);
options.action = {
type: 'emit',
args: ['fromClient', options.applicationId, message]
};
exports.application.addActionAndConfirmNoResponsesBatch(batchName, suite, options);
};
/**
* Disconnect from one Thywill instance and wait on a response from one or more
* other instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to disconnect form.
* actionIndex: number,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number | array,
* // The Message instance or message data expected in the response.
* responseMessage: mixed,
* // Optional vows functions to run tests, keyed by display name. Signatures
* // are function (error, results).
* vows: object
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addDisconnectAndAwaitResponsesBatch = function (batchName, suite, options) {
var message = exports.wrapAsMessage(options.sendMessage);
options.action = {
type: 'unload'
};
exports.application.addActionAndAwaitResponsesBatch(batchName, suite, options);
};
/**
* Connect to one Thywill instance and wait on a message from one or more other
* instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instance / client instance to connect to.
* actionIndex: number,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number | array,
* // The Message instance or message data expected in the response.
* responseMessage: mixed,
* // Optional vows functions to run tests, keyed by display name. Signatures
* // are function (error, results).
* vows: object
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addConnectAndAwaitResponsesBatch = function (batchName, suite, options) {
options.action = {
type: 'connect',
socketConfig: {
'resource': options.applicationId + '/socket.io'
}
};
exports.application.addActionAndAwaitResponsesBatch(batchName, suite, options);
};
/**
* Wait on a message from one or more instances.
*
* Options has the form:
*
* {
* // Which application this involves.
* applicationId: string,
* // Which Thywill instances / client instances expect the response.
* responseIndexes: number | array,
* // The Message instance or message data expected in the response.
* responseMessage: mixed,
* // Optional vows functions to run tests, keyed by display name. Signatures
* // are function (error, results).
* vows: object
* }
*
* @param {string} batchName
* Name of the batch.
* @param {object} suite
* A Vows test suite instance.
* @param {object} options
* The rest of the needed parameters.
*/
exports.application.addAwaitResponsesBatch = function (batchName, suite, options) {
options.action = undefined;
options.actionIndex = undefined;
exports.application.addActionAndAwaitResponsesBatch(batchName, suite, options);
};
// ------------------------------------------------
// Utilities.
// ------------------------------------------------
/**
* For associating RPC messages and responses.
*/
exports.rpcMessageId = 0;
/**
* Create an RPC Message instance. Data has the form:
*
* {
* // Name of the remote function.
* name: string
* // Does the remote function have a callback?
* cb: boolean
* // Arguments for the function, lacking the final callback.
* args: array
* }
*
* @param {object} data
* Data for the message.
* @return {Message}
* A Message instance.
*/
exports.createRpcMessage = function (data) {
var rpcId = exports.rpcMessageId++;
var sendData = {
id: rpcId,
name: data.name,
cb: data.hasCallback,
args: data.args
};
var message = new Message(sendData);
message.setType(Message.TYPES.RPC);
return message;
};
/**
* Create a response message of the sort returned by an RPC call.
*
* @param {mixed} id
* Must match the ID of the sent RPC message.
* @param {array} args
* Returned arguments as for a callback with leading null if successful.
* @return {Message}
* A Message instance.
*/
exports.createRpcResponseMessage = function (id, args) {
var message = new Message({
id: id,
cbArgs: args
});
message.setType(Message.TYPES.RPC);
return message;
};
/**
* Ensure message data is wrapped in a Message instance.
*
* @param {mixed|Message} message
* The message data.
*/
exports.wrapAsMessage = function (message) {
if (!(message instanceof Message)) {
message = new Message(message);
}
return message;
};
/**
* Add a batch that does nothing but delay.
*
* @param {object} suite
* A Vows suite.
* @param {number} delay
* Delay in milliseconds.
*/
exports.addDelayBatch = function (suite, delay) {
var batchContents = {
topic: function () {
setTimeout(this.callback, delay);
},
'delayed': function () {}
};
var batchName = 'Delay by ' + delay + 'ms';
var batch = {};
batch[batchName] = batchContents;
suite.addBatch(batch);
};