Files
taimed/node_modules/testswarm/lib/testswarm.js
2025-07-24 17:21:45 +08:00

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" )
};