285 lines
No EOL
11 KiB
JavaScript
285 lines
No EOL
11 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
/**
|
|
* The launcher is responsible for parsing the capabilities from the
|
|
* input configuration and launching test runners.
|
|
*/
|
|
const fs = require("fs");
|
|
const q = require("q");
|
|
const configParser_1 = require("./configParser");
|
|
const exitCodes_1 = require("./exitCodes");
|
|
const logger_1 = require("./logger");
|
|
const runner_1 = require("./runner");
|
|
const taskRunner_1 = require("./taskRunner");
|
|
const taskScheduler_1 = require("./taskScheduler");
|
|
const helper = require("./util");
|
|
let logger = new logger_1.Logger('launcher');
|
|
let RUNNERS_FAILED_EXIT_CODE = 100;
|
|
/**
|
|
* Keeps track of a list of task results. Provides method to add a new
|
|
* result, aggregate the results into a summary, count failures,
|
|
* and save results into a JSON file.
|
|
*/
|
|
class TaskResults {
|
|
constructor() {
|
|
// TODO: set a type for result
|
|
this.results_ = [];
|
|
}
|
|
add(result) {
|
|
this.results_.push(result);
|
|
}
|
|
totalSpecFailures() {
|
|
return this.results_.reduce((specFailures, result) => {
|
|
return specFailures + result.failedCount;
|
|
}, 0);
|
|
}
|
|
totalProcessFailures() {
|
|
return this.results_.reduce((processFailures, result) => {
|
|
return !result.failedCount && result.exitCode !== 0 ? processFailures + 1 : processFailures;
|
|
}, 0);
|
|
}
|
|
saveResults(filepath) {
|
|
let jsonOutput = this.results_.reduce((jsonOutput, result) => {
|
|
return jsonOutput.concat(result.specResults);
|
|
}, []);
|
|
let json = JSON.stringify(jsonOutput, null, ' ');
|
|
fs.writeFileSync(filepath, json);
|
|
}
|
|
reportSummary() {
|
|
let specFailures = this.totalSpecFailures();
|
|
let processFailures = this.totalProcessFailures();
|
|
this.results_.forEach((result) => {
|
|
let capabilities = result.capabilities;
|
|
let shortName = (capabilities.browserName) ? capabilities.browserName : '';
|
|
shortName = (capabilities.logName) ?
|
|
capabilities.logName :
|
|
(capabilities.browserName) ? capabilities.browserName : '';
|
|
shortName += (capabilities.version) ? capabilities.version : '';
|
|
shortName += (capabilities.logName && capabilities.count < 2) ? '' : ' #' + result.taskId;
|
|
if (result.failedCount) {
|
|
logger.info(shortName + ' failed ' + result.failedCount + ' test(s)');
|
|
}
|
|
else if (result.exitCode !== 0) {
|
|
logger.info(shortName + ' failed with exit code: ' + result.exitCode);
|
|
}
|
|
else {
|
|
logger.info(shortName + ' passed');
|
|
}
|
|
});
|
|
if (specFailures && processFailures) {
|
|
logger.info('overall: ' + specFailures + ' failed spec(s) and ' + processFailures +
|
|
' process(es) failed to complete');
|
|
}
|
|
else if (specFailures) {
|
|
logger.info('overall: ' + specFailures + ' failed spec(s)');
|
|
}
|
|
else if (processFailures) {
|
|
logger.info('overall: ' + processFailures + ' process(es) failed to complete');
|
|
}
|
|
}
|
|
}
|
|
let taskResults_ = new TaskResults();
|
|
/**
|
|
* Initialize and run the tests.
|
|
* Exits with 1 on test failure, and RUNNERS_FAILED_EXIT_CODE on unexpected
|
|
* failures.
|
|
*
|
|
* @param {string=} configFile
|
|
* @param {Object=} additionalConfig
|
|
*/
|
|
let initFn = function (configFile, additionalConfig) {
|
|
let configParser = new configParser_1.ConfigParser();
|
|
if (configFile) {
|
|
configParser.addFileConfig(configFile);
|
|
}
|
|
if (additionalConfig) {
|
|
configParser.addConfig(additionalConfig);
|
|
}
|
|
let config = configParser.getConfig();
|
|
logger_1.Logger.set(config);
|
|
logger.debug('Running with --troubleshoot');
|
|
logger.debug('Protractor version: ' + require('../package.json').version);
|
|
logger.debug('Your base url for tests is ' + config.baseUrl);
|
|
// Run beforeLaunch
|
|
helper.runFilenameOrFn_(config.configDir, config.beforeLaunch)
|
|
.then(() => {
|
|
return q
|
|
.Promise((resolve, reject) => {
|
|
// 1) If getMultiCapabilities is set, resolve that as
|
|
// `multiCapabilities`.
|
|
if (config.getMultiCapabilities &&
|
|
typeof config.getMultiCapabilities === 'function') {
|
|
if (config.multiCapabilities.length || config.capabilities) {
|
|
logger.warn('getMultiCapabilities() will override both capabilities ' +
|
|
'and multiCapabilities');
|
|
}
|
|
// If getMultiCapabilities is defined and a function, use this.
|
|
q(config.getMultiCapabilities())
|
|
.then((multiCapabilities) => {
|
|
config.multiCapabilities = multiCapabilities;
|
|
config.capabilities = null;
|
|
})
|
|
.then(() => {
|
|
resolve();
|
|
})
|
|
.catch(err => {
|
|
reject(err);
|
|
});
|
|
}
|
|
else {
|
|
resolve();
|
|
}
|
|
})
|
|
.then(() => {
|
|
// 2) Set `multicapabilities` using `capabilities`,
|
|
// `multicapabilities`,
|
|
// or default
|
|
if (config.capabilities) {
|
|
if (config.multiCapabilities.length) {
|
|
logger.warn('You have specified both capabilities and ' +
|
|
'multiCapabilities. This will result in capabilities being ' +
|
|
'ignored');
|
|
}
|
|
else {
|
|
// Use capabilities if multiCapabilities is empty.
|
|
config.multiCapabilities = [config.capabilities];
|
|
}
|
|
}
|
|
else if (!config.multiCapabilities.length) {
|
|
// Default to chrome if no capabilities given
|
|
config.multiCapabilities = [{ browserName: 'chrome' }];
|
|
}
|
|
});
|
|
})
|
|
.then(() => {
|
|
// 3) If we're in `elementExplorer` mode, run only that.
|
|
if (config.elementExplorer || config.framework === 'explorer') {
|
|
if (config.multiCapabilities.length != 1) {
|
|
throw new Error('Must specify only 1 browser while using elementExplorer');
|
|
}
|
|
else {
|
|
config.capabilities = config.multiCapabilities[0];
|
|
}
|
|
config.framework = 'explorer';
|
|
let runner = new runner_1.Runner(config);
|
|
return runner.run().then((exitCode) => {
|
|
process.exit(exitCode);
|
|
}, (err) => {
|
|
logger.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
})
|
|
.then(() => {
|
|
// 4) Run tests.
|
|
let scheduler = new taskScheduler_1.TaskScheduler(config);
|
|
process.on('uncaughtException', (exc) => {
|
|
let e = (exc instanceof Error) ? exc : new Error(exc);
|
|
if (config.ignoreUncaughtExceptions) {
|
|
// This can be a sign of a bug in the test framework, that it may
|
|
// not be handling WebDriver errors properly. However, we don't
|
|
// want these errors to prevent running the tests.
|
|
logger.warn('Ignoring uncaught error ' + exc);
|
|
return;
|
|
}
|
|
let errorCode = exitCodes_1.ErrorHandler.parseError(e);
|
|
if (errorCode) {
|
|
let protractorError = e;
|
|
exitCodes_1.ProtractorError.log(logger, errorCode, protractorError.message, protractorError.stack);
|
|
process.exit(errorCode);
|
|
}
|
|
else {
|
|
logger.error(e.message);
|
|
logger.error(e.stack);
|
|
process.exit(exitCodes_1.ProtractorError.CODE);
|
|
}
|
|
});
|
|
process.on('exit', (code) => {
|
|
if (code) {
|
|
logger.error('Process exited with error code ' + code);
|
|
}
|
|
else if (scheduler.numTasksOutstanding() > 0) {
|
|
logger.error('BUG: launcher exited with ' + scheduler.numTasksOutstanding() +
|
|
' tasks remaining');
|
|
process.exit(RUNNERS_FAILED_EXIT_CODE);
|
|
}
|
|
});
|
|
// Run afterlaunch and exit
|
|
let cleanUpAndExit = (exitCode) => {
|
|
return helper.runFilenameOrFn_(config.configDir, config.afterLaunch, [exitCode])
|
|
.then((returned) => {
|
|
if (typeof returned === 'number') {
|
|
process.exit(returned);
|
|
}
|
|
else {
|
|
process.exit(exitCode);
|
|
}
|
|
}, (err) => {
|
|
logger.error('Error:', err);
|
|
process.exit(1);
|
|
});
|
|
};
|
|
let totalTasks = scheduler.numTasksOutstanding();
|
|
let forkProcess = false;
|
|
if (totalTasks > 1) {
|
|
forkProcess = true;
|
|
if (config.debug) {
|
|
throw new exitCodes_1.ConfigError(logger, 'Cannot run in debug mode with multiCapabilities, count > 1, or sharding');
|
|
}
|
|
}
|
|
let deferred = q.defer(); // Resolved when all tasks are completed
|
|
let createNextTaskRunner = () => {
|
|
let task = scheduler.nextTask();
|
|
if (task) {
|
|
let taskRunner = new taskRunner_1.TaskRunner(configFile, additionalConfig, task, forkProcess);
|
|
taskRunner.run()
|
|
.then((result) => {
|
|
if (result.exitCode && !result.failedCount) {
|
|
logger.error('Runner process exited unexpectedly with error code: ' + result.exitCode);
|
|
}
|
|
taskResults_.add(result);
|
|
task.done();
|
|
createNextTaskRunner();
|
|
// If all tasks are finished
|
|
if (scheduler.numTasksOutstanding() === 0) {
|
|
deferred.resolve();
|
|
}
|
|
logger.info(scheduler.countActiveTasks() + ' instance(s) of WebDriver still running');
|
|
})
|
|
.catch((err) => {
|
|
logger.error('Error:', err.stack || err.message || err);
|
|
cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE);
|
|
});
|
|
}
|
|
};
|
|
// Start `scheduler.maxConcurrentTasks()` workers for handling tasks in
|
|
// the beginning. As a worker finishes a task, it will pick up the next
|
|
// task
|
|
// from the scheduler's queue until all tasks are gone.
|
|
for (let i = 0; i < scheduler.maxConcurrentTasks(); ++i) {
|
|
createNextTaskRunner();
|
|
}
|
|
logger.info('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver');
|
|
// By now all runners have completed.
|
|
deferred.promise
|
|
.then(function () {
|
|
// Save results if desired
|
|
if (config.resultJsonOutputFile) {
|
|
taskResults_.saveResults(config.resultJsonOutputFile);
|
|
}
|
|
taskResults_.reportSummary();
|
|
let exitCode = 0;
|
|
if (taskResults_.totalProcessFailures() > 0) {
|
|
exitCode = RUNNERS_FAILED_EXIT_CODE;
|
|
}
|
|
else if (taskResults_.totalSpecFailures() > 0) {
|
|
exitCode = 1;
|
|
}
|
|
return cleanUpAndExit(exitCode);
|
|
})
|
|
.done();
|
|
})
|
|
.done();
|
|
};
|
|
exports.init = initFn;
|
|
//# sourceMappingURL=launcher.js.map
|