/** * Creates a tunnel, and calls a function with that tunnel. That function * should return a future. When that future resolves, the tunnel will be * automatically closed. This function itself returns a future. */ function startTunnel( withTunnel ) { grunt.log.writeln('=> Starting Tunnel to Sauce Labs'.inverse.bold); var tunnel = new SauceTunnel( process.env.SAUCE_USERNAME, process.env.SAUCE_ACCESS_KEY, Math.floor((new Date()).getTime() / 1000 - 1230768000).toString(), true, ['-P', '0'] ); var defer = Q.defer(); tunnel.start(function(status){ if (status === false){ defer.reject(new Error('Unable to open tunnel')); } else { grunt.log.ok('Connected to Tunnel'); defer.resolve(); } }); return defer.promise .timeout(60000, "Timed out trying to create tunnel") .then(function () { return withTunnel(tunnel).fin(function () { grunt.log.writeln('=> Closing Tunnel'.inverse.bold); tunnel.stop(function () { grunt.log.ok('Tunnel Closed'); }); }); }); }
function runTestsOnSaucelabs(fileGroup, opts, next) { if (opts.browsers) { var tunnel = new SauceTunnel(opts.username, opts.key, opts.identifier, true, opts.tunnelTimeout); configureLogEvents(tunnel); grunt.log.writeln("=> Connecting to Saucelabs ..."); tunnel.start(function(isCreated) { if (!isCreated) { return next(new Error('Failed to create Sauce tunnel.')); } grunt.log.ok("Connected to Saucelabs."); var browser_failed = false; var testQueue = async.queue(function (browserOpts, cb) { var browser = wd.remote('ondemand.saucelabs.com', 80, opts.username, opts.key); browser.browserTitle = browserOpts.browserTitle; browserOpts = _.extend(browserOpts, { name: opts.testName, tags: opts.testTags, 'tunnel-identifier': opts.identifier }); browser.init(browserOpts, function (err) { if (err) { grunt.log.error("Could not initialize browser on Saucelabs"); return cb(false); } runTestsForBrowser(opts, fileGroup, browser, cb); }); }, opts.concurrency); opts.browsers.forEach(function (browserOpts) { var browserTitle = ''+browserOpts.browserName + ' ' + browserOpts.version + ' on ' + browserOpts.platform; browserOpts.browserTitle = browserTitle; grunt.log.verbose.writeln('Queueing ' + browserTitle + ' on Saucelabs.'); testQueue.push(browserOpts, function (err) { if (err) { browser_failed = true; } grunt.log.verbose.writeln('%s test complete, %s tests remaining', browserTitle, testQueue.length()); }); }); testQueue.drain = function () { var err; if (browser_failed) { err = new Error('One or more tests on Sauce Labs failed.'); } tunnel.stop(function () { next(err); }); }; }); } else { grunt.log.writeln('No browsers configured for running on Saucelabs.'); } }
return new Promise(function (resolve, reject) { sauceTunnel = new SauceTunnel(SAUCE_LABS_USERNAME, SAUCE_LABS_PASSWORD, tunnelIdentifier, true); sauceTunnel.start(function (isCreated) { if (!isCreated) reject('Failed to create Sauce tunnel'); else { sauceTunnelOpened = true; resolve('Connected to Sauce Labs'); } }); });
function startTunnel(user, pass, tunnelId, done) { const tunnel = new SauceTunnel(user, pass, tunnelId, true, []); tunnel.on('log:error', data => { Log(data); }); tunnel.on('verbose:debug', data => { Log(data); }); tunnel.start(success => { if (!success) { //throw new PluginError('test:sauce', 'Tunnel failed to open'); console.log('Tunnel failed to open'); } done(tunnel); }); }
function startSauceTunnel(done) { if(!sauceUsername || !sauceAccessKey){ console.warn( '\nPlease configure your Sauce Labs credentials:\n\n' + 'export SAUCE_USERNAME=<SAUCE_USERNAME>\n' + 'export SAUCE_ACCESS_KEY=<SAUCE_ACCESS_KEY>\n\n' ); throw new Error("Missing Sauce Labs credentials"); } tunnel = new SauceTunnel(sauceUsername, sauceAccessKey, 'tunnel-id', true, ['--verbose']); tunnel.start(function (status) { if (status === false) { throw new Error('Something went wrong with the Sauce Labs tunnel'); } done(); }); }
webserver.start(function() { var tunnel = new SauceTunnel(arg.username, arg.key, arg.identifier, arg.tunneled, ["-l sc.log"]); log("Starting Tunnel to Sauce Labs"); configureLogEvents(tunnel); tunnel.start(function(isCreated) { if (!isCreated) { log("Could not create tunnel to Sauce Labs"); callback(false); webserver.stop(); return; } log("Connected to Saucelabs"); try { test.runTests( arg.browsers, arg.urls, framework, arg.identifier, arg.testname, arg.tags, arg.build, arg.onTestComplete, function(status) { status = status.every(function(passed){ return passed; }); //log("All tests completed with status %s", status); //log("Stopping Tunnel to Sauce Labs".inverse.bold); tunnel.stop(function() { callback(status); }); webserver.stop(); } ); } catch (ex) { webserver.stop(); throw ex; } }); });
function runTestsOnSaucelabs(fileGroup, opts, next) { if (opts.browsers) { var tunnel = new SauceTunnel(opts.username, opts.key, opts.identifier, opts.tunneled, opts.tunnelFlags); configureLogEvents(tunnel); grunt.log.writeln("=> Connecting to Sauce Labs ..."); tunnel.start(function(isCreated) { if (!isCreated) { return next(new Error('Failed to create Sauce tunnel.')); } grunt.log.ok("Connected to Sauce Labs."); var testQueue = async.queue(function (browserOpts, cb) { // browserOpts, opts, usePromises, errorMsg, fileGroup, cb initBrowser(browserOpts, opts, "saucelabs", fileGroup, cb); }, opts.concurrency); opts.browsers.forEach(function(browserOpts) { startBrowserTests(testQueue, 'saucelabs', browserOpts); }); testQueue.drain = function () { var err; if (browserFailed) { err = new Error('One or more tests on Sauce Labs failed.'); } tunnel.stop(function () { next(err); }); }; }); } else { grunt.log.writeln('No browsers configured for running on Saucelabs.'); } }
function start(options) { if (tunnel) { stop(); if (grunt.task.current.flags.stop) { finished(); return; } } done = grunt.task.current.async(); tunnel = new SauceTunnel( options.username, options.key, options.identifier, true, // tunneled = true ['-v'] ); // keep actives tunnel in memory for stop task tunnels[tunnel.identifier] = tunnel; configureLogEvents(tunnel); grunt.log.writeln('Open'.cyan + ' Sauce Labs tunnel: ' + tunnel.identifier.cyan); tunnel.start(function (status) { if (status === false) { grunt.fatal('Failed'.red + ' to open Sauce Labs tunnel: ' + tunnel.identifier.cyan); } grunt.log.ok('Successfully'.green + ' opened Sauce Labs tunnel: ' + tunnel.identifier.cyan); finished(); }); tunnel.on('exit', finished); tunnel.on('exit', stop); }
grunt.registerMultiTask('perfjankie', 'Run rendering performance test cases', function() { var done = this.async(), path = require('path'), options = this.options({ log: { // Expects the following methods, fatal: grunt.fail.fatal.bind(grunt.fail), error: grunt.fail.warn.bind(grunt.fail), warn: grunt.log.error.bind(grunt.log), info: grunt.log.ok.bind(grunt.log), debug: grunt.verbose.writeln.bind(grunt.verbose), trace: grunt.log.debug.bind(grunt.log) }, time: new Date().getTime() }), files = options.urls; options.time = parseFloat(options.time, 10); if (options.sauceTunnel) { var SauceTunnel = require('sauce-tunnel'); grunt.log.writeln('Starting Saucelabs Tunnel'); var tunnel = new SauceTunnel(options.SAUCE_USERNAME, options.SAUCE_ACCESSKEY, options.sauceTunnel, true); tunnel.start(function(status) { grunt.log.ok('Saucelabs Tunnel started - ' + status); if (status === false) { done(false); } else { runPerfTest(files, options, function(res) { grunt.verbose.writeln('All perf tests completed'); tunnel.stop(function() { done(res); }); }); } }); } else { runPerfTest(files, options, done); } });
grunt.registerMultiTask('saucelabs-mocha', 'Run Mocha test cases using Sauce Labs browsers', function() { var done = this.async(), arg = defaults(this.options(defaultsObj)); var tunnel = new SauceTunnel(arg.username, arg.key, arg.identifier, arg.tunneled, arg.tunnelTimeout); grunt.log.writeln("=> Connecting to Saucelabs ..."); if (this.tunneled) { grunt.verbose.writeln("=> Starting Tunnel to Sauce Labs".inverse.bold); } tunnel.start(function(isCreated) { if (!isCreated) { done(false); return; } grunt.log.ok("Connected to Saucelabs"); var test = new TestRunner(arg.username, arg.key); test.forEachBrowser(arg.browsers, test.mochaRunner, test.mochaSaucify, arg.concurrency, arg.onTestComplete).testPages(arg.pages, arg.testTimeout, arg.testInterval, arg.testReadyTimeout, arg.detailedError, function(status) { grunt.log[status ? 'ok' : 'error']("All tests completed with status %s", status); tunnel.stop(function() { done(status); }); }); }); });
;(function() { 'use strict'; /** Environment shortcut */ var env = process.env; if (isFinite(env.TRAVIS_PULL_REQUEST)) { console.error('Testing skipped for pull requests'); process.exit(0); } /** Load Node.js modules */ var http = require('http'), path = require('path'), url = require('url'); /** Load other modules */ var _ = require('../lodash.js'), ecstatic = require('ecstatic'), request = require('request'), SauceTunnel = require('sauce-tunnel'); /** Used by `logInline` to clear previously logged messages */ var prevLine = ''; /** Used to display the wait throbber */ var throbberId, throbberDelay = 500, waitCount = -1; /** Used as request `auth` and `options` values */ var accessKey = env.SAUCE_ACCESS_KEY, build = env.TRAVIS_COMMIT, port = 8081, tunnelId = 'lodash_' + env.TRAVIS_JOB_NUMBER, username = env.SAUCE_USERNAME; var compatMode = process.argv.reduce(function(result, value) { return optionToValue('compatMode', value) || result; }, null); var runner = process.argv.reduce(function(result, value) { value = optionToValue('runner', value); return value == null ? result : '/' + value.replace(/^\W+/, ''); }, '/test/index.html'); var sessionName = process.argv.reduce(function(result, value) { return optionToValue('name', value) || result; }, 'lodash tests'); var tags = process.argv.reduce(function(result, value) { value = optionToArray('tags', value); return value.length ? _.union(result, value) : result; }, []); /** List of platforms to load the runner on */ var platforms = [ ['Windows 8.1', 'chrome', '31'], ['Windows 8.1', 'chrome', '28'], ['Windows 8.1', 'chrome', '26'], ['OS X 10.6', 'firefox', '25'], ['OS X 10.6', 'firefox', '20'], ['OS X 10.6', 'firefox', '10'], ['OS X 10.6', 'firefox', '6'], ['OS X 10.6', 'firefox', '4'], ['Windows 7', 'firefox', '3.6'], ['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.8', 'safari', '6'], ['Windows 7', 'safari', '5'], ['Windows XP', 'safari', '4'] ]; /** 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 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; }); } /*--------------------------------------------------------------------------*/ /** * Writes an inline message to standard output. * * @private * @param {string} text The text to log. */ function logInline(text) { var blankLine = repeat(' ', prevLine.length); if (text.length > 40) { text = text.slice(0, 37) + '...'; } prevLine = text; 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(_.isArray(string) ? string : _.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].trim(); return result || undefined; } /** * Creates a string with `text` repeated `n` number of times. * * @private * @param {string} text The text to repeat. * @param {number} n The number of times to repeat `text`. * @returns {string} The created string. */ function repeat(text, n) { return Array(n + 1).join(text); } /*--------------------------------------------------------------------------*/ /** * Processes the result object of the test session. * * @private * @param {Object} results The result object to process. */ function handleTestResults(results) { var failingTests = results.filter(function(test) { var result = test.result; return !result || result.failed || /\berror\b/i.test(result.message); }); var failingPlatforms = failingTests.map(function(test) { return test.platform; }); if (!failingTests.length) { console.log('Tests passed'); } else { console.error('Tests failed on platforms: ' + JSON.stringify(failingPlatforms)); failingTests.forEach(function(test) { var details = 'See ' + test.url + ' for details.', platform = JSON.stringify(test.platform), result = test.result; if (result && result.failed) { console.error(result.failed + ' failures on ' + platform + '. ' + details); } else { console.error('Testing on ' + platform + ' failed; no results available. ' + details); } }); } clearInterval(throbberId); console.log('Shutting down Sauce Connect tunnel...'); tunnel.stop(function() { process.exit(failingTests.length ? 1 : 0); }); } /** * Makes a request for Sauce Labs to start the test session. * * @private */ function runTests() { var options = { 'build': build, 'framework': 'qunit', 'name': sessionName, 'public': 'public', 'platforms': platforms, 'record-screenshots': false, 'tags': tags, 'tunnel': 'tunnel-identifier:' + tunnelId, 'url': 'http://localhost:' + port + runner, 'video-upload-on-pass': false }; console.log('Starting saucelabs tests: ' + JSON.stringify(options)); request.post('https://saucelabs.com/rest/v1/' + username + '/js-tests', { 'auth': { 'user': username, 'pass': accessKey }, 'json': options }, function(error, response, body) { var statusCode = response && response.statusCode; if (statusCode == 200) { waitForTestCompletion(body); } else { console.error('Failed to submit test to Sauce Labs; status: ' + statusCode + ', body:\n' + JSON.stringify(body)); if (error) { console.error(error); } process.exit(3); } }); // initialize the wait throbber if (!throbberId) { throbberId = setInterval(logThrobber, throbberDelay); logThrobber(); } } /** * Checks the status of the test session. If the session has completed it * passes the result object to `handleTestResults`, else it checks the status * again in five seconds. * * @private * @param {Object} testIdentifier The object used to identify the session. */ function waitForTestCompletion(testIdentifier) { request.post('https://saucelabs.com/rest/v1/' + username + '/js-tests/status', { 'auth': { 'user': username, 'pass': accessKey }, 'json': testIdentifier }, function(error, response, body) { var statusCode = response && response.statusCode; if (statusCode == 200) { if (body.completed) { logInline(''); handleTestResults(body['js tests']); } else { setTimeout(function() { waitForTestCompletion(testIdentifier); }, 5000); } } else { logInline(''); console.error('Failed to check test status on Sauce Labs; status: ' + statusCode + ', body:\n' + JSON.stringify(body)); if (error) { console.error(error); } process.exit(4); } }); } /*--------------------------------------------------------------------------*/ // create a web server for the local dir var mount = ecstatic({ 'cache': false, 'root': process.cwd() }); 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 tunnelTimeout = 10000, tunnel = new SauceTunnel(username, accessKey, tunnelId, true, tunnelTimeout); console.log('Opening Sauce Connect tunnel...'); tunnel.start(function(success) { if (success) { console.log('Sauce Connect tunnel opened'); runTests(); } else { console.error('Failed to open Sauce Connect tunnel'); process.exit(2); } }); }());
readResultsFile(function(testResults) { var noop = true; options.browsers.forEach(function(conf) { conf['name'] = options.name; conf['record-video'] = options.video; conf['record-screenshots'] = options.screenshots; conf['tunnel-identifier'] = tunnelId; if (options.tags) conf['tags'] = options.tags; if (options.build) conf['build'] = options.build; Object.keys(options.urls).forEach(function(urlName) { var url = options.urls[urlName], state; try { testResults[UA.normalizeName(conf.browserName)][conf.version][urlName].length; state = 'skipped - data already in result file'; } catch(e) { batch.push(function(done) { var testJob = newTestJob(url, urlName, conf); testJob().then(function(data) { done(null, data); }).catch(function(e) { done(e); }); }); state = 'queued'; noop = false; } grunt.log.writeln(conf.browserName + ' ' + conf.version + ' (' + urlName + ' mode): ' + state); }); }); if (noop) { grunt.log.writeln("Nothing to do"); return gruntDone(); } grunt.log.writeln('Opening tunnel to Sauce Labs'); tunnel = new SauceTunnel(options.username, options.key, tunnelId, true); tunnel.start(function(status) { var cumFailCount = 0; grunt.log.writeln("Tunnel Started"); if (status !== true) { gruntDone(status); } grunt.log.writeln("Starting test jobs"); batch.on('progress', function(e) { if (e.error) { throw e.error; } if (e.value.browserName) { var browserName = UA.normalizeName(e.value.browserName) || e.value.browserName; if (!testResults[browserName]) { testResults[browserName] = {}; } if (!testResults[browserName][e.value.version]) { testResults[browserName][e.value.version] = {}; } testResults[browserName][e.value.version][e.value.urlName] = { passed: e.value.results.passed, failed: e.value.results.failed, failingSuites: e.value.results.failingSuites ? Object.keys(e.value.results.failingSuites) : [], testedSuites: e.value.results.testedSuites }; cumFailCount += e.value.results.failed; writeResultsFile(testResults); } // Pending count appears to have an off by one error grunt.log.writeln("Progress (browsers): " + e.complete + ' / ' + e.total + ' (' + (e.pending-1) + ' browsers remaining, ' + cumFailCount + ' test failures so far)'); }); batch.end(function(err, jobresults) { grunt.log.writeln('All jobs complete'); tunnel.stop(function() { var passingUAs = []; var failed = false; grunt.log.writeln('Sauce tunnel stopped'); grunt.log.writeln("travis_fold:end:Sauce test progress"); grunt.log.writeln("Failed tests:"); jobresults.forEach(function(job) { if (!job.results) { grunt.warn('No results reported for '+ job.browserName+'/'+job.version+' '+job.urlName); return true; } if (job.results.failed) { grunt.log.writeln(' - '+job.browserName+' '+job.version+' (Sauce results: https://saucelabs.com/tests/' + job.id+')'); if (job.results.failingSuites) { Object.keys(job.results.failingSuites).forEach(function(feature) { var url = options.urls[job.urlName].replace(/test\/director/, 'test/tests')+'&feature='+feature; grunt.log.writeln(' -> '+feature); grunt.log.writeln(' '+url); }); } failed = true; } else { passingUAs.push(job.browserName+'/'+job.version); } }); if (!failed) { grunt.log.writeln(' - None!'); } if (passingUAs.length) { grunt.log.writeln('Browsers tested with no failures: '+passingUAs.join(', ')); } process.nextTick(function() { // Always report the grunt task as successful if not running as CI if (!options.cibuild) { gruntDone(null, true); } else { gruntDone(!failed, failed); } }); }); }); }); });
readResultsFile(function(testResults) { grunt.log.writeln('Opening tunnel to Sauce Labs'); tunnel.start(function(status) { grunt.log.writeln("Tunnel Started"); if (status !== true) { gruntDone(status); } grunt.log.writeln("Starting test jobs"); batch.end(function(err, jobresults) { tunnel.stop(function() { var passingUAs = []; var failed = false; grunt.log.writeln('Sauce tunnel stopped'); if (err) { gruntDone(err); return; } grunt.log.writeln("Failed tests:") jobresults.forEach(function(job) { if (job.results.failed && job.results.failingSuites) { grunt.log.writeln(' - '+job.browserName+' '+job.version+' (Sauce results: https://saucelabs.com/tests/' + job.id+')') Object.keys(job.results.failingSuites).forEach(function(feature) { var url = options.urls[job.urlName].replace(/test\/director/, 'test/tests')+'&feature='+feature; grunt.log.writeln(' -> '+feature); grunt.log.writeln(' '+url); }); failed = true; } else { passingUAs.push(job.browserName+'/'+job.version); } var browserName = UA.normalizeName(job.browserName) || job.browserName; if (!testResults[browserName]) { testResults[browserName] = {}; } if (!testResults[browserName][job.version]) { testResults[browserName][job.version] = {}; } testResults[browserName][job.version][job.urlName] = { passed: job.results.passed, failed: job.results.failed, failingSuites: job.results.failingSuites ? Object.keys(job.results.failingSuites) : [], testedSuites: job.results.testedSuites }; }); if (passingUAs.length) { console.log('No failures in: '+passingUAs.join(', ')); } writeResultsFile(testResults); process.nextTick(function() { // Always report the grunt task as successful if not running as CI if (!options.cibuild) { gruntDone(null, true); } else { gruntDone(!failed, failed); } }); }); }); }); });
;(function() { 'use strict'; /** Environment shortcut */ var env = process.env; 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 other modules */ var _ = require('../lodash.js'), chalk = require('chalk'), ecstatic = require('ecstatic'), request = require('request'), SauceTunnel = require('sauce-tunnel'); /** 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 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 by `logInline` to clear previously logged messages */ var prevLine = ''; /** Used to detect error messages */ var reError = /\berror\b/i; /** Used to display the wait throbber */ var throbberId, 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', false), 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); /** List of platforms to load the runner on */ var platforms = [ ['Windows 8.1', 'googlechrome', '33'], ['Windows 8.1', 'googlechrome', '32'], ['Windows 8.1', 'firefox', '27'], ['Windows 8.1', 'firefox', '26'], ['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'] ]; } // 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 = 'tunnel-identifier:' + tunnelId; } /*--------------------------------------------------------------------------*/ 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; } /*--------------------------------------------------------------------------*/ 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)); } 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 = capitalizeWords(platform[1].replace('google', '')) + ' ' + platform[2] + ' on ' + capitalizeWords(platform[0]), failures = _.result(result, 'failed'), label = options.name + ':'; if (!completed) { setTimeout(check.bind(this), statusInterval); return; } if (!result || failures || reError.test(result.message)) { if (this.attempts <= maxRetries) { this.attempts++; console.log(label + ' attempt %d', this.attempts); this.run(); return; } _.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'); } 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); } /*--------------------------------------------------------------------------*/ function Job(options) { EventEmitter.call(this); _.merge(this, { 'attempts': 0, 'options': {} }, options); _.defaults(this.options, _.cloneDeep(defaultOptions)); } Job.prototype = _.create(EventEmitter.prototype); 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)); }; /*--------------------------------------------------------------------------*/ 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(); }); // 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...'); clearInterval(throbberId); tunnel.stop(function() { process.exit(success ? 0 : 1); }); }); throbberId = setInterval(logThrobber, throbberDelay); logThrobber(); }); }());
;(function() { 'use strict'; var ecstatic = require('ecstatic'), http = require('http'), path = require('path'), request = require('request'), SauceTunnel = require('sauce-tunnel'), url = require('url'); var attempts = -1, prevLine = ''; var port = 8081, username = process.env.SAUCE_USERNAME, accessKey = process.env.SAUCE_ACCESS_KEY, tunnelId = 'lodash_' + process.env.TRAVIS_JOB_NUMBER; if (!accessKey) { console.error('Testing skipped for pull requests'); process.exit(0); } var runnerPathname = (function() { var args = process.argv; return args.length > 2 ? '/' + args[args.length - 1].replace(/^\W+/, '') : '/test/index.html'; }()); var runnerQuery = url.parse(runnerPathname, true).query, isMobile = /\bmobile\b/i.test(runnerQuery.build), isModern = /\bmodern\b/i.test(runnerQuery.build); var platforms = [ ['Windows 7', 'chrome', ''], ['Windows 7', 'firefox', '25'], ['Windows 7', 'firefox', '20'], ['Windows 7', 'firefox', '10'], ['Windows 7', 'firefox', '6'], ['Windows 7', 'firefox', '4'], ['Windows 7', 'firefox', '3'], ['WIN8.1', 'internet explorer', '11'], ['Windows 7', 'internet explorer', '10'], ['Windows 7', 'internet explorer', '9'], ['Windows 7', 'internet explorer', '8'], ['Windows XP', 'internet explorer', '7'], ['Windows XP', 'internet explorer', '6'], ['OS X 10.8', 'safari', '6'], ['Windows 7', 'safari', '5'] ]; // test IE compat mode if (runnerQuery.compat) { platforms = [ ['WIN8.1', 'internet explorer', '11'], ['Windows 7', 'internet explorer', '10'], ['Windows 7', 'internet explorer', '9'], ['Windows 7', 'internet explorer', '8'] ]; } // test mobile & modern browsers 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 'safari': return version >= (isMobile ? 5 : 6); } return true }); } // create a web server for the local dir var mount = ecstatic({ root: process.cwd(), cache: false }); http.createServer(function(req, res) { var parsedUrl = url.parse(req.url, true); var compat = parsedUrl.query.compat; if (compat) { // see http://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx res.setHeader('X-UA-Compatible', 'IE=' + compat); } mount(req, res); }).listen(port); // set up Sauce Connect so we can use this server from Sauce Labs var tunnelTimeout = 10000, tunnel = new SauceTunnel(username, accessKey, tunnelId, true, tunnelTimeout); console.log('Opening Sauce Connect tunnel...'); tunnel.start(function(success) { if (success) { console.log('Sauce Connect tunnel opened'); runTests(); } else { console.error('Failed to open Sauce Connect tunnel'); process.exit(2); } }); /*--------------------------------------------------------------------------*/ function logInline(text) { var blankLine = repeat(' ', prevLine.length); if (text.length > 40) { text = text.slice(0, 37) + '...'; } prevLine = text; process.stdout.write(text + blankLine.slice(text.length) + '\r'); } function repeat(text, times) { return Array(times + 1).join(text); } /*--------------------------------------------------------------------------*/ function runTests() { var testDefinition = { 'framework': 'qunit', 'platforms': platforms, 'tunnel': 'tunnel-identifier:' + tunnelId, 'url': 'http://localhost:' + port + runnerPathname }; console.log('Starting saucelabs tests: ' + JSON.stringify(testDefinition)); request.post('https://saucelabs.com/rest/v1/' + username + '/js-tests', { 'auth': { 'user': username, 'pass': accessKey }, 'json': testDefinition }, function(error, response, body) { if (response.statusCode == 200) { waitForTestCompletion(body); } else { console.error('Failed to submit test to Sauce Labs; status ' + response.statusCode + ', body:\n' + JSON.stringify(body)); process.exit(3); } }); } function waitForTestCompletion(testIdentifier) { request.post('https://saucelabs.com/rest/v1/' + username + '/js-tests/status', { 'auth': { 'user': username, 'pass': accessKey }, 'json': testIdentifier }, function(error, response, body) { if (response.statusCode == 200) { if (body.completed) { logInline(''); handleTestResults(body['js tests']); } else { logInline('Please wait' + repeat('.', (++attempts % 3) + 1)); setTimeout(function() { waitForTestCompletion(testIdentifier); }, 5000); } } else { logInline(''); console.error('Failed to check test status on Sauce Labs; status ' + response.statusCode + ', body:\n' + JSON.stringify(body)); process.exit(4); } }); } function handleTestResults(results) { var failingTests = results.filter(function(test) { var result = test.result; return !result || result.failed || /\berror\b/i.test(result.message); }); var failingPlatforms = failingTests.map(function(test) { return test.platform; }); if (!failingTests.length) { console.log('Tests passed'); } else { console.error('Tests failed on platforms: ' + JSON.stringify(failingPlatforms)); failingTests.forEach(function(test) { var details = 'See ' + test.url + ' for details.', platform = JSON.stringify(test.platform), result = test.result; if (result && result.failed) { console.error(result.failed + ' failures on ' + platform + '. ' + details); } else { console.error('Testing on ' + platform + ' failed; no results available. ' + details); } }); } console.log('Shutting down Sauce Connect tunnel...'); tunnel.stop(function() { process.exit(failingTests.length ? 1 : 0); }); } }());