Пример #1
0
    /**
     * 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.');
    }
  }
Пример #3
0
    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');
            }
        });
    });
Пример #4
0
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();
    });
  }
Пример #6
0
	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.');
    }
  }
Пример #8
0
			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);
			}
Пример #9
0
  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);
    }
  });
Пример #10
0
 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);
       });
     });
   });
 });
Пример #11
0
;(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);
    }
  });
}());
Пример #12
0
		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);
							}
						});
					});
				});
			});
		});
Пример #13
0
		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);
							}
						});
					});
				});
			});
		});
Пример #14
0
;(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();
  });
}());
Пример #15
0
;(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);
    });
  }
}());