clirunner.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. var fs = require('fs');
  2. var path = require('path');
  3. var Q = require('q');
  4. var clone = require('lodash.clone');
  5. var defaults = require('lodash.defaultsdeep');
  6. var Runner = require('../run.js');
  7. var Logger = require('../../util/logger.js');
  8. var Utils = require('../../util/utils.js');
  9. var Selenium = require('../selenium.js');
  10. var ChildProcess = require('./child-process.js');
  11. var ErrorHandler = require('./errorhandler.js');
  12. var SETTINGS_DEPRECTED_VAL = './settings.json';
  13. var SETTINGS_JS_FILE = './nightwatch.conf.js';
  14. function CliRunner(argv) {
  15. this.settings = null;
  16. this.argv = argv;
  17. this.test_settings = null;
  18. this.output_folder = '';
  19. this.parallelMode = false;
  20. this.runningProcesses = {};
  21. this.cli = require('./cli.js');
  22. }
  23. CliRunner.prototype = {
  24. /**
  25. * @param {function} [done]
  26. * @deprecated - use .setup instead
  27. * @returns {CliRunner}
  28. */
  29. init : function(done) {
  30. this
  31. .readSettings()
  32. .parseTestSettings({}, done);
  33. return this;
  34. },
  35. /**
  36. * @param {object} [settings]
  37. * @param {function} [done]
  38. * @returns {CliRunner}
  39. */
  40. setup : function(settings, done) {
  41. this
  42. .readSettings()
  43. .parseTestSettings(settings, done);
  44. return this;
  45. },
  46. /**
  47. * Read the provided config json file; defaults to settings.json if one isn't provided
  48. */
  49. readSettings : function() {
  50. // use default nightwatch.json file if we haven't received another value
  51. if (this.cli.command('config').isDefault(this.argv.config)) {
  52. var defaultValue = this.cli.command('config').defaults();
  53. var localJsValue = path.resolve(SETTINGS_JS_FILE);
  54. if (Utils.fileExistsSync(SETTINGS_JS_FILE)) {
  55. this.argv.config = localJsValue;
  56. } else if (Utils.fileExistsSync(defaultValue)) {
  57. this.argv.config = path.join(path.resolve('./'), this.argv.config);
  58. } else if (Utils.fileExistsSync(SETTINGS_DEPRECTED_VAL)) {
  59. this.argv.config = path.join(path.resolve('./'), SETTINGS_DEPRECTED_VAL);
  60. } else {
  61. var binFolder = path.resolve(__dirname + '/../../../bin');
  62. var defaultFile = path.join(binFolder, this.argv.config);
  63. if (Utils.fileExistsSync(defaultFile)) {
  64. this.argv.config = defaultFile;
  65. } else {
  66. this.argv.config = path.join(binFolder, SETTINGS_DEPRECTED_VAL);
  67. }
  68. }
  69. } else {
  70. this.argv.config = path.resolve(this.argv.config);
  71. }
  72. this.argv.env = typeof this.argv.env == 'string' ? this.argv.env : 'default';
  73. // reading the settings file
  74. try {
  75. this.settings = require(this.argv.config);
  76. this.replaceEnvVariables(this.settings);
  77. this.manageSelenium = !this.isParallelMode() && this.settings.selenium &&
  78. this.settings.selenium.start_process || false;
  79. if (typeof this.settings.src_folders == 'string') {
  80. this.settings.src_folders = [this.settings.src_folders];
  81. }
  82. this.settings.output = this.settings.output || typeof this.settings.output == 'undefined';
  83. this.settings.detailed_output = this.settings.detailed_output || typeof this.settings.detailed_output == 'undefined';
  84. } catch (ex) {
  85. Logger.error(ex.stack);
  86. this.settings = {};
  87. }
  88. return this;
  89. },
  90. /**
  91. * Looks for pattern ${VAR_NAME} in settings
  92. * @param {Object} [target]
  93. */
  94. replaceEnvVariables : function(target) {
  95. for (var key in target) {
  96. switch(typeof target[key]) {
  97. case 'object':
  98. this.replaceEnvVariables(target[key]);
  99. break;
  100. case 'string':
  101. target[key] = target[key].replace(/\$\{(\w+)\}/g, function(match, varName) {
  102. return process.env[varName] || '${' + varName + '}';
  103. });
  104. break;
  105. }
  106. }
  107. return this;
  108. },
  109. /**
  110. * Reads globals from an external js or json file set in the settings file
  111. * @return {CliRunner}
  112. */
  113. readExternalGlobals : function () {
  114. if (!this.settings.globals_path) {
  115. return this;
  116. }
  117. var fullPath = path.resolve(this.settings.globals_path);
  118. try {
  119. var externalGlobals = require(fullPath);
  120. if (!externalGlobals) {
  121. return this;
  122. }
  123. // if we already have globals, make a copy of them
  124. var globals = this.test_settings.globals ? clone(this.test_settings.globals, true) : {};
  125. if (externalGlobals.hasOwnProperty(this.argv.env)) {
  126. for (var prop in externalGlobals[this.argv.env]) {
  127. externalGlobals[prop] = externalGlobals[this.argv.env][prop];
  128. }
  129. }
  130. defaults(globals, externalGlobals);
  131. this.test_settings.globals = globals;
  132. return this;
  133. } catch (err) {
  134. var originalMsg = Logger.colors.red(err.name + ': ' + err.message);
  135. err.name = 'Error reading external global file failed';
  136. err.message = 'using '+ fullPath + ':\n' + originalMsg;
  137. throw err;
  138. }
  139. },
  140. /**
  141. * @returns {CliRunner}
  142. */
  143. setOutputFolder : function() {
  144. var isDisabled = this.settings.output_folder === false && typeof(this.test_settings.output_folder) == 'undefined' ||
  145. this.test_settings.output_folder === false;
  146. var isDefault = this.cli.command('output').isDefault(this.argv.output);
  147. var folder = this.test_settings.output_folder || this.settings.output_folder;
  148. this.output_folder = isDisabled ? false : (isDefault && folder || this.argv.output);
  149. return this;
  150. },
  151. singleTestRun: function () {
  152. return typeof this.argv.test == 'string';
  153. },
  154. singleSourceFile: function() {
  155. if (this.singleTestRun()) {
  156. return fs.statSync(this.argv.test).isFile();
  157. }
  158. return (Array.isArray(this.argv._source) && this.argv._source.length == 1) && fs.statSync(this.argv._source[0]).isFile();
  159. },
  160. getTestSourceForSingle: function(targetPath) {
  161. var testsource;
  162. if (targetPath && path.resolve(targetPath) === targetPath) {
  163. testsource = targetPath;
  164. } else {
  165. testsource = path.join(process.cwd(), targetPath);
  166. }
  167. if (testsource.substr(-3) != '.js') {
  168. testsource += '.js';
  169. }
  170. return testsource;
  171. },
  172. /**
  173. * Returns the path where the tests are located
  174. * @returns {*}
  175. */
  176. getTestSource : function() {
  177. var testsource;
  178. if (this.isTestWorker() || this.singleSourceFile()) {
  179. testsource = this.getTestSourceForSingle(this.argv.test || this.argv._source[0]);
  180. } else {
  181. if (this.argv.testcase) {
  182. this.argv.testcase = null;
  183. if (this.test_settings.output) {
  184. ErrorHandler.logWarning('Option --testcase used without --test is ignored.');
  185. }
  186. }
  187. if (Array.isArray(this.argv._source) && (this.argv._source.length > 0)) {
  188. testsource = this.argv._source;
  189. } else if (this.argv.group) {
  190. // add support for multiple groups
  191. if (typeof this.argv.group == 'string') {
  192. this.argv.group = this.argv.group.split(',');
  193. }
  194. var groupTestsource = this.findGroupPathMultiple(this.argv.group);
  195. // If a group does not exist in the multiple src folder case, it is removed
  196. // from the test path list.
  197. if (this.settings.src_folders.length === 1) {
  198. // any incorrect path here will result in a run error
  199. testsource = groupTestsource;
  200. } else {
  201. // only when all groups fail to match will there be a run error
  202. testsource = groupTestsource.filter(Utils.dirExistsSync);
  203. if (this.argv.verbose) {
  204. var ignoredSource = groupTestsource.filter(function(path) {
  205. return testsource.indexOf(path) === -1;
  206. });
  207. if (ignoredSource.length) {
  208. Logger.warn('The following group paths were not found and will be excluded from the run:\n - ' + ignoredSource.join('\n - '));
  209. }
  210. }
  211. }
  212. } else {
  213. testsource = this.settings.src_folders;
  214. }
  215. }
  216. return testsource;
  217. },
  218. /**
  219. * Gets test paths from each of the src folders for a single group.
  220. *
  221. * @param {string} groupName
  222. * @return {Array}
  223. */
  224. findGroupPath : function(groupName) {
  225. var fullGroupPath = path.resolve(groupName);
  226. // for each src folder, append the group to the path
  227. // to resolve the full test path
  228. return this.settings.src_folders.map(function(srcFolder) {
  229. var fullSrcFolder = path.resolve(srcFolder);
  230. if (fullGroupPath.indexOf(fullSrcFolder) === 0) {
  231. return groupName;
  232. }
  233. return path.join(srcFolder, groupName);
  234. });
  235. },
  236. /**
  237. * Gets test paths for tests from any number of groups.
  238. *
  239. * @param {Array} groups
  240. */
  241. findGroupPathMultiple : function(groups) {
  242. var paths = [];
  243. groups.forEach(function(groupName) {
  244. // findGroupPath will return an array of paths
  245. paths = paths.concat(this.findGroupPath(groupName));
  246. }.bind(this));
  247. return paths;
  248. },
  249. /**
  250. * Starts the selenium server process
  251. * @param {function} [callback]
  252. * @returns {CliRunner}
  253. */
  254. startSelenium : function(callback) {
  255. callback = callback || function() {};
  256. if (this.isTestWorker()) {
  257. callback();
  258. return this;
  259. }
  260. var globalsContext = this.test_settings && this.test_settings.globals || null;
  261. // adding a link to the test_settings object on the globals context
  262. if (globalsContext) {
  263. globalsContext.test_settings = this.test_settings;
  264. }
  265. var beforeGlobal = Utils.checkFunction('before', globalsContext) || function(done) {done();};
  266. var self = this;
  267. if (!this.manageSelenium) {
  268. beforeGlobal.call(globalsContext, function() {
  269. callback();
  270. });
  271. return this;
  272. }
  273. this.settings.parallelMode = this.parallelMode;
  274. beforeGlobal.call(globalsContext, function() {
  275. Selenium.startServer(self.settings, function(error, child, error_out, exitcode) {
  276. if (error) {
  277. console.error('There was an error while starting the Selenium server:');
  278. ErrorHandler.handle({
  279. message : error_out
  280. });
  281. return;
  282. }
  283. callback();
  284. });
  285. });
  286. return this;
  287. },
  288. /**
  289. * Stops the selenium server if it is running
  290. * @return {*|promise}
  291. */
  292. stopSelenium : function() {
  293. var deferred = Q.defer();
  294. if (this.manageSelenium) {
  295. Selenium.stopServer(function() {
  296. deferred.resolve();
  297. });
  298. } else {
  299. deferred.resolve();
  300. }
  301. return deferred.promise;
  302. },
  303. /**
  304. * Starts the test runner
  305. *
  306. * @param {function} [finished] optional callback
  307. * @returns {CliRunner}
  308. */
  309. runTests : function(finished) {
  310. if (this.parallelMode) {
  311. return this;
  312. }
  313. var self = this;
  314. var handleError = ErrorHandler.handle;
  315. this.startSelenium(function() {
  316. self.runner(function(err, results) {
  317. self.stopSelenium().then(function() {
  318. if (self.isTestWorker()) {
  319. handleError(err, results, finished);
  320. return;
  321. }
  322. self.runGlobalAfter().then(function (ex) {
  323. handleError(ex || err, results, finished);
  324. });
  325. });
  326. });
  327. });
  328. return this;
  329. },
  330. runner : function(fn) {
  331. var testRunner = this.settings.test_runner || 'default';
  332. var testRunnerType = testRunner.type ? testRunner.type : testRunner;
  333. // getTestSource() will throw on an error, so we need
  334. // to wrap and pass along any error that does occur
  335. // to the callback fn
  336. var source;
  337. try {
  338. source = this.getTestSource();
  339. } catch (err) {
  340. fn(err, {});
  341. return;
  342. }
  343. switch (testRunnerType) {
  344. case 'default':
  345. var runner = new Runner(source, this.test_settings, {
  346. output_folder : this.output_folder,
  347. src_folders : this.settings.src_folders,
  348. live_output : this.settings.live_output,
  349. detailed_output : this.settings.detailed_output,
  350. start_session: this.startSession,
  351. reporter : this.argv.reporter,
  352. testcase : this.argv.testcase,
  353. end_session_on_fail : this.endSessionOnFail,
  354. retries : this.argv.retries,
  355. test_worker : this.isTestWorker(),
  356. suite_retries : this.argv.suiteRetries
  357. }, fn);
  358. return runner.run();
  359. case 'mocha':
  360. var MochaNightwatch = require('mocha-nightwatch');
  361. var mochaOpts = testRunner.options || {
  362. ui : 'bdd'
  363. };
  364. var mocha = new MochaNightwatch(mochaOpts);
  365. var nightwatch = require('../../index.js');
  366. Runner.setFinishCallback(fn);
  367. Runner.readPaths(source, this.test_settings, function(error, modulePaths) {
  368. if (error) {
  369. fn(error, {});
  370. return;
  371. }
  372. modulePaths.forEach(function(file) {
  373. mocha.addFile(file);
  374. });
  375. mocha.run(nightwatch, this.test_settings, function(failures) {
  376. fn(null, {
  377. failed : failures
  378. });
  379. if (failures) {
  380. process.exit(10);
  381. }
  382. });
  383. }.bind(this));
  384. return mocha;
  385. }
  386. },
  387. inheritFromDefaultEnv : function() {
  388. if (this.argv.env == 'default') {
  389. return;
  390. }
  391. var defaultEnv = this.settings.test_settings['default'] || {};
  392. defaults(this.test_settings, defaultEnv);
  393. return this;
  394. },
  395. runGlobalAfter : function() {
  396. var deferred = Q.defer();
  397. var globalsContext = this.test_settings && this.test_settings.globals || null;
  398. var afterGlobal = Utils.checkFunction('after', globalsContext) || function(done) {done();};
  399. try {
  400. afterGlobal.call(globalsContext, function done() {
  401. deferred.resolve(null);
  402. });
  403. } catch (ex) {
  404. deferred.resolve(ex);
  405. }
  406. return deferred.promise;
  407. },
  408. /**
  409. * Validates and parses the test settings
  410. * @param {object} [settings]
  411. * @param {function} [done]
  412. * @returns {CliRunner}
  413. */
  414. parseTestSettings : function(settings, done) {
  415. // checking if the env passed is valid
  416. if (!this.settings.test_settings) {
  417. throw new Error('No testing environment specified.');
  418. }
  419. var envs = this.argv.env.split(',');
  420. for (var i = 0; i < envs.length; i++) {
  421. if (!(envs[i] in this.settings.test_settings)) {
  422. throw new Error('Invalid testing environment specified: ' + envs[i]);
  423. }
  424. }
  425. if (envs.length > 1) {
  426. this.setupParallelMode(envs, done);
  427. return this;
  428. }
  429. this.initTestSettings(this.argv.env, settings);
  430. if (this.parallelModeWorkers()) {
  431. this.setupParallelMode(null, done);
  432. }
  433. return this;
  434. },
  435. setGlobalOutputOptions: function () {
  436. this.test_settings.output = this.test_settings.output || (this.settings.output && typeof this.test_settings.output == 'undefined');
  437. this.test_settings.silent = this.test_settings.silent || typeof this.test_settings.silent == 'undefined';
  438. if (this.argv.verbose) {
  439. this.test_settings.silent = false;
  440. }
  441. return this;
  442. },
  443. /**
  444. * Sets the specific test settings for the specified environment
  445. * @param {string} [env]
  446. * @param {object} [settings]
  447. * @returns {CliRunner}
  448. */
  449. initTestSettings : function(env, settings) {
  450. // picking the environment specific test settings
  451. this.test_settings = env && this.settings.test_settings[env] || {};
  452. if (env) {
  453. this.test_settings.custom_commands_path = this.settings.custom_commands_path || '';
  454. this.test_settings.custom_assertions_path = this.settings.custom_assertions_path || '';
  455. this.test_settings.page_objects_path = this.settings.page_objects_path || '';
  456. this.inheritFromDefaultEnv();
  457. this.updateTestSettings(settings || {});
  458. this.readExternalGlobals();
  459. }
  460. this.setOutputFolder();
  461. this.setGlobalOutputOptions();
  462. if (typeof this.test_settings.test_workers != 'undefined') {
  463. this.settings.test_workers = this.test_settings.test_workers;
  464. } else if (typeof this.settings.test_workers != 'undefined') {
  465. this.test_settings.test_workers = this.settings.test_workers;
  466. }
  467. if (typeof this.test_settings.test_runner != 'undefined') {
  468. this.settings.test_runner = this.test_settings.test_runner;
  469. }
  470. if (typeof this.test_settings.detailed_output != 'undefined') {
  471. this.settings.detailed_output = this.test_settings.detailed_output;
  472. }
  473. if (typeof this.argv.skipgroup == 'string') {
  474. this.test_settings.skipgroup = this.argv.skipgroup.split(',');
  475. }
  476. if (this.argv.filter) {
  477. this.test_settings.filename_filter = this.argv.filter;
  478. }
  479. if (this.argv.tag) {
  480. this.test_settings.tag_filter = this.argv.tag;
  481. }
  482. if (typeof this.argv.skiptags == 'string') {
  483. this.test_settings.skiptags = this.argv.skiptags.split(',');
  484. }
  485. return this;
  486. },
  487. /**
  488. *
  489. * @param {Object} [test_settings]
  490. * @returns {CliRunner}
  491. */
  492. updateTestSettings : function(test_settings) {
  493. if (this.parallelMode && !this.parallelModeWorkers()) {
  494. return this;
  495. }
  496. if (test_settings && typeof test_settings == 'object') {
  497. for (var key in test_settings) {
  498. this.test_settings[key] = test_settings[key];
  499. }
  500. }
  501. this.settings.selenium = this.settings.selenium || {};
  502. // overwrite selenium settings per environment
  503. if (Utils.isObject(this.test_settings.selenium)) {
  504. for (var prop in this.test_settings.selenium) {
  505. this.settings.selenium[prop] = this.test_settings.selenium[prop];
  506. }
  507. }
  508. this.manageSelenium = !this.isParallelMode() && this.settings.selenium.start_process || false;
  509. this.startSession = this.settings.selenium.start_session || typeof this.settings.selenium.start_session == 'undefined';
  510. this.mergeSeleniumOptions();
  511. this.disableCliColorsIfNeeded();
  512. this.endSessionOnFail = typeof this.settings.end_session_on_fail == 'undefined' || this.settings.end_session_on_fail;
  513. if (typeof this.test_settings.end_session_on_fail != 'undefined') {
  514. this.endSessionOnFail = this.test_settings.end_session_on_fail;
  515. }
  516. return this;
  517. },
  518. disableCliColorsIfNeeded : function() {
  519. if (this.settings.disable_colors || this.test_settings.disable_colors) {
  520. Logger.disableColors();
  521. }
  522. return this;
  523. },
  524. /**
  525. * Backwards compatible method which attempts to merge deprecated driver specific options for selenium
  526. * @returns {CliRunner}
  527. */
  528. mergeSeleniumOptions : function() {
  529. if (!this.manageSelenium) {
  530. return this;
  531. }
  532. this.settings.selenium.cli_args = this.settings.selenium.cli_args || {};
  533. this.mergeCliArgs();
  534. var deprecationNotice = function(propertyName, newSettingName) {
  535. ErrorHandler.logWarning('DEPRECATION NOTICE: Property ' + propertyName + ' is deprecated since v0.5. Please' +
  536. ' use the "cli_args" object on the "selenium" property to define "' + newSettingName + '". E.g.:');
  537. var demoObj = '{\n' +
  538. ' "cli_args": {\n' +
  539. ' "'+ Logger.colors.yellow(newSettingName) +'": "<VALUE>"\n' +
  540. ' }\n' +
  541. '}';
  542. console.log(demoObj, '\n');
  543. };
  544. if (this.test_settings.firefox_profile) {
  545. deprecationNotice('firefox_profile', 'webdriver.firefox.profile');
  546. this.settings.selenium.cli_args['webdriver.firefox.profile'] = this.test_settings.firefox_profile;
  547. }
  548. if (this.test_settings.chrome_driver) {
  549. deprecationNotice('chrome_driver', 'webdriver.chrome.driver');
  550. this.settings.selenium.cli_args['webdriver.chrome.driver'] = this.test_settings.chrome_driver;
  551. }
  552. if (this.test_settings.ie_driver) {
  553. deprecationNotice('ie_driver', 'webdriver.ie.driver');
  554. this.settings.selenium.cli_args['webdriver.ie.driver'] = this.test_settings.ie_driver;
  555. }
  556. return this;
  557. },
  558. /**
  559. * Merge current environment specific cli_args into the main cli_args object to be passed to selenium
  560. *
  561. * @returns {CliRunner}
  562. */
  563. mergeCliArgs : function() {
  564. if (Utils.isObject(this.test_settings.cli_args)) {
  565. for (var prop in this.test_settings.cli_args) {
  566. if (this.test_settings.cli_args.hasOwnProperty(prop)) {
  567. this.settings.selenium.cli_args[prop] = this.test_settings.cli_args[prop];
  568. }
  569. }
  570. }
  571. return this;
  572. },
  573. ////////////////////////////////////////////////////////////////////////////////////
  574. // Parallelism related
  575. ////////////////////////////////////////////////////////////////////////////////////
  576. isParallelMode : function() {
  577. return process.env.__NIGHTWATCH_PARALLEL_MODE === '1';
  578. },
  579. isTestWorker : function() {
  580. return this.isParallelMode() && this.argv['test-worker'];
  581. },
  582. parallelModeWorkers: function () {
  583. return !this.isParallelMode() && !this.singleSourceFile() && (this.settings.test_workers === true ||
  584. Utils.isObject(this.settings.test_workers) && this.settings.test_workers.enabled);
  585. },
  586. /**
  587. * Enables parallel execution mode
  588. * @param {Array} envs
  589. * @param {function} [done]
  590. * @returns {CliRunner}
  591. */
  592. setupParallelMode : function(envs, done) {
  593. this.parallelMode = true;
  594. var self = this;
  595. this.startSelenium(function() {
  596. self.startChildProcesses(envs, function(code) {
  597. self.stopSelenium().then(function() {
  598. if (self.parallelModeWorkers()) {
  599. return self.runGlobalAfter();
  600. }
  601. return true;
  602. }).then(function() {
  603. if (done) {
  604. try {
  605. done(self.childProcessOutput, code);
  606. } catch (err) {
  607. done(err);
  608. }
  609. }
  610. if (code) {
  611. process.exit(code);
  612. }
  613. });
  614. });
  615. });
  616. return this;
  617. },
  618. getAvailableColors : function () {
  619. var availColors = [
  620. ['red', 'light_gray'],
  621. ['green', 'black'],
  622. ['blue', 'light_gray'],
  623. ['magenta', 'light_gray']
  624. ];
  625. var currentIndex = availColors.length, temporaryValue, randomIndex;
  626. // While there remain elements to shuffle...
  627. while (0 !== currentIndex) {
  628. randomIndex = Math.floor(Math.random() * currentIndex);
  629. currentIndex -= 1;
  630. // And swap it with the current element.
  631. temporaryValue = availColors[currentIndex];
  632. availColors[currentIndex] = availColors[randomIndex];
  633. availColors[randomIndex] = temporaryValue;
  634. }
  635. return availColors;
  636. },
  637. /**
  638. * Start a new child process for each environment
  639. * @param {Array} envs
  640. * @param {function} doneCallback
  641. */
  642. startChildProcesses : function(envs, doneCallback) {
  643. doneCallback = doneCallback || function() {};
  644. var availColors = this.getAvailableColors();
  645. this.childProcessOutput = {};
  646. ChildProcess.prevIndex = 0;
  647. var args = this.getChildProcessArgs(envs);
  648. if (envs === null) {
  649. this.startTestWorkers(availColors, args, doneCallback);
  650. return this;
  651. }
  652. this.startEnvChildren(envs, availColors, args, doneCallback);
  653. },
  654. startEnvChildren : function(envs, availColors, args, doneCallback) {
  655. var self = this;
  656. var globalExitCode = 0;
  657. envs.forEach(function(environment, index) {
  658. self.childProcessOutput[environment] = [];
  659. var childArgs = args.slice();
  660. childArgs.push('--env', environment);
  661. var child = new ChildProcess(environment, index, self.childProcessOutput[environment], self.settings, childArgs);
  662. child.setLabel(environment + ' environment');
  663. self.runningProcesses[child.itemKey] = child;
  664. self.runningProcesses[child.itemKey].run(availColors, function(output, exitCode) {
  665. if (exitCode > 0) {
  666. globalExitCode = exitCode;
  667. }
  668. if (self.processesRunning() === 0) {
  669. if (!self.settings.live_output) {
  670. self.printChildProcessOutput();
  671. }
  672. doneCallback(globalExitCode);
  673. }
  674. });
  675. });
  676. },
  677. getChildProcessArgs : function(envs) {
  678. var childProcessArgs = [];
  679. var arg;
  680. for (var i = 2; i < process.argv.length; i++) {
  681. arg = process.argv[i];
  682. if (envs && (arg == '-e' || arg == '--env')) {
  683. i++;
  684. } else {
  685. childProcessArgs.push(arg);
  686. }
  687. }
  688. return childProcessArgs;
  689. },
  690. startTestWorkers : function(availColors, args, doneCallback) {
  691. var workerCount = this.getTestWorkersCount();
  692. var source = this.getTestSource();
  693. var self = this;
  694. var globalExitCode = 0;
  695. var globalStartTime = new Date().getTime();
  696. var globalResults = {
  697. errmessages : [],
  698. modules : {},
  699. passed : 0,
  700. failed : 0,
  701. errors : 0,
  702. skipped : 0,
  703. tests : 0
  704. };
  705. var Reporter = require('../../runner/reporter.js');
  706. var globalReporter = new Reporter(globalResults, {}, globalStartTime, {start_session: this.startSession});
  707. Runner.readPaths(source, this.test_settings, function(error, modulePaths) {
  708. if (error) {
  709. ErrorHandler.handle(error, null, doneCallback);
  710. return;
  711. }
  712. var remaining = modulePaths.length;
  713. Utils.processAsyncQueue(workerCount, modulePaths, function(modulePath, index, next) {
  714. var outputLabel = Utils.getModuleKey(modulePath, self.settings.src_folders, modulePaths);
  715. var childOutput = self.childProcessOutput[outputLabel] = [];
  716. // arguments to the new child process, essentially running a single test suite
  717. var childArgs = args.slice();
  718. childArgs.push('--test', modulePath, '--test-worker');
  719. var settings = clone(self.settings);
  720. settings.output = settings.output && settings.detailed_output;
  721. var child = new ChildProcess(outputLabel, index, childOutput, settings, childArgs);
  722. child.setLabel(outputLabel);
  723. child.on('result', function(childResult) {
  724. switch (childResult.type) {
  725. case 'testsuite_finished':
  726. globalResults.modules[childResult.moduleKey] = childResult.results;
  727. if (childResult.errmessages.length) {
  728. globalResults.errmessages.concat(childResult.errmessages);
  729. }
  730. globalResults.passed += childResult.passed;
  731. globalResults.failed += childResult.failed;
  732. globalResults.errors += childResult.errors;
  733. globalResults.skipped += childResult.skipped;
  734. globalResults.tests += childResult.tests;
  735. self.printChildProcessOutput(childResult.itemKey);
  736. break;
  737. case 'testsuite_started':
  738. break;
  739. }
  740. });
  741. self.runningProcesses[child.itemKey] = child;
  742. self.runningProcesses[child.itemKey].run(availColors, function (output, exitCode) {
  743. if (exitCode > 0) {
  744. globalExitCode = exitCode;
  745. }
  746. remaining -=1;
  747. if (remaining > 0) {
  748. next();
  749. } else {
  750. if (!self.settings.live_output) {
  751. globalReporter.printTotalResults();
  752. }
  753. doneCallback(globalExitCode);
  754. }
  755. });
  756. });
  757. });
  758. },
  759. printChildProcessOutput : function(label) {
  760. var self = this;
  761. if (label) {
  762. self.childProcessOutput[label] = self.childProcessOutput[label].filter(function(item) {
  763. return item !== '';
  764. }).map(function(item) {
  765. if (item == '\\n') {
  766. item = '\n';
  767. }
  768. return item;
  769. });
  770. self.childProcessOutput[label].forEach(function(output) {
  771. process.stdout.write(output + '\n');
  772. });
  773. self.childProcessOutput[label].length = 0;
  774. return;
  775. }
  776. Object.keys(this.childProcessOutput).forEach(function(environment) {
  777. self.printChildProcessOutput(environment);
  778. });
  779. },
  780. getTestWorkersCount : function() {
  781. var workers = 1;
  782. if (this.settings.test_workers === true || this.settings.test_workers.workers === 'auto') {
  783. workers = require('os').cpus().length;
  784. } else if ('number' === typeof this.settings.test_workers.workers) {
  785. workers = this.settings.test_workers.workers;
  786. }
  787. return workers;
  788. },
  789. processesRunning : function() {
  790. var running = 0;
  791. for (var item in this.runningProcesses) {
  792. if (this.runningProcesses.hasOwnProperty(item) && this.runningProcesses[item].processRunning) {
  793. running += 1;
  794. }
  795. }
  796. return running;
  797. }
  798. };
  799. module.exports = CliRunner;