From 78f3c64e6d466b44174417d4d6fbc382dbad34b1 Mon Sep 17 00:00:00 2001 From: Craig Date: Tue, 14 Jun 2016 11:48:46 -0700 Subject: [PATCH] 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 --- lib/driverProviders/browserStack.ts | 77 ++++++++-------- lib/driverProviders/direct.ts | 16 ++-- lib/driverProviders/local.ts | 12 +-- lib/exitCodes.ts | 94 +++++++++++++++++--- lib/launcher.ts | 20 ++++- scripts/exitCodes.js | 46 ++++++++++ scripts/test.js | 1 + spec/errorTest/browserStackAuthentication.js | 23 +++++ spec/errorTest/debugMultiCapabilities.js | 16 ++++ spec/errorTest/sauceLabsAuthentication.js | 23 +++++ spec/unit/configParser_test.js | 7 +- spec/unit/driverProviders/direct_test.js | 68 ++++++++++++++ spec/unit/driverProviders/local_test.js | 71 +++++++++++++++ 13 files changed, 408 insertions(+), 66 deletions(-) create mode 100644 scripts/exitCodes.js create mode 100644 spec/errorTest/browserStackAuthentication.js create mode 100644 spec/errorTest/debugMultiCapabilities.js create mode 100644 spec/errorTest/sauceLabsAuthentication.js create mode 100644 spec/unit/driverProviders/direct_test.js create mode 100644 spec/unit/driverProviders/local_test.js diff --git a/lib/driverProviders/browserStack.ts b/lib/driverProviders/browserStack.ts index 87690ffce..6b2dcfc0f 100644 --- a/lib/driverProviders/browserStack.ts +++ b/lib/driverProviders/browserStack.ts @@ -7,6 +7,7 @@ import * as request from 'request'; 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'; @@ -22,42 +23,46 @@ export class BrowserStack extends DriverProvider { * @param {Object} update * @return {q.promise} A promise that will resolve when the update is complete. */ - updateJob(update: any): q.Promise { - let deferredArray = this.drivers_.map((driver: webdriver.WebDriver) => { - let deferred = q.defer(); - driver.getSession().then((session: webdriver.Session) => { - var jobStatus = update.passed ? 'completed' : 'error'; - 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(); - }); - return deferred.promise; - }); - return q.all(deferredArray); - } + updateJob(update: any): q.Promise { + let deferredArray = this.drivers_.map((driver: webdriver.WebDriver) => { + let deferred = q.defer(); + driver.getSession().then((session: webdriver.Session) => { + var jobStatus = update.passed ? 'completed' : 'error'; + 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 BrowserError( + logger, + 'Error updating BrowserStack pass/fail status: ' + + util.inspect(error)); + } + }); + deferred.resolve(); + }); + return deferred.promise; + }); + return q.all(deferredArray); + } + + + /** * Configure and launch (if applicable) the object's environment. diff --git a/lib/driverProviders/direct.ts b/lib/driverProviders/direct.ts index b21a919d0..48b90645d 100644 --- a/lib/driverProviders/direct.ts +++ b/lib/driverProviders/direct.ts @@ -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'; @@ -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() {}); } @@ -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(); @@ -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; diff --git a/lib/driverProviders/local.ts b/lib/driverProviders/local.ts index 3bd739834..87aca2f36 100644 --- a/lib/driverProviders/local.ts +++ b/lib/driverProviders/local.ts @@ -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'; @@ -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) { @@ -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); } } diff --git a/lib/exitCodes.ts b/lib/exitCodes.ts index 01701c9c0..f30945525 100644 --- a/lib/exitCodes.ts +++ b/lib/exitCodes.ts @@ -1,19 +1,51 @@ import {Logger} from './logger2'; const CONFIG_ERROR_CODE = 105; +const BROWSER_CONNECT_ERROR_CODE = 135; +const KITCHEN_SINK_CODE = 199; -export class ProtractorError { - error: Error; - description: string; +export class ProtractorError extends Error { + static ERR_MSGS: string[]; + static CODE = KITCHEN_SINK_CODE; + static SUPRESS_EXIT_CODE = false; + + message: string; // a one liner, if more than one line is sent, it will be cut off + stack: string; // has the message with the stack trace code: number; - stack: string; - constructor(logger: Logger, description: string, code: number) { - this.error = new Error(); - this.description = description; + + /** + * Captures the stack trace to this.stack from the Error.captureStackTrace. + * this allows us to capture the error of this error object. Note: + * called with Error set to any to quiet typescript warnings. + */ + captureStackTrace() { + (Error as any).captureStackTrace(this, this.constructor); + } + + constructor(logger: Logger, message: string, code: number) { + super(message); + this.message = message; this.code = code; - logger.error('error code: ' + this.code); - logger.error('description: ' + this.description); - this.stack = this.error.stack; + this.captureStackTrace(); + this.logError(logger); + + if (!ProtractorError.SUPRESS_EXIT_CODE) { + process.exit(this.code); + } + } + + logError(logger: Logger) { + ProtractorError.log(logger, this.code, this.message, this.stack); + } + + static log(logger: Logger, code: number, message: string, stack: string) { + let messages = message.split('\n'); + if (messages.length > 1) { + message = messages[0]; + } + logger.error('Error code: ' + code); + logger.error('Error message: ' + message); + logger.error(stack); } } @@ -22,7 +54,45 @@ export class ProtractorError { */ export class ConfigError extends ProtractorError { static CODE = CONFIG_ERROR_CODE; - constructor(logger: Logger, description: string) { - super(logger, description, ConfigError.CODE); + constructor(logger: Logger, message: string) { + super(logger, message, ConfigError.CODE); + } +} + +/** + * Browser errors including getting a driver session, direct connect, etc. + */ +export class BrowserError extends ProtractorError { + static CODE = BROWSER_CONNECT_ERROR_CODE; + static ERR_MSGS = [ + 'ECONNREFUSED connect ECONNREFUSED', 'Sauce Labs Authentication Error', + 'Invalid username or password' + ]; + constructor(logger: Logger, message: string) { + super(logger, message, 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; } } diff --git a/lib/launcher.ts b/lib/launcher.ts index 1998bf2e1..ab9ea78e8 100644 --- a/lib/launcher.ts +++ b/lib/launcher.ts @@ -4,6 +4,7 @@ */ import * as q from 'q'; import {Config, ConfigParser} from './configParser'; +import {ProtractorError, ConfigError, ErrorHandler} from './exitCodes'; import {Logger} from './logger2'; import {Runner} from './runner'; import {TaskRunner} from './taskRunner'; @@ -176,6 +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) { + let protractorError = e as ProtractorError; + ProtractorError.log(logger, errorCode, protractorError.message, protractorError.stack); + process.exit(errorCode); + } else { + logger.error('"process.on(\'uncaughtException\'" error, see launcher'); + process.exit(ProtractorError.CODE); + } + }); + process.on('exit', (code: number) => { if (code) { logger.error('Process exited with error code ' + code); @@ -212,9 +225,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'); } } @@ -243,7 +255,7 @@ let initFn = function(configFile: string, additionalConfig: Config) { ' instance(s) of WebDriver still running'); }) .catch((err: Error) => { - logger.error('Error:', err.stack || err.message || err); + logger.error('Error:', (err as any).stack || err.message || err); cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); }); } diff --git a/scripts/exitCodes.js b/scripts/exitCodes.js new file mode 100644 index 000000000..2446b7dc1 --- /dev/null +++ b/scripts/exitCodes.js @@ -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); diff --git a/scripts/test.js b/scripts/test.js index 5ef697538..d29acac6d 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -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 diff --git a/spec/errorTest/browserStackAuthentication.js b/spec/errorTest/browserStackAuthentication.js new file mode 100644 index 000000000..6e954ca56 --- /dev/null +++ b/spec/errorTest/browserStackAuthentication.js @@ -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 + } + +}; diff --git a/spec/errorTest/debugMultiCapabilities.js b/spec/errorTest/debugMultiCapabilities.js new file mode 100644 index 000000000..188f1e39d --- /dev/null +++ b/spec/errorTest/debugMultiCapabilities.js @@ -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, +}; diff --git a/spec/errorTest/sauceLabsAuthentication.js b/spec/errorTest/sauceLabsAuthentication.js new file mode 100644 index 000000000..35f2180f8 --- /dev/null +++ b/spec/errorTest/sauceLabsAuthentication.js @@ -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 + } + +}; diff --git a/spec/unit/configParser_test.js b/spec/unit/configParser_test.js index d532eccf4..51a54d6b2 100644 --- a/spec/unit/configParser_test.js +++ b/spec/unit/configParser_test.js @@ -1,5 +1,6 @@ var ConfigParser = require('../../built/configParser').ConfigParser; var ConfigError = require('../../built/exitCodes').ConfigError; +var ProtractorError = require('../../built/exitCodes').ProtractorError; var Logger = require('../../built/logger2').Logger; var WriteTo = require('../../built/logger2').WriteTo; var path = require('path'); @@ -7,11 +8,13 @@ var path = require('path'); describe('the config parser', function() { describe('exceptions', function() { - beforeEach(function() { + beforeAll(function() { + ProtractorError.SUPRESS_EXIT_CODE = true; Logger.writeTo = WriteTo.NONE; }); - afterEach(function() { + afterAll(function() { + ProtractorError.SUPRESS_EXIT_CODE = false; Logger.writeTo = WriteTo.CONSOLE; }); diff --git a/spec/unit/driverProviders/direct_test.js b/spec/unit/driverProviders/direct_test.js new file mode 100644 index 000000000..ad9c27a1d --- /dev/null +++ b/spec/unit/driverProviders/direct_test.js @@ -0,0 +1,68 @@ +var fs = require('fs'), + os = require('os'), + path = require('path'); +var BrowserError = require('../../../built/exitCodes').BrowserError, + Logger = require('../../../built/logger2').Logger, + WriteTo = require('../../../built/logger2').WriteTo; + +describe('direct connect', function() { + beforeEach(function() { + Logger.setWrite(WriteTo.NONE); + }); + + afterEach(function() { + Logger.setWrite(WriteTo.CONSOLE); + }); + + describe('without the chrome driver', function() { + it('should throw an error if no drivers are present', function() { + var config = { + directConnect: true, + capabilities: { browserName: 'chrome' }, + chromeDriver: '/foo/bar/chromedriver' + }; + var errorFound = false; + try { + webdriver = require('../../../built/driverProviders/direct')(config); + webdriver.getNewDriver(); + } catch(e) { + errorFound = true; + expect(e.code).toBe(BrowserError.CODE); + } + expect(errorFound).toBe(true); + }); + }); + + describe('with chromedriver drivers', function() { + var chromedriver = ''; + beforeEach(function() { + // add files to selenium folder + file = 'chromedriver'; + chromedriver = path.resolve(os.tmpdir(), file); + fs.openSync(chromedriver, 'w'); + }); + + afterEach(function() { + try { + fs.unlinkSync(chromedriver); + } catch(e) { + } + }); + it('should throw an error if the driver cannot be used', function() { + var config = { + directConnect: true, + capabilities: { browserName: 'foobar explorer' }, + chromeDriver: chromedriver + }; + var errorFound = false; + try { + webdriver = require('../../../built/driverProviders/direct')(config); + webdriver.getNewDriver(); + } catch(e) { + errorFound = true; + expect(e.code).toBe(BrowserError.CODE); + } + expect(errorFound).toBe(true); + }); + }); +}); diff --git a/spec/unit/driverProviders/local_test.js b/spec/unit/driverProviders/local_test.js new file mode 100644 index 000000000..8427223ac --- /dev/null +++ b/spec/unit/driverProviders/local_test.js @@ -0,0 +1,71 @@ +var fs = require('fs'), + os = require('os'), + path = require('path'); +var BrowserError = require('../../../built/exitCodes').BrowserError, + Logger = require('../../../built/logger2').Logger, + WriteTo = require('../../../built/logger2').WriteTo; + +describe('local connect', function() { + beforeEach(function() { + Logger.setWrite(WriteTo.NONE); + }); + + afterEach(function() { + Logger.setWrite(WriteTo.CONSOLE); + }); + + describe('without the selenium standalone jar', function() { + it('should throw an error jar file is not present', function() { + var config = { + directConnect: true, + capabilities: { browserName: 'chrome' }, + seleniumServerJar: '/foo/bar/selenium.jar' + }; + var errorFound = false; + try { + webdriver = require('../../../built/driverProviders/local')(config); + webdriver.setupEnv(); + } catch(e) { + errorFound = true; + expect(e.code).toBe(BrowserError.CODE); + } + expect(errorFound).toBe(true); + }); + }); + + describe('with the selenium standalone jar', function() { + it('should throw an error if the jar file does not work', function() { + var jarFile = ''; + beforeEach(function() { + // add files to selenium folder + file = 'selenium.jar'; + jarFile = path.resolve(os.tmpdir(), file); + fs.openSync(jarFile, 'w'); + }); + + afterEach(function() { + try { + fs.unlinkSync(jarFile); + } catch(e) { + } + }); + + it('should throw an error if the selenium sever jar cannot be used', function() { + var config = { + directConnect: true, + capabilities: { browserName: 'foobar explorer' }, + seleniumServerJar: jarFile + }; + var errorFound = false; + try { + webdriver = require('../../../built/driverProviders/local')(config); + webdriver.getNewDriver(); + } catch(e) { + errorFound = true; + expect(e.code).toBe(BrowserError.CODE); + } + expect(errorFound).toBe(true); + }); + }); + }); +});