nexus site path corrected
[portal.git] / ecomp-portal-FE / client / bower_components / lodash / test / saucelabs.js
1 #!/usr/bin/env node
2 'use strict';
3
4 /** Environment shortcut. */
5 var env = process.env;
6
7 if (env.TRAVIS_SECURE_ENV_VARS == 'false') {
8   console.log('Skipping Sauce Labs jobs; secure environment variables are unavailable');
9   process.exit(0);
10 }
11
12 /** Load Node.js modules. */
13 var EventEmitter = require('events').EventEmitter,
14     http = require('http'),
15     path = require('path'),
16     url = require('url'),
17     util = require('util');
18
19 /** Load other modules. */
20 var _ = require('../lodash.js'),
21     chalk = require('chalk'),
22     ecstatic = require('ecstatic'),
23     request = require('request'),
24     SauceTunnel = require('sauce-tunnel');
25
26 /** Used for Sauce Labs credentials. */
27 var accessKey = env.SAUCE_ACCESS_KEY,
28     username = env.SAUCE_USERNAME;
29
30 /** Used as the default maximum number of times to retry a job and tunnel. */
31 var maxJobRetries = 3,
32     maxTunnelRetries = 3;
33
34 /** Used as the static file server middleware. */
35 var mount = ecstatic({
36   'cache': 'no-cache',
37   'root': process.cwd()
38 });
39
40 /** Used as the list of ports supported by Sauce Connect. */
41 var ports = [
42   80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210,
43   3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432,
44   6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031,
45   8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221,
46   55001
47 ];
48
49 /** Used by `logInline` to clear previously logged messages. */
50 var prevLine = '';
51
52 /** Method shortcut. */
53 var push = Array.prototype.push;
54
55 /** Used to detect error messages. */
56 var reError = /(?:\be|E)rror\b/;
57
58 /** Used to detect valid job ids. */
59 var reJobId = /^[a-z0-9]{32}$/;
60
61 /** Used to display the wait throbber. */
62 var throbberDelay = 500,
63     waitCount = -1;
64
65 /**
66  * Used as Sauce Labs config values.
67  * See the [Sauce Labs documentation](https://docs.saucelabs.com/reference/test-configuration/)
68  * for more details.
69  */
70 var advisor = getOption('advisor', false),
71     build = getOption('build', (env.TRAVIS_COMMIT || '').slice(0, 10)),
72     commandTimeout = getOption('commandTimeout', 90),
73     compatMode = getOption('compatMode', null),
74     customData = Function('return {' + getOption('customData', '').replace(/^\{|}$/g, '') + '}')(),
75     deviceOrientation = getOption('deviceOrientation', 'portrait'),
76     framework = getOption('framework', 'qunit'),
77     idleTimeout = getOption('idleTimeout', 60),
78     jobName = getOption('name', 'unit tests'),
79     maxDuration = getOption('maxDuration', 180),
80     port = ports[Math.min(_.sortedIndex(ports, getOption('port', 9001)), ports.length - 1)],
81     publicAccess = getOption('public', true),
82     queueTimeout = getOption('queueTimeout', 240),
83     recordVideo = getOption('recordVideo', true),
84     recordScreenshots = getOption('recordScreenshots', false),
85     runner = getOption('runner', 'test/index.html').replace(/^\W+/, ''),
86     runnerUrl = getOption('runnerUrl', 'http://localhost:' + port + '/' + runner),
87     statusInterval = getOption('statusInterval', 5),
88     tags = getOption('tags', []),
89     throttled = getOption('throttled', 10),
90     tunneled = getOption('tunneled', true),
91     tunnelId = getOption('tunnelId', 'tunnel_' + (env.TRAVIS_JOB_ID || 0)),
92     tunnelTimeout = getOption('tunnelTimeout', 120),
93     videoUploadOnPass = getOption('videoUploadOnPass', false);
94
95 /** Used to convert Sauce Labs browser identifiers to their formal names. */
96 var browserNameMap = {
97   'googlechrome': 'Chrome',
98   'iehta': 'Internet Explorer',
99   'ipad': 'iPad',
100   'iphone': 'iPhone',
101   'microsoftedge': 'Edge'
102 };
103
104 /** List of platforms to load the runner on. */
105 var platforms = [
106   ['Linux', 'android', '5.1'],
107   ['Windows 10', 'chrome', '50'],
108   ['Windows 10', 'chrome', '49'],
109   ['Windows 10', 'firefox', '46'],
110   ['Windows 10', 'firefox', '45'],
111   ['Windows 10', 'microsoftedge', '13'],
112   ['Windows 10', 'internet explorer', '11'],
113   ['Windows 8', 'internet explorer', '10'],
114   ['Windows 7', 'internet explorer', '9'],
115   // ['OS X 10.10', 'ipad', '9.1'],
116   ['OS X 10.11', 'safari', '9'],
117   ['OS X 10.10', 'safari', '8']
118 ];
119
120 /** Used to tailor the `platforms` array. */
121 var isAMD = _.includes(tags, 'amd'),
122     isBackbone = _.includes(tags, 'backbone'),
123     isModern = _.includes(tags, 'modern');
124
125 // The platforms to test IE compatibility modes.
126 if (compatMode) {
127   platforms = [
128     ['Windows 10', 'internet explorer', '11'],
129     ['Windows 8', 'internet explorer', '10'],
130     ['Windows 7', 'internet explorer', '9'],
131     ['Windows 7', 'internet explorer', '8']
132   ];
133 }
134 // The platforms for AMD tests.
135 if (isAMD) {
136   platforms = _.filter(platforms, function(platform) {
137     var browser = browserName(platform[1]),
138         version = +platform[2];
139
140     switch (browser) {
141       case 'Android': return version >= 4.4;
142       case 'Opera': return version >= 10;
143     }
144     return true;
145   });
146 }
147 // The platforms for Backbone tests.
148 if (isBackbone) {
149   platforms = _.filter(platforms, function(platform) {
150     var browser = browserName(platform[1]),
151         version = +platform[2];
152
153     switch (browser) {
154       case 'Firefox': return version >= 4;
155       case 'Internet Explorer': return version >= 7;
156       case 'iPad': return version >= 5;
157       case 'Opera': return version >= 12;
158     }
159     return true;
160   });
161 }
162 // The platforms for modern builds.
163 if (isModern) {
164   platforms = _.filter(platforms, function(platform) {
165     var browser = browserName(platform[1]),
166         version = +platform[2];
167
168     switch (browser) {
169       case 'Android': return version >= 4.1;
170       case 'Firefox': return version >= 10;
171       case 'Internet Explorer': return version >= 9;
172       case 'iPad': return version >= 6;
173       case 'Opera': return version >= 12;
174       case 'Safari': return version >= 6;
175     }
176     return true;
177   });
178 }
179
180 /** Used as the default `Job` options object. */
181 var jobOptions = {
182   'build': build,
183   'command-timeout': commandTimeout,
184   'custom-data': customData,
185   'device-orientation': deviceOrientation,
186   'framework': framework,
187   'idle-timeout': idleTimeout,
188   'max-duration': maxDuration,
189   'name': jobName,
190   'public': publicAccess,
191   'platforms': platforms,
192   'record-screenshots': recordScreenshots,
193   'record-video': recordVideo,
194   'sauce-advisor': advisor,
195   'tags': tags,
196   'url': runnerUrl,
197   'video-upload-on-pass': videoUploadOnPass
198 };
199
200 if (publicAccess === true) {
201   jobOptions['public'] = 'public';
202 }
203 if (tunneled) {
204   jobOptions['tunnel-identifier'] = tunnelId;
205 }
206
207 /*----------------------------------------------------------------------------*/
208
209 /**
210  * Resolves the formal browser name for a given Sauce Labs browser identifier.
211  *
212  * @private
213  * @param {string} identifier The browser identifier.
214  * @returns {string} Returns the formal browser name.
215  */
216 function browserName(identifier) {
217   return browserNameMap[identifier] || _.startCase(identifier);
218 }
219
220 /**
221  * Gets the value for the given option name. If no value is available the
222  * `defaultValue` is returned.
223  *
224  * @private
225  * @param {string} name The name of the option.
226  * @param {*} defaultValue The default option value.
227  * @returns {*} Returns the option value.
228  */
229 function getOption(name, defaultValue) {
230   var isArr = _.isArray(defaultValue);
231   return _.reduce(process.argv, function(result, value) {
232     if (isArr) {
233       value = optionToArray(name, value);
234       return _.isEmpty(value) ? result : value;
235     }
236     value = optionToValue(name, value);
237
238     return value == null ? result : value;
239   }, defaultValue);
240 }
241
242 /**
243  * Checks if `value` is a job ID.
244  *
245  * @private
246  * @param {*} value The value to check.
247  * @returns {boolean} Returns `true` if `value` is a job ID, else `false`.
248  */
249 function isJobId(value) {
250   return reJobId.test(value);
251 }
252
253 /**
254  * Writes an inline message to standard output.
255  *
256  * @private
257  * @param {string} [text=''] The text to log.
258  */
259 function logInline(text) {
260   var blankLine = _.repeat(' ', _.size(prevLine));
261   prevLine = text = _.truncate(text, { 'length': 40 });
262   process.stdout.write(text + blankLine.slice(text.length) + '\r');
263 }
264
265 /**
266  * Writes the wait throbber to standard output.
267  *
268  * @private
269  */
270 function logThrobber() {
271   logInline('Please wait' + _.repeat('.', (++waitCount % 3) + 1));
272 }
273
274 /**
275  * Converts a comma separated option value into an array.
276  *
277  * @private
278  * @param {string} name The name of the option to inspect.
279  * @param {string} string The options string.
280  * @returns {Array} Returns the new converted array.
281  */
282 function optionToArray(name, string) {
283   return _.compact(_.invokeMap((optionToValue(name, string) || '').split(/, */), 'trim'));
284 }
285
286 /**
287  * Extracts the option value from an option string.
288  *
289  * @private
290  * @param {string} name The name of the option to inspect.
291  * @param {string} string The options string.
292  * @returns {string|undefined} Returns the option value, else `undefined`.
293  */
294 function optionToValue(name, string) {
295   var result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'));
296   if (result) {
297     result = _.result(result, 1);
298     result = result ? _.trim(result) : true;
299   }
300   if (result === 'false') {
301     return false;
302   }
303   return result || undefined;
304 }
305
306 /*----------------------------------------------------------------------------*/
307
308 /**
309  * The `Job#remove` and `Tunnel#stop` callback used by `Jobs#restart`
310  * and `Tunnel#restart` respectively.
311  *
312  * @private
313  */
314 function onGenericRestart() {
315   this.restarting = false;
316   this.emit('restart');
317   this.start();
318 }
319
320 /**
321  * The `request.put` and `SauceTunnel#stop` callback used by `Jobs#stop`
322  * and `Tunnel#stop` respectively.
323  *
324  * @private
325  * @param {Object} [error] The error object.
326  */
327 function onGenericStop(error) {
328   this.running = this.stopping = false;
329   this.emit('stop', error);
330 }
331
332 /**
333  * The `request.del` callback used by `Jobs#remove`.
334  *
335  * @private
336  */
337 function onJobRemove(error, res, body) {
338   this.id = this.taskId = this.url = null;
339   this.removing = false;
340   this.emit('remove');
341 }
342
343 /**
344  * The `Job#remove` callback used by `Jobs#reset`.
345  *
346  * @private
347  */
348 function onJobReset() {
349   this.attempts = 0;
350   this.failed = this.resetting = false;
351   this._pollerId = this.id = this.result = this.taskId = this.url = null;
352   this.emit('reset');
353 }
354
355 /**
356  * The `request.post` callback used by `Jobs#start`.
357  *
358  * @private
359  * @param {Object} [error] The error object.
360  * @param {Object} res The response data object.
361  * @param {Object} body The response body JSON object.
362  */
363 function onJobStart(error, res, body) {
364   this.starting = false;
365
366   if (this.stopping) {
367     return;
368   }
369   var statusCode = _.result(res, 'statusCode'),
370       taskId = _.first(_.result(body, 'js tests'));
371
372   if (error || !taskId || statusCode != 200) {
373     if (this.attempts < this.retries) {
374       this.restart();
375       return;
376     }
377     var na = 'unavailable',
378         bodyStr = _.isObject(body) ? '\n' + JSON.stringify(body) : na,
379         statusStr = _.isFinite(statusCode) ? statusCode : na;
380
381     logInline();
382     console.error('Failed to start job; status: %s, body: %s', statusStr, bodyStr);
383     if (error) {
384       console.error(error);
385     }
386     this.failed = true;
387     this.emit('complete');
388     return;
389   }
390   this.running = true;
391   this.taskId = taskId;
392   this.timestamp = _.now();
393   this.emit('start');
394   this.status();
395 }
396
397 /**
398  * The `request.post` callback used by `Job#status`.
399  *
400  * @private
401  * @param {Object} [error] The error object.
402  * @param {Object} res The response data object.
403  * @param {Object} body The response body JSON object.
404  */
405 function onJobStatus(error, res, body) {
406   this.checking = false;
407
408   if (!this.running || this.stopping) {
409     return;
410   }
411   var completed = _.result(body, 'completed', false),
412       data = _.first(_.result(body, 'js tests')),
413       elapsed = (_.now() - this.timestamp) / 1000,
414       jobId = _.result(data, 'job_id', null),
415       jobResult = _.result(data, 'result', null),
416       jobStatus = _.result(data, 'status', ''),
417       jobUrl = _.result(data, 'url', null),
418       expired = (elapsed >= queueTimeout && !_.includes(jobStatus, 'in progress')),
419       options = this.options,
420       platform = options.platforms[0];
421
422   if (_.isObject(jobResult)) {
423     var message = _.result(jobResult, 'message');
424   } else {
425     if (typeof jobResult == 'string') {
426       message = jobResult;
427     }
428     jobResult = null;
429   }
430   if (isJobId(jobId)) {
431     this.id = jobId;
432     this.result = jobResult;
433     this.url = jobUrl;
434   } else {
435     completed = false;
436   }
437   this.emit('status', jobStatus);
438
439   if (!completed && !expired) {
440     this._pollerId = _.delay(_.bind(this.status, this), this.statusInterval * 1000);
441     return;
442   }
443   var description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + _.startCase(platform[0]),
444       errored = !jobResult || !jobResult.passed || reError.test(message) || reError.test(jobStatus),
445       failures = _.result(jobResult, 'failed'),
446       label = options.name + ':',
447       tunnel = this.tunnel;
448
449   if (errored || failures) {
450     if (errored && this.attempts < this.retries) {
451       this.restart();
452       return;
453     }
454     var details = 'See ' + jobUrl + ' for details.';
455     this.failed = true;
456
457     logInline();
458     if (failures) {
459       console.error(label + ' %s ' + chalk.red('failed') + ' %d test' + (failures > 1 ? 's' : '') + '. %s', description, failures, details);
460     }
461     else if (tunnel.attempts < tunnel.retries) {
462       tunnel.restart();
463       return;
464     }
465     else {
466       if (typeof message == 'undefined') {
467         message = 'Results are unavailable. ' + details;
468       }
469       console.error(label, description, chalk.red('failed') + ';', message);
470     }
471   }
472   else {
473     logInline();
474     console.log(label, description, chalk.green('passed'));
475   }
476   this.running = false;
477   this.emit('complete');
478 }
479
480 /**
481  * The `SauceTunnel#start` callback used by `Tunnel#start`.
482  *
483  * @private
484  * @param {boolean} success The connection success indicator.
485  */
486 function onTunnelStart(success) {
487   this.starting = false;
488
489   if (this._timeoutId) {
490     clearTimeout(this._timeoutId);
491     this._timeoutId = null;
492   }
493   if (!success) {
494     if (this.attempts < this.retries) {
495       this.restart();
496       return;
497     }
498     logInline();
499     console.error('Failed to open Sauce Connect tunnel');
500     process.exit(2);
501   }
502   logInline();
503   console.log('Sauce Connect tunnel opened');
504
505   var jobs = this.jobs;
506   push.apply(jobs.queue, jobs.all);
507
508   this.running = true;
509   this.emit('start');
510
511   console.log('Starting jobs...');
512   this.dequeue();
513 }
514
515 /*----------------------------------------------------------------------------*/
516
517 /**
518  * The Job constructor.
519  *
520  * @private
521  * @param {Object} [properties] The properties to initialize a job with.
522  */
523 function Job(properties) {
524   EventEmitter.call(this);
525
526   this.options = {};
527   _.merge(this, properties);
528   _.defaults(this.options, _.cloneDeep(jobOptions));
529
530   this.attempts = 0;
531   this.checking = this.failed = this.removing = this.resetting = this.restarting = this.running = this.starting = this.stopping = false;
532   this._pollerId = this.id = this.result = this.taskId = this.url = null;
533 }
534
535 util.inherits(Job, EventEmitter);
536
537 /**
538  * Removes the job.
539  *
540  * @memberOf Job
541  * @param {Function} callback The function called once the job is removed.
542  * @param {Object} Returns the job instance.
543  */
544 Job.prototype.remove = function(callback) {
545   this.once('remove', _.iteratee(callback));
546   if (this.removing) {
547     return this;
548   }
549   this.removing = true;
550   return this.stop(function() {
551     var onRemove = _.bind(onJobRemove, this);
552     if (!this.id) {
553       _.defer(onRemove);
554       return;
555     }
556     request.del(_.template('https://saucelabs.com/rest/v1/${user}/jobs/${id}')(this), {
557       'auth': { 'user': this.user, 'pass': this.pass }
558     }, onRemove);
559   });
560 };
561
562 /**
563  * Resets the job.
564  *
565  * @memberOf Job
566  * @param {Function} callback The function called once the job is reset.
567  * @param {Object} Returns the job instance.
568  */
569 Job.prototype.reset = function(callback) {
570   this.once('reset', _.iteratee(callback));
571   if (this.resetting) {
572     return this;
573   }
574   this.resetting = true;
575   return this.remove(onJobReset);
576 };
577
578 /**
579  * Restarts the job.
580  *
581  * @memberOf Job
582  * @param {Function} callback The function called once the job is restarted.
583  * @param {Object} Returns the job instance.
584  */
585 Job.prototype.restart = function(callback) {
586   this.once('restart', _.iteratee(callback));
587   if (this.restarting) {
588     return this;
589   }
590   this.restarting = true;
591
592   var options = this.options,
593       platform = options.platforms[0],
594       description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + _.startCase(platform[0]),
595       label = options.name + ':';
596
597   logInline();
598   console.log('%s %s restart %d of %d', label, description, ++this.attempts, this.retries);
599
600   return this.remove(onGenericRestart);
601 };
602
603 /**
604  * Starts the job.
605  *
606  * @memberOf Job
607  * @param {Function} callback The function called once the job is started.
608  * @param {Object} Returns the job instance.
609  */
610 Job.prototype.start = function(callback) {
611   this.once('start', _.iteratee(callback));
612   if (this.starting || this.running) {
613     return this;
614   }
615   this.starting = true;
616   request.post(_.template('https://saucelabs.com/rest/v1/${user}/js-tests')(this), {
617     'auth': { 'user': this.user, 'pass': this.pass },
618     'json': this.options
619   }, _.bind(onJobStart, this));
620
621   return this;
622 };
623
624 /**
625  * Checks the status of a job.
626  *
627  * @memberOf Job
628  * @param {Function} callback The function called once the status is resolved.
629  * @param {Object} Returns the job instance.
630  */
631 Job.prototype.status = function(callback) {
632   this.once('status', _.iteratee(callback));
633   if (this.checking || this.removing || this.resetting || this.restarting || this.starting || this.stopping) {
634     return this;
635   }
636   this._pollerId = null;
637   this.checking = true;
638   request.post(_.template('https://saucelabs.com/rest/v1/${user}/js-tests/status')(this), {
639     'auth': { 'user': this.user, 'pass': this.pass },
640     'json': { 'js tests': [this.taskId] }
641   }, _.bind(onJobStatus, this));
642
643   return this;
644 };
645
646 /**
647  * Stops the job.
648  *
649  * @memberOf Job
650  * @param {Function} callback The function called once the job is stopped.
651  * @param {Object} Returns the job instance.
652  */
653 Job.prototype.stop = function(callback) {
654   this.once('stop', _.iteratee(callback));
655   if (this.stopping) {
656     return this;
657   }
658   this.stopping = true;
659   if (this._pollerId) {
660     clearTimeout(this._pollerId);
661     this._pollerId = null;
662     this.checking = false;
663   }
664   var onStop = _.bind(onGenericStop, this);
665   if (!this.running || !this.id) {
666     _.defer(onStop);
667     return this;
668   }
669   request.put(_.template('https://saucelabs.com/rest/v1/${user}/jobs/${id}/stop')(this), {
670     'auth': { 'user': this.user, 'pass': this.pass }
671   }, onStop);
672
673   return this;
674 };
675
676 /*----------------------------------------------------------------------------*/
677
678 /**
679  * The Tunnel constructor.
680  *
681  * @private
682  * @param {Object} [properties] The properties to initialize the tunnel with.
683  */
684 function Tunnel(properties) {
685   EventEmitter.call(this);
686
687   _.merge(this, properties);
688
689   var active = [],
690       queue = [];
691
692   var all = _.map(this.platforms, _.bind(function(platform) {
693     return new Job(_.merge({
694       'user': this.user,
695       'pass': this.pass,
696       'tunnel': this,
697       'options': { 'platforms': [platform] }
698     }, this.job));
699   }, this));
700
701   var completed = 0,
702       restarted = [],
703       success = true,
704       total = all.length,
705       tunnel = this;
706
707   _.invokeMap(all, 'on', 'complete', function() {
708     _.pull(active, this);
709     if (success) {
710       success = !this.failed;
711     }
712     if (++completed == total) {
713       tunnel.stop(_.partial(tunnel.emit, 'complete', success));
714       return;
715     }
716     tunnel.dequeue();
717   });
718
719   _.invokeMap(all, 'on', 'restart', function() {
720     if (!_.includes(restarted, this)) {
721       restarted.push(this);
722     }
723     // Restart tunnel if all active jobs have restarted.
724     var threshold = Math.min(all.length, _.isFinite(throttled) ? throttled : 3);
725     if (tunnel.attempts < tunnel.retries &&
726         active.length >= threshold && _.isEmpty(_.difference(active, restarted))) {
727       tunnel.restart();
728     }
729   });
730
731   this.on('restart', function() {
732     completed = 0;
733     success = true;
734     restarted.length = 0;
735   });
736
737   this._timeoutId = null;
738   this.attempts = 0;
739   this.restarting = this.running = this.starting = this.stopping = false;
740   this.jobs = { 'active': active, 'all': all, 'queue': queue };
741   this.connection = new SauceTunnel(this.user, this.pass, this.id, this.tunneled, ['-P', '0']);
742 }
743
744 util.inherits(Tunnel, EventEmitter);
745
746 /**
747  * Restarts the tunnel.
748  *
749  * @memberOf Tunnel
750  * @param {Function} callback The function called once the tunnel is restarted.
751  */
752 Tunnel.prototype.restart = function(callback) {
753   this.once('restart', _.iteratee(callback));
754   if (this.restarting) {
755     return this;
756   }
757   this.restarting = true;
758
759   logInline();
760   console.log('Tunnel %s: restart %d of %d', this.id, ++this.attempts, this.retries);
761
762   var jobs = this.jobs,
763       active = jobs.active,
764       all = jobs.all;
765
766   var reset = _.after(all.length, _.bind(this.stop, this, onGenericRestart)),
767       stop = _.after(active.length, _.partial(_.invokeMap, all, 'reset', reset));
768
769   if (_.isEmpty(active)) {
770     _.defer(stop);
771   }
772   if (_.isEmpty(all)) {
773     _.defer(reset);
774   }
775   _.invokeMap(active, 'stop', function() {
776     _.pull(active, this);
777     stop();
778   });
779
780   if (this._timeoutId) {
781     clearTimeout(this._timeoutId);
782     this._timeoutId = null;
783   }
784   return this;
785 };
786
787 /**
788  * Starts the tunnel.
789  *
790  * @memberOf Tunnel
791  * @param {Function} callback The function called once the tunnel is started.
792  * @param {Object} Returns the tunnel instance.
793  */
794 Tunnel.prototype.start = function(callback) {
795   this.once('start', _.iteratee(callback));
796   if (this.starting || this.running) {
797     return this;
798   }
799   this.starting = true;
800
801   logInline();
802   console.log('Opening Sauce Connect tunnel...');
803
804   var onStart = _.bind(onTunnelStart, this);
805   if (this.timeout) {
806     this._timeoutId = _.delay(onStart, this.timeout * 1000, false);
807   }
808   this.connection.start(onStart);
809   return this;
810 };
811
812 /**
813  * Removes jobs from the queue and starts them.
814  *
815  * @memberOf Tunnel
816  * @param {Object} Returns the tunnel instance.
817  */
818 Tunnel.prototype.dequeue = function() {
819   var count = 0,
820       jobs = this.jobs,
821       active = jobs.active,
822       queue = jobs.queue,
823       throttled = this.throttled;
824
825   while (queue.length && (active.length < throttled)) {
826     var job = queue.shift();
827     active.push(job);
828     _.delay(_.bind(job.start, job), ++count * 1000);
829   }
830   return this;
831 };
832
833 /**
834  * Stops the tunnel.
835  *
836  * @memberOf Tunnel
837  * @param {Function} callback The function called once the tunnel is stopped.
838  * @param {Object} Returns the tunnel instance.
839  */
840 Tunnel.prototype.stop = function(callback) {
841   this.once('stop', _.iteratee(callback));
842   if (this.stopping) {
843     return this;
844   }
845   this.stopping = true;
846
847   logInline();
848   console.log('Shutting down Sauce Connect tunnel...');
849
850   var jobs = this.jobs,
851       active = jobs.active;
852
853   var stop = _.after(active.length, _.bind(function() {
854     var onStop = _.bind(onGenericStop, this);
855     if (this.running) {
856       this.connection.stop(onStop);
857     } else {
858       onStop();
859     }
860   }, this));
861
862   jobs.queue.length = 0;
863   if (_.isEmpty(active)) {
864     _.defer(stop);
865   }
866   _.invokeMap(active, 'stop', function() {
867     _.pull(active, this);
868     stop();
869   });
870
871   if (this._timeoutId) {
872     clearTimeout(this._timeoutId);
873     this._timeoutId = null;
874   }
875   return this;
876 };
877
878 /*----------------------------------------------------------------------------*/
879
880 // Cleanup any inline logs when exited via `ctrl+c`.
881 process.on('SIGINT', function() {
882   logInline();
883   process.exit();
884 });
885
886 // Create a web server for the current working directory.
887 http.createServer(function(req, res) {
888   // See http://msdn.microsoft.com/en-us/library/ff955275(v=vs.85).aspx.
889   if (compatMode && path.extname(url.parse(req.url).pathname) == '.html') {
890     res.setHeader('X-UA-Compatible', 'IE=' + compatMode);
891   }
892   mount(req, res);
893 }).listen(port);
894
895 // Setup Sauce Connect so we can use this server from Sauce Labs.
896 var tunnel = new Tunnel({
897   'user': username,
898   'pass': accessKey,
899   'id': tunnelId,
900   'job': { 'retries': maxJobRetries, 'statusInterval': statusInterval },
901   'platforms': platforms,
902   'retries': maxTunnelRetries,
903   'throttled': throttled,
904   'tunneled': tunneled,
905   'timeout': tunnelTimeout
906 });
907
908 tunnel.on('complete', function(success) {
909   process.exit(success ? 0 : 1);
910 });
911
912 tunnel.start();
913
914 setInterval(logThrobber, throbberDelay);