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

chore(exitCodes): adding exit code for browser connect errors #3133

Merged
merged 8 commits into from
Jun 14, 2016
61 changes: 37 additions & 24 deletions lib/driverProviders/browserStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
* It is responsible for setting up the account object, tearing
* it down, and setting up the driver correctly.
*/
import * as request from 'request';
import * as https from 'https';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this 'request' removal be split into its own PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

import * as q from 'q';
import * as util from 'util';

import {BrowserError} from '../exitCodes';
import {Config} from '../configParser';
import {DriverProvider} from './driverProvider';
import {Logger} from '../logger2';
Expand All @@ -30,29 +31,41 @@ export class BrowserStack extends DriverProvider {
logger.info(
'BrowserStack results available at ' +
'https://www.browserstack.com/automate');
request(
{
url: 'https://www.browserstack.com/automate/sessions/' +
session.getId() + '.json',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' +
new Buffer(
this.config_.browserstackUser + ':' +
this.config_.browserstackKey)
.toString('base64')
},
method: 'PUT',
form: {'status': jobStatus}
},
(error: Error) => {
if (error) {
throw new Error(
'Error updating BrowserStack pass/fail status: ' +
util.inspect(error));
}
});
deferred.resolve();
let headers: Object = {
'Content-Type': 'application/json',
'Authorization': 'Basic ' +
new Buffer(
this.config_.browserstackUser + ':' +
this.config_.browserstackKey)
.toString('base64')
};
let options = {
hostname: 'www.browserstack.com',
port: 443,
path: '/automate/sessions/' + session.getId() + '.json',
method: 'PUT',
headers: headers
};
https
.request(
options,
(res) => {
let responseStr = '';
res.on('data', (data: Buffer) => {
responseStr += data.toString();
});
res.on('end', () => {
logger.info(responseStr);
deferred.resolve();
});
res.on('error', (e: Error) => {
throw new BrowserError(
logger,
'Error updating BrowserStack pass/fail status: ' +
util.inspect(e));
});
})
.write('{\'status\': ' + jobStatus + '}');
});
return deferred.promise;
});
Expand Down
16 changes: 9 additions & 7 deletions lib/driverProviders/direct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';

