#!/usr/bin/env node var ansi = require('ansi'); var program = require('commander'); var path = require('path'); var fs = require('fs'); var make_list = function(val) { return val.split(','); }; program .option('-t, --tests ', 'Run the specified html-file-based test(s). \'testlist\' should be a comma-separated list', make_list, []) .option('-a, --print-all', 'Print all tests, not just the failures') .option('--disable-color', 'Explicitly disable color') .option('-c, --color', 'Explicitly enable color (default is to use color when not outputting to a pipe)') .option('-i, --auto-inject ', 'Treat the test list as a set of mocha JS files, and automatically generate HTML files with which to test test. \'includefiles\' should be a comma-separated list of paths to javascript files to include in each of the generated HTML files', make_list, null) .option('-p, --provider ', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper') .option('-g, --generate-html', 'Instead of running the tests, just return the path to the generated HTML file, then wait for user interaction to exit (should be used with .js tests).') .option('-o, --open-in-browser', 'Open the generated HTML files in a web browser using the "open" module (must be used with the "-g"/"--generate-html" option).') .option('--output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') .option('-d, --debug', 'Show debug output (the "console" event) from the provider') .option('-r, --relative', 'Use relative paths in the generated HTML file') .option('--debugger ', 'Enable the remote debugger for CasperJS') .parse(process.argv); if (program.tests.length === 0) { program.tests = fs.readdirSync(__dirname).filter(function(f) { return (/^test\.(\w|\.|-)+\.js$/).test(f); }); program.tests = program.tests.map(function (f) { return path.resolve(__dirname, f); }); // add full paths in console.log('using files %s', program.tests); } var file_paths = []; var all_js = program.tests.reduce(function(a,e) { return a && e.slice(-3) == '.js'; }, true); var get_path = function (/* arguments */) { if (program.relative) { return path.join.apply(null, arguments); } else { var args = Array.prototype.slice.call(arguments); args.unshift(__dirname, '..'); return path.resolve.apply(null, args); } }; var get_path_cwd = function (/* arguments */) { if (program.relative) { var part_path = path.join.apply(null, arguments); return path.relative(path.join(__dirname, '..'), path.resolve(process.cwd(), part_path)); } else { var args = Array.prototype.slice.call(arguments); args.unshift(process.cwd()); return path.resolve.apply(null, args); } }; if (all_js && !program.autoInject) { var all_modules = {}; // uses the first instance of the string 'requires local modules: ' program.tests.forEach(function (testname) { var full_path = path.resolve(process.cwd(), testname); var content = fs.readFileSync(full_path).toString(); var ind = content.indexOf('requires local modules: '); if (ind > -1) { ind += 'requires local modules: '.length; var eol = content.indexOf('\n', ind); var modules = content.slice(ind, eol).split(/,\s*/); modules.forEach(function (mod) { all_modules[get_path('include/', mod) + '.js'] = 1; }); } var fakes_ind = content.indexOf('requires test modules: '); if (fakes_ind > -1) { fakes_ind += 'requires test modules: '.length; var fakes_eol = content.indexOf('\n', fakes_ind); var fakes_modules = content.slice(fakes_ind, fakes_eol).split(/,\s*/); fakes_modules.forEach(function (mod) { all_modules[get_path('tests/', mod) + '.js'] = 1; }); } }); program.autoInject = Object.keys(all_modules); } if (program.autoInject) { var temp = require('temp'); temp.track(); var template = { header: "\n\n\n\n\n
", script_tag: function(p) { return ""; }, footer: "\n\n" }; template.header += "\n" + template.script_tag(get_path('node_modules/chai/chai.js')); template.header += "\n" + template.script_tag(get_path('node_modules/mocha/mocha.js')); template.header += "\n" + template.script_tag(get_path('node_modules/sinon/pkg/sinon.js')); template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n"; template.header = program.autoInject.reduce(function(acc, sn) { return acc + "\n" + template.script_tag(get_path_cwd(sn)); }, template.header); file_paths = program.tests.map(function(jsn, ind) { var templ = template.header; templ += "\n"; templ += template.script_tag(get_path_cwd(jsn)); templ += template.footer; var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' }); fs.writeSync(tempfile.fd, templ); fs.closeSync(tempfile.fd); return tempfile.path; }); } else { file_paths = program.tests.map(function(fn) { return path.resolve(process.cwd(), fn); }); } var use_ansi = false; if (program.color) use_ansi = true; else if (program.disableColor) use_ansi = false; else if (process.stdout.isTTY) use_ansi = true; var cursor = ansi(process.stdout, { enabled: use_ansi }); if (program.outputHtml) { file_paths.forEach(function(path, path_ind) { fs.readFile(path, function(err, data) { if (err) { console.warn(error.stack); return; } if (use_ansi) { cursor .bold() .write(program.tests[path_ind]) .reset() .write("\n") .write(Array(program.tests[path_ind].length+1).join('=')) .write("\n\n"); } cursor .write(data) .write("\n\n"); }); }); } if (program.generateHtml) { var open_browser; if (program.openInBrowser) { open_browser = require('open'); } file_paths.forEach(function(path, path_ind) { cursor .bold() .write(program.tests[path_ind]) .write(": ") .reset() .write(path) .write("\n"); if (program.openInBrowser) { open_browser(path); } }); console.log(''); } if (program.generateHtml) { process.stdin.resume(); // pause until C-c process.on('SIGINT', function() { process.stdin.pause(); // exit }); } if (!program.outputHtml && !program.generateHtml) { var failure_count = 0; var prov = require(path.resolve(__dirname, 'run_from_console.'+program.provider+'.js')); cursor .write("Running tests ") .bold() .write(program.tests.join(', ')) .reset() .grey() .write(' using provider '+prov.name) .reset() .write("\n"); //console.log("Running tests %s using provider %s", program.tests.join(', '), prov.name); var provider = prov.provide_emitter(file_paths, program.debugger); provider.on('test_ready', function(test_json) { console.log(''); filename = program.tests[test_json.file_ind]; cursor.bold(); console.log('Results for %s:', filename); console.log(Array('Results for :'.length+filename.length+1).join('=')); cursor.reset(); console.log(''); cursor .write(''+test_json.num_tests+' tests run, ') .green() .write(''+test_json.num_passes+' passed'); if (test_json.num_slow > 0) { cursor .reset() .write(' ('); cursor .yellow() .write(''+test_json.num_slow+' slow') .reset() .write(')'); } cursor .reset() .write(', '); cursor .red() .write(''+test_json.num_fails+' failed'); if (test_json.num_skipped > 0) { cursor .reset() .write(', ') .grey() .write(''+test_json.num_skipped+' skipped'); } cursor .reset() .write(' -- duration: '+test_json.duration+"s\n"); console.log(''); if (test_json.num_fails > 0 || program.printAll) { var extract_error_lines = function (err) { // the split is to avoid a weird thing where in PhantomJS where we get a stack trace too var err_lines = err.split('\n'); if (err_lines.length == 1) { return err_lines[0]; } else { var ind; for (ind = 0; ind < err_lines.length; ind++) { var at_ind = err_lines[ind].trim().indexOf('at '); if (at_ind === 0) { break; } } return err_lines.slice(0, ind).join('\n'); } }; var traverse_tree = function(indentation, node) { if (node.type == 'suite') { if (!node.has_subfailures && !program.printAll) return; if (indentation === 0) { cursor.bold(); console.log(node.name); console.log(Array(node.name.length+1).join('-')); cursor.reset(); } else { cursor .write(Array(indentation+3).join('#')) .bold() .write(' '+node.name+' ') .reset() .write(Array(indentation+3).join('#')) .write("\n"); } console.log(''); for (var i = 0; i < node.children.length; i++) { traverse_tree(indentation+1, node.children[i]); } } else { if (!node.pass) { cursor.magenta(); console.log('- failed: '+node.text+test_json.replay); cursor.red(); console.log(' '+extract_error_lines(node.error)); cursor.reset(); console.log(''); } else if (program.printAll) { if (node.skipped) { cursor .grey() .write('- skipped: '+node.text); } else { if (node.slow) cursor.yellow(); else cursor.green(); cursor .write('- pass: '+node.text) .grey() .write(' ('+node.duration+') '); } /*if (node.slow) cursor.yellow(); else cursor.green();*/ cursor //.write(test_json.replay) .reset() .write("\n"); console.log(''); } } }; for (var i = 0; i < test_json.suites.length; i++) { traverse_tree(0, test_json.suites[i]); } } if (test_json.num_fails === 0) { cursor.fg.green(); console.log('all tests passed :-)'); cursor.reset(); } }); if (program.debug) { provider.on('console', function(line) { // log to stderr console.error(line); }); } provider.on('error', function(line) { // log to stderr console.error('ERROR: ' + line); }); /*gprom.finally(function(ph) { ph.exit(); // exit with a status code that actually gives information if (program.exitWithFailureCount) process.exit(failure_count); });*/ }