#!/usr/bin/env node var path = require('path'); var fs = require('fs'); var colors = require('colors'); var program = require('commander'); var yaml = require('yamljs'); var xtend = require('xtend'); var osenv = require('osenv'); var find_nearest_file = require('find-nearest-file'); var _ = require('lodash'); var Zuul = require('../lib/zuul'); var scout_browser = require('../lib/scout_browser'); var flatten_browser = require('../lib/flatten_browser'); program .version(require('../package.json').version) .usage('[options] ') .option('--ui ', 'ui for tests (mocha-bdd, mocha-tdd, qunit, tape)') .option('--local [port]', 'port for manual testing in a local browser') .option('--tunnel [type]', 'establish a tunnel for outside access. only used when --local is specified') .option('--disable-tunnel', 'don\'t establish a tunnel for outside access. override any config in .zuul.yml and .zuulrc') .option('--phantom [port]', 'run tests in phantomjs. PhantomJS must be installed separately.') .option('--phantom-remote-debugger-port [port]', 'connect phantom to remote debugger') .option('--phantom-remote-debugger-autorun', 'run tests automatically when --phantom-remote-debugger-port is specified') .option('--electron', 'run tests in electron. electron must be installed separately.') .option('--tunnel-host ', 'specify a localtunnel server to use for forwarding') .option('--sauce-connect [tunnel-identifier]', 'use saucelabs with sauce connect instead of localtunnel. Optionally specify the tunnel-identifier') .option('--loopback ', 'hostname to use instead of localhost, to accomodate Safari and Edge with Sauce Connect. Must resolve to 127.0.0.1') .option('--server ', 'specify a server script to be run') .option('--list-available-browsers', 'list available browsers and versions') .option('--browser-name ', 'specficy the browser name to test an individual browser') .option('--browser-version ', 'specficy the browser version to test an individual browser') .option('--browser-platform ', 'specficy the browser platform to test an individual browser') .option('--browser-retries ', 'number of retries allowed when trying to start a cloud browser, default to 6') .option('--browser-output-timeout ', 'how much time to wait between two test results, default to -1 (no timeout)') .option('--concurrency ', 'specify the number of concurrent browsers to test') .option('--no-coverage', 'disable code coverage analysis with istanbul') .option('--no-instrument', 'disable code coverage instrumentation with istanbul') .option('--open', 'open a browser automatically. only used when --local is specified') .parse(process.argv); var config = { files: program.args, local: program.local, ui: program.ui, tunnel: program.tunnel, phantom: program.phantom, phantomRemoteDebuggerPort: program.phantomRemoteDebuggerPort, phantomRemoteDebuggerAutorun: program.phantomRemoteDebuggerAutorun, electron: program.electron, prj_dir: process.cwd(), tunnel_host: program.tunnelHost, sauce_connect: program.sauceConnect, loopback: program.loopback, server: program.server, concurrency: program.concurrency, coverage: program.coverage, instrument: program.instrument, open: program.open, browser_retries: program.browserRetries && parseInt(program.browserRetries, 10), browser_output_timeout: program.browserOutputTimeout && parseInt(program.browserOutputTimeout, 10), browser_open_timeout: program.browserOpenTimeout && parseInt(program.browserOpenTimeout, 10) }; // Remove unspecified flags for (var key in config) { if (typeof config[key] === 'undefined') { delete config[key]; } } if(!process.stdout.isTTY){ colors.setTheme({ bold: 'stripColors', italic: 'stripColors', underline: 'stripColors', inverse: 'stripColors', yellow: 'stripColors', cyan: 'stripColors', white: 'stripColors', magenta: 'stripColors', green: 'stripColors', red: 'stripColors', grey: 'stripColors', blue: 'stripColors', rainbow: 'stripColors', zebra: 'stripColors', random: 'stripColors' }); } if (program.listAvailableBrowsers) { scout_browser(function(err, all_browsers) { if (err) { console.error('Unable to get available browsers for saucelabs'.red); console.error(err.stack); return process.exit(1); } for (var browser in all_browsers) { console.log(browser); var versions = _.uniq(_.pluck(all_browsers[browser], 'version')).sort(function(a, b) { var a_num = Number(a); var b_num = Number(b); if (a_num && !b_num) { return -1; } else if (!a_num && b_num) { return 1; } else if (a === b) { return 0; } else if (a_num > b_num) { return 1; } return -1; }); var platforms = _.sortBy(_.uniq(_.pluck(all_browsers[browser], 'platform'))); console.log(' Versions: ' + versions.join(', ')); console.log(' Platforms: ' + platforms.join(', ')); } }); return; } if (config.files.length === 0) { console.error('at least one `js` test file must be specified'); return process.exit(1); } if ((program.browserVersion || program.browserPlatform) && !program.browserName) { console.error('the browser name needs to be specified (via --browser-name)'); return process.exit(1); } if ((program.browserName || program.browserPlatform) && !program.browserVersion) { console.error('the browser version needs to be specified (via --browser-version)'); return process.exit(1); } config = readLocalConfig(config) // Overwrite browsers from command line arguments if (program.browserName) { config = xtend(config, { browsers: [{ name: program.browserName, version: program.browserVersion, platform: program.browserPlatform }] }); } if (typeof program.tunnel === 'string') { config.tunnel.type = program.tunnel; } config = readGlobalConfig(config) // Overwrite tunnel option if --disable-tunnel specified if (program.disableTunnel) { config.tunnel = false; } var sauce_username = process.env.SAUCE_USERNAME; var sauce_key = process.env.SAUCE_ACCESS_KEY; config.username = sauce_username || config.sauce_username; config.key = sauce_key || config.sauce_key; if (!config.ui) { console.error('Error: `ui` must be configured in .zuul.yml or specified with the --ui flag'); return process.exit(1); } var pkg = {}; try { pkg = require(process.cwd() + '/package.json'); } catch (err) {} config.name = config.name || pkg.name || 'there is only zuul'; if (config.builder) { // relative path will needs to be under project dir if (config.builder[0] === '.') { config.builder = path.resolve(config.prj_dir, config.builder); } config.builder = require.resolve(config.builder); } var zuul = Zuul(config); if (config.local) { return zuul.run(function(passed) { }); } else if (config.phantom || config.electron) { return zuul.run(function(passed) { process.exit(passed ? 0 : 1); }); } if (!config.username || !config.key) { console.error('Error:'); console.error('Zuul tried to run tests in saucelabs, however no saucelabs credentials were provided.'); console.error('See the zuul wiki (https://github.com/defunctzombie/zuul/wiki/Cloud-testing) for info on how to setup cloud testing.'); process.exit(1); return; } scout_browser(function(err, all_browsers) { var browsers = []; if (err) { console.error('Unable to get available browsers for saucelabs'.red); console.error(err.stack); return process.exit(1); } // common mappings for some of us senile folks all_browsers.iexplore = all_browsers['internet explorer']; all_browsers.ie = all_browsers['internet explorer']; all_browsers.googlechrome = all_browsers.chrome; if (!config.browsers) { console.error('no cloud browsers specified in .zuul.yml'); return process.exit(1); } // flatten into list of testable browsers var to_test = flatten_browser(config.browsers, all_browsers); // pretty prints which browsers we will test on what platforms { var by_os = {}; to_test.forEach(function(browser) { var key = browser.name + ' @ ' + browser.platform; (by_os[key] = by_os[key] || []).push(browser.version); }); for (var item in by_os) { console.log('- testing: %s: %s'.grey, item, by_os[item].join(' ')); } } to_test.forEach(function(info) { zuul.browser(info); }); var passed_tests_count = 0; var failed_tests_count = 0; var failed_browsers_count = 0; var lastOutputName; zuul.on('browser', function(browser) { browsers.push(browser); var name = browser.toString(); var wait_interval; browser.once('init', function() { console.log('- queuing: %s'.grey, name); }); browser.on('start', function(reporter) { console.log('- starting: %s'.white, name); clearInterval(wait_interval); wait_interval = setInterval(function() { console.log('- waiting: %s'.yellow, name); }, 1000 * 30); var current_test = undefined; reporter.on('test', function(test) { current_test = test; }); reporter.on('console', function(msg) { if (lastOutputName !== name) { lastOutputName = name; console.log('%s console'.white, name); } //When testing with microsoft edge: //Adds length property to array-like object if not defined to execute console.log properly if (msg.args.length === undefined) { msg.args.length = Object.keys(msg.args).length; } console.log.apply(console, msg.args); }); reporter.on('assertion', function(assertion) { console.log(); console.log('%s %s'.red, name, current_test ? current_test.name : 'undefined test'); console.log('Error: %s'.red, assertion.message); //When testing with microsoft edge: //Adds length property to array-like object if not defined to execute forEach properly if (assertion.frames.length === undefined) { assertion.frames.length = Object.keys(assertion.frames).length; } Array.prototype.forEach.call(assertion.frames, function(frame) { console.log(' %s %s:%d'.grey, frame.func, frame.filename, frame.line); }); console.log(); }); reporter.once('done', function() { clearInterval(wait_interval); }); }); browser.once('done', function(results) { passed_tests_count += results.passed; failed_tests_count += results.failed; if (results.failed > 0 || results.passed === 0) { console.log('- failed: %s (%d failed, %d passed)'.red, name, results.failed, results.passed); failed_browsers_count++; return; } console.log('- passed: %s'.green, name); }); }); zuul.on('restart', function(browser) { var name = browser.toString(); console.log('- restarting: %s'.red, name); }); zuul.on('error', function(err) { shutdownAllBrowsers(function() { throw err.message; }); }); zuul.run(function(passed) { if (passed instanceof Error) { throw passed; } if (failed_browsers_count > 0) { console.log('%d browser(s) failed'.red, failed_browsers_count); } else if (passed_tests_count === 0) { console.log('no tests ran'.yellow); } else { console.log('all browsers passed'.green); } process.exit((passed_tests_count > 0 && failed_browsers_count == 0) ? 0 : 1); }); function shutdownAllBrowsers(done) { var Batch = require('batch') var batch = new Batch; browsers.forEach(function(browser) { batch.push(function(done) { browser.shutdown(); browser.once('done', done); }); }); batch.end(done); } }); function readLocalConfig(config) { var yaml = path.join(process.cwd(), '.zuul.yml') var js = path.join(process.cwd(), 'zuul.config.js') var yamlExists = fs.existsSync(yaml) var jsExists = fs.existsSync(js) if (yamlExists && jsExists) { console.error('both .zuul.yaml and zuul.config.js are found in the project directory, please choose one') process.exit(1) } else if (yamlExists) { return mergeConfig(config, readYAMLConfig(yaml)) } else if (jsExists) { return mergeConfig(config, require(js)) } return config } function readGlobalConfig(config) { var filename = find_nearest_file('.zuulrc') || path.join(osenv.home(), '.zuulrc') if (fs.existsSync(filename)) { var globalConfig; try { globalConfig = require(filename); } catch(_err) { globalConfig = readYAMLConfig(filename); } return mergeConfig(config, globalConfig); } return config } function readYAMLConfig(filename) { return yaml.parse(fs.readFileSync(filename, 'utf-8')) } function mergeConfig(config, update) { config = xtend(update, config) if (update.tunnel && program.tunnel === true) { config.tunnel = update.tunnel } return config } // vim: ft=javascript