import {BrowserError} from '../exitCodes';
import {Config} from '../configParser';
import {DriverProvider} from './driverProvider';
import {Logger} from '../logger2';
Expand Down Expand Up @@ -39,9 +40,9 @@ export class Direct extends DriverProvider {
logger.info('Using FirefoxDriver directly...');
break;
default:
throw new Error(
'browserName (' + this.config_.capabilities.browserName +
') is not supported with directConnect.');
throw new BrowserError(
logger, 'browserName ' + this.config_.capabilities.browserName +
' is not supported with directConnect.');
}
return q.fcall(function() {});
}
Expand Down Expand Up @@ -69,7 +70,8 @@ export class Direct extends DriverProvider {
this.config_.chromeDriver || defaultChromeDriverPath;

if (!fs.existsSync(chromeDriverFile)) {
throw new Error('Could not find chromedriver at ' + chromeDriverFile);
throw new BrowserError(
logger, 'Could not find chromedriver at ' + chromeDriverFile);
}

let service = new chrome.ServiceBuilder(chromeDriverFile).build();
Expand All @@ -83,9 +85,9 @@ export class Direct extends DriverProvider {
driver = new firefox.Driver(this.config_.capabilities);
break;
default:
throw new Error(
'browserName ' + this.config_.capabilities.browserName +
'is not supported with directConnect.');
throw new BrowserError(
logger, 'browserName ' + this.config_.capabilities.browserName +
' is not supported with directConnect.');
}
this.drivers_.push(driver);
return driver;
Expand Down
12 changes: 7 additions & 5 deletions lib/driverProviders/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as path from 'path';
import * as q from 'q';
import * as util from 'util';

import {BrowserError} from '../exitCodes';
import {Config} from '../configParser';
import {DriverProvider} from './driverProvider';
import {Logger} from '../logger2';
Expand Down Expand Up @@ -42,10 +43,10 @@ export class Local extends DriverProvider {
new SeleniumStandAlone().executableFilename());
}
if (!fs.existsSync(this.config_.seleniumServerJar)) {
throw new Error(
'No selenium server jar found at the specified ' +
'location (' + this.config_.seleniumServerJar +
'). Check that the version number is up to date.');
throw new BrowserError(
logger, 'No selenium server jar found at the specified ' +
'location (' + this.config_.seleniumServerJar +
'). Check that the version number is up to date.');
}
if (this.config_.capabilities.browserName === 'chrome') {
if (!this.config_.chromeDriver) {
Expand All @@ -62,7 +63,8 @@ export class Local extends DriverProvider {
if (fs.existsSync(this.config_.chromeDriver + '.exe')) {
this.config_.chromeDriver += '.exe';
} else {
throw new Error(
throw new BrowserError(
logger,
'Could not find chromedriver at ' + this.config_.chromeDriver);
}
}
Expand Down
63 changes: 57 additions & 6 deletions lib/exitCodes.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
import {Logger} from './logger2';

const CONFIG_ERROR_CODE = 105;
const BROWSER_ERROR_CODE = 135;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think BROWSER_ERROR is too ambiguous - let's make this BROWSER_CONNECT_ERROR_CODE. It's long but our users shouldn't need to type it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


export class ProtractorError extends Error {
static ERR_MSGS: string[];
static DEFAULT_MSG = 'protractor encountered an error';

export class ProtractorError {
error: Error;
description: string;
code: number;
stack: string;
constructor(logger: Logger, description: string, code: number) {
super();
this.error = new Error();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have both inheritance and composition - if we're inheriting from error can we just use this.stack?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, not inherit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a weird work around because stack is not being part of node typings. Using captureStackTrace with any to get the stack.

this.description = description;
this.code = code;
this.description = description;
this.stack = this.error.stack;
this.logError(logger);
}

logError(logger: Logger) {
logger.error('error code: ' + this.code);
logger.error('description: ' + this.description);
this.stack = this.error.stack;
logger.error(this.stack);
}
}

/**
* Configuration file error
*/
export class ConfigError extends ProtractorError {
static DEFAULT_MSG = 'configuration error';
static CODE = CONFIG_ERROR_CODE;
constructor(logger: Logger, description: string) {
super(logger, description, ConfigError.CODE);
constructor(logger: Logger, opt_msg?: string) {
super(logger, opt_msg || ConfigError.DEFAULT_MSG, ConfigError.CODE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have a default configuration msg? 'configuration error' is pretty useless by itself, I'd say keep this required.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the default message and plan to use the message from the thrown error.

process.exit(ConfigError.CODE);
}
}

/**
* Browser errors including getting a driver session, direct connect, etc.
*/
export class BrowserError extends ProtractorError {
static DEFAULT_MSG = 'browser error';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto for not having a default error, make the error-thrower say something useful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

static CODE = BROWSER_ERROR_CODE;
static ERR_MSGS = [
'ECONNREFUSED connect ECONNREFUSED', 'Sauce Labs Authentication Error',
'Invalid username or password'
];
constructor(logger: Logger, opt_msg?: string) {
super(logger, opt_msg || BrowserError.DEFAULT_MSG, BrowserError.CODE);
process.exit(BrowserError.CODE);
}
}

export class ErrorHandler {
static isError(errMsgs: string[], e: Error): boolean {
if (errMsgs && errMsgs.length > 0) {
for (let errPos in errMsgs) {
let errMsg = errMsgs[errPos];
if (e.message.indexOf(errMsg) !== -1) {
return true;
}
}
}
return false;
}

static parseError(e: Error): number {
if (ErrorHandler.isError(ConfigError.ERR_MSGS, e)) {
return ConfigError.CODE;
}
if (ErrorHandler.isError(BrowserError.ERR_MSGS, e)) {
return BrowserError.CODE;
}
return null;
}
}
17 changes: 14 additions & 3 deletions lib/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
import * as q from 'q';
import {Config, ConfigParser} from './configParser';
import {ConfigError, ErrorHandler} from './exitCodes';
import {Logger} from './logger2';
import {Runner} from './runner';
import {TaskRunner} from './taskRunner';
Expand Down Expand Up @@ -176,7 +177,18 @@ let initFn = function(configFile: string, additionalConfig: Config) {
// 4) Run tests.
let scheduler = new TaskScheduler(config);

process.on('uncaughtException', (e: Error) => {
let errorCode = ErrorHandler.parseError(e);
if (errorCode) {
logger.error(e.stack);
process.exit(errorCode);
} else {
logger.error(e.stack);
}
});

process.on('exit', (code: number) => {
console.log(code);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be logging this? If someone wants to use it, they can just use the exit code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it's noted below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

if (code) {
logger.error('Process exited with error code ' + code);
} else if (scheduler.numTasksOutstanding() > 0) {
Expand Down Expand Up @@ -212,9 +224,8 @@ let initFn = function(configFile: string, additionalConfig: Config) {
1) { // Start new processes only if there are >1 tasks.
forkProcess = true;
if (config.debug) {
throw new Error(
'Cannot run in debug mode with ' +
'multiCapabilities, count > 1, or sharding');
throw new ConfigError(logger,
'Cannot run in debug mode with multiCapabilities, count > 1, or sharding');
}
}

Expand Down
46 changes: 46 additions & 0 deletions scripts/exitCodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env node

'use strict';

var spawn = require('child_process').spawnSync;
var exitCodes = require('../built/exitCodes');

var runProtractor, output, messages;
var checkLogs = function(output, messages) {
for (var pos in messages) {
var message = messages[pos];
if (output.indexOf(message) === -1) {
throw new Error('does not exist in logs: ' + message);
}
}
};

/******************************
*Below are exit failure tests*
******************************/

// assert authentication error for sauce labs
runProtractor = spawn('node',
['bin/protractor', 'spec/errorTest/sauceLabsAuthentication.js']);
output = runProtractor.stdout.toString();
messages = ['WebDriverError: Sauce Labs Authentication Error.',
'Process exited with error code ' + exitCodes.BrowserError.CODE];
checkLogs(output, messages);

// assert authentication error for browser stack
runProtractor = spawn('node',
['bin/protractor', 'spec/errorTest/browserStackAuthentication.js']);
output = runProtractor.stdout.toString();
messages = ['WebDriverError: Invalid username or password',
'Process exited with error code ' + exitCodes.BrowserError.CODE];
checkLogs(output, messages);


// assert there is no capabilities in the config
runProtractor = spawn('node',
['bin/protractor', 'spec/errorTest/debugMultiCapabilities.js']);
output = runProtractor.stdout.toString();
messages = [
'Cannot run in debug mode with multiCapabilities, count > 1, or sharding',
'Process exited with error code ' + exitCodes.ConfigError.CODE];
checkLogs(output, messages);
1 change: 1 addition & 0 deletions scripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var passingTests = [
'node built/cli.js spec/angular2Conf.js',
'node built/cli.js spec/hybridConf.js',
'node scripts/attachSession.js',
'node scripts/exitCodes.js',
'node scripts/interactive_tests/interactive_test.js',
'node scripts/interactive_tests/with_base_url.js',
// Unit tests
Expand Down
23 changes: 23 additions & 0 deletions spec/errorTest/browserStackAuthentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var env = require('../environment.js');

exports.config = {
browserstackUser: 'foobar',
browserstackKey: 'foobar',

framework: 'jasmine',

specs: [
'../../example/example_spec.js'
],

capabilities: {
'browserName': 'chrome'
},

baseUrl: env.baseUrl + '/ng1/',

jasmineNodeOpts: {
defaultTimeoutInterval: 90000
}

};
16 changes: 16 additions & 0 deletions spec/errorTest/debugMultiCapabilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var env = require('../environment.js');

exports.config = {
seleniumAddress: env.seleniumAddress,
framework: 'jasmine',
debug: true,
specs: [
'../../example/example_spec.js'
],
multiCapabilities: [{
'browserName': 'chrome'
},{
'browserName': 'firefox'
}],
baseUrl: env.baseUrl,
};
23 changes: 23 additions & 0 deletions spec/errorTest/sauceLabsAuthentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var env = require('../environment.js');

exports.config = {
sauceUser: 'foobar',
sauceKey: 'foobar',

framework: 'jasmine',

specs: [
'../../example/example_spec.js'
],

capabilities: {
'browserName': 'chrome'
},

baseUrl: env.baseUrl + '/ng1/',

jasmineNodeOpts: {
defaultTimeoutInterval: 90000
}

};
Loading