Skip to content
This repository was archived by the owner on Jul 29, 2024. It is now read-only.

Commit 78f3c64

Browse files
authored
chore(exitCodes): adding exit code for browser connect errors (#3133)
* add exit code for browser connect errors * add exit code for browserstack error * add browser error for debug with multiple capabilities * use thrown stack traces for errors (instead of creating new ones) with captureStackTrace * allow for errors to suppress exit code for config parser thrown error
1 parent 85209f4 commit 78f3c64

13 files changed

+408
-66
lines changed

lib/driverProviders/browserStack.ts

+41-36
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as request from 'request';
77
import * as q from 'q';
88
import * as util from 'util';
99

10+
import {BrowserError} from '../exitCodes';
1011
import {Config} from '../configParser';
1112
import {DriverProvider} from './driverProvider';
1213
import {Logger} from '../logger2';
@@ -22,42 +23,46 @@ export class BrowserStack extends DriverProvider {
2223
* @param {Object} update
2324
* @return {q.promise} A promise that will resolve when the update is complete.
2425
*/
25-
updateJob(update: any): q.Promise<any> {
26-
let deferredArray = this.drivers_.map((driver: webdriver.WebDriver) => {
27-
let deferred = q.defer();
28-
driver.getSession().then((session: webdriver.Session) => {
29-
var jobStatus = update.passed ? 'completed' : 'error';
30-
logger.info(
31-
'BrowserStack results available at ' +
32-
'https://www.browserstack.com/automate');
33-
request(
34-
{
35-
url: 'https://www.browserstack.com/automate/sessions/' +
36-
session.getId() + '.json',
37-
headers: {
38-
'Content-Type': 'application/json',
39-
'Authorization': 'Basic ' +
40-
new Buffer(
41-
this.config_.browserstackUser + ':' +
42-
this.config_.browserstackKey)
43-
.toString('base64')
44-
},
45-
method: 'PUT',
46-
form: {'status': jobStatus}
47-
},
48-
(error: Error) => {
49-
if (error) {
50-
throw new Error(
51-
'Error updating BrowserStack pass/fail status: ' +
52-
util.inspect(error));
53-
}
54-
});
55-
deferred.resolve();
56-
});
57-
return deferred.promise;
58-
});
59-
return q.all(deferredArray);
60-
}
26+
updateJob(update: any): q.Promise<any> {
27+
let deferredArray = this.drivers_.map((driver: webdriver.WebDriver) => {
28+
let deferred = q.defer();
29+
driver.getSession().then((session: webdriver.Session) => {
30+
var jobStatus = update.passed ? 'completed' : 'error';
31+
logger.info(
32+
'BrowserStack results available at ' +
33+
'https://www.browserstack.com/automate');
34+
request(
35+
{
36+
url: 'https://www.browserstack.com/automate/sessions/' +
37+
session.getId() + '.json',
38+
headers: {
39+
'Content-Type': 'application/json',
40+
'Authorization': 'Basic ' +
41+
new Buffer(
42+
this.config_.browserstackUser + ':' +
43+
this.config_.browserstackKey)
44+
.toString('base64')
45+
},
46+
method: 'PUT',
47+
form: {'status': jobStatus}
48+
},
49+
(error: Error) => {
50+
if (error) {
51+
throw new BrowserError(
52+
logger,
53+
'Error updating BrowserStack pass/fail status: ' +
54+
util.inspect(error));
55+
}
56+
});
57+
deferred.resolve();
58+
});
59+
return deferred.promise;
60+
});
61+
return q.all(deferredArray);
62+
}
63+
64+
65+
6166

6267
/**
6368
* Configure and launch (if applicable) the object's environment.

lib/driverProviders/direct.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as fs from 'fs';
88
import * as path from 'path';
99
import * as util from 'util';
1010

11+
import {BrowserError} from '../exitCodes';
1112
import {Config} from '../configParser';
1213
import {DriverProvider} from './driverProvider';
1314
import {Logger} from '../logger2';
@@ -39,9 +40,9 @@ export class Direct extends DriverProvider {
3940
logger.info('Using FirefoxDriver directly...');
4041
break;
4142
default:
42-
throw new Error(
43-
'browserName (' + this.config_.capabilities.browserName +
44-
') is not supported with directConnect.');
43+
throw new BrowserError(
44+
logger, 'browserName ' + this.config_.capabilities.browserName +
45+
' is not supported with directConnect.');
4546
}
4647
return q.fcall(function() {});
4748
}
@@ -69,7 +70,8 @@ export class Direct extends DriverProvider {
6970
this.config_.chromeDriver || defaultChromeDriverPath;
7071

7172
if (!fs.existsSync(chromeDriverFile)) {
72-
throw new Error('Could not find chromedriver at ' + chromeDriverFile);
73+
throw new BrowserError(
74+
logger, 'Could not find chromedriver at ' + chromeDriverFile);
7375
}
7476

7577
let service = new chrome.ServiceBuilder(chromeDriverFile).build();
@@ -83,9 +85,9 @@ export class Direct extends DriverProvider {
8385
driver = new firefox.Driver(this.config_.capabilities);
8486
break;
8587
default:
86-
throw new Error(
87-
'browserName ' + this.config_.capabilities.browserName +
88-
'is not supported with directConnect.');
88+
throw new BrowserError(
89+
logger, 'browserName ' + this.config_.capabilities.browserName +
90+
' is not supported with directConnect.');
8991
}
9092
this.drivers_.push(driver);
9193
return driver;

lib/driverProviders/local.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as path from 'path';
1111
import * as q from 'q';
1212
import * as util from 'util';
1313

14+
import {BrowserError} from '../exitCodes';
1415
import {Config} from '../configParser';
1516
import {DriverProvider} from './driverProvider';
1617
import {Logger} from '../logger2';
@@ -42,10 +43,10 @@ export class Local extends DriverProvider {
4243
new SeleniumStandAlone().executableFilename());
4344
}
4445
if (!fs.existsSync(this.config_.seleniumServerJar)) {
45-
throw new Error(
46-
'No selenium server jar found at the specified ' +
47-
'location (' + this.config_.seleniumServerJar +
48-
'). Check that the version number is up to date.');
46+
throw new BrowserError(
47+
logger, 'No selenium server jar found at the specified ' +
48+
'location (' + this.config_.seleniumServerJar +
49+
'). Check that the version number is up to date.');
4950
}
5051
if (this.config_.capabilities.browserName === 'chrome') {
5152
if (!this.config_.chromeDriver) {
@@ -62,7 +63,8 @@ export class Local extends DriverProvider {
6263
if (fs.existsSync(this.config_.chromeDriver + '.exe')) {
6364
this.config_.chromeDriver += '.exe';
6465
} else {
65-
throw new Error(
66+
throw new BrowserError(
67+
logger,
6668
'Could not find chromedriver at ' + this.config_.chromeDriver);
6769
}
6870
}

lib/exitCodes.ts

+82-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
11
import {Logger} from './logger2';
22

33
const CONFIG_ERROR_CODE = 105;
4+
const BROWSER_CONNECT_ERROR_CODE = 135;
5+
const KITCHEN_SINK_CODE = 199;
46

5-
export class ProtractorError {
6-
error: Error;
7-
description: string;
7+
export class ProtractorError extends Error {
8+
static ERR_MSGS: string[];
9+
static CODE = KITCHEN_SINK_CODE;
10+
static SUPRESS_EXIT_CODE = false;
11+
12+
message: string; // a one liner, if more than one line is sent, it will be cut off
13+
stack: string; // has the message with the stack trace
814
code: number;
9-
stack: string;
10-
constructor(logger: Logger, description: string, code: number) {
11-
this.error = new Error();
12-
this.description = description;
15+
16+
/**
17+
* Captures the stack trace to this.stack from the Error.captureStackTrace.
18+
* this allows us to capture the error of this error object. Note:
19+
* called with Error set to any to quiet typescript warnings.
20+
*/
21+
captureStackTrace() {
22+
(Error as any).captureStackTrace(this, this.constructor);
23+
}
24+
25+
constructor(logger: Logger, message: string, code: number) {
26+
super(message);
27+
this.message = message;
1328
this.code = code;
14-
logger.error('error code: ' + this.code);
15-
logger.error('description: ' + this.description);
16-
this.stack = this.error.stack;
29+
this.captureStackTrace();
30+
this.logError(logger);
31+
32+
if (!ProtractorError.SUPRESS_EXIT_CODE) {
33+
process.exit(this.code);
34+
}
35+
}
36+
37+
logError(logger: Logger) {
38+
ProtractorError.log(logger, this.code, this.message, this.stack);
39+
}
40+
41+
static log(logger: Logger, code: number, message: string, stack: string) {
42+
let messages = message.split('\n');
43+
if (messages.length > 1) {
44+
message = messages[0];
45+
}
46+
logger.error('Error code: ' + code);
47+
logger.error('Error message: ' + message);
48+
logger.error(stack);
1749
}
1850
}
1951

@@ -22,7 +54,45 @@ export class ProtractorError {
2254
*/
2355
export class ConfigError extends ProtractorError {
2456
static CODE = CONFIG_ERROR_CODE;
25-
constructor(logger: Logger, description: string) {
26-
super(logger, description, ConfigError.CODE);
57+
constructor(logger: Logger, message: string) {
58+
super(logger, message, ConfigError.CODE);
59+
}
60+
}
61+
62+
/**
63+
* Browser errors including getting a driver session, direct connect, etc.
64+
*/
65+
export class BrowserError extends ProtractorError {
66+
static CODE = BROWSER_CONNECT_ERROR_CODE;
67+
static ERR_MSGS = [
68+
'ECONNREFUSED connect ECONNREFUSED', 'Sauce Labs Authentication Error',
69+
'Invalid username or password'
70+
];
71+
constructor(logger: Logger, message: string) {
72+
super(logger, message, BrowserError.CODE);
73+
}
74+
}
75+
76+
export class ErrorHandler {
77+
static isError(errMsgs: string[], e: Error): boolean {
78+
if (errMsgs && errMsgs.length > 0) {
79+
for (let errPos in errMsgs) {
80+
let errMsg = errMsgs[errPos];
81+
if (e.message.indexOf(errMsg) !== -1) {
82+
return true;
83+
}
84+
}
85+
}
86+
return false;
87+
}
88+
89+
static parseError(e: Error): number {
90+
if (ErrorHandler.isError(ConfigError.ERR_MSGS, e)) {
91+
return ConfigError.CODE;
92+
}
93+
if (ErrorHandler.isError(BrowserError.ERR_MSGS, e)) {
94+
return BrowserError.CODE;
95+
}
96+
return null;
2797
}
2898
}

lib/launcher.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
import * as q from 'q';
66
import {Config, ConfigParser} from './configParser';
7+
import {ProtractorError, ConfigError, ErrorHandler} from './exitCodes';
78
import {Logger} from './logger2';
89
import {Runner} from './runner';
910
import {TaskRunner} from './taskRunner';
@@ -176,6 +177,18 @@ let initFn = function(configFile: string, additionalConfig: Config) {
176177
// 4) Run tests.
177178
let scheduler = new TaskScheduler(config);
178179

180+
process.on('uncaughtException', (e: Error) => {
181+
let errorCode = ErrorHandler.parseError(e);
182+
if (errorCode) {
183+
let protractorError = e as ProtractorError;
184+
ProtractorError.log(logger, errorCode, protractorError.message, protractorError.stack);
185+
process.exit(errorCode);
186+
} else {
187+
logger.error('"process.on(\'uncaughtException\'" error, see launcher');
188+
process.exit(ProtractorError.CODE);
189+
}
190+
});
191+
179192
process.on('exit', (code: number) => {
180193
if (code) {
181194
logger.error('Process exited with error code ' + code);
@@ -212,9 +225,8 @@ let initFn = function(configFile: string, additionalConfig: Config) {
212225
1) { // Start new processes only if there are >1 tasks.
213226
forkProcess = true;
214227
if (config.debug) {
215-
throw new Error(
216-
'Cannot run in debug mode with ' +
217-
'multiCapabilities, count > 1, or sharding');
228+
throw new ConfigError(logger,
229+
'Cannot run in debug mode with multiCapabilities, count > 1, or sharding');
218230
}
219231
}
220232

@@ -243,7 +255,7 @@ let initFn = function(configFile: string, additionalConfig: Config) {
243255
' instance(s) of WebDriver still running');
244256
})
245257
.catch((err: Error) => {
246-
logger.error('Error:', err.stack || err.message || err);
258+
logger.error('Error:', (err as any).stack || err.message || err);
247259
cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE);
248260
});
249261
}

scripts/exitCodes.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env node
2+
3+
'use strict';
4+
5+
var spawn = require('child_process').spawnSync;
6+
var exitCodes = require('../built/exitCodes');
7+
8+
var runProtractor, output, messages;
9+
var checkLogs = function(output, messages) {
10+
for (var pos in messages) {
11+
var message = messages[pos];
12+
if (output.indexOf(message) === -1) {
13+
throw new Error('does not exist in logs: ' + message);
14+
}
15+
}
16+
};
17+
18+
/******************************
19+
*Below are exit failure tests*
20+
******************************/
21+
22+
// assert authentication error for sauce labs
23+
runProtractor = spawn('node',
24+
['bin/protractor', 'spec/errorTest/sauceLabsAuthentication.js']);
25+
output = runProtractor.stdout.toString();
26+
messages = ['WebDriverError: Sauce Labs Authentication Error.',
27+
'Process exited with error code ' + exitCodes.BrowserError.CODE];
28+
checkLogs(output, messages);
29+
30+
// assert authentication error for browser stack
31+
runProtractor = spawn('node',
32+
['bin/protractor', 'spec/errorTest/browserStackAuthentication.js']);
33+
output = runProtractor.stdout.toString();
34+
messages = ['WebDriverError: Invalid username or password',
35+
'Process exited with error code ' + exitCodes.BrowserError.CODE];
36+
checkLogs(output, messages);
37+
38+
39+
// assert there is no capabilities in the config
40+
runProtractor = spawn('node',
41+
['bin/protractor', 'spec/errorTest/debugMultiCapabilities.js']);
42+
output = runProtractor.stdout.toString();
43+
messages = [
44+
'Cannot run in debug mode with multiCapabilities, count > 1, or sharding',
45+
'Process exited with error code ' + exitCodes.ConfigError.CODE];
46+
checkLogs(output, messages);

scripts/test.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var passingTests = [
3535
'node built/cli.js spec/angular2Conf.js',
3636
'node built/cli.js spec/hybridConf.js',
3737
'node scripts/attachSession.js',
38+
'node scripts/exitCodes.js',
3839
'node scripts/interactive_tests/interactive_test.js',
3940
'node scripts/interactive_tests/with_base_url.js',
4041
// Unit tests

0 commit comments

Comments
 (0)