282 lines
7.3 KiB
JavaScript
282 lines
7.3 KiB
JavaScript
var request = require( "request" ),
|
|
querystring = require( "querystring" ).stringify,
|
|
url = require( "url" ),
|
|
util = require( "util" ),
|
|
events = require( "events" );
|
|
|
|
function extend( a, b ) {
|
|
for ( var key in b ) {
|
|
a[key] = b[key];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* Utility for polling the API.
|
|
* @param {Object} params
|
|
* - {number} interval
|
|
* - {number} timeout
|
|
* - {Function} makeRequest
|
|
* - {Function} doContinue
|
|
* - {Function} onSuccess [optional] Called when continue returns false.
|
|
* - {Function} onTimeout [optional] Called if continue returns true after timeout is reached.
|
|
*/
|
|
function pollJSON( params ) {
|
|
var tsStart = Date.now();
|
|
|
|
params = extend({
|
|
onSuccess: function() {},
|
|
onTimeout: function() {}
|
|
}, params );
|
|
|
|
function handleRequest( err, resp, body ) {
|
|
var data;
|
|
if ( err ) {
|
|
throw err;
|
|
}
|
|
try {
|
|
data = JSON.parse( body );
|
|
} catch ( e ) {
|
|
throw e;
|
|
}
|
|
if ( params.doContinue( data ) ) {
|
|
if ( tsStart + params.timeout < Date.now() ) {
|
|
params.onTimeout( data );
|
|
} else {
|
|
setTimeout( function() {
|
|
params.makeRequest( handleRequest );
|
|
}, params.interval );
|
|
}
|
|
} else {
|
|
params.onSuccess( data );
|
|
}
|
|
}
|
|
|
|
params.makeRequest( handleRequest );
|
|
}
|
|
|
|
function isJobDone( job ) {
|
|
return !job.runs.filter(function( run ) {
|
|
return Object.keys(run.uaRuns).filter(function( uaRun ) {
|
|
return (/new|progress/).test( run.uaRuns[ uaRun ].runStatus );
|
|
}).length;
|
|
}).length;
|
|
}
|
|
|
|
/**
|
|
* @param {TestSwarm} testswarm
|
|
* @param {Object} options From TestSwarm..addjob.
|
|
* @param {Object} jobInfo
|
|
* @param {Function( {mixed} err, {Object} job )} callback
|
|
*/
|
|
function pollJobStatus( testswarm, options, jobInfo, callback ) {
|
|
var config = testswarm.config,
|
|
i = 1;
|
|
pollJSON({
|
|
interval: options.pollInterval,
|
|
timeout: options.timeout,
|
|
makeRequest: function( handleRequest ) {
|
|
request.get( config.url + "/api.php?" + querystring({
|
|
action: "job",
|
|
item: jobInfo.id
|
|
}), handleRequest);
|
|
},
|
|
doContinue: function( data ) {
|
|
if ( !data.job ) {
|
|
throw new Error( "Invalid API response, can't continue. Response was: " + data );
|
|
}
|
|
testswarm.emit( "addjob-poll", ++i );
|
|
return !isJobDone( data.job );
|
|
},
|
|
onTimeout: function( data ) {
|
|
callback( "Timed out after " + options.timeout + "ms", data.job );
|
|
},
|
|
onSuccess: function( data ) {
|
|
callback( null, data.job );
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get url to root of testswarm server (without the path to the installation, if any).
|
|
*
|
|
* @param {TestSwarm} testswarm
|
|
* @return {string} Domain root of url.
|
|
*/
|
|
function getServerRoot( testswarm ) {
|
|
var obj = url.parse( testswarm.config.url );
|
|
obj.href = obj.pathname = obj.search = obj.path = obj.query = obj.hash = undefined;
|
|
// Normalise url to no trailing slash
|
|
return url.format( obj ).replace( /\/$/, "" );
|
|
}
|
|
|
|
/**
|
|
* Add authentication parameters to request data.
|
|
*
|
|
* @param {TestSwarm} testswarm
|
|
* @param {Object} data
|
|
* @return {Object} data
|
|
*/
|
|
function authenticateRequest( testswarm, data ) {
|
|
if ( !testswarm.auth ) {
|
|
throw new Error( "Authentication required, call .auth() first." );
|
|
}
|
|
data.authID = testswarm.auth.id;
|
|
data.authToken = testswarm.auth.token;
|
|
return data;
|
|
}
|
|
|
|
function keysMissingFrom( obj, keys ) {
|
|
return keys.filter(function( val ) {
|
|
if ( obj[ val ] === undefined ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @extends EventEmitter
|
|
* @param {Object} config
|
|
* - {string} url Url to root of TestSwarm install.
|
|
* @emits {string} log Regular output regarding progress of performed actions.
|
|
* @emits {string} verbose Verbose output for debugging.
|
|
*/
|
|
function TestSwarm( config ) {
|
|
var missing = keysMissingFrom( config, [ "url" ] );
|
|
if ( missing.length ) {
|
|
throw new Error( "Invalid configuration, missing key '" + missing[0] + "'." );
|
|
}
|
|
|
|
// Normalise url to no trailing slash
|
|
config.url = url.format( url.parse( config.url ) ).replace( /\/$/, "" );
|
|
this.config = config;
|
|
}
|
|
|
|
util.inherits( TestSwarm, events.EventEmitter );
|
|
|
|
TestSwarm.prototype.addReporter = function( reporter ) {
|
|
reporter( this );
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @param {Object} auth
|
|
* - {string} id Username of TestSwarm account.
|
|
* - {string} token Authentication token of account.
|
|
*/
|
|
TestSwarm.prototype.auth = function( auth ) {
|
|
var missing = keysMissingFrom( auth, [ "id", "token" ] );
|
|
if ( missing.length ) {
|
|
throw new Error( "Invalid authentication, missing key '" + missing[0] + "'." );
|
|
}
|
|
|
|
this.auth = auth;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @method
|
|
* @param {Object} options
|
|
* - {string} name
|
|
* - {number} runMax [optional]
|
|
* - {Object} runs Run urls by run name.
|
|
* - {Array|string} browserSets
|
|
* - {number} pollInterval [optional] In milliseconds.
|
|
* - {number} timeout [optional] In milliseconds.
|
|
* @param {Function( {mixed} err, {boolean} passed, {Object} results )} callback
|
|
* @emits addjob-created ( {Object} jobInfo )
|
|
* @emits addjob-poll ( {number} iteration ) Starts with 1.
|
|
* @emits addjob-ready ( {boolean} passsed, {Object} results )
|
|
*/
|
|
TestSwarm.prototype.addjob = function( options, callback ) {
|
|
var testswarm, missing, runNames, runUrls, browserSets;
|
|
|
|
testswarm = this;
|
|
missing = keysMissingFrom( options, [ "name", "runs", "browserSets" ] );
|
|
if ( missing.length ) {
|
|
callback( "Invalid options, missing key '" + missing[0] + "'." );
|
|
return;
|
|
}
|
|
|
|
options = extend({
|
|
runMax: 2,
|
|
pollInterval: 5000, // 5 seconds
|
|
timeout: 15 * 60 * 1000 // 15 minutes
|
|
}, options);
|
|
|
|
runNames = Object.keys( options.runs );
|
|
runUrls = runNames.slice().map(function( name ) {
|
|
return options.runs[name];
|
|
});
|
|
|
|
browserSets = Array.isArray( options.browserSets ) ?
|
|
options.browserSets :
|
|
[ options.browserSets ];
|
|
|
|
request.post({
|
|
url: testswarm.config.url + "/api.php",
|
|
form: authenticateRequest( testswarm, {
|
|
action: "addjob",
|
|
jobName: options.name,
|
|
runMax: options.runMax,
|
|
runNames: runNames,
|
|
runUrls: runUrls,
|
|
browserSets: browserSets
|
|
})
|
|
}, function( err, resp, body ) {
|
|
var data, jobInfo;
|
|
if ( err ) {
|
|
callback( err, false );
|
|
return;
|
|
}
|
|
try {
|
|
data = JSON.parse( body );
|
|
} catch ( e ) {
|
|
callback( "Failed parsing body as json, was: " + body, false );
|
|
}
|
|
if ( !data.addjob ) {
|
|
callback( "API returned error, can't continue. Response was: " + body, false );
|
|
return;
|
|
}
|
|
jobInfo = data.addjob;
|
|
testswarm.emit( "log", "Submitted job " + jobInfo.id + " " + testswarm.config.url + "/job/" + jobInfo.id );
|
|
testswarm.emit( "log", "Polling for results" );
|
|
testswarm.emit( "verbose", JSON.stringify( jobInfo ) );
|
|
testswarm.emit( "addjob-created", jobInfo );
|
|
pollJobStatus( testswarm, options, jobInfo, function( err, job ) {
|
|
var passed = true, results = {};
|
|
|
|
function runStats( uaID, uaRun ) {
|
|
var val = uaRun.runStatus;
|
|
if ( uaRun.runResultsUrl ) {
|
|
val += " (" + uaRun.runResultsLabel + "). " + getServerRoot( testswarm ) + uaRun.runResultsUrl;
|
|
}
|
|
return val;
|
|
}
|
|
|
|
job.runs.filter(function( run ) {
|
|
results[run.info.name] = {};
|
|
for ( var uaID in run.uaRuns ) {
|
|
results[run.info.name][uaID] = runStats( uaID, run.uaRuns[uaID] );
|
|
if ( passed && run.uaRuns[uaID].runStatus !== "passed" ) {
|
|
// "new", "failed", "error", "timedout", ..
|
|
passed = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
testswarm.emit( "addjob-ready", err, passed, results );
|
|
callback( err, passed, results );
|
|
} );
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
createClient: function( config ) {
|
|
return new TestSwarm( config );
|
|
},
|
|
reporters: require( "./reporters" )
|
|
};
|