From 3efa9d20302c67aba9024555087a090c729dbb80 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 28 Apr 2014 08:41:24 -0700 Subject: [PATCH] Remove IIFE in test/saucelabs.js. --- test/saucelabs.js | 863 +++++++++++++++++++++++----------------------- 1 file changed, 429 insertions(+), 434 deletions(-) diff --git a/test/saucelabs.js b/test/saucelabs.js index 3b8bfeb15..4f3988f24 100644 --- a/test/saucelabs.js +++ b/test/saucelabs.js @@ -1,467 +1,462 @@ #!/usr/bin/env node -;(function() { - 'use strict'; +'use strict'; - /** Environment shortcut */ - var env = process.env; +/** Environment shortcut */ +var env = process.env; - if (isFinite(env.TRAVIS_PULL_REQUEST)) { - console.log('Skipping Sauce Labs jobs for pull requests'); - process.exit(0); - } +if (isFinite(env.TRAVIS_PULL_REQUEST)) { + console.log('Skipping Sauce Labs jobs for pull requests'); + process.exit(0); +} - /** Load Node.js modules */ - var EventEmitter = require('events').EventEmitter, - http = require('http'), - path = require('path'), - url = require('url'); +/** Load Node.js modules */ +var EventEmitter = require('events').EventEmitter, + http = require('http'), + path = require('path'), + url = require('url'); - /** Load other modules */ - var _ = require('../lodash.js'), - chalk = require('chalk'), - ecstatic = require('ecstatic'), - request = require('request'), - SauceTunnel = require('sauce-tunnel-sc3-1'); +/** Load other modules */ +var _ = require('../lodash.js'), + chalk = require('chalk'), + ecstatic = require('ecstatic'), + request = require('request'), + SauceTunnel = require('sauce-tunnel-sc3-1'); - /** Used for Sauce Labs credentials */ - var accessKey = env.SAUCE_ACCESS_KEY, - username = env.SAUCE_USERNAME; +/** Used for Sauce Labs credentials */ +var accessKey = env.SAUCE_ACCESS_KEY, + username = env.SAUCE_USERNAME; - /** Used as the maximum number of times to retry a job */ - var maxRetries = 3; +/** Used as the maximum number of times to retry a job */ +var maxRetries = 3; - /** Used as the static file server middleware */ - var mount = ecstatic({ - 'cache': false, - 'root': process.cwd() - }); +/** Used as the static file server middleware */ +var mount = ecstatic({ + 'cache': false, + 'root': process.cwd() +}); - /** Used as the list of ports supported by Sauce Connect */ - var ports = [ - 80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, - 3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432, - 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, - 8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221, - 55001 - ]; +/** Used as the list of ports supported by Sauce Connect */ +var ports = [ + 80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, + 3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432, + 6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, + 8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221, + 55001 +]; - /** Used by `logInline` to clear previously logged messages */ - var prevLine = ''; +/** Used by `logInline` to clear previously logged messages */ +var prevLine = ''; - /** Used to detect error messages */ - var reError = /\berror\b/i; +/** Used to detect error messages */ +var reError = /\berror\b/i; - /** Used to display the wait throbber */ - var throbberId, - throbberDelay = 500, - waitCount = -1; +/** Used to display the wait throbber */ +var throbberDelay = 500, + waitCount = -1; - /** Used as Sauce Labs config values */ - var advisor = getOption('advisor', true), - build = getOption('build', env.TRAVIS_COMMIT.slice(0, 10)), - compatMode = getOption('compatMode', null), - customData = Function('return {' + getOption('customData', '').replace(/^\{|}$/g, '') + '}')(), - framework = getOption('framework', 'qunit'), - idleTimeout = getOption('idleTimeout', 180), - jobName = getOption('name', 'unit tests'), - maxDuration = getOption('maxDuration', 360), - port = ports[Math.min(_.sortedIndex(ports, getOption('port', 9001)), ports.length - 1)], - publicAccess = getOption('public', true), - recordVideo = getOption('recordVideo', true), - recordScreenshots = getOption('recordScreenshots', false), - runner = getOption('runner', 'test/index.html').replace(/^\W+/, ''), - runnerUrl = getOption('runnerUrl', 'http://localhost:' + port + '/' + runner), - statusInterval = getOption('statusInterval', 5000), - tags = getOption('tags', []), - tunneled = getOption('tunneled', true), - tunnelId = getOption('tunnelId', 'tunnel_' + env.TRAVIS_JOB_NUMBER), - tunnelTimeout = getOption('tunnelTimeout', 10000), - videoUploadOnPass = getOption('videoUploadOnPass', false); +/** Used as Sauce Labs config values */ +var advisor = getOption('advisor', true), + build = getOption('build', env.TRAVIS_COMMIT.slice(0, 10)), + compatMode = getOption('compatMode', null), + customData = Function('return {' + getOption('customData', '').replace(/^\{|}$/g, '') + '}')(), + framework = getOption('framework', 'qunit'), + idleTimeout = getOption('idleTimeout', 180), + jobName = getOption('name', 'unit tests'), + maxDuration = getOption('maxDuration', 360), + port = ports[Math.min(_.sortedIndex(ports, getOption('port', 9001)), ports.length - 1)], + publicAccess = getOption('public', true), + recordVideo = getOption('recordVideo', true), + recordScreenshots = getOption('recordScreenshots', false), + runner = getOption('runner', 'test/index.html').replace(/^\W+/, ''), + runnerUrl = getOption('runnerUrl', 'http://localhost:' + port + '/' + runner), + statusInterval = getOption('statusInterval', 5000), + tags = getOption('tags', []), + tunneled = getOption('tunneled', true), + tunnelId = getOption('tunnelId', 'tunnel_' + env.TRAVIS_JOB_NUMBER), + tunnelTimeout = getOption('tunnelTimeout', 10000), + videoUploadOnPass = getOption('videoUploadOnPass', false); - /** Used to convert Sauce Labs browser identifiers to their formal names */ - var browserNameMap = { - 'googlechrome': 'Chrome', - 'iehta': 'Internet Explorer' - }; +/** Used to convert Sauce Labs browser identifiers to their formal names */ +var browserNameMap = { + 'googlechrome': 'Chrome', + 'iehta': 'Internet Explorer' +}; - /** List of platforms to load the runner on */ - var platforms = [ - ['Linux', 'android', '4.3'], - ['OS X 10.9', 'iphone', '7.1'], - ['OS X 10.9', 'ipad', '7.1'], - ['Windows 8.1', 'googlechrome', '34'], - ['Windows 8.1', 'googlechrome', '33'], - ['Windows 8.1', 'firefox', '28'], - ['Windows 8.1', 'firefox', '27'], - ['Windows 8.1', 'firefox', '20'], - ['Windows 8.1', 'firefox', '3.0'], +/** List of platforms to load the runner on */ +var platforms = [ + ['Linux', 'android', '4.3'], + ['OS X 10.9', 'iphone', '7.1'], + ['OS X 10.9', 'ipad', '7.1'], + ['Windows 8.1', 'googlechrome', '34'], + ['Windows 8.1', 'googlechrome', '33'], + ['Windows 8.1', 'firefox', '28'], + ['Windows 8.1', 'firefox', '27'], + ['Windows 8.1', 'firefox', '20'], + ['Windows 8.1', 'firefox', '3.0'], + ['Windows 8.1', 'internet explorer', '11'], + ['Windows 8', 'internet explorer', '10'], + ['Windows 7', 'internet explorer', '9'], + ['Windows 7', 'internet explorer', '8'], + ['Windows XP', 'internet explorer', '7'], + ['Windows XP', 'internet explorer', '6'], + ['Windows 7', 'opera', '12'], + ['Windows 7', 'opera', '11'], + ['OS X 10.9', 'safari', '7'], + ['OS X 10.8', 'safari', '6'], + ['OS X 10.6', 'safari', '5'] +]; + +/** Used to tailor the `platforms` array */ +var runnerQuery = url.parse(runner, true).query, + isBackbone = /\bbackbone\b/i.test(runner), + isMobile = /\bmobile\b/i.test(runnerQuery.build), + isModern = /\bmodern\b/i.test(runnerQuery.build); + +// platforms to test IE compat mode +if (compatMode) { + platforms = [ ['Windows 8.1', 'internet explorer', '11'], ['Windows 8', 'internet explorer', '10'], ['Windows 7', 'internet explorer', '9'], - ['Windows 7', 'internet explorer', '8'], - ['Windows XP', 'internet explorer', '7'], - ['Windows XP', 'internet explorer', '6'], - ['Windows 7', 'opera', '12'], - ['Windows 7', 'opera', '11'], - ['OS X 10.9', 'safari', '7'], - ['OS X 10.8', 'safari', '6'], - ['OS X 10.6', 'safari', '5'] + ['Windows 7', 'internet explorer', '8'] ]; +} +// platforms for AMD tests +if (_.contains(tags, 'amd')) { + platforms = platforms.filter(function(platform) { + var browser = platform[1], + version = +platform[2]; - /** Used to tailor the `platforms` array */ - var runnerQuery = url.parse(runner, true).query, - isBackbone = /\bbackbone\b/i.test(runner), - isMobile = /\bmobile\b/i.test(runnerQuery.build), - isModern = /\bmodern\b/i.test(runnerQuery.build); - - // platforms to test IE compat mode - if (compatMode) { - platforms = [ - ['Windows 8.1', 'internet explorer', '11'], - ['Windows 8', 'internet explorer', '10'], - ['Windows 7', 'internet explorer', '9'], - ['Windows 7', 'internet explorer', '8'] - ]; - } - // platforms for AMD tests - if (_.contains(tags, 'amd')) { - platforms = platforms.filter(function(platform) { - var browser = platform[1], - version = +platform[2]; - - if (browser == 'opera') { - return version >= 10; - } - return true; - }); - } - // platforms for Backbone tests - if (isBackbone) { - platforms = platforms.filter(function(platform) { - var browser = platform[1], - version = +platform[2]; - - switch (browser) { - case 'firefox': return version >= 4; - case 'opera': return version >= 12; - } - return true; - }); - } - // platforms for mobile and modern builds - if (isMobile || isModern) { - platforms = platforms.filter(function(platform) { - var browser = platform[1], - version = +platform[2]; - - switch (browser) { - case 'firefox': return version >= 10; - case 'internet explorer': return version >= 9; - case 'opera': return version >= 12; - case 'safari': return version >= (isMobile ? 3 : 6); - } - return true; - }); - } - - /** Used as the default `Job` options object */ - var defaultOptions = { - 'build': build, - 'custom-data': customData, - 'framework': framework, - 'idle-timeout': idleTimeout, - 'max-duration': maxDuration, - 'name': jobName, - 'public': publicAccess, - 'platforms': platforms, - 'record-screenshots': recordScreenshots, - 'record-video': recordVideo, - 'sauce-advisor': advisor, - 'tags': tags, - 'url': runnerUrl, - 'video-upload-on-pass': videoUploadOnPass - }; - - if (publicAccess === true) { - defaultOptions['public'] = 'public'; - } - if (tunneled) { - defaultOptions['tunnel-identifier'] = tunnelId; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Resolves the formal browser name for a given Sauce Labs browser identifier. - * - * @private - * @param {string} identifier The browser identifier. - * @returns {string} Returns the formal browser name. - */ - function browserName(identifier) { - return capitalizeWords(browserNameMap[identifier] || identifier); - } - - /** - * Capitalizes the first character of each word in `string`. - * - * @private - * @param {string} string The string to augment. - * @returns {string} Returns the augmented string. - */ - function capitalizeWords(string) { - return _.map(string.split(' '), _.capitalize).join(' '); - } - - /** - * Gets the value for the given option name. If no value is available the - * `defaultValue` is returned. - * - * @private - * @param {string} name The name of the option. - * @param {*} defaultValue The default option value. - * @returns {*} Returns the option value. - */ - function getOption(name, defaultValue) { - var isArr = _.isArray(defaultValue); - return _.reduce(process.argv, function(result, value) { - if (isArr) { - value = optionToArray(name, value); - return _.isEmpty(value) ? result : value; - } - value = optionToValue(name, value); - - return value == null ? result : value; - }, defaultValue); - } - - /** - * Writes an inline message to standard output. - * - * @private - * @param {string} text The text to log. - */ - function logInline(text) { - var blankLine = _.repeat(' ', _.size(prevLine)); - prevLine = text = _.truncate(text, 40); - process.stdout.write(text + blankLine.slice(text.length) + '\r'); - } - - /** - * Writes the wait throbber to standard output. - * - * @private - */ - function logThrobber() { - logInline('Please wait' + _.repeat('.', (++waitCount % 3) + 1)); - } - - /** - * Converts a comma separated option value into an array. - * - * @private - * @param {string} name The name of the option to inspect. - * @param {string} string The options string. - * @returns {Array} Returns the new converted array. - */ - function optionToArray(name, string) { - return _.compact(_.invoke((optionToValue(name, string) || '').split(/, */), 'trim')); - } - - /** - * Extracts the option value from an option string. - * - * @private - * @param {string} name The name of the option to inspect. - * @param {string} string The options string. - * @returns {string|undefined} Returns the option value, else `undefined`. - */ - function optionToValue(name, string) { - var result = (result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'))) && (result[1] ? result[1].trim() : true); - if (result === 'false') { - return false; + if (browser == 'opera') { + return version >= 10; } - return result || undefined; + return true; + }); +} +// platforms for Backbone tests +if (isBackbone) { + platforms = platforms.filter(function(platform) { + var browser = platform[1], + version = +platform[2]; + + switch (browser) { + case 'firefox': return version >= 4; + case 'opera': return version >= 12; + } + return true; + }); +} +// platforms for mobile and modern builds +if (isMobile || isModern) { + platforms = platforms.filter(function(platform) { + var browser = platform[1], + version = +platform[2]; + + switch (browser) { + case 'firefox': return version >= 10; + case 'internet explorer': return version >= 9; + case 'opera': return version >= 12; + case 'safari': return version >= (isMobile ? 3 : 6); + } + return true; + }); +} + +/** Used as the default `Job` options object */ +var defaultOptions = { + 'build': build, + 'custom-data': customData, + 'framework': framework, + 'idle-timeout': idleTimeout, + 'max-duration': maxDuration, + 'name': jobName, + 'public': publicAccess, + 'platforms': platforms, + 'record-screenshots': recordScreenshots, + 'record-video': recordVideo, + 'sauce-advisor': advisor, + 'tags': tags, + 'url': runnerUrl, + 'video-upload-on-pass': videoUploadOnPass +}; + +if (publicAccess === true) { + defaultOptions['public'] = 'public'; +} +if (tunneled) { + defaultOptions['tunnel-identifier'] = tunnelId; +} + +/*--------------------------------------------------------------------------*/ + +/** + * Resolves the formal browser name for a given Sauce Labs browser identifier. + * + * @private + * @param {string} identifier The browser identifier. + * @returns {string} Returns the formal browser name. + */ +function browserName(identifier) { + return capitalizeWords(browserNameMap[identifier] || identifier); +} + +/** + * Capitalizes the first character of each word in `string`. + * + * @private + * @param {string} string The string to augment. + * @returns {string} Returns the augmented string. + */ +function capitalizeWords(string) { + return _.map(string.split(' '), _.capitalize).join(' '); +} + +/** + * Gets the value for the given option name. If no value is available the + * `defaultValue` is returned. + * + * @private + * @param {string} name The name of the option. + * @param {*} defaultValue The default option value. + * @returns {*} Returns the option value. + */ +function getOption(name, defaultValue) { + var isArr = _.isArray(defaultValue); + return _.reduce(process.argv, function(result, value) { + if (isArr) { + value = optionToArray(name, value); + return _.isEmpty(value) ? result : value; + } + value = optionToValue(name, value); + + return value == null ? result : value; + }, defaultValue); +} + +/** + * Writes an inline message to standard output. + * + * @private + * @param {string} text The text to log. + */ +function logInline(text) { + var blankLine = _.repeat(' ', _.size(prevLine)); + prevLine = text = _.truncate(text, 40); + process.stdout.write(text + blankLine.slice(text.length) + '\r'); +} + +/** + * Writes the wait throbber to standard output. + * + * @private + */ +function logThrobber() { + logInline('Please wait' + _.repeat('.', (++waitCount % 3) + 1)); +} + +/** + * Converts a comma separated option value into an array. + * + * @private + * @param {string} name The name of the option to inspect. + * @param {string} string The options string. + * @returns {Array} Returns the new converted array. + */ +function optionToArray(name, string) { + return _.compact(_.invoke((optionToValue(name, string) || '').split(/, */), 'trim')); +} + +/** + * Extracts the option value from an option string. + * + * @private + * @param {string} name The name of the option to inspect. + * @param {string} string The options string. + * @returns {string|undefined} Returns the option value, else `undefined`. + */ +function optionToValue(name, string) { + var result = (result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'))) && (result[1] ? result[1].trim() : true); + if (result === 'false') { + return false; } + return result || undefined; +} - /*--------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------*/ - /** - * Used by the `onRun` callback to check the status of a job. - * - * @private - */ - function check() { - request.post('https://saucelabs.com/rest/v1/' + this.user + '/js-tests/status', { - 'auth': { 'user': this.user, 'pass': this.pass }, - 'json': { 'js tests': [this.id] } - }, onCheck.bind(this)); +/** + * Used by the `onRun` callback to check the status of a job. + * + * @private + */ +function check() { + request.post('https://saucelabs.com/rest/v1/' + this.user + '/js-tests/status', { + 'auth': { 'user': this.user, 'pass': this.pass }, + 'json': { 'js tests': [this.id] } + }, onCheck.bind(this)); +} + +/** + * The `request.post` callback used by `check`. + * + * @private + * @param {Object} [error] The error object. + * @param {Object} response The response data object. + * @param {Object} body The response body JSON object. + */ +function onCheck(error, response, body) { + var data = _.result(body, 'js tests', [{}])[0], + options = this.options, + platform = options.platforms[0], + result = data.result, + completed = _.result(body, 'completed'), + description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + capitalizeWords(platform[0]), + failures = _.result(result, 'failed'), + label = options.name + ':'; + + if (!completed) { + setTimeout(check.bind(this), statusInterval); + return; } - - /** - * The `request.post` callback used by `check`. - * - * @private - * @param {Object} [error] The error object. - * @param {Object} response The response data object. - * @param {Object} body The response body JSON object. - */ - function onCheck(error, response, body) { - var data = _.result(body, 'js tests', [{}])[0], - options = this.options, - platform = options.platforms[0], - result = data.result, - completed = _.result(body, 'completed'), - description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + capitalizeWords(platform[0]), - failures = _.result(result, 'failed'), - label = options.name + ':'; - - if (!completed) { - setTimeout(check.bind(this), statusInterval); + if (!result || failures || reError.test(result.message)) { + if (this.attempts < maxRetries) { + this.attempts++; + console.log(label + ' ' + description + ' retry #%d', this.attempts); + this.run(); return; } - if (!result || failures || reError.test(result.message)) { - if (this.attempts < maxRetries) { - this.attempts++; - console.log(label + ' ' + description + ' retry #%d', this.attempts); - this.run(); - return; - } - _.assign(this, data, { 'failed': true }); - var details = 'See ' + this.url + ' for details.'; + _.assign(this, data, { 'failed': true }); + var details = 'See ' + this.url + ' for details.'; - logInline(''); - if (failures) { - console.error(label + ' %s ' + chalk.red('failed') + ' %d test' + (failures > 1 ? 's' : '') + '. %s', description, failures, details); - } else { - var message = _.result(result, 'message', 'no results available. ' + details); - console.error(label, description, chalk.red('failed') + ';', message); - } - } else { - console.log(label, description, chalk.green('passed')); - } - this.emit('complete'); - } - - /** - * The `request.post` callback used by `Jobs#run`. - * - * @private - * @param {Object} [error] The error object. - * @param {Object} response The response data object. - * @param {Object} body The response body JSON object. - */ - function onRun(error, response, body) { - var id = _.result(body, 'js tests', [])[0], - statusCode = _.result(response, 'statusCode'); - - if (error || !id || statusCode != 200) { - console.error('Failed to start job; status: %d, body:\n%s', statusCode, JSON.stringify(body)); - if (error) { - console.error(error); - } - process.exit(3); - } - this.id = id; - check.call(this); - } - - /*--------------------------------------------------------------------------*/ - - /** - * The Job constructor. - * - * @private - * @param {Object} [properties] The properties to initial a job with. - */ - function Job(properties) { - EventEmitter.call(this); - _.merge(this, { 'attempts': 0, 'options': {} }, properties); - _.defaults(this.options, _.cloneDeep(defaultOptions)); - } - - Job.prototype = _.create(EventEmitter.prototype); - - /** - * Runs the job on Sauce Labs. - * - * @private - */ - Job.prototype.run = function() { - request.post('https://saucelabs.com/rest/v1/' + this.user + '/js-tests', { - 'auth': { 'user': this.user, 'pass': this.pass }, - 'json': this.options - }, onRun.bind(this)); - }; - - /*--------------------------------------------------------------------------*/ - - /** - * Runs jobs for the given platforms. - * - * @private - * @param {Array} platforms The platforms to run jobs for. - * @param {Function} onComplete The function called once all jobs have completed. - */ - function run(platforms, onComplete) { - var jobs = _.map(platforms, function(platform) { - return new Job({ - 'user': username, - 'pass': accessKey, - 'options': { 'platforms': [platform] } - }) - }); - - var finishedJobs = 0, - success = true, - totalJobs = jobs.length; - - _.invoke(jobs, 'on', 'complete', function() { - if (++finishedJobs == totalJobs) { - onComplete(success); - } else if (success) { - success = !this.failed; - } - }); - - console.log('Starting jobs...'); - _.invoke(jobs, 'run'); - } - - // cleanup any inline logs when exited via `ctrl+c` - process.on('SIGINT', function() { logInline(''); - process.exit(); + if (failures) { + console.error(label + ' %s ' + chalk.red('failed') + ' %d test' + (failures > 1 ? 's' : '') + '. %s', description, failures, details); + } else { + var message = _.result(result, 'message', 'no results available. ' + details); + console.error(label, description, chalk.red('failed') + ';', message); + } + } else { + console.log(label, description, chalk.green('passed')); + } + this.emit('complete'); +} + +/** + * The `request.post` callback used by `Jobs#run`. + * + * @private + * @param {Object} [error] The error object. + * @param {Object} response The response data object. + * @param {Object} body The response body JSON object. + */ +function onRun(error, response, body) { + var id = _.result(body, 'js tests', [])[0], + statusCode = _.result(response, 'statusCode'); + + if (error || !id || statusCode != 200) { + console.error('Failed to start job; status: %d, body:\n%s', statusCode, JSON.stringify(body)); + if (error) { + console.error(error); + } + process.exit(3); + } + this.id = id; + check.call(this); +} + +/*--------------------------------------------------------------------------*/ + +/** + * The Job constructor. + * + * @private + * @param {Object} [properties] The properties to initial a job with. + */ +function Job(properties) { + EventEmitter.call(this); + _.merge(this, { 'attempts': 0, 'options': {} }, properties); + _.defaults(this.options, _.cloneDeep(defaultOptions)); +} + +Job.prototype = _.create(EventEmitter.prototype); + +/** + * Runs the job on Sauce Labs. + * + * @private + */ +Job.prototype.run = function() { + request.post('https://saucelabs.com/rest/v1/' + this.user + '/js-tests', { + 'auth': { 'user': this.user, 'pass': this.pass }, + 'json': this.options + }, onRun.bind(this)); +}; + +/*--------------------------------------------------------------------------*/ + +/** + * Runs jobs for the given platforms. + * + * @private + * @param {Array} platforms The platforms to run jobs for. + * @param {Function} onComplete The function called once all jobs have completed. + */ +function run(platforms, onComplete) { + var jobs = _.map(platforms, function(platform) { + return new Job({ + 'user': username, + 'pass': accessKey, + 'options': { 'platforms': [platform] } + }) }); - // create a web server for the local dir - http.createServer(function(req, res) { - // see http://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx - if (compatMode && path.extname(url.parse(req.url).pathname) == '.html') { - res.setHeader('X-UA-Compatible', 'IE=' + compatMode); + var finishedJobs = 0, + success = true, + totalJobs = jobs.length; + + _.invoke(jobs, 'on', 'complete', function() { + if (++finishedJobs == totalJobs) { + onComplete(success); + } else if (success) { + success = !this.failed; } - mount(req, res); - }).listen(port); - - // set up Sauce Connect so we can use this server from Sauce Labs - var tunnel = new SauceTunnel(username, accessKey, tunnelId, tunneled, tunnelTimeout); - - console.log('Opening Sauce Connect tunnel...'); - - tunnel.start(function(success) { - if (!success) { - console.error('Failed to open Sauce Connect tunnel'); - process.exit(2); - } - console.log('Sauce Connect tunnel opened'); - - run(platforms, function(success) { - console.log('Shutting down Sauce Connect tunnel...'); - clearInterval(throbberId); - tunnel.stop(function() { process.exit(success ? 0 : 1); }); - }); - - throbberId = setInterval(logThrobber, throbberDelay); - logThrobber(); }); -}()); + + console.log('Starting jobs...'); + _.invoke(jobs, 'run'); +} + +// cleanup any inline logs when exited via `ctrl+c` +process.on('SIGINT', function() { + logInline(''); + process.exit(); +}); + +// create a web server for the local dir +http.createServer(function(req, res) { + // see http://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx + if (compatMode && path.extname(url.parse(req.url).pathname) == '.html') { + res.setHeader('X-UA-Compatible', 'IE=' + compatMode); + } + mount(req, res); +}).listen(port); + +// set up Sauce Connect so we can use this server from Sauce Labs +var tunnel = new SauceTunnel(username, accessKey, tunnelId, tunneled, tunnelTimeout); + +console.log('Opening Sauce Connect tunnel...'); + +tunnel.start(function(success) { + if (!success) { + console.error('Failed to open Sauce Connect tunnel'); + process.exit(2); + } + console.log('Sauce Connect tunnel opened'); + + run(platforms, function(success) { + console.log('Shutting down Sauce Connect tunnel...'); + tunnel.stop(function() { process.exit(success ? 0 : 1); }); + }); + + setInterval(logThrobber, throbberDelay); +